From 2f28cbc073a8615236a3e0fbeef4c69ccc34b40d Mon Sep 17 00:00:00 2001
From: Dmitry Nehaychik <4dmitr@gmail.com>
Date: Thu, 14 Jun 2018 19:36:35 +0300
Subject: [PATCH] feat(auth): add OAuth2 strategy
basic configuration example:
```
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',
},
}),
],
```
---
docs/articles/auth-intro.md | 1 +
docs/structure.ts | 11 +
.../auth/services/token/token.spec.ts | 258 +++++++----
src/framework/auth/services/token/token.ts | 101 ++++-
.../auth/strategies/auth-strategy.ts | 10 +-
.../oauth2/oauth2-strategy.options.ts | 62 +++
.../strategies/oauth2/oauth2-strategy.spec.ts | 412 ++++++++++++++++++
.../auth/strategies/oauth2/oauth2-strategy.ts | 315 +++++++++++++
.../strategies/password/password-strategy.ts | 6 -
9 files changed, 1081 insertions(+), 95 deletions(-)
create mode 100644 src/framework/auth/strategies/oauth2/oauth2-strategy.options.ts
create mode 100644 src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts
create mode 100644 src/framework/auth/strategies/oauth2/oauth2-strategy.ts
diff --git a/docs/articles/auth-intro.md b/docs/articles/auth-intro.md
index e4963db5b3..ab4f166ee7 100644
--- a/docs/articles/auth-intro.md
+++ b/docs/articles/auth-intro.md
@@ -24,6 +24,7 @@ You can use the built-in components or create your custom ones.
## Auth Strategies
- `NbDummyAuthStrategy` - simple strategy for testing purposes, could be used to simulate backend responses while API is in the development;
- `NbPasswordAuthStrategy` - the most common email/login/password authentication strategy.
+ - `NbOAuth2AuthStrategy` - the most popular authentication framework that enables applications to obtain limited access to user accounts on an HTTP service.
## Other helper services
diff --git a/docs/structure.ts b/docs/structure.ts
index 754116b01b..dcc8f0be1f 100644
--- a/docs/structure.ts
+++ b/docs/structure.ts
@@ -463,6 +463,17 @@ export const structure = [
},
],
},
+ {
+ type: 'page',
+ name: 'NbOAuth2AuthStrategy',
+ children: [
+ {
+ type: 'block',
+ block: 'component',
+ source: 'NbOAuth2AuthStrategy',
+ },
+ ],
+ },
{
type: 'page',
name: 'NbDummyAuthStrategy',
diff --git a/src/framework/auth/services/token/token.spec.ts b/src/framework/auth/services/token/token.spec.ts
index 08a04e6e70..730630ae02 100644
--- a/src/framework/auth/services/token/token.spec.ts
+++ b/src/framework/auth/services/token/token.spec.ts
@@ -4,90 +4,182 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*/
-import { NbAuthJWTToken, NbAuthSimpleToken } from './token';
-
-
-describe('auth JWT token', () => {
- // tslint:disable
- const simpleToken = new NbAuthSimpleToken('token');
- const validJWTToken = new NbAuthJWTToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzY290Y2guaW8iLCJleHAiOjI1MTczMTQwNjYxNzUsIm5hbWUiOiJDaHJpcyBTZXZpbGxlamEiLCJhZG1pbiI6dHJ1ZX0=.03f329983b86f7d9a9f5fef85305880101d5e302afafa20154d094b229f75773');
- const emptyJWTToken = new NbAuthJWTToken('..');
- const invalidBase64JWTToken = new NbAuthJWTToken('h%2BHY.h%2BHY.h%2BHY');
-
- const invalidJWTToken = new NbAuthJWTToken('.');
-
- const noExpJWTToken = new NbAuthJWTToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzY290Y2guaW8iLCJuYW1lIjoiQ2hyaXMgU2V2aWxsZWphIiwiYWRtaW4iOnRydWV9.03f329983b86f7d9a9f5fef85305880101d5e302afafa20154d094b229f75773');
-
- const expiredJWTToken = new NbAuthJWTToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzY290Y2guaW8iLCJleHAiOjEzMDA4MTkzODAsIm5hbWUiOiJDaHJpcyBTZXZpbGxlamEiLCJhZG1pbiI6dHJ1ZX0.03f329983b86f7d9a9f5fef85305880101d5e302afafa20154d094b229f75773');
- // tslint:enable
-
- it('getPayload success', () => {
- expect(validJWTToken.getPayload())
- .toEqual(JSON.parse('{"iss":"scotch.io","exp":2517314066175,"name":"Chris Sevilleja","admin":true}'));
- });
-
- it('getPayload, not valid JWT token, must consist of three parts', () => {
- expect(() => {
- invalidJWTToken.getPayload();
- })
- .toThrow(new Error(
- `The token ${invalidJWTToken.getValue()} is not valid JWT token and must consist of three parts.`));
- });
-
- it('getPayload, not valid JWT token, cannot be decoded', () => {
- expect(() => {
- emptyJWTToken.getPayload();
- })
- .toThrow(new Error(
- `The token ${emptyJWTToken.getValue()} is not valid JWT token and cannot be decoded.`));
- });
-
- it('getPayload, not valid base64 in JWT token, cannot be decoded', () => {
- expect(() => {
- invalidBase64JWTToken.getPayload();
- })
- .toThrow(new Error(
- `The token ${invalidBase64JWTToken.getValue()} is not valid JWT token and cannot be parsed.`));
- });
-
- it('getTokenExpDate success', () => {
- const date = new Date(0);
- date.setUTCSeconds(2517314066175);
- expect(validJWTToken.getTokenExpDate()).toEqual(date);
- });
-
- it('getTokenExpDate is empty', () => {
- expect(noExpJWTToken.getTokenExpDate()).toBeNull();
- });
-
- it('no exp date token is valid', () => {
- expect(noExpJWTToken.isValid()).toEqual(true);
- });
-
- it('isValid success', () => {
- expect(validJWTToken.isValid()).toEqual(true);
- });
-
- it('isValid fail', () => {
- // without token
- expect(new NbAuthJWTToken('').isValid()).toBeFalsy();
-
- // expired date
- expect(expiredJWTToken.isValid()).toBeFalsy();
- });
-
- it('NbAuthJWTToken name', () => {
- // without token
- expect(NbAuthJWTToken.NAME).toEqual(validJWTToken.getName());
- });
-
- it('NbAuthSimpleToken name', () => {
- // without token
- expect(NbAuthSimpleToken.NAME).toEqual(simpleToken.getName());
+import { NbAuthOAuth2Token, NbAuthJWTToken, NbAuthSimpleToken } from './token';
+
+
+describe('auth token', () => {
+ describe('NbAuthJWTToken', () => {
+ // tslint:disable
+ const simpleToken = new NbAuthSimpleToken('token');
+ const validJWTToken = new NbAuthJWTToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzY290Y2guaW8iLCJleHAiOjI1MTczMTQwNjYxNzUsIm5hbWUiOiJDaHJpcyBTZXZpbGxlamEiLCJhZG1pbiI6dHJ1ZX0=.03f329983b86f7d9a9f5fef85305880101d5e302afafa20154d094b229f75773');
+ const emptyJWTToken = new NbAuthJWTToken('..');
+ const invalidBase64JWTToken = new NbAuthJWTToken('h%2BHY.h%2BHY.h%2BHY');
+
+ const invalidJWTToken = new NbAuthJWTToken('.');
+
+ const noExpJWTToken = new NbAuthJWTToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzY290Y2guaW8iLCJuYW1lIjoiQ2hyaXMgU2V2aWxsZWphIiwiYWRtaW4iOnRydWV9.03f329983b86f7d9a9f5fef85305880101d5e302afafa20154d094b229f75773');
+
+ const expiredJWTToken = new NbAuthJWTToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzY290Y2guaW8iLCJleHAiOjEzMDA4MTkzODAsIm5hbWUiOiJDaHJpcyBTZXZpbGxlamEiLCJhZG1pbiI6dHJ1ZX0.03f329983b86f7d9a9f5fef85305880101d5e302afafa20154d094b229f75773');
+ // tslint:enable
+
+ it('getPayload success', () => {
+ expect(validJWTToken.getPayload())
+ .toEqual(JSON.parse('{"iss":"scotch.io","exp":2517314066175,"name":"Chris Sevilleja","admin":true}'));
+ });
+
+ it('getPayload, not valid JWT token, must consist of three parts', () => {
+ expect(() => {
+ invalidJWTToken.getPayload();
+ })
+ .toThrow(new Error(
+ `The token ${invalidJWTToken.getValue()} is not valid JWT token and must consist of three parts.`));
+ });
+
+ it('getPayload, not valid JWT token, cannot be decoded', () => {
+ expect(() => {
+ emptyJWTToken.getPayload();
+ })
+ .toThrow(new Error(
+ `The token ${emptyJWTToken.getValue()} is not valid JWT token and cannot be decoded.`));
+ });
+
+ it('getPayload, not valid base64 in JWT token, cannot be decoded', () => {
+ expect(() => {
+ invalidBase64JWTToken.getPayload();
+ })
+ .toThrow(new Error(
+ `The token ${invalidBase64JWTToken.getValue()} is not valid JWT token and cannot be parsed.`));
+ });
+
+ it('getTokenExpDate success', () => {
+ const date = new Date(0);
+ date.setUTCSeconds(2517314066175);
+ expect(validJWTToken.getTokenExpDate()).toEqual(date);
+ });
+
+ it('getTokenExpDate is empty', () => {
+ expect(noExpJWTToken.getTokenExpDate()).toBeNull();
+ });
+
+ it('no exp date token is valid', () => {
+ expect(noExpJWTToken.isValid()).toEqual(true);
+ });
+
+ it('isValid success', () => {
+ expect(validJWTToken.isValid()).toEqual(true);
+ });
+
+ it('isValid fail', () => {
+ // without token
+ expect(new NbAuthJWTToken('').isValid()).toBeFalsy();
+
+ // expired date
+ expect(expiredJWTToken.isValid()).toBeFalsy();
+ });
+
+ it('NbAuthJWTToken name', () => {
+ // without token
+ expect(NbAuthJWTToken.NAME).toEqual(validJWTToken.getName());
+ });
+
+ it('NbAuthSimpleToken name', () => {
+ // without token
+ expect(NbAuthSimpleToken.NAME).toEqual(simpleToken.getName());
+ });
+
+ it('NbAuthSimpleToken has payload', () => {
+ // without token
+ expect(simpleToken.getPayload()).toEqual(null);
+ });
+
+ it('getPayload success', () => {
+ expect(validJWTToken.getPayload())
+ .toEqual(JSON.parse('{"iss":"scotch.io","exp":2517314066175,"name":"Chris Sevilleja","admin":true}'));
+ });
+
+ it('NbAuthJWTToken name', () => {
+ // without token
+ expect(NbAuthJWTToken.NAME).toEqual(validJWTToken.getName());
+ });
+
+ it('NbAuthSimpleToken name', () => {
+ // without token
+ expect(NbAuthSimpleToken.NAME).toEqual(simpleToken.getName());
+ });
+
+ it('NbAuthSimpleToken has payload', () => {
+ // without token
+ expect(simpleToken.getPayload()).toEqual(null);
+ });
});
- it('NbAuthSimpleToken has payload', () => {
- // without token
- expect(simpleToken.getPayload()).toEqual(null);
+ describe('NbAuthOAuth2Token', () => {
+
+ const token = {
+ access_token: '2YotnFZFEjr1zCsicMWpAA',
+ expires_in: 3600,
+ refresh_token: 'tGzv3JOkF0XG5Qx2TlKWIA',
+ token_type: 'bearer',
+ example_parameter: 'example_value',
+ };
+
+ const validToken = new NbAuthOAuth2Token(token);
+ const emptyToken = new NbAuthOAuth2Token({});
+
+ const noExpToken = new NbAuthOAuth2Token({
+ access_token: '2YotnFZFEjr1zCsicMWpAA',
+ refresh_token: 'tGzv3JOkF0XG5Qx2TlKWIA',
+ example_parameter: 'example_value',
+ });
+
+ it('getPayload success', () => {
+ expect(validToken.getPayload()).toEqual(token);
+ });
+
+ it('getPayload, not valid token, cannot be decoded', () => {
+ expect(() => {
+ emptyToken.getPayload();
+ })
+ .toThrow(new Error(
+ `Cannot extract payload from an empty token.`));
+ });
+
+ it('getTokenExpDate success', () => {
+ const date = new Date();
+ date.setUTCSeconds(date.getUTCSeconds() + 3600);
+ expect(validToken.getTokenExpDate().getFullYear()).toEqual(date.getFullYear());
+ expect(validToken.getTokenExpDate().getDate()).toEqual(date.getDate());
+ expect(validToken.getTokenExpDate().getMonth()).toEqual(date.getMonth());
+ expect(validToken.getTokenExpDate().getMinutes()).toEqual(date.getMinutes());
+ expect(validToken.getTokenExpDate().getSeconds()).toEqual(date.getSeconds());
+ });
+
+ it('getTokenExpDate is empty', () => {
+ expect(noExpToken.getTokenExpDate()).toBeNull();
+ });
+
+ it('toString is json', () => {
+ expect(String(validToken)).toEqual(JSON.stringify(token));
+ });
+
+ it('getTokenExpDate is empty', () => {
+ expect(validToken.getType()).toEqual(token.token_type);
+ });
+
+ it('getTokenExpDate is empty', () => {
+ expect(noExpToken.getRefreshToken()).toEqual(token.refresh_token);
+ });
+
+ it('no exp date token is valid', () => {
+ expect(noExpToken.isValid()).toEqual(true);
+ });
+
+ it('isValid success', () => {
+ expect(validToken.isValid()).toEqual(true);
+ });
+
+ it('name', () => {
+ expect(NbAuthOAuth2Token.NAME).toEqual(validToken.getName());
+ });
});
});
diff --git a/src/framework/auth/services/token/token.ts b/src/framework/auth/services/token/token.ts
index 2a3cffa29a..7f6d6ae5e2 100644
--- a/src/framework/auth/services/token/token.ts
+++ b/src/framework/auth/services/token/token.ts
@@ -11,12 +11,16 @@ export abstract class NbAuthToken {
}
}
+export interface NbAuthRefreshableToken {
+ getRefreshToken(): string;
+}
+
export interface NbAuthTokenClass {
NAME: string;
- new (raw: string): NbAuthToken;
+ new (raw: any): NbAuthToken;
}
-export function nbAuthCreateToken(tokenClass: NbAuthTokenClass, token: string) {
+export function nbAuthCreateToken(tokenClass: NbAuthTokenClass, token: any) {
return new tokenClass(token);
}
@@ -27,7 +31,7 @@ export class NbAuthSimpleToken extends NbAuthToken {
static NAME = 'nb:auth:simple:token';
- constructor(readonly token: string) {
+ constructor(protected readonly token: any) {
super();
}
@@ -48,7 +52,7 @@ export class NbAuthSimpleToken extends NbAuthToken {
* @returns {boolean}
*/
isValid(): boolean {
- return !!this.token;
+ return !!this.getValue();
}
/**
@@ -121,3 +125,92 @@ export class NbAuthJWTToken extends NbAuthSimpleToken {
return super.isValid() && (!this.getTokenExpDate() || new Date() < this.getTokenExpDate());
}
}
+
+const prepareOAuth2Token = (data) => {
+ if (typeof data === 'string') {
+ try {
+ return JSON.parse(data);
+ } catch (e) {}
+ }
+ return data;
+};
+
+/**
+ * Wrapper for OAuth2 token
+ */
+export class NbAuthOAuth2Token extends NbAuthSimpleToken {
+
+ static NAME = 'nb:auth:oauth2:token';
+
+ constructor(protected data: { [key: string]: string|number }|string = {}) {
+ // we may get it as string when retrieving from a storage
+ super(prepareOAuth2Token(data));
+ }
+
+ /**
+ * Returns the token value
+ * @returns string
+ */
+ getValue(): string {
+ return this.token.access_token;
+ }
+
+ /**
+ * Returns the refresh token
+ * @returns string
+ */
+ getRefreshToken(): string {
+ return this.token.refresh_token;
+ }
+
+ /**
+ * Returns token payload
+ * @returns any
+ */
+ getPayload(): any {
+ if (!this.token || !Object.keys(this.token).length) {
+ throw new Error('Cannot extract payload from an empty token.');
+ }
+
+ return this.token;
+ }
+
+ /**
+ * Returns the token type
+ * @returns string
+ */
+ getType(): string {
+ return this.token.token_type;
+ }
+
+ /**
+ * Is data expired
+ * @returns {boolean}
+ */
+ isValid(): boolean {
+ return super.isValid() && (!this.getTokenExpDate() || new Date() < this.getTokenExpDate());
+ }
+
+ /**
+ * Returns expiration date
+ * @returns Date
+ */
+ getTokenExpDate(): Date {
+ if (!this.token.hasOwnProperty('expires_in')) {
+ return null;
+ }
+
+ const date = new Date();
+ date.setUTCSeconds(new Date().getUTCSeconds() + Number(this.token.expires_in));
+
+ return date;
+ }
+
+ /**
+ * Convert to string
+ * @returns {string}
+ */
+ toString(): string {
+ return JSON.stringify(this.token);
+ }
+}
diff --git a/src/framework/auth/strategies/auth-strategy.ts b/src/framework/auth/strategies/auth-strategy.ts
index 59d24dab49..2d17246cee 100644
--- a/src/framework/auth/strategies/auth-strategy.ts
+++ b/src/framework/auth/strategies/auth-strategy.ts
@@ -20,7 +20,7 @@ export abstract class NbAuthStrategy {
return getDeepFromObject(this.options, key, null);
}
- createToken(value: string): NbAuthToken {
+ createToken(value: any): NbAuthToken {
return nbAuthCreateToken(this.getOption('token.class'), value);
}
@@ -38,7 +38,7 @@ export abstract class NbAuthStrategy {
abstract logout(): Observable;
- abstract refreshToken(): Observable;
+ abstract refreshToken(data?: any): Observable;
protected createFailResponse(data?: any): HttpResponse