From b61d3791aea50ac6ca366cb3078c2e3565dfe1aa Mon Sep 17 00:00:00 2001 From: Piotr Kaczmarek Date: Fri, 18 Nov 2016 02:24:56 +0100 Subject: [PATCH 1/3] Implements OAuth Sign In newWindow and sameWindow modes --- src/angular2-token.model.ts | 8 ++-- src/angular2-token.service.ts | 87 ++++++++++++++++++++++++++++++++--- 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/src/angular2-token.model.ts b/src/angular2-token.model.ts index 45187203..8e8a1f4a 100644 --- a/src/angular2-token.model.ts +++ b/src/angular2-token.model.ts @@ -23,10 +23,6 @@ export interface UserType { path: string; } -export interface OAuthPaths { - github?: string; -} - export interface GlobalOptions { headers?: { [key:string]: string; } } @@ -52,7 +48,9 @@ export interface Angular2TokenOptions { userTypes?: UserType[]; - oAuthPaths?: OAuthPaths; + oAuthPaths?: { [key:string]: string; }; + oAuthCallbackPath?: string; + oAuthWindowType?: string; globalOptions?: GlobalOptions; } \ No newline at end of file diff --git a/src/angular2-token.service.ts b/src/angular2-token.service.ts index ee727cd6..5dc2fc1f 100644 --- a/src/angular2-token.service.ts +++ b/src/angular2-token.service.ts @@ -11,6 +11,10 @@ import { import { ActivatedRoute, Router } from '@angular/router'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/operator/share'; +import 'rxjs/add/observable/interval'; +import 'rxjs/add/observable/fromEvent'; +import 'rxjs/add/operator/pluck'; +import 'rxjs/add/operator/filter'; import { UserType, @@ -99,7 +103,8 @@ export class Angular2TokenService implements CanActivate { oAuthPaths: { github: 'auth/github' }, - + oAuthCallbackPath: 'oauth_callback', + oAuthWindowType: 'newWindow', globalOptions: { headers: { 'Content-Type': 'application/json', @@ -156,15 +161,25 @@ export class Angular2TokenService implements CanActivate { return observ; } - signInOAuth(oAuthType: string) { + signInOAuth(oAuthType: string): Observable { - let oAuthPath: string; + let oAuthPath: string = this._getOAuthPath(oAuthType); + let callbackUrl: string = `${window.location.origin}/${this._options.oAuthCallbackPath}`; + let oAuthWindowType: string = this._options.oAuthWindowType; + let authUrl: string = this._buildOAuthUrl(oAuthPath, callbackUrl, oAuthWindowType); - if (oAuthType == 'github') { - oAuthPath = this._options.oAuthPaths.github + if (oAuthWindowType == 'newWindow') { + let popup = window.open(authUrl, '_blank', 'closebuttoncaption=Cancel'); + return this._requestCredentialsViaPostMessage(popup); + } else if (oAuthWindowType == 'sameWindow') { + window.location.href = authUrl; + } else { + throw `Unsupported oAuthWindowType "${oAuthWindowType}"`; } + } - window.open(this._constructUserPath() + oAuthPath); + processOAuthCallback() { + this._getAuthDataFromParams(); } // Sign out request and delete storage @@ -361,7 +376,7 @@ export class Angular2TokenService implements CanActivate { if(this._activatedRoute.queryParams) // Fix for Testing, needs to be removed later this._activatedRoute.queryParams.subscribe(queryParams => { let authData: AuthData = { - accessToken: queryParams['token'], + accessToken: queryParams['token'] || queryParams['auth_token'], client: queryParams['client_id'], expiry: queryParams['expiry'], tokenType: 'Bearer', @@ -374,6 +389,19 @@ export class Angular2TokenService implements CanActivate { } + private _parseAuthDataFromPostMessage(data: any){ + let authData: AuthData = { + accessToken: data['auth_token'], + client: data['client_id'], + expiry: data['expiry'], + tokenType: 'Bearer', + uid: data['uid'] + }; + + this._setAuthData(authData); + } + + // Write auth data to storage private _setAuthData(authData: AuthData) { @@ -453,4 +481,49 @@ export class Angular2TokenService implements CanActivate { else return this._options.apiPath + '/'; } + + private _getOAuthPath(oAuthType: string): string { + let oAuthPath: string; + + oAuthPath = this._options.oAuthPaths[oAuthType]; + if (oAuthPath == null) { + oAuthPath = `/auth/${oAuthType}`; + } + return oAuthPath; + } + + private _buildOAuthUrl(oAuthPath: string, callbackUrl: string, windowType: string): string { + let url: string; + + url = `${window.location.origin}/${oAuthPath}`; + url += `?omniauth_window_type=${windowType}`; + url += `&auth_origin_url=${encodeURIComponent(callbackUrl)}`; + if (this._currentUserType != null) { + url += `&resource_class=${this._currentUserType.name}`; + } + return url; + } + + private _requestCredentialsViaPostMessage(authWindow: any): Observable { + let poller_observ = Observable.interval(500); + let response_observ = Observable.fromEvent(window, 'message') + .pluck('data') + .filter(this._oauthWindowResponseFilter); + + let response_subscription = response_observ.subscribe(this._parseAuthDataFromPostMessage.bind(this)); + let poller_subscription = poller_observ.subscribe(() => { + if (authWindow.closed) { + poller_subscription.unsubscribe(); + } else { + authWindow.postMessage('requestCredentials', '*'); + } + }); + return response_observ; + } + + private _oauthWindowResponseFilter(data: any) { + if(data.message == 'deliverCredentials' || data.message == 'authFailure') { + return data; + } + } } From f4702392103d0dae5ae2c15251846eaea5a0e126 Mon Sep 17 00:00:00 2001 From: Piotr Kaczmarek Date: Wed, 23 Nov 2016 20:45:14 +0100 Subject: [PATCH 2/3] Describes OAuth flow in the readme --- README.md | 62 ++++++++++++++++++++++++++++++++++- src/angular2-token.service.ts | 2 +- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8ae45dfb..750aa569 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,8 @@ The repository can be found [here](https://github.com/neroniaky/angular2-token-e - [`.validateToken()`](#validatetoken) - [`.updatePassword()`](#updatepassword) - [`.resetPassword()`](#resetpassword) + - [`.signInOAuth()`](#signinoauth) + - [`.processOAuthCallback()`](#processoauthcallback) - [HTTP Service Wrapper](#http-service-wrapper) - [Multiple User Types](#multiple-user-types) - [Route Guards](#route-guards) @@ -106,6 +108,12 @@ constructor(private _tokenService: Angular2TokenService) { resetPasswordPath: 'auth/password', resetPasswordCallback: window.location.href, + oAuthPaths: { + github: 'auth/github' + }, + oAuthCallbackPath: 'oauth_callback', + oAuthWindowType: 'newWindow', + userTypes: null, globalOptions: { @@ -134,7 +142,9 @@ constructor(private _tokenService: Angular2TokenService) { | `resetPasswordCallback?: string` | Sets the path user are redirected to after email confirmation for password reset | | `userTypes?: UserTypes[]` | Allows the configuration of multiple user types (see [Multiple User Types](#multiple-user-types)) | | `globalOptions?: GlobalOptions` | Allows the configuration of global options (see below) | - +| `oAuthPaths?: { [key:string]: string }` | Sets paths for sign in with OAuth | +| `oAuthCallbackPath?: string` | Sets path for OAuth sameWindow callback | +| `oAuthWindowType?:`string` | Window type for Oauth authentication | ### Global Options | Options | Description | | ------------------------------------- | ----------------------------------------------- | @@ -251,6 +261,56 @@ this._tokenService.updatePassword( ); ``` +### .signInOAuth() +Initiates OAuth authentication flow by redirecting to OAuth path. Currently, it supports two window modes(oAuthWindowType): +'newWindow' (default) and 'sameWindow'. +When `oAuthWindowType` is set to `newWindow`, `.signInOAuth()` opens a new window and returns an observable. + +When `oAuthWindowType` is set to 'sameWindow', `.signInOAuth()` returns nothing and redirects user to auth provider. +After successful authentication, it redirects back to `oAuthCallbackPath`. Application router needs to intercept +this route and call `processOAuthCallback()` to fetch `AuthData` from params. + +`signInOAuth(oAuthType: string)` + +#### Example: + +```javascript +this._tokenService.signInOAuth( +'github' +).subscribe( + res => console.log(res), + error => console.log(error) +); +``` + +### .processOAuthCallback() +Fetches AuthData from params sent via OAuth redirection when `oAuthWindowType` is set to `sameWindow`. + +`processOAuthCallback()` + +#### Example + +Callback route: +```javascript +RouterModule.forRoot([ + { path: 'oauth_callback', component: OauthCallbackComponent } +]) +``` + +Callback component: +```javascript +@Component({ + template: '' +}) +export class OauthCallbackComponent implements OnInit { + constructor(private _tokenService: Angular2TokenService) {} + + ngOnInit() { + this._tokenService.processOAuthCallback(); + } +} +``` + ## HTTP Service Wrapper `Angular2TokenService` wraps all standard Angular2 Http Service calls for authentication and token processing. If `apiPath` is configured it gets added in front of path. diff --git a/src/angular2-token.service.ts b/src/angular2-token.service.ts index 5dc2fc1f..7bb7647d 100644 --- a/src/angular2-token.service.ts +++ b/src/angular2-token.service.ts @@ -161,7 +161,7 @@ export class Angular2TokenService implements CanActivate { return observ; } - signInOAuth(oAuthType: string): Observable { + signInOAuth(oAuthType: string) { let oAuthPath: string = this._getOAuthPath(oAuthType); let callbackUrl: string = `${window.location.origin}/${this._options.oAuthCallbackPath}`; From 656ac025674a9a57b14fa10acee8ebd01804ef8a Mon Sep 17 00:00:00 2001 From: Piotr Kaczmarek Date: Wed, 23 Nov 2016 21:17:54 +0100 Subject: [PATCH 3/3] Styling fixes to signInOAuth readme section --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8b515e5c..1c63e070 100644 --- a/README.md +++ b/README.md @@ -285,11 +285,11 @@ this._tokenService.updatePassword({ ``` ### .signInOAuth() -Initiates OAuth authentication flow by redirecting to OAuth path. Currently, it supports two window modes(oAuthWindowType): -'newWindow' (default) and 'sameWindow'. -When `oAuthWindowType` is set to `newWindow`, `.signInOAuth()` opens a new window and returns an observable. +Initiates OAuth authentication flow. Currently, it supports two window modes: +`newWindow` (default) and `sameWindow` (settable in config as `oAuthWindowType`). +- When `oAuthWindowType` is set to `newWindow`, `.signInOAuth()` opens a new window and returns an observable. -When `oAuthWindowType` is set to 'sameWindow', `.signInOAuth()` returns nothing and redirects user to auth provider. +- When `oAuthWindowType` is set to `sameWindow`, `.signInOAuth()` returns nothing and redirects user to auth provider. After successful authentication, it redirects back to `oAuthCallbackPath`. Application router needs to intercept this route and call `processOAuthCallback()` to fetch `AuthData` from params. @@ -307,7 +307,7 @@ this._tokenService.signInOAuth( ``` ### .processOAuthCallback() -Fetches AuthData from params sent via OAuth redirection when `oAuthWindowType` is set to `sameWindow`. +Fetches AuthData from params sent via OAuth redirection in `sameWindow` flow. `processOAuthCallback()`