diff --git a/README.md b/README.md index 379ba80fb3..e071dc00c5 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,14 @@ Main Nebular module which includes UI Kit and Theme System. | Sidebar | Layout sidebar with multiple states. | | Menu | Multi-depth menu component. | | Card | Basic card with arbitrary header and footer. | +| Alert | Alert component showing message and statuses with close button. | | Flip Card | A card with back and front sides and "flip" switching effect. | | Reveal Card | A card with back and front sides and "reveal" switching effect. | | Search | Global search with amazing showing animations. | | Tabs | Basic and route-based tab components. | | Actions | Horizontal actions bar. | | User | User avatar with a context menu. | +| Progress Bar | Component for progress indication. | | Badge | Simple helper components for showing a badge. | | Popover | Pop-up box that appears when a user clicks on an element. | | Context Menu | A directive to attach a menu to any element. | diff --git a/docs/articles/auth-oauth2.md b/docs/articles/auth-oauth2.md new file mode 100644 index 0000000000..02b2826cb6 --- /dev/null +++ b/docs/articles/auth-oauth2.md @@ -0,0 +1,205 @@ +# Strategy + +Using `NbOAuth2AuthStrategy` is becomes possible to configure authentication with a lot of 3rd party authentication providers, such as Google, Facebook, etc. +There is no need in any backend implementation, as [OAuth2](https://tools.ietf.org/html/rfc6749) protocol enables completely server-less authentication flow as one of the options. + +In this article we will setup and configure `NbOAuth2AuthStrategy` for [Google Authentication](https://developers.google.com/identity/protocols/OAuth2UserAgent) +based on [Implicit](https://tools.ietf.org/html/rfc6749#section-4.2) flow. +
+ +## Step 1. Obtain keys + +As first step we need to setup an application and obtain its keys on the authentication server (Google in our case). +More details how to do this you can find on [Enable APIs for your project](https://developers.google.com/identity/protocols/OAuth2UserAgent#enable-apis) page. +We won't copy over this part of the article here, but as a result you should have your `client_id` - unique application identifier. +
+ +## Step 2. Enable a Strategy + +Next step would be to register `NbOAuth2AuthStrategy` in your `app.module.ts`: + +```ts +@NgModule({ + imports: [ + // ... + + NbAuthModule.forRoot({ + strategies: [ + NbOAuth2AuthStrategy.setup({ + name: 'google', + // ... + }), + ], + }), + ], +}) +export class YourModule { +} +``` + +So we imported `NbAuthModule` and provided a strategy we want to use. If you already have some strategy configuted - don't worry, you can simple append a new one to the `strategies` array. +We also assigned a `name` - `google`. We will use this alias later on to call the strategy. +
+ +## Step 3. Configure + +Let's fill in our strategy with some settings. We add the `client_id` obtained in Step 1. We don't need client_secret for this authentication flow, so we leave it empty. +Then we set `authorize` endpoint, response_type (which is `token` in our case) and [scope](https://tools.ietf.org/html/rfc6749#section-3.3) of the authentication: + +```ts +@NgModule({ + imports: [ + // ... + + NbAuthModule.forRoot({ + strategies: [ + NbOAuth2AuthStrategy.setup({ + name: 'google', + clientId: 'YOUR_CLIENT_ID', + clientSecret: '', + authorize: { + endpoint: 'https://accounts.google.com/o/oauth2/v2/auth', + responseType: NbOAuth2ResponseType.TOKEN, + scope: 'https://www.googleapis.com/auth/userinfo.profile', + }, + }), + ], + }), + ], +}) +export class YourModule { +} +``` +
+ +## Step 4. Routes + +We need at least two routes to be able to organize OAuth2 flow. First one - "login" route, where we can simply have a button to initiate authentication process. +The second one - so-called "callback" route, we need to handle OAuth2 server response. +Let's add both to our routing referring some empty components: + +```ts +RouterModule.forChild([ + { + path: '', + component: NbOAuth2LoginComponent, + }, + { + path: 'callback', + component: NbOAuth2CallbackComponent, + }, +]), +``` +
+ +## Step 5. Redirect URI + +The last configuration bit is to setup the `redirect_uri` parameter. Make sure you added the url to the Google Console as per the [documentation](https://developers.google.com/identity/protocols/OAuth2UserAgent#redirecting). + +Now let's complete the setup: +```ts +@NgModule({ + imports: [ + // ... + + NbAuthModule.forRoot({ + strategies: [ + NbOAuth2AuthStrategy.setup({ + name: 'google', + clientId: 'YOUR_CLIENT_ID', + clientSecret: '', + authorize: { + endpoint: 'https://accounts.google.com/o/oauth2/v2/auth', + responseType: NbOAuth2ResponseType.TOKEN, + scope: 'https://www.googleapis.com/auth/userinfo.profile', + + + redirectUri: 'http://localhost:4100/example/oauth2/callback', + }, + }), + ], + }), + ], +}) +export class YourModule { +} +``` +
+ +## Step 6. Complete your components + +And finally, let's add some code to our component to initiate the authentication. First - `NbOAuth2LoginComponent`: + + +```ts +@Component({ + selector: 'nb-oauth2-login', + template: ` + + `, +}) +export class NbOAuth2LoginComponent implements OnDestroy { + + alive = true; + + login() { + this.authService.authenticate('google') + .pipe(takeWhile(() => this.alive)) + .subscribe((authResult: NbAuthResult) => { + }); + } + + ngOnDestroy(): void { + this.alive = false; + } +} +``` +The code is pretty much straightforward - we call `NbAuthService`.`authenticate` method and pass our strategy alias - `google` subscribing to result. +This will prepare an `authorization` request url and redirect us to google authentication server. + +Now, we need to configure that "callback" url to be able to properly handle response: + +```ts +@Component({ + selector: 'nb-playground-oauth2-callback', + template: ` + + Authenticating... + + `, +}) +export class NbOAuth2CallbackPlaygroundComponent implements OnDestroy { + + alive = true; + + constructor(private authService: NbAuthService, private router: Router) { + this.authService.authenticate('google') + .pipe(takeWhile(() => this.alive)) + .subscribe((authResult: NbAuthResult) => { + if (authResult.isSuccess()) { + this.router.navigateByUrl('/pages/dashboard'); + } + }); + } + + ngOnDestroy(): void { + this.alive = false; + } +} +``` +Here we call the same `authenticate` method, which will determines that we are in the `redirect` state, handle the response, save your token and redirect you back to your app. +
+ +## Complete example + +A complete code example could be found on [GitHub](https://github.com/akveo/nebular/tree/master/src/playground/oauth2). +And here the playground example avaible to play around with [OAuth2 Nebular Example](/example/oauth2). + +
+ +## Related Articles + +- [NbAuthService](/docs/auth/nbauthservice) +- [NbTokenService](/docs/auth/nbtokenservice) +- Receiving [user token after authentication](/docs/auth/getting-user-token) +- [NbOAuth2AuthStrategy](/docs/auth/nboauth2authstrategy) diff --git a/docs/structure.ts b/docs/structure.ts index dcc8f0be1f..6e77578442 100644 --- a/docs/structure.ts +++ b/docs/structure.ts @@ -441,6 +441,17 @@ export const structure = [ }, ], }, + { + type: 'page', + name: 'Configuring Google OAuth2', + children: [ + { + type: 'block', + block: 'markdown', + source: 'auth-oauth2.md', + }, + ], + }, { type: 'page', name: 'NbAuthService', diff --git a/src/framework/auth/auth.module.ts b/src/framework/auth/auth.module.ts index f2330bfede..a337249ed8 100644 --- a/src/framework/auth/auth.module.ts +++ b/src/framework/auth/auth.module.ts @@ -10,19 +10,26 @@ import { NB_AUTH_FALLBACK_TOKEN, NbAuthService, NbAuthSimpleToken, + NbAuthTokenClass, + NbAuthTokenParceler, NbTokenLocalStorage, NbTokenService, NbTokenStorage, - NbAuthTokenClass, - NbAuthTokenParceler, } from './services'; -import { NbAuthStrategy, NbAuthStrategyOptions, NbDummyAuthStrategy, NbPasswordAuthStrategy } from './strategies'; +import { + NbAuthStrategy, + NbAuthStrategyOptions, + NbDummyAuthStrategy, + NbOAuth2AuthStrategy, + NbPasswordAuthStrategy, +} from './strategies'; import { defaultAuthOptions, NB_AUTH_INTERCEPTOR_HEADER, NB_AUTH_OPTIONS, - NB_AUTH_STRATEGIES, NB_AUTH_TOKENS, + NB_AUTH_STRATEGIES, + NB_AUTH_TOKENS, NB_AUTH_USER_OPTIONS, NbAuthOptions, NbAuthStrategyClass, @@ -111,6 +118,7 @@ export class NbAuthModule { NbTokenService, NbDummyAuthStrategy, NbPasswordAuthStrategy, + NbOAuth2AuthStrategy, ], }; } diff --git a/src/framework/auth/strategies/index.ts b/src/framework/auth/strategies/index.ts index f77ba11989..d907090f3d 100644 --- a/src/framework/auth/strategies/index.ts +++ b/src/framework/auth/strategies/index.ts @@ -4,3 +4,5 @@ export * from './dummy/dummy-strategy'; export * from './dummy/dummy-strategy-options'; export * from './password/password-strategy'; export * from './password/password-strategy-options'; +export * from './oauth2/oauth2-strategy'; +export * from './oauth2/oauth2-strategy.options'; diff --git a/src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts b/src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts index 12268a404f..60131df64c 100644 --- a/src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts +++ b/src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts @@ -9,12 +9,16 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/ import { RouterTestingModule } from '@angular/router/testing'; import { ActivatedRoute } from '@angular/router'; import { NB_WINDOW } from '@nebular/theme'; -import { of as observableOf } from 'rxjs'; import { NbOAuth2AuthStrategy } from './oauth2-strategy'; import { NbOAuth2GrantType, NbOAuth2ResponseType } from './oauth2-strategy.options'; import { NbAuthResult, nbAuthCreateToken, NbAuthOAuth2Token } from '../../services'; +function createURL(params: any) { + return Object.keys(params).map((k) => { + return `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`; + }).join('&'); +} describe('oauth2-auth-strategy', () => { @@ -44,7 +48,7 @@ describe('oauth2-auth-strategy', () => { beforeEach(() => { windowMock = { location: { href: '' } }; - routeMock = { params: observableOf({}), queryParams: observableOf({}) }; + routeMock = { snapshot: { params: {}, queryParams: {}, fragment: '' } }; TestBed.configureTestingModule({ imports: [HttpClientTestingModule, RouterTestingModule], @@ -93,7 +97,7 @@ describe('oauth2-auth-strategy', () => { }); it('handle success redirect and sends correct token request', (done: DoneFn) => { - routeMock.queryParams = observableOf({code: 'code'}); + routeMock.snapshot.queryParams = { code: 'code' }; strategy.authenticate() .subscribe((result: NbAuthResult) => { @@ -117,7 +121,7 @@ describe('oauth2-auth-strategy', () => { }); it('handle error redirect back', (done: DoneFn) => { - routeMock.queryParams = observableOf(tokenErrorResponse); + routeMock.snapshot.queryParams = tokenErrorResponse; strategy.authenticate() .subscribe((result: NbAuthResult) => { @@ -134,7 +138,7 @@ describe('oauth2-auth-strategy', () => { }); it('handle error token response', (done: DoneFn) => { - routeMock.queryParams = observableOf({code: 'code'}); + routeMock.snapshot.queryParams = {code: 'code'}; strategy.authenticate() .subscribe((result: NbAuthResult) => { @@ -226,7 +230,7 @@ describe('oauth2-auth-strategy', () => { it('handle success redirect back with token', (done: DoneFn) => { const token = { access_token: 'token', token_type: 'bearer' }; - routeMock.params = observableOf(token); + routeMock.snapshot.fragment = createURL(token); strategy.authenticate() .subscribe((result: NbAuthResult) => { @@ -242,7 +246,7 @@ describe('oauth2-auth-strategy', () => { }); it('handle error redirect back', (done: DoneFn) => { - routeMock.params = observableOf(tokenErrorResponse); + routeMock.snapshot.fragment = createURL(tokenErrorResponse); strategy.authenticate() .subscribe((result: NbAuthResult) => { @@ -307,7 +311,7 @@ describe('oauth2-auth-strategy', () => { }); it('handle success redirect and sends correct token request', (done: DoneFn) => { - routeMock.queryParams = observableOf({code: 'code'}); + routeMock.snapshot.queryParams = { code: 'code' }; strategy.authenticate() .subscribe((result: NbAuthResult) => { @@ -331,7 +335,7 @@ describe('oauth2-auth-strategy', () => { }); it('handle success redirect back with token request', (done: DoneFn) => { - routeMock.queryParams = observableOf({code: 'code'}); + routeMock.snapshot.queryParams = { code: 'code' }; strategy.authenticate() .subscribe((result: NbAuthResult) => { @@ -350,7 +354,7 @@ describe('oauth2-auth-strategy', () => { }); it('handle error redirect back', (done: DoneFn) => { - routeMock.queryParams = observableOf(tokenErrorResponse); + routeMock.snapshot.queryParams = tokenErrorResponse; strategy.authenticate() .subscribe((result: NbAuthResult) => { @@ -389,7 +393,7 @@ describe('oauth2-auth-strategy', () => { }); it('handle error token response', (done: DoneFn) => { - routeMock.queryParams = observableOf({code: 'code'}); + routeMock.snapshot.queryParams = { code: 'code' }; strategy.authenticate() .subscribe((result: NbAuthResult) => { diff --git a/src/framework/auth/strategies/oauth2/oauth2-strategy.ts b/src/framework/auth/strategies/oauth2/oauth2-strategy.ts index f350eaee93..cbf5829c3b 100644 --- a/src/framework/auth/strategies/oauth2/oauth2-strategy.ts +++ b/src/framework/auth/strategies/oauth2/oauth2-strategy.ts @@ -13,23 +13,20 @@ import { NB_WINDOW } from '@nebular/theme'; import { NbAuthStrategy } from '../auth-strategy'; import { NbAuthRefreshableToken, NbAuthResult } from '../../services/'; import { NbOAuth2AuthStrategyOptions, NbOAuth2ResponseType, auth2StrategyOptions } from './oauth2-strategy.options'; +import { NbAuthStrategyClass } from '../../auth.options'; /** * OAuth2 authentication strategy. * - * @example - * * Strategy settings: * - * ``` - * + * ```ts * export enum NbOAuth2ResponseType { * CODE = 'code', * TOKEN = 'token', * } * - * // TODO: password, client_credentials * export enum NbOAuth2GrantType { * AUTHORIZATION_CODE = 'authorization_code', * REFRESH_TOKEN = 'refresh_token', @@ -82,13 +79,17 @@ import { NbOAuth2AuthStrategyOptions, NbOAuth2ResponseType, auth2StrategyOptions @Injectable() export class NbOAuth2AuthStrategy extends NbAuthStrategy { + static setup(options: NbOAuth2AuthStrategyOptions): [NbAuthStrategyClass, NbOAuth2AuthStrategyOptions] { + return [NbOAuth2AuthStrategy, options]; + } + get responseType() { return this.getOption('authorize.responseType'); } protected redirectResultHandlers = { [NbOAuth2ResponseType.CODE]: () => { - return this.route.queryParams.pipe( + return observableOf(this.route.snapshot.queryParams).pipe( switchMap((params: any) => { if (params.code) { return this.requestToken(params.code) @@ -106,7 +107,8 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { ); }, [NbOAuth2ResponseType.TOKEN]: () => { - return this.route.params.pipe( + return observableOf(this.route.snapshot.fragment).pipe( + map(fragment => this.parseHashAsQueryParams(fragment)), map((params: any) => { if (!params.error) { return new NbAuthResult( @@ -132,12 +134,13 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { protected redirectResults = { [NbOAuth2ResponseType.CODE]: () => { - return this.route.queryParams.pipe( + return observableOf(this.route.snapshot.queryParams).pipe( map((params: any) => !!(params && (params.code || params.error))), ); }, [NbOAuth2ResponseType.TOKEN]: () => { - return this.route.params.pipe( + return observableOf(this.route.snapshot.fragment).pipe( + map(fragment => this.parseHashAsQueryParams(fragment)), map((params: any) => !!(params && (params.access_token || params.error))), ); }, @@ -157,7 +160,7 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { switchMap((result: boolean) => { if (!result) { this.authorizeRedirect(); - return observableOf(null); + return observableOf(new NbAuthResult(true)); } return this.getAuthorizationResult(); }), @@ -297,6 +300,14 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { return `${endpoint}?${query}`; } + protected parseHashAsQueryParams(hash: string): { [key: string]: string } { + return hash ? hash.split('&').reduce((acc: any, part: string) => { + const item = part.split('='); + acc[item[0]] = decodeURIComponent(item[1]); + return acc; + }, {}) : {}; + } + register(data?: any): Observable { throw new Error('`register` is not supported by `NbOAuth2AuthStrategy`, use `authenticate`.'); } @@ -310,6 +321,6 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { } logout(): Observable { - throw new Error('`logout` is not supported by `NbOAuth2AuthStrategy`, use `authenticate`.'); + return observableOf(new NbAuthResult(true)); } } diff --git a/src/playground/oauth2/oauth2-callback.component.ts b/src/playground/oauth2/oauth2-callback.component.ts new file mode 100644 index 0000000000..99c88c3d99 --- /dev/null +++ b/src/playground/oauth2/oauth2-callback.component.ts @@ -0,0 +1,37 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { Component, OnDestroy } from '@angular/core'; +import { NbAuthResult, NbAuthService } from '@nebular/auth'; +import { Router } from '@angular/router'; +import { takeWhile } from 'rxjs/operators'; + +@Component({ + selector: 'nb-playground-oauth2-callback', + template: ` + + Authenticating... + + `, +}) +export class NbOAuth2CallbackComponent implements OnDestroy { + + alive = true; + + constructor(private authService: NbAuthService, private router: Router) { + this.authService.authenticate('google') + .pipe(takeWhile(() => this.alive)) + .subscribe((authResult: NbAuthResult) => { + if (authResult.isSuccess() && authResult.getRedirect()) { + this.router.navigateByUrl(authResult.getRedirect()); + } + }); + } + + ngOnDestroy(): void { + this.alive = false; + } +} diff --git a/src/playground/oauth2/oauth2-login.component.ts b/src/playground/oauth2/oauth2-login.component.ts new file mode 100644 index 0000000000..dd52d1c537 --- /dev/null +++ b/src/playground/oauth2/oauth2-login.component.ts @@ -0,0 +1,63 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { Component, OnDestroy } from '@angular/core'; +import { NbAuthOAuth2Token, NbAuthResult, NbAuthService } from '@nebular/auth'; +import { takeWhile } from 'rxjs/operators'; + +@Component({ + selector: 'nb-playground-auth', + template: ` + + + + +

Current User Authenticated: {{ !!token }}

+

Current User Token: {{ token|json }}

+ + + +
+
+
+
+ `, +}) +export class NbOAuth2LoginComponent implements OnDestroy { + + token: NbAuthOAuth2Token; + + alive = true; + + constructor(private authService: NbAuthService) { + this.authService.onTokenChange() + .pipe(takeWhile(() => this.alive)) + .subscribe((token: NbAuthOAuth2Token) => { + this.token = null; + if (token && token.isValid()) { + this.token = token; + } + }); + } + + login() { + this.authService.authenticate('google') + .pipe(takeWhile(() => this.alive)) + .subscribe((authResult: NbAuthResult) => { + }); + } + + logout() { + this.authService.logout('google') + .pipe(takeWhile(() => this.alive)) + .subscribe((authResult: NbAuthResult) => { + }); + } + + ngOnDestroy(): void { + this.alive = false; + } +} diff --git a/src/playground/oauth2/oauth2.module.ts b/src/playground/oauth2/oauth2.module.ts new file mode 100644 index 0000000000..78e964cd53 --- /dev/null +++ b/src/playground/oauth2/oauth2.module.ts @@ -0,0 +1,74 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterModule } from '@angular/router'; +import { HttpClientModule } from '@angular/common/http'; + +import { + NbCardModule, + NbLayoutModule, +} from '@nebular/theme'; + +import { + NbAuthModule, + NbOAuth2AuthStrategy, + NbOAuth2ResponseType, +} from '@nebular/auth'; + +import { NbOAuth2LoginComponent } from './oauth2-login.component'; +import { NbOAuth2CallbackComponent } from './oauth2-callback.component'; + + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + HttpClientModule, + RouterModule, + RouterModule.forChild([ + { + path: '', + component: NbOAuth2LoginComponent, + }, + { + path: 'callback', + component: NbOAuth2CallbackComponent, + }, + ]), + + NbAuthModule.forRoot({ + strategies: [ + NbOAuth2AuthStrategy.setup({ + name: 'google', + clientId: '806751403568-03376bvlin9n3rhid0cahus6ei3lc69q.apps.googleusercontent.com', + clientSecret: '', + authorize: { + endpoint: 'https://accounts.google.com/o/oauth2/v2/auth', + responseType: NbOAuth2ResponseType.TOKEN, + scope: 'https://www.googleapis.com/auth/userinfo.profile', + redirectUri: 'https://akveo.github.io/nebular/example/oauth2/callback', + }, + + redirect: { + success: '/example/oauth2', + }, + }), + ], + }), + + NbCardModule, + NbLayoutModule, + ], + declarations: [ + NbOAuth2LoginComponent, + NbOAuth2CallbackComponent, + ], +}) +export class NbOAuth2PlaygroundModule { +} diff --git a/src/playground/playground-routing.module.ts b/src/playground/playground-routing.module.ts index c005ba4be5..1dc741cbde 100644 --- a/src/playground/playground-routing.module.ts +++ b/src/playground/playground-routing.module.ts @@ -616,6 +616,10 @@ export const routes: Routes = [ }, ], }, + { + path: 'oauth2', + loadChildren: './oauth2/oauth2.module#NbOAuth2PlaygroundModule', + }, { path: '', loadChildren: './auth/auth.module#NbAuthPlaygroundModule',