diff --git a/README.md b/README.md index fda881b3..985bf277 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,8 @@ On successful sign in the user will be redirect to `restricted`. - [`.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) @@ -128,6 +130,12 @@ constructor(private _tokenService: Angular2TokenService) { resetPasswordPath: 'auth/password', resetPasswordCallback: window.location.href, + oAuthPaths: { + github: 'auth/github' + }, + oAuthCallbackPath: 'oauth_callback', + oAuthWindowType: 'newWindow', + userTypes: null, globalOptions: { @@ -157,7 +165,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 | | ------------------------------------- | ----------------------------------------------- | @@ -274,6 +284,56 @@ this._tokenService.updatePassword({ ); ``` +### .signInOAuth() +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. +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 in `sameWindow` flow. + +`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.model.ts b/src/angular2-token.model.ts index b8687de6..ea040fd0 100644 --- a/src/angular2-token.model.ts +++ b/src/angular2-token.model.ts @@ -52,10 +52,6 @@ export interface UserType { path: string; } -export interface OAuthPaths { - github?: string; -} - export interface GlobalOptions { headers?: { [key:string]: string; } } @@ -82,7 +78,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 7607f38b..750655dc 100644 --- a/src/angular2-token.service.ts +++ b/src/angular2-token.service.ts @@ -12,6 +12,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 { SignInData, @@ -107,7 +111,8 @@ export class Angular2TokenService implements CanActivate { oAuthPaths: { github: 'auth/github' }, - + oAuthCallbackPath: 'oauth_callback', + oAuthWindowType: 'newWindow', globalOptions: { headers: { 'Content-Type': 'application/json', @@ -166,13 +171,23 @@ export class Angular2TokenService implements CanActivate { signInOAuth(oAuthType: string) { - 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 @@ -385,7 +400,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', @@ -397,6 +412,18 @@ 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) { @@ -476,4 +503,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; + } + } }