From 5d937960eaa6ebe305a6de10ec4982e0bb1fdc7e Mon Sep 17 00:00:00 2001 From: Dmitry Nehaychik <4dmitr@gmail.com> Date: Sat, 28 Jul 2018 14:06:49 +0300 Subject: [PATCH 1/5] chore(build): fix browser no activity timeout (#588) As we need to wait for the build to be ready while browser is already waiting --- karma.conf.js | 1 + 1 file changed, 1 insertion(+) diff --git a/karma.conf.js b/karma.conf.js index aa66be13bb..0493000338 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -28,6 +28,7 @@ module.exports = function (config) { }, reporters: ['spec', 'kjhtml'], port: 9876, + browserNoActivityTimeout : 60000, colors: true, logLevel: config.LOG_INFO, autoWatch: true, From a7b8ff4e7aa4c0844522179102b3dfa92472cdff Mon Sep 17 00:00:00 2001 From: Dmitry Nehaychik <4dmitr@gmail.com> Date: Mon, 30 Jul 2018 18:01:41 +0300 Subject: [PATCH 2/5] feat(alert): add outline alerts (#590) --- .../alert/_alert.component.theme.scss | 30 +++++++- .../theme/components/alert/alert.component.ts | 72 ++++++++++++++++++- .../theme/components/alert/alert.spec.ts | 59 +++++++++++++++ .../theme/styles/themes/_default.scss | 3 +- .../alert/alert-outline.component.html | 11 +++ .../alert/alert-outline.component.ts | 15 ++++ src/playground/playground-routing.module.ts | 5 ++ src/playground/playground.module.ts | 2 + 8 files changed, 191 insertions(+), 6 deletions(-) create mode 100644 src/framework/theme/components/alert/alert.spec.ts create mode 100644 src/playground/alert/alert-outline.component.html create mode 100644 src/playground/alert/alert-outline.component.ts diff --git a/src/framework/theme/components/alert/_alert.component.theme.scss b/src/framework/theme/components/alert/_alert.component.theme.scss index ec5507d909..b9472502e5 100644 --- a/src/framework/theme/components/alert/_alert.component.theme.scss +++ b/src/framework/theme/components/alert/_alert.component.theme.scss @@ -11,8 +11,6 @@ border-top-right-radius: nb-theme(alert-border-radius); color: nb-theme(alert-fg-heading); - - @include nb-headings(); } @@ -23,7 +21,7 @@ font-weight: nb-theme(alert-font-weight); background: nb-theme(alert-bg); - color: nb-theme(alert-fg); + color: nb-theme(alert-outline-fg); // TODO: move alert margin style to layout margin-bottom: nb-theme(alert-margin); border-radius: nb-theme(alert-border-radius); @@ -35,6 +33,10 @@ nb-theme(scrollbar-bg), nb-theme(scrollbar-width)); + &.status { + color: nb-theme(alert-fg); + } + &.xxsmall-alert { height: nb-theme(alert-height-xxsmall); } @@ -111,6 +113,28 @@ border-top-color: nb-theme(alert-danger-bg); } + &.outline-active { + border: 2px solid nb-theme(alert-active-bg); + } + &.outline-disabled { + border: 2px solid nb-theme(alert-disabled-bg); + } + &.outline-primary { + border: 2px solid nb-theme(alert-primary-bg); + } + &.outline-success { + border: 2px solid nb-theme(alert-success-bg); + } + &.outline-info { + border: 2px solid nb-theme(alert-info-bg); + } + &.outline-warning { + border: 2px solid nb-theme(alert-warning-bg); + } + &.outline-danger { + border: 2px solid nb-theme(alert-danger-bg); + } + .close { padding: nb-theme(alert-padding); font-size: 1.5rem; diff --git a/src/framework/theme/components/alert/alert.component.ts b/src/framework/theme/components/alert/alert.component.ts index 9e6bff2407..5413a4b7f3 100644 --- a/src/framework/theme/components/alert/alert.component.ts +++ b/src/framework/theme/components/alert/alert.component.ts @@ -32,10 +32,13 @@ import { convertToBoolProperty } from '../helpers'; * Colored alerts could be simply configured by providing a `status` property: * @stacked-example(Colored Alert, alert/alert-colors.component) * - * It is also possible to assign an `accent` property for a slight card highlight + * It is also possible to assign an `accent` property for a slight alert highlight * as well as combine it with `status`: * @stacked-example(Accent Alert, alert/alert-accents.component) * + * And `outline` property: + * @stacked-example(Outline Alert, alert/alert-outline.component) + * * @additional-example(Multiple Sizes, alert/alert-sizes.component) * * @styles @@ -44,6 +47,7 @@ import { convertToBoolProperty } from '../helpers'; * alert-line-height: * alert-font-weight: * alert-fg: + * alert-outline-fg: * alert-bg: * alert-active-bg: * alert-disabled-bg: @@ -103,9 +107,18 @@ export class NbAlertComponent { static readonly ACCENT_WARNING = 'warning'; static readonly ACCENT_DANGER = 'danger'; + static readonly OUTLINE_ACTIVE = 'active'; + static readonly OUTLINE_DISABLED = 'disabled'; + static readonly OUTLINE_PRIMARY = 'primary'; + static readonly OUTLINE_INFO = 'info'; + static readonly OUTLINE_SUCCESS = 'success'; + static readonly OUTLINE_WARNING = 'warning'; + static readonly OUTLINE_DANGER = 'danger'; + size: string; - status: string = NbAlertComponent.STATUS_SUCCESS; + status: string; accent: string; + outline: string; @HostBinding('class.closable') closableValue: boolean = false; @@ -193,6 +206,11 @@ export class NbAlertComponent { return this.accent; } + @HostBinding('class.status') + get hasStatus() { + return this.status; + } + @HostBinding('class.accent-primary') get primaryAccent() { return this.accent === NbAlertComponent.ACCENT_PRIMARY; @@ -228,6 +246,46 @@ export class NbAlertComponent { return this.accent === NbAlertComponent.ACCENT_DISABLED; } + @HostBinding('class.outline') + get hasOutline() { + return this.outline; + } + + @HostBinding('class.outline-primary') + get primaryOutline() { + return this.outline === NbAlertComponent.OUTLINE_PRIMARY; + } + + @HostBinding('class.outline-info') + get infoOutline() { + return this.outline === NbAlertComponent.OUTLINE_INFO; + } + + @HostBinding('class.outline-success') + get successOutline() { + return this.outline === NbAlertComponent.OUTLINE_SUCCESS; + } + + @HostBinding('class.outline-warning') + get warningOutline() { + return this.outline === NbAlertComponent.OUTLINE_WARNING; + } + + @HostBinding('class.outline-danger') + get dangerOutline() { + return this.outline === NbAlertComponent.OUTLINE_DANGER; + } + + @HostBinding('class.outline-active') + get activeOutline() { + return this.outline === NbAlertComponent.OUTLINE_ACTIVE; + } + + @HostBinding('class.outline-disabled') + get disabledOutline() { + return this.outline === NbAlertComponent.OUTLINE_DISABLED; + } + /** * Alert size, available sizes: * xxsmall, xsmall, small, medium, large, xlarge, xxlarge @@ -258,6 +316,16 @@ export class NbAlertComponent { this.accent = val; } + /** + * Alert outline (color of the border): + * active, disabled, primary, info, success, warning, danger + * @param {string} val + */ + @Input('outline') + private set setOutline(val: string) { + this.outline = val; + } + /** * Emits when chip is removed * @type EventEmitter diff --git a/src/framework/theme/components/alert/alert.spec.ts b/src/framework/theme/components/alert/alert.spec.ts new file mode 100644 index 0000000000..27a4643243 --- /dev/null +++ b/src/framework/theme/components/alert/alert.spec.ts @@ -0,0 +1,59 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { NbAlertComponent } from './alert.component'; + +describe('Component: NbAlert', () => { + + let alert: NbAlertComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [NbAlertComponent], + }); + + fixture = TestBed.createComponent(NbAlertComponent); + alert = fixture.componentInstance; + }); + + it('should set class danger', () => { + alert.status = 'danger'; + fixture.detectChanges(); + expect( + fixture + .debugElement.nativeElement.classList.contains('danger-alert')) + .toBeTruthy() + }); + + it('should set outline class', () => { + alert.outline = 'success'; + fixture.detectChanges(); + expect( + fixture + .debugElement.nativeElement.classList.contains('outline-success')) + .toBeTruthy() + }); + + it('should set shape class', () => { + alert.accent = 'warning'; + fixture.detectChanges(); + expect( + fixture + .debugElement.nativeElement.classList.contains('accent-warning')) + .toBeTruthy() + }); + + it('should set size class', () => { + alert.size = 'xxsmall'; + fixture.detectChanges(); + expect( + fixture + .debugElement.nativeElement.classList.contains('xxsmall-alert')) + .toBeTruthy() + }); +}); diff --git a/src/framework/theme/styles/themes/_default.scss b/src/framework/theme/styles/themes/_default.scss index 10398dd1dd..500063e310 100644 --- a/src/framework/theme/styles/themes/_default.scss +++ b/src/framework/theme/styles/themes/_default.scss @@ -469,8 +469,9 @@ $theme: ( alert-font-size: font-size, alert-line-height: line-height, - alert-font-weight: font-weight-bold, + alert-font-weight: font-weight-normal, alert-fg: color-white, + alert-outline-fg: color-fg, alert-bg: color-bg, alert-active-bg: color-fg, alert-disabled-bg: color-disabled, diff --git a/src/playground/alert/alert-outline.component.html b/src/playground/alert/alert-outline.component.html new file mode 100644 index 0000000000..975a78847f --- /dev/null +++ b/src/playground/alert/alert-outline.component.html @@ -0,0 +1,11 @@ + + + You have been successfully authenticated! + You have been successfully authenticated! + You have been successfully authenticated! + You have been successfully authenticated! + You have been successfully authenticated! + You have been successfully authenticated! + You have been successfully authenticated! + + diff --git a/src/playground/alert/alert-outline.component.ts b/src/playground/alert/alert-outline.component.ts new file mode 100644 index 0000000000..9b72d07f35 --- /dev/null +++ b/src/playground/alert/alert-outline.component.ts @@ -0,0 +1,15 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'nb-alert-outline', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './alert-outline.component.html', +}) +export class NbAlertOutlineComponent { +} diff --git a/src/playground/playground-routing.module.ts b/src/playground/playground-routing.module.ts index 74674e82a8..71c8dd478e 100644 --- a/src/playground/playground-routing.module.ts +++ b/src/playground/playground-routing.module.ts @@ -103,6 +103,7 @@ import { NbAlertShowcaseComponent } from './alert/alert-showcase.component'; import { NbAlertColorsComponent } from './alert/alert-colors.component'; import { NbAlertAccentsComponent } from './alert/alert-accents.component'; import { NbAlertSizesComponent } from './alert/alert-sizes.component'; +import { NbAlertOutlineComponent } from './alert/alert-outline.component'; import { NbChatShowcaseComponent } from './chat/chat-showcase.component'; import { NbChatColorsComponent } from './chat/chat-colors.component'; import { NbChatSizesComponent } from './chat/chat-sizes.component'; @@ -359,6 +360,10 @@ export const routes: Routes = [ path: 'alert-sizes.component', component: NbAlertSizesComponent, }, + { + path: 'alert-outline.component', + component: NbAlertOutlineComponent, + }, ], }, { diff --git a/src/playground/playground.module.ts b/src/playground/playground.module.ts index 98d644265c..d99836d2b6 100644 --- a/src/playground/playground.module.ts +++ b/src/playground/playground.module.ts @@ -137,6 +137,7 @@ import { NbAlertColorsComponent } from './alert/alert-colors.component'; import { NbAlertAccentsComponent } from './alert/alert-accents.component'; import { NbAlertSizesComponent } from './alert/alert-sizes.component'; import { NbAlertTestComponent } from './alert/alert-test.component'; +import { NbAlertOutlineComponent } from './alert/alert-outline.component'; import { NbChatShowcaseComponent } from './chat/chat-showcase.component'; import { NbChatColorsComponent } from './chat/chat-colors.component'; import { NbChatSizesComponent } from './chat/chat-sizes.component'; @@ -294,6 +295,7 @@ export const NB_EXAMPLE_COMPONENTS = [ NbAlertAccentsComponent, NbAlertSizesComponent, NbAlertTestComponent, + NbAlertOutlineComponent, NbChatShowcaseComponent, NbChatColorsComponent, NbChatSizesComponent, From aed209916a3f67d4da4998a73ad5c7c2d8c613b9 Mon Sep 17 00:00:00 2001 From: Alain CHARLES <40032406+alain-charles@users.noreply.github.com> Date: Mon, 30 Jul 2018 17:05:30 +0200 Subject: [PATCH 3/5] feat(auth): add new NbAuthOAuth2JWTToken (#583) The token presumes that `access_token` is a JWT token itself. --- .../auth/services/token/token.spec.ts | 134 +++++++++++++++++- src/framework/auth/services/token/token.ts | 107 ++++++++++---- 2 files changed, 208 insertions(+), 33 deletions(-) diff --git a/src/framework/auth/services/token/token.spec.ts b/src/framework/auth/services/token/token.spec.ts index f88962cfcf..d6d0f449a2 100644 --- a/src/framework/auth/services/token/token.spec.ts +++ b/src/framework/auth/services/token/token.spec.ts @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ -import { NbAuthOAuth2Token, NbAuthJWTToken, NbAuthSimpleToken } from './token'; +import { NbAuthOAuth2Token, NbAuthJWTToken, NbAuthSimpleToken, NbAuthOAuth2JWTToken } from './token'; describe('auth token', () => { @@ -38,7 +38,7 @@ describe('auth token', () => { invalidJWTToken.getPayload(); }) .toThrow(new Error( - `The token ${invalidJWTToken.getValue()} is not valid JWT token and must consist of three parts.`)); + `The payload ${invalidJWTToken.getValue()} is not valid JWT payload and must consist of three parts.`)); }); it('getPayload, not valid JWT token, cannot be decoded', () => { @@ -46,7 +46,7 @@ describe('auth token', () => { emptyJWTToken.getPayload(); }) .toThrow(new Error( - `The token ${emptyJWTToken.getValue()} is not valid JWT token and cannot be decoded.`)); + `The payload ${emptyJWTToken.getValue()} is not valid JWT payload and cannot be decoded.`)); }); it('getPayload, not valid base64 in JWT token, cannot be decoded', () => { @@ -54,7 +54,23 @@ describe('auth token', () => { invalidBase64JWTToken.getPayload(); }) .toThrow(new Error( - `The token ${invalidBase64JWTToken.getValue()} is not valid JWT token and cannot be parsed.`)); + `The payload ${invalidBase64JWTToken.getValue()} is not valid JWT payload and cannot be parsed.`)); + }); + + it('getCreatedAt success : now for simpleToken', () => { + // we consider dates are the same if differing from minus than 10 ms + expect(simpleToken.getCreatedAt().getTime() - now.getTime() < 10); + }); + + it('getCreatedAt success : exp for validJWTToken', () => { + const date = new Date(); + date.setTime(1532350800000) + expect(validJWTToken.getCreatedAt()).toEqual(date); + }); + + it('getCreatedAt success : now for noIatJWTToken', () => { + // we consider dates are the same if differing from minus than 10 ms + expect(noIatJWTToken.getCreatedAt().getTime() - now.getTime() < 10); }); it('getCreatedAt success : now for simpleToken', () => { @@ -206,4 +222,114 @@ describe('auth token', () => { expect(NbAuthOAuth2Token.NAME).toEqual(validToken.getName()); }); }); + + describe('NbAuthOAuth2JWTToken', () => { + + const exp = 2532350800; + const iat = 1532350800; + const expires_in = 1000000000; + + const accessTokenPayload = { + 'iss': 'cerema.fr', + 'iat': 1532350800, + 'exp': 2532350800, + 'sub': 'Alain CHARLES', + 'admin': true, + }; + + const validPayload = { + // tslint:disable-next-line + access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJpYXQiOjE1MzIzNTA4MDAsImV4cCI6MjUzMjM1MDgwMCwic3ViIjoiQWxhaW4gQ0hBUkxFUyIsImFkbWluIjp0cnVlfQ.Rgkgb4KvxY2wp2niXIyLJNJeapFp9z3tCF-zK6Omc8c', + expires_in: 1000000000, + refresh_token: 'tGzv3JOkF0XG5Qx2TlKWIA', + token_type: 'bearer', + example_parameter: 'example_value', + }; + + const noExpButIatPayload = { + // tslint:disable-next-line + access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJpYXQiOjE1MzIzNTA4MDAsInN1YiI6IkFsYWluIENIQVJMRVMiLCJhZG1pbiI6dHJ1ZX0.heHVXkHexwqbPCPUAvkJlXO6tvxzxTKf4iP0OWBbp7Y', + expires_in: expires_in, + refresh_token: 'tGzv3JOkF0XG5Qx2TlKWIA', + token_type: 'bearer', + example_parameter: 'example_value', + }; + + const noExpNoIatPayload = { + // tslint:disable-next-line + access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJzdWIiOiJBbGFpbiBDSEFSTEVTIiwiYWRtaW4iOnRydWV9.LKZggkN-r_5hnEcCg5GzbSqZz5_SUHEB1Bf9Sy1qJd4', + expires_in: expires_in, + refresh_token: 'tGzv3JOkF0XG5Qx2TlKWIA', + token_type: 'bearer', + example_parameter: 'example_value', + }; + + const permanentPayload = { + // tslint:disable-next-line + access_token: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJjZXJlbWEuZnIiLCJzdWIiOiJBbGFpbiBDSEFSTEVTIiwiYWRtaW4iOnRydWV9.LKZggkN-r_5hnEcCg5GzbSqZz5_SUHEB1Bf9Sy1qJd4', + token_type: 'bearer', + example_parameter: 'example_value', + }; + + const validToken = new NbAuthOAuth2JWTToken(validPayload, 'strategy'); + let noExpButIatToken = new NbAuthOAuth2JWTToken(noExpButIatPayload, 'strategy'); + const emptyToken = new NbAuthOAuth2JWTToken({}, 'strategy'); + const permanentToken = new NbAuthOAuth2JWTToken(permanentPayload, 'strategy'); + + it('getPayload success', () => { + expect(validToken.getPayload()).toEqual(validPayload); + }); + + it('getAccessTokenPayload success', () => { + expect(validToken.getAccessTokenPayload()).toEqual(accessTokenPayload); + }); + + it('getPayload, not valid token, cannot be decoded', () => { + expect(() => { + emptyToken.getPayload(); + }) + .toThrow(new Error( + `Cannot extract payload from an empty token.`)); + }); + + it('getCreatedAt success for valid token', () => { + const date = new Date(0); + date.setUTCSeconds(iat); + expect(validToken.getCreatedAt()).toEqual(date); + }); + + it('getCreatedAt success for no iat token', () => { + noExpButIatToken = new NbAuthOAuth2JWTToken(noExpButIatPayload, 'strategy'); + const date = new Date(); + expect(noExpButIatToken.getTokenExpDate().getTime() - date.getTime() < 10); + }); + + it('getExpDate success when exp is set', () => { + const date = new Date(0); + date.setUTCSeconds(exp); + expect(validToken.getTokenExpDate()).toEqual(date); + }); + + it('getExpDate success when exp is not set but iat and expires_in are set', () => { + const date = new Date(0); + date.setUTCSeconds(iat + expires_in); + expect(noExpButIatToken.getTokenExpDate()).toEqual(date); + }); + + it('getExpDate success when only expires_in is set', () => { + const NoExpNoIatToken = new NbAuthOAuth2JWTToken(noExpNoIatPayload, 'strategy'); + const date = new Date(); + date.setTime(date.getTime() + expires_in * 1000); + expect(NoExpNoIatToken.getTokenExpDate().getTime() - date.getTime() < 10); + }); + + it('getTokenExpDate is empty', () => { + expect(permanentToken.getTokenExpDate()).toBeNull(); + }); + + it('name', () => { + expect(NbAuthOAuth2JWTToken.NAME).toEqual(validToken.getName()); + }); + }); + }); diff --git a/src/framework/auth/services/token/token.ts b/src/framework/auth/services/token/token.ts index ea02db6e3b..94431826c6 100644 --- a/src/framework/auth/services/token/token.ts +++ b/src/framework/auth/services/token/token.ts @@ -30,6 +30,32 @@ export function nbAuthCreateToken(tokenClass: NbAuthTokenClass, return new tokenClass(token, ownerStrategyName, createdAt); } +export function decodeJwtPayload(payload: string): string { + + if (!payload) { + throw new Error('Cannot extract payload from an empty token.'); + } + + const parts = payload.split('.'); + + if (parts.length !== 3) { + throw new Error(`The payload ${payload} is not valid JWT payload and must consist of three parts.`); + } + + let decoded; + try { + decoded = urlBase64Decode(parts[1]); + } catch (e) { + throw new Error(`The payload ${payload} is not valid JWT payload and cannot be parsed.`); + } + + if (!decoded) { + throw new Error(`The payload ${payload} is not valid JWT payload and cannot be decoded.`); + } + + return JSON.parse(decoded); +} + /** * Wrapper for simple (text) token */ @@ -45,7 +71,6 @@ export class NbAuthSimpleToken extends NbAuthToken { } protected prepareCreatedAt(date: Date) { - // For simple tokens, if not set the creation date is 'now' return date ? date : new Date(); } @@ -101,13 +126,12 @@ export class NbAuthJWTToken extends NbAuthSimpleToken { * for JWT token, the iat (issued at) field of the token payload contains the creation Date */ protected prepareCreatedAt(date: Date) { - date = super.prepareCreatedAt(date); - let decoded = null; - try { // needed as getPayload() throws error and we want the token to be created in any case + let decoded; + try { decoded = this.getPayload(); } finally { - return decoded && decoded.iat ? new Date(Number(decoded.iat) * 1000) : date; + return decoded && decoded.iat ? new Date(Number(decoded.iat) * 1000) : super.prepareCreatedAt(date); } } @@ -116,29 +140,7 @@ export class NbAuthJWTToken extends NbAuthSimpleToken { * @returns any */ getPayload(): any { - - if (!this.token) { - throw new Error('Cannot extract payload from an empty token.'); - } - - const parts = this.token.split('.'); - - if (parts.length !== 3) { - throw new Error(`The token ${this.token} is not valid JWT token and must consist of three parts.`); - } - - let decoded; - try { - decoded = urlBase64Decode(parts[1]); - } catch (e) { - throw new Error(`The token ${this.token} is not valid JWT token and cannot be parsed.`); - } - - if (!decoded) { - throw new Error(`The token ${this.token} is not valid JWT token and cannot be decoded.`); - } - - return JSON.parse(decoded); + return decodeJwtPayload(this.token); } /** @@ -174,7 +176,7 @@ const prepareOAuth2Token = (data) => { }; /** - * Wrapper for OAuth2 token + * Wrapper for OAuth2 token whose access_token is a JWT Token */ export class NbAuthOAuth2Token extends NbAuthSimpleToken { @@ -251,3 +253,50 @@ export class NbAuthOAuth2Token extends NbAuthSimpleToken { return JSON.stringify(this.token); } } + +/** + * Wrapper for OAuth2 token + */ +export class NbAuthOAuth2JWTToken extends NbAuthOAuth2Token { + + static NAME = 'nb:auth:oauth2:jwt:token'; + + /** + * for Oauth2 JWT token, the iat (issued at) field of the access_token payload + */ + protected prepareCreatedAt(date: Date) { + let decoded; + try { + decoded = this.getAccessTokenPayload(); + } + finally { + return decoded && decoded.iat ? new Date(Number(decoded.iat) * 1000) : super.prepareCreatedAt(date); + } + } + + + /** + * Returns access token payload + * @returns any + */ + getAccessTokenPayload(): any { + return decodeJwtPayload(this.getValue()) + } + + /** + * Returns expiration date : + * - exp if set, + * - super.getExpDate() otherwise + * @returns Date + */ + getTokenExpDate(): Date { + const accessTokenPayload = this.getAccessTokenPayload(); + if (accessTokenPayload.hasOwnProperty('exp')) { + const date = new Date(0); + date.setUTCSeconds(accessTokenPayload.exp); + return date; + } else { + return super.getTokenExpDate(); + } + } +} From 13014d40fd8cacaf7fdb6037a6abb52b1f5ec156 Mon Sep 17 00:00:00 2001 From: Dmitry Nehaychik <4dmitr@gmail.com> Date: Mon, 30 Jul 2018 18:44:02 +0300 Subject: [PATCH 4/5] feat(button): add `fullWidth` mode (#591) --- .../components/button/button.component.scss | 4 ++++ .../components/button/button.component.ts | 15 ++++++++++++++ .../theme/components/button/button.spec.ts | 9 +++++++++ .../button/button-full-width.component.html | 10 ++++++++++ .../button/button-full-width.component.ts | 20 +++++++++++++++++++ src/playground/playground-routing.module.ts | 5 +++++ src/playground/playground.module.ts | 2 ++ 7 files changed, 65 insertions(+) create mode 100644 src/playground/button/button-full-width.component.html create mode 100644 src/playground/button/button-full-width.component.ts diff --git a/src/framework/theme/components/button/button.component.scss b/src/framework/theme/components/button/button.component.scss index 699110fdbe..cbbae4913f 100644 --- a/src/framework/theme/components/button/button.component.scss +++ b/src/framework/theme/components/button/button.component.scss @@ -22,4 +22,8 @@ &:hover, &:focus { text-decoration: none; } + + &.btn-full-width { + width: 100%; + } } diff --git a/src/framework/theme/components/button/button.component.ts b/src/framework/theme/components/button/button.component.ts index 70a4f889fc..e987fdeab1 100644 --- a/src/framework/theme/components/button/button.component.ts +++ b/src/framework/theme/components/button/button.component.ts @@ -39,6 +39,9 @@ import { convertToBoolProperty } from '../helpers'; * and `a`: * @stacked-example(Button Elements, button/button-types.component.html) * + * Button can be made `fullWidth`: + * @stacked-example(Full Width Button, button/button-full-width.component.html) + * * @styles * * btn-fg: @@ -192,6 +195,9 @@ export class NbButtonComponent { return this.disabled ? '-1' : '0'; } + @HostBinding('class.btn-full-width') + fullWidth = false; + /** * Button size, available sizes: * `xxsmall`, `xsmall`, `small`, `medium`, `large` @@ -239,6 +245,15 @@ export class NbButtonComponent { this.disabled = convertToBoolProperty(val); } + /** + * If set element will fill its container + * @param {boolean} + */ + @Input('fullWidth') + set setFullWidth(value) { + this.fullWidth = convertToBoolProperty(value); + } + /** * Adds `outline` styles * @param {boolean} val diff --git a/src/framework/theme/components/button/button.spec.ts b/src/framework/theme/components/button/button.spec.ts index 9acd3dd047..2dee24004f 100644 --- a/src/framework/theme/components/button/button.spec.ts +++ b/src/framework/theme/components/button/button.spec.ts @@ -65,4 +65,13 @@ describe('Component: NbButton', () => { .debugElement.nativeElement.classList.contains('btn-semi-round')) .toBeTruthy() }); + + it('should set full-width class', () => { + button.fullWidth = true; + fixture.detectChanges(); + expect( + fixture + .debugElement.nativeElement.classList.contains('btn-full-width')) + .toBeTruthy() + }); }); diff --git a/src/playground/button/button-full-width.component.html b/src/playground/button/button-full-width.component.html new file mode 100644 index 0000000000..72f01f1df7 --- /dev/null +++ b/src/playground/button/button-full-width.component.html @@ -0,0 +1,10 @@ + + Button Full Width + + + + + + + + diff --git a/src/playground/button/button-full-width.component.ts b/src/playground/button/button-full-width.component.ts new file mode 100644 index 0000000000..4c99246336 --- /dev/null +++ b/src/playground/button/button-full-width.component.ts @@ -0,0 +1,20 @@ +/** + * @license + * Copyright Akveo. All Rights Reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { ChangeDetectionStrategy, Component } from '@angular/core'; + +@Component({ + selector: 'nb-button-full-width', + changeDetection: ChangeDetectionStrategy.OnPush, + templateUrl: './button-full-width.component.html', + styles: [` + [nbButton] { + margin-bottom: 1rem; + } + `], +}) +export class NbButtonFullWidthComponent { +} diff --git a/src/playground/playground-routing.module.ts b/src/playground/playground-routing.module.ts index 71c8dd478e..911b568d0d 100644 --- a/src/playground/playground-routing.module.ts +++ b/src/playground/playground-routing.module.ts @@ -133,6 +133,7 @@ import { NbButtonHeroComponent } from './button/button-hero.component'; import { NbButtonOutlineComponent } from './button/button-outline.component'; import { NbButtonSizesComponent } from './button/button-sizes.component'; import { NbButtonTypesComponent } from './button/button-types.component'; +import { NbButtonFullWidthComponent } from './button/button-full-width.component'; import { NbInputsShowcaseComponent } from './input/input-showcase.component'; import { NbInputColorsComponent } from './input/input-colors.component'; import { NbInputSizesComponent } from './input/input-sizes.component'; @@ -206,6 +207,10 @@ export const routes: Routes = [ path: 'button-types.component', component: NbButtonTypesComponent, }, + { + path: 'button-full-width.component', + component: NbButtonFullWidthComponent, + }, ], }, { diff --git a/src/playground/playground.module.ts b/src/playground/playground.module.ts index d99836d2b6..4d22a96760 100644 --- a/src/playground/playground.module.ts +++ b/src/playground/playground.module.ts @@ -163,6 +163,7 @@ import { NbButtonHeroComponent } from './button/button-hero.component'; import { NbButtonOutlineComponent } from './button/button-outline.component'; import { NbButtonSizesComponent } from './button/button-sizes.component'; import { NbButtonTypesComponent } from './button/button-types.component'; +import { NbButtonFullWidthComponent } from './button/button-full-width.component'; import { NbInputsShowcaseComponent } from './input/input-showcase.component'; import { NbInputColorsComponent } from './input/input-colors.component'; import { NbInputSizesComponent } from './input/input-sizes.component'; @@ -323,6 +324,7 @@ export const NB_EXAMPLE_COMPONENTS = [ NbButtonOutlineComponent, NbButtonSizesComponent, NbButtonTypesComponent, + NbButtonFullWidthComponent, NbInputsShowcaseComponent, NbInputColorsComponent, NbInputSizesComponent, From 4360a189c3c3174052952bc0bd4eb920b6bc9641 Mon Sep 17 00:00:00 2001 From: Alain CHARLES <40032406+alain-charles@users.noreply.github.com> Date: Mon, 30 Jul 2018 18:32:48 +0200 Subject: [PATCH 5/5] feat(auth): NbOAuth2AuthStrategy add basic authentication scheme against token endpoints (#582) #### What it resolves NbOAuth2Strategy now implements client authentication as specified in [RFC 6749 section 2-3](https://tools.ietf.org/html/rfc6749#section-2.3) There is a new optional parameter of `NbOAuth2StrategyOption`. The parameter is `clientAuthMethod`, and is a member of `NbOAuth2ClientAuthMethod ` enum: - `NONE` (default) : no credentials are sent => No breaking change, - `BASIC` : credentials are sent in the authorization header - `REQUEST_BODY`: credentials are sent in the request body AuthMethod is used (credentials are sent) when accessing to the authServer for : - Getting token with `authorization_code` grant_type - Getting token with `password` grant-type - Getting token with `refresh_token` grant-type RFC6749 says the client must not authenticate when hitting authorize endpoints, even if asking for a token. So nothing changed here, only clientId is sent in the url. Closes #581 --- .../oauth2/oauth2-strategy.options.ts | 9 +- .../strategies/oauth2/oauth2-strategy.spec.ts | 344 ++++++++++++++---- .../auth/strategies/oauth2/oauth2-strategy.ts | 71 +++- .../oauth2-password/oauth2-password.module.ts | 7 +- 4 files changed, 351 insertions(+), 80 deletions(-) diff --git a/src/framework/auth/strategies/oauth2/oauth2-strategy.options.ts b/src/framework/auth/strategies/oauth2/oauth2-strategy.options.ts index fbe14bfe7f..b7c2c9813f 100644 --- a/src/framework/auth/strategies/oauth2/oauth2-strategy.options.ts +++ b/src/framework/auth/strategies/oauth2/oauth2-strategy.options.ts @@ -18,11 +18,18 @@ export enum NbOAuth2GrantType { REFRESH_TOKEN = 'refresh_token', } +export enum NbOAuth2ClientAuthMethod { + NONE = 'none', + BASIC = 'basic', + REQUEST_BODY = 'request-body', +} + export class NbOAuth2AuthStrategyOptions { name: string; baseEndpoint?: string = ''; clientId: string = ''; - clientSecret: string = ''; + clientSecret?: string = ''; + clientAuthMethod?: string = NbOAuth2ClientAuthMethod.NONE; redirect?: { success?: string; failure?: string } = { success: '/', failure: null, diff --git a/src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts b/src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts index deba650250..a48186ad07 100644 --- a/src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts +++ b/src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts @@ -9,9 +9,8 @@ import { HttpClientTestingModule, HttpTestingController } from '@angular/common/ import { RouterTestingModule } from '@angular/router/testing'; import { ActivatedRoute } from '@angular/router'; import { NB_WINDOW } from '@nebular/theme'; - import { NbOAuth2AuthStrategy } from './oauth2-strategy'; -import { NbOAuth2GrantType, NbOAuth2ResponseType } from './oauth2-strategy.options'; +import { NbOAuth2ClientAuthMethod, NbOAuth2GrantType, NbOAuth2ResponseType } from './oauth2-strategy.options'; import { NbAuthResult, nbAuthCreateToken, NbAuthOAuth2Token } from '../../services'; function createURL(params: any) { @@ -29,6 +28,7 @@ describe('oauth2-auth-strategy', () => { const successMessages = ['You have been successfully authenticated.']; const errorMessages = ['Something went wrong, please try again.']; + const authHeader = 'Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0'; const tokenSuccessResponse = { access_token: '2YotnFZFEjr1zCsicMWpAA', @@ -76,13 +76,14 @@ describe('oauth2-auth-strategy', () => { describe('out of the box: type CODE', () => { + const basicOptions = { + name: 'strategy', + baseEndpoint: 'http://example.com/', + clientId: 'clientId', + } + beforeEach(() => { - strategy.setOptions({ - name: 'strategy', - baseEndpoint: 'http://example.com/', - clientId: 'clientId', - clientSecret: 'clientSecret', - }); + strategy.setOptions(basicOptions); }); it('redirect to auth server', (done: DoneFn) => { @@ -160,9 +161,65 @@ describe('oauth2-auth-strategy', () => { .flush(tokenErrorResponse, { status: 400, statusText: 'Bad Request' }); }); - it('handle refresh token', (done: DoneFn) => { + it('handle refresh token with basic client auth', (done: DoneFn) => { + strategy.setOptions({ + ... basicOptions, + clientSecret: 'clientSecret', + clientAuthMethod: NbOAuth2ClientAuthMethod.BASIC, + }); + strategy.refreshToken(successToken) + .subscribe((result: NbAuthResult) => { + expect(result).toBeTruthy(); + expect(result.isSuccess()).toBe(true); + expect(result.isFailure()).toBe(false); + expect(result.getToken().getValue()).toEqual(successToken.getValue()); + expect(result.getToken().getOwnerStrategyName()).toEqual(successToken.getOwnerStrategyName()); + expect(result.getMessages()).toEqual(successMessages); + expect(result.getErrors()).toEqual([]); // no error message, response is success + expect(result.getRedirect()).toEqual('/'); + done(); + }); + httpMock.expectOne( + req => req.url === 'http://example.com/token' + && req.headers.get('Authorization') === authHeader + && req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN + && req.body['refresh_token'] === successToken.getRefreshToken() + && !req.body['scope'], + ).flush(tokenSuccessResponse); + }); + + it('handle refresh token with requestBody client auth', (done: DoneFn) => { + strategy.setOptions({ + ... basicOptions, + clientSecret: 'clientSecret', + clientAuthMethod: NbOAuth2ClientAuthMethod.REQUEST_BODY, + }); + strategy.refreshToken(successToken) + .subscribe((result: NbAuthResult) => { + expect(result).toBeTruthy(); + expect(result.isSuccess()).toBe(true); + expect(result.isFailure()).toBe(false); + expect(result.getToken().getValue()).toEqual(successToken.getValue()); + expect(result.getToken().getOwnerStrategyName()).toEqual(successToken.getOwnerStrategyName()); + expect(result.getMessages()).toEqual(successMessages); + expect(result.getErrors()).toEqual([]); // no error message, response is success + expect(result.getRedirect()).toEqual('/'); + done(); + }); + + httpMock.expectOne( + req => req.url === 'http://example.com/token' + && req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN + && req.body['refresh_token'] === successToken.getRefreshToken() + && req.body['client_id'] === strategy.getOption('clientId') + && req.body['client_secret'] === strategy.getOption('clientSecret') + && !req.body['scope'], + ).flush(tokenSuccessResponse); + }); + it('handle refresh token with NO client auth', (done: DoneFn) => { + strategy.setOptions(basicOptions); strategy.refreshToken(successToken) .subscribe((result: NbAuthResult) => { expect(result).toBeTruthy(); @@ -206,16 +263,17 @@ describe('oauth2-auth-strategy', () => { describe('configured: type TOKEN', () => { + const basicOptions = { + name: 'strategy', + baseEndpoint: 'http://example.com/', + clientId: 'clientId', + authorize: { + responseType: NbOAuth2ResponseType.TOKEN, + }, + } + beforeEach(() => { - strategy.setOptions({ - name: 'strategy', - baseEndpoint: 'http://example.com/', - clientId: 'clientId', - clientSecret: 'clientSecret', - authorize: { - responseType: NbOAuth2ResponseType.TOKEN, - }, - }); + strategy.setOptions(basicOptions); }); it('redirect to auth server', (done: DoneFn) => { @@ -269,34 +327,35 @@ describe('oauth2-auth-strategy', () => { describe('configured redirect, redirectUri, scope and additional params: type TOKEN', () => { - beforeEach(() => { - strategy.setOptions({ - name: 'strategy', - baseEndpoint: 'http://example.com/', - clientId: 'clientId', - clientSecret: 'clientSecret', - redirect: { - success: '/success', - failure: '/failure', - }, - authorize: { - endpoint: 'custom', - redirectUri: 'http://localhost:4200/callback', - scope: 'read', - params: { - display: 'popup', - foo: 'bar', - }, - }, - token: { - endpoint: 'custom', - redirectUri: 'http://localhost:4200/callback', + const basicOptions = { + name: 'strategy', + baseEndpoint: 'http://example.com/', + clientId: 'clientId', + redirect: { + success: '/success', + failure: '/failure', + }, + authorize: { + endpoint: 'custom', + redirectUri: 'http://localhost:4200/callback', + scope: 'read', + params: { + display: 'popup', + foo: 'bar', }, - refresh: { - endpoint: 'custom', - scope: 'read', - }, - }); + }, + token: { + endpoint: 'custom', + redirectUri: 'http://localhost:4200/callback', + }, + refresh: { + endpoint: 'custom', + scope: 'read', + }, + } + + beforeEach(() => { + strategy.setOptions(basicOptions); }); it('redirect to auth server', (done: DoneFn) => { @@ -315,7 +374,7 @@ describe('oauth2-auth-strategy', () => { .subscribe(() => {}); }); - it('handle success redirect and sends correct token request', (done: DoneFn) => { + it('handle success redirect and sends correct token request with NO client Auth', (done: DoneFn) => { routeMock.snapshot.queryParams = { code: 'code' }; strategy.authenticate() @@ -340,8 +399,13 @@ describe('oauth2-auth-strategy', () => { ).flush(tokenSuccessResponse); }); - it('handle success redirect back with token request', (done: DoneFn) => { + it('handle success redirect and sends correct token request with BASIC client Auth', (done: DoneFn) => { routeMock.snapshot.queryParams = { code: 'code' }; + strategy.setOptions({ + ... basicOptions, + clientSecret: 'clientSecret', + clientAuthMethod: NbOAuth2ClientAuthMethod.BASIC, + }) strategy.authenticate() .subscribe((result: NbAuthResult) => { @@ -356,8 +420,45 @@ describe('oauth2-auth-strategy', () => { done(); }); - httpMock.expectOne('http://example.com/custom') - .flush(tokenSuccessResponse); + httpMock.expectOne( + req => req.url === 'http://example.com/custom' + && req.headers.get('Authorization') === authHeader + && req.body['grant_type'] === NbOAuth2GrantType.AUTHORIZATION_CODE + && req.body['code'] === 'code' + && req.body['client_id'] === 'clientId' + && req.body['redirect_uri'] === 'http://localhost:4200/callback', + ).flush(tokenSuccessResponse); + }); + + it('handle success redirect and sends correct token request with REQUEST_BODY client Auth', (done: DoneFn) => { + routeMock.snapshot.queryParams = { code: 'code' }; + strategy.setOptions({ + ... basicOptions, + clientSecret: 'clientSecret', + clientAuthMethod: NbOAuth2ClientAuthMethod.REQUEST_BODY, + }) + + strategy.authenticate() + .subscribe((result: NbAuthResult) => { + expect(result).toBeTruthy(); + expect(result.isSuccess()).toBe(true); + expect(result.isFailure()).toBe(false); + expect(result.getToken().getValue()).toEqual(successToken.getValue()); + expect(result.getToken().getOwnerStrategyName()).toEqual(successToken.getOwnerStrategyName()); + expect(result.getMessages()).toEqual(successMessages); + expect(result.getErrors()).toEqual([]); // no error message, response is success + expect(result.getRedirect()).toEqual('/success'); + done(); + }); + + httpMock.expectOne( + req => req.url === 'http://example.com/custom' + && req.body['grant_type'] === NbOAuth2GrantType.AUTHORIZATION_CODE + && req.body['code'] === 'code' + && req.body['client_id'] === strategy.getOption('clientId') + && req.body['client_secret'] === strategy.getOption('clientSecret') + && req.body['redirect_uri'] === 'http://localhost:4200/callback', + ).flush(tokenSuccessResponse); }); it('handle error redirect back', (done: DoneFn) => { @@ -376,8 +477,36 @@ describe('oauth2-auth-strategy', () => { done(); }); }); + it('handle refresh token with NO client auth', (done: DoneFn) => { + strategy.setOptions(basicOptions); - it('handle refresh token', (done: DoneFn) => { + strategy.refreshToken(successToken) + .subscribe((result: NbAuthResult) => { + expect(result).toBeTruthy(); + expect(result.isSuccess()).toBe(true); + expect(result.isFailure()).toBe(false); + expect(result.getToken().getValue()).toEqual(successToken.getValue()); + expect(result.getToken().getOwnerStrategyName()).toEqual(successToken.getOwnerStrategyName()); + expect(result.getMessages()).toEqual(successMessages); + expect(result.getErrors()).toEqual([]); // no error message, response is success + expect(result.getRedirect()).toEqual('/success'); + done(); + }); + + httpMock.expectOne( + req => req.url === 'http://example.com/custom' + && req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN + && req.body['refresh_token'] === successToken.getRefreshToken() + && req.body['scope'] === 'read', + ).flush(tokenSuccessResponse); + }); + + it('handle refresh token with BASIC client auth', (done: DoneFn) => { + strategy.setOptions({ + ... basicOptions, + clientSecret: 'clientSecret', + clientAuthMethod: NbOAuth2ClientAuthMethod.BASIC, + }); strategy.refreshToken(successToken) .subscribe((result: NbAuthResult) => { @@ -394,12 +523,43 @@ describe('oauth2-auth-strategy', () => { httpMock.expectOne( req => req.url === 'http://example.com/custom' + && req.headers.get('Authorization') === authHeader && req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN && req.body['refresh_token'] === successToken.getRefreshToken() && req.body['scope'] === 'read', ).flush(tokenSuccessResponse); }); + it('handle refresh token with REQUEST_BODY client auth', (done: DoneFn) => { + strategy.setOptions({ + ... basicOptions, + clientSecret: 'clientSecret', + clientAuthMethod: NbOAuth2ClientAuthMethod.REQUEST_BODY, + }); + + strategy.refreshToken(successToken) + .subscribe((result: NbAuthResult) => { + expect(result).toBeTruthy(); + expect(result.isSuccess()).toBe(true); + expect(result.isFailure()).toBe(false); + expect(result.getToken().getValue()).toEqual(successToken.getValue()); + expect(result.getToken().getOwnerStrategyName()).toEqual(successToken.getOwnerStrategyName()); + expect(result.getMessages()).toEqual(successMessages); + expect(result.getErrors()).toEqual([]); // no error message, response is success + expect(result.getRedirect()).toEqual('/success'); + done(); + }); + + httpMock.expectOne( + req => req.url === 'http://example.com/custom' + && req.body['grant_type'] === NbOAuth2GrantType.REFRESH_TOKEN + && req.body['refresh_token'] === successToken.getRefreshToken() + && req.body['client_id'] === strategy.getOption('clientId') + && req.body['client_secret'] === strategy.getOption('clientSecret') + && req.body['scope'] === 'read', + ).flush(tokenSuccessResponse); + }); + it('handle error token response', (done: DoneFn) => { routeMock.snapshot.queryParams = { code: 'code' }; @@ -424,22 +584,51 @@ describe('oauth2-auth-strategy', () => { describe('configured: additionnal param: token, grant_type:password', () => { + const basicOptions = { + name: 'strategy', + baseEndpoint: 'http://example.com/', + clientId: 'clientId', + token: { + grantType: NbOAuth2GrantType.PASSWORD, + endpoint: 'token', + }, + } + beforeEach(() => { - strategy.setOptions({ - name: 'strategy', - baseEndpoint: 'http://example.com/', - clientId: 'clientId', - clientSecret: 'clientSecret', - token: { - grantType: NbOAuth2GrantType.PASSWORD, - endpoint: 'token', - }, - }); + strategy.setOptions(basicOptions); }); - it('handle success login', (done: DoneFn) => { + it('handle success login with NO client auth', (done: DoneFn) => { const credentials = { email: 'example@akveo.com', password: '123456' }; + strategy.authenticate(credentials) + .subscribe((result: NbAuthResult) => { + expect(result).toBeTruthy(); + expect(result.isSuccess()).toBe(true); + expect(result.isFailure()).toBe(false); + expect(result.getToken().getValue()).toEqual(successToken.getValue()); + expect(result.getToken().getOwnerStrategyName()).toEqual(successToken.getOwnerStrategyName()); + expect(result.getMessages()).toEqual(successMessages); + expect(result.getErrors()).toEqual([]); // no error message, response is success + expect(result.getRedirect()).toEqual('/'); + done(); + }); + + httpMock.expectOne( + req => req.url === 'http://example.com/token' + && req.body['grant_type'] === NbOAuth2GrantType.PASSWORD + && req.body['email'] === credentials.email + && req.body['password'] === credentials.password, + ).flush(tokenSuccessResponse); + }); + + it('handle success login with BASIC client auth', (done: DoneFn) => { + const credentials = { email: 'example@akveo.com', password: '123456' }; + strategy.setOptions({ + ... basicOptions, + clientSecret: 'clientSecret', + clientAuthMethod: NbOAuth2ClientAuthMethod.BASIC, + }) strategy.authenticate(credentials) .subscribe((result: NbAuthResult) => { @@ -456,13 +645,46 @@ describe('oauth2-auth-strategy', () => { httpMock.expectOne( req => req.url === 'http://example.com/token' + && req.headers.get('Authorization') === authHeader && req.body['grant_type'] === NbOAuth2GrantType.PASSWORD && req.body['email'] === credentials.email && req.body['password'] === credentials.password, ).flush(tokenSuccessResponse); }); + it('handle success login with REQUEST_BODY client auth', (done: DoneFn) => { + const credentials = { email: 'example@akveo.com', password: '123456' }; + strategy.setOptions({ + ... basicOptions, + clientSecret: 'clientSecret', + clientAuthMethod: NbOAuth2ClientAuthMethod.REQUEST_BODY, + }) + + strategy.authenticate(credentials) + .subscribe((result: NbAuthResult) => { + expect(result).toBeTruthy(); + expect(result.isSuccess()).toBe(true); + expect(result.isFailure()).toBe(false); + expect(result.getToken().getValue()).toEqual(successToken.getValue()); + expect(result.getToken().getOwnerStrategyName()).toEqual(successToken.getOwnerStrategyName()); + expect(result.getMessages()).toEqual(successMessages); + expect(result.getErrors()).toEqual([]); // no error message, response is success + expect(result.getRedirect()).toEqual('/'); + done(); + }); + + httpMock.expectOne( + req => req.url === 'http://example.com/token' + && req.body['grant_type'] === NbOAuth2GrantType.PASSWORD + && req.body['email'] === credentials.email + && req.body['password'] === credentials.password + && req.body['client_id'] === strategy.getOption('clientId') + && req.body['client_secret'] === strategy.getOption('clientSecret'), + ).flush(tokenSuccessResponse); + }); + it('handle error login', (done: DoneFn) => { + strategy.setOptions(basicOptions); const credentials = { email: 'example@akveo.com', password: '123456' }; strategy.authenticate(credentials) diff --git a/src/framework/auth/strategies/oauth2/oauth2-strategy.ts b/src/framework/auth/strategies/oauth2/oauth2-strategy.ts index 5e2062fd61..51916c40e4 100644 --- a/src/framework/auth/strategies/oauth2/oauth2-strategy.ts +++ b/src/framework/auth/strategies/oauth2/oauth2-strategy.ts @@ -4,7 +4,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. */ import { Inject, Injectable } from '@angular/core'; -import { HttpClient, HttpErrorResponse } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; import { ActivatedRoute } from '@angular/router'; import { Observable, of as observableOf } from 'rxjs'; import { switchMap, map, catchError } from 'rxjs/operators'; @@ -12,10 +12,12 @@ import { NB_WINDOW } from '@nebular/theme'; import { NbAuthStrategy } from '../auth-strategy'; import { NbAuthRefreshableToken, NbAuthResult } from '../../services/'; -import { NbOAuth2AuthStrategyOptions, - NbOAuth2ResponseType, - auth2StrategyOptions, - NbOAuth2GrantType } from './oauth2-strategy.options'; +import { + NbOAuth2AuthStrategyOptions, + NbOAuth2ResponseType, + auth2StrategyOptions, + NbOAuth2GrantType, NbOAuth2ClientAuthMethod, +} from './oauth2-strategy.options'; import { NbAuthStrategyClass } from '../../auth.options'; @@ -41,6 +43,7 @@ import { NbAuthStrategyClass } from '../../auth.options'; * baseEndpoint?: string = ''; * clientId: string = ''; * clientSecret: string = ''; + * clientAuthMethod: string = NbOAuth2ClientAuthMethod.NONE; * redirect?: { success?: string; failure?: string } = { * success: '/', * failure: null, @@ -91,10 +94,8 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { return this.getOption('authorize.responseType'); } - protected cleanParams(params: any): any { - Object.entries(params) - .forEach(([key, val]) => !val && delete params[key]); - return params; + get clientAuthMethod() { + return this.getOption('clientAuthMethod'); } protected redirectResultHandlers = { @@ -195,7 +196,7 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { refreshToken(token: NbAuthRefreshableToken): Observable { const url = this.getActionEndpoint('refresh'); - return this.http.post(url, this.buildRefreshRequestData(token)) + return this.http.post(url, this.buildRefreshRequestData(token), this.buildAuthHeader()) .pipe( map((res) => { return new NbAuthResult( @@ -213,7 +214,7 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { passwordToken(email: string, password: string): Observable { const url = this.getActionEndpoint('token'); - return this.http.post(url, this.buildPasswordRequestData(email, password)) + return this.http.post(url, this.buildPasswordRequestData(email, password), this.buildAuthHeader() ) .pipe( map((res) => { return new NbAuthResult( @@ -239,7 +240,8 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { protected requestToken(code: string) { const url = this.getActionEndpoint('token'); - return this.http.post(url, this.buildCodeRequestData(code)) + return this.http.post(url, this.buildCodeRequestData(code), + this.buildAuthHeader()) .pipe( map((res) => { return new NbAuthResult( @@ -261,7 +263,7 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { redirect_uri: this.getOption('token.redirectUri'), client_id: this.getOption('clientId'), }; - return this.cleanParams(params); + return this.cleanParams(this.addCredentialsToParams(params)); } protected buildRefreshRequestData(token: NbAuthRefreshableToken): any { @@ -270,7 +272,7 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { refresh_token: token.getRefreshToken(), scope: this.getOption('refresh.scope'), }; - return this.cleanParams(params); + return this.cleanParams(this.addCredentialsToParams(params)); } protected buildPasswordRequestData(email: string, password: string ): any { @@ -279,9 +281,48 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy { email: email, password: password, }; - return this.cleanParams(params); + return this.cleanParams(this.addCredentialsToParams(params)); + } + + protected buildAuthHeader(): any { + if (this.clientAuthMethod === NbOAuth2ClientAuthMethod.BASIC) { + if (this.getOption('clientId') && this.getOption('clientSecret')) { + return { + headers: new HttpHeaders( + { + 'Authorization': 'Basic ' + btoa( + this.getOption('clientId') + ':' + this.getOption('clientSecret')), + }, + ), + }; + } else { + throw Error('For basic client authentication method, please provide both clientId & clientSecret.'); + } + } + } + + protected cleanParams(params: any): any { + Object.entries(params) + .forEach(([key, val]) => !val && delete params[key]); + return params; } + protected addCredentialsToParams(params: any): any { + if (this.clientAuthMethod === NbOAuth2ClientAuthMethod.REQUEST_BODY) { + if (this.getOption('clientId') && this.getOption('clientSecret')) { + return { + ... params, + client_id: this.getOption('clientId'), + client_secret: this.getOption('clientSecret'), + } + } else { + throw Error('For request body client authentication method, please provide both clientId & clientSecret.') + } + } + return params; + } + + protected handleResponseError(res: any): Observable { let errors = []; if (res instanceof HttpErrorResponse) { diff --git a/src/playground/oauth2-password/oauth2-password.module.ts b/src/playground/oauth2-password/oauth2-password.module.ts index 00f2f12285..c650f75d98 100644 --- a/src/playground/oauth2-password/oauth2-password.module.ts +++ b/src/playground/oauth2-password/oauth2-password.module.ts @@ -17,7 +17,7 @@ import { import { NbAuthModule, - NbOAuth2AuthStrategy, NbOAuth2GrantType, + NbOAuth2AuthStrategy, NbOAuth2ClientAuthMethod, NbOAuth2GrantType, } from '@nebular/auth'; import { NbOAuth2PasswordLoginComponent } from './oauth2-password-login.component'; @@ -52,8 +52,9 @@ import { NbAuthOAuth2Token } from '@nebular/auth'; strategies: [ NbOAuth2AuthStrategy.setup({ name: 'password', - clientId: 'test', - clientSecret: 'secret', + clientId: 'Aladdin', + clientSecret: 'open sesame', + clientAuthMethod: NbOAuth2ClientAuthMethod.BASIC, baseEndpoint: 'http://localhost:4400/api/auth/', token: { endpoint: 'token',