Skip to content

Commit

Permalink
feat(auth): add a new grant type password to OAuth2 Strategy
Browse files Browse the repository at this point in the history
  • Loading branch information
alain-charles authored and nnixaa committed Jul 5, 2018
1 parent 1b6c10e commit d8a66a8
Show file tree
Hide file tree
Showing 7 changed files with 400 additions and 57 deletions.
29 changes: 29 additions & 0 deletions src/backend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,35 @@ app.post('/api/auth/login', function (req, res) {
});
});

app.post('/api/auth/token', function (req, res) {

if (req.body.email && req.body.password) {
var email = req.body.email;
var password = req.body.password;
var user = users.find(function (u) {
return u.email === email && u.password === password;
});
if (user) {
var payload = {
id: user.id,
email: user.email,
role: 'user',
};
var token = jwt.encode(payload, cfg.jwtSecret);
return res.json({
token_type: 'Bearer',
access_token: token,
expires_in: 3600,
refresh_token: 'eb4e1584-0117-437c-bfd7-343f257c4aae',
});
}
}
return res.status(401).json({
error: 'invalid_client',
error_description: 'Invalid Login/Password combination.'
});
});

app.post('/api/auth/register', function (req, res) {

if (req.body.email && req.body.password && req.body.password === req.body.confirmPassword) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ export enum NbOAuth2ResponseType {
TOKEN = 'token',
}

// TODO: password, client_credentials
// TODO: client_credentials
export enum NbOAuth2GrantType {
AUTHORIZATION_CODE = 'authorization_code',
PASSWORD = 'password',
REFRESH_TOKEN = 'refresh_token',
}

Expand Down
70 changes: 66 additions & 4 deletions src/framework/auth/strategies/oauth2/oauth2-strategy.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ describe('oauth2-auth-strategy', () => {
expect(result.getToken()).toBeNull();
expect(result.getResponse().error).toEqual(tokenErrorResponse);
expect(result.getMessages()).toEqual([]);
expect(result.getErrors()).toEqual(errorMessages);
expect(result.getErrors()).toEqual([ tokenErrorResponse.error_description] );
expect(result.getRedirect()).toEqual(null);
done();
});
Expand Down Expand Up @@ -185,14 +185,13 @@ describe('oauth2-auth-strategy', () => {

strategy.refreshToken(successToken)
.subscribe((result: NbAuthResult) => {

expect(result).toBeTruthy();
expect(result.isSuccess()).toBe(false);
expect(result.isFailure()).toBe(true);
expect(result.getToken()).toBeNull(); // we don't have a token at this stage yet
expect(result.getResponse().error).toEqual(tokenErrorResponse);
expect(result.getMessages()).toEqual([]);
expect(result.getErrors()).toEqual(errorMessages);
expect(result.getErrors()).toEqual([ tokenErrorResponse.error_description] );
expect(result.getRedirect()).toEqual(null);
done();
});
Expand Down Expand Up @@ -404,7 +403,7 @@ describe('oauth2-auth-strategy', () => {
expect(result.getToken()).toBeNull();
expect(result.getResponse().error).toEqual(tokenErrorResponse);
expect(result.getMessages()).toEqual([]);
expect(result.getErrors()).toEqual(errorMessages);
expect(result.getErrors()).toEqual([ tokenErrorResponse.error_description] );
expect(result.getRedirect()).toEqual('/failure');
done();
});
Expand All @@ -413,4 +412,67 @@ describe('oauth2-auth-strategy', () => {
.flush(tokenErrorResponse, { status: 400, statusText: 'Bad Request' });
});
});

describe('configured: additionnal param: token, grant_type:password', () => {

beforeEach(() => {
strategy.setOptions({
baseEndpoint: 'http://example.com/',
clientId: 'clientId',
clientSecret: 'clientSecret',
token: {
grantType: NbOAuth2GrantType.PASSWORD,
endpoint: 'token',
},
});
});

it('handle success login', (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()).toEqual(successToken);
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 error login', (done: DoneFn) => {
const credentials = { email: 'example@akveo.com', password: '123456' };

strategy.authenticate(credentials)
.subscribe((result: NbAuthResult) => {
expect(result).toBeTruthy();
expect(result.isSuccess()).toBe(false);
expect(result.isFailure()).toBe(true);
expect(result.getToken()).toBeNull(); // we don't have a token at this stage yet
expect(result.getResponse().error).toEqual(tokenErrorResponse);
expect(result.getMessages()).toEqual([]);
expect(result.getErrors()).toEqual([tokenErrorResponse.error_description]);
expect(result.getRedirect()).toEqual(null);
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(tokenErrorResponse, {status: 401, statusText: 'unauthorized'});
});
});
});
127 changes: 75 additions & 52 deletions src/framework/auth/strategies/oauth2/oauth2-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ 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 { NbOAuth2AuthStrategyOptions,
NbOAuth2ResponseType,
auth2StrategyOptions,
NbOAuth2GrantType } from './oauth2-strategy.options';
import { NbAuthStrategyClass } from '../../auth.options';


Expand All @@ -29,6 +32,7 @@ import { NbAuthStrategyClass } from '../../auth.options';
*
* export enum NbOAuth2GrantType {
* AUTHORIZATION_CODE = 'authorization_code',
* PASSWORD = 'password',
* REFRESH_TOKEN = 'refresh_token',
* }
*
Expand Down Expand Up @@ -87,6 +91,12 @@ 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;
}

protected redirectResultHandlers = {
[NbOAuth2ResponseType.CODE]: () => {
return observableOf(this.route.snapshot.queryParams).pipe(
Expand Down Expand Up @@ -154,17 +164,22 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy {
super();
}

authenticate(): Observable<NbAuthResult> {
return this.isRedirectResult()
.pipe(
switchMap((result: boolean) => {
if (!result) {
this.authorizeRedirect();
return observableOf(new NbAuthResult(true));
}
return this.getAuthorizationResult();
}),
);
authenticate(data?: any): Observable<NbAuthResult> {

if (this.getOption('token.grantType') === NbOAuth2GrantType.PASSWORD) {
return this.passwordToken(data.email, data.password)
} else {
return this.isRedirectResult()
.pipe(
switchMap((result: boolean) => {
if (!result) {
this.authorizeRedirect();
return observableOf(new NbAuthResult(true));
}
return this.getAuthorizationResult();
}),
);
}
}

getAuthorizationResult(): Observable<any> {
Expand All @@ -191,23 +206,25 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy {
this.getOption('defaultMessages'),
this.createToken(res));
}),
catchError((res) => {
let errors = [];
if (res instanceof HttpErrorResponse) {
errors = this.getOption('defaultErrors');
} else {
errors.push('Something went wrong.');
}
catchError((res) => this.handleResponseError(res)),
);
}

return observableOf(
new NbAuthResult(
false,
res,
this.getOption('redirect.failure'),
errors,
[],
));
passwordToken(email: string, password: string): Observable<NbAuthResult> {
const url = this.getActionEndpoint('token');

return this.http.post(url, this.buildPasswordRequestData(email, password))
.pipe(
map((res) => {
return new NbAuthResult(
true,
res,
this.getOption('redirect.success'),
[],
this.getOption('defaultMessages'),
this.createToken(res));
}),
catchError((res) => this.handleResponseError(res)),
);
}

Expand All @@ -233,23 +250,7 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy {
this.getOption('defaultMessages'),
this.createToken(res));
}),
catchError((res) => {
let errors = [];
if (res instanceof HttpErrorResponse) {
errors = this.getOption('defaultErrors');
} else {
errors.push('Something went wrong.');
}

return observableOf(
new NbAuthResult(
false,
res,
this.getOption('redirect.failure'),
errors,
[],
));
}),
catchError((res) => this.handleResponseError(res)),
);
}

Expand All @@ -260,11 +261,7 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy {
redirect_uri: this.getOption('token.redirectUri'),
client_id: this.getOption('clientId'),
};

Object.entries(params)
.forEach(([key, val]) => !val && delete params[key]);

return params;
return this.cleanParams(params);
}

protected buildRefreshRequestData(token: NbAuthRefreshableToken): any {
Expand All @@ -273,11 +270,37 @@ export class NbOAuth2AuthStrategy extends NbAuthStrategy {
refresh_token: token.getRefreshToken(),
scope: this.getOption('refresh.scope'),
};
return this.cleanParams(params);
}

Object.entries(params)
.forEach(([key, val]) => !val && delete params[key]);
protected buildPasswordRequestData(email: string, password: string ): any {
const params = {
grant_type: this.getOption('token.grantType'),
email: email,
password: password,
};
return this.cleanParams(params);
}

return params;
protected handleResponseError(res: any): Observable<NbAuthResult> {
let errors = [];
if (res instanceof HttpErrorResponse) {
if (res.error.error_description) {
errors.push(res.error.error_description);
} else {
errors = this.getOption('defaultErrors');
}
} else {
errors.push('Something went wrong.');
}
return observableOf(
new NbAuthResult(
false,
res,
this.getOption('redirect.failure'),
errors,
[],
));
}

protected buildRedirectUrl() {
Expand Down
Loading

0 comments on commit d8a66a8

Please sign in to comment.