Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat/auth : created NbAuthOauth2JWTToken class for handling Oauth2 tokens encapsulating jwt access_token #583

Merged
merged 50 commits into from
Jul 30, 2018
Merged
Changes from 25 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
cef7a1d
Added password grant_type option
alain-charles Jun 29, 2018
9f0dbab
Added password grant_type option implementation
alain-charles Jun 29, 2018
44d7d2e
Playground sample for oAuth2 password grant-type
alain-charles Jun 29, 2018
f57b737
Added route to oAuth2 password grant-type sample
alain-charles Jun 29, 2018
ed4628e
Addes angular-jwt for decoding jwt token (used in playground only)
alain-charles Jun 29, 2018
d7f01df
Added /api/auth/token endpoint (oAuth2 password grant-type playground)
alain-charles Jun 29, 2018
af52e88
Patched code for passing ci
alain-charles Jun 29, 2018
554f7dc
code optimization for passing ci
alain-charles Jun 29, 2018
fa0548f
Merge branch 'master' into master
nnixaa Jun 29, 2018
b72adae
Changes request by Dmitry (first review)
alain-charles Jun 29, 2018
733efc0
Generalized code in oauth2-strategy
alain-charles Jun 29, 2018
57fab69
Merge remote-tracking branch 'origin/master'
alain-charles Jun 29, 2018
c50df3a
Generalized code in oauth2-strategy
alain-charles Jun 29, 2018
dda5436
Added unit tests for oAuth-strategy password authentication
alain-charles Jul 2, 2018
ef0be25
Merge branch 'master' into master
alain-charles Jul 3, 2018
e180dc3
Merge branch 'master' into master
nnixaa Jul 4, 2018
6aa1fdb
Cleaned code according to nnixaa second review
alain-charles Jul 4, 2018
96c7344
Merge branch 'master' of https://github.com/alain-charles/nebular
alain-charles Jul 4, 2018
cf82c42
Removed package-lock.json from git repo
alain-charles Jul 4, 2018
dcff930
package-lock.json to revert
alain-charles Jul 4, 2018
315662f
reverted package-lock.json
alain-charles Jul 4, 2018
57a0230
Added new grant_type 'PASSWORD' in the block comment for it to be ins…
alain-charles Jul 4, 2018
d69d633
Merge remote-tracking branch 'upstream/master'
alain-charles Jul 5, 2018
1be8d91
Add the refreshtoken request management in NbJwtInterceptor and NbAut…
alain-charles Jul 13, 2018
4b67825
Merge branch 'master' of https://github.com/alain-charles/nebular
alain-charles Jul 19, 2018
deea3e2
Merge branch 'master' into temp
alain-charles Jul 19, 2018
acd0241
The token now contains ownerStrategyName, with is a back link to the …
alain-charles Jul 19, 2018
25250f0
feature/auth:
alain-charles Jul 19, 2018
c0fdd31
feature/auth:
alain-charles Jul 19, 2018
bd564b3
feature/auth:
alain-charles Jul 20, 2018
44ed61a
Merge branch 'master' into master
nnixaa Jul 20, 2018
9c16387
feature/auth
alain-charles Jul 24, 2018
2dd26c4
Merge remote-tracking branch 'origin/master'
alain-charles Jul 24, 2018
b284377
Merge branch 'master' into master
alain-charles Jul 24, 2018
e462c7d
feature/auth
alain-charles Jul 24, 2018
a3ee10b
Merge branch 'master' into master
nnixaa Jul 24, 2018
b38ae55
Merge branch 'master' into master
nnixaa Jul 25, 2018
62a2aff
feature/auth
alain-charles Jul 25, 2018
66ca493
Merge remote-tracking branch 'origin/master'
alain-charles Jul 25, 2018
3bd5a50
feature/auth
alain-charles Jul 25, 2018
398c058
feature/auth
alain-charles Jul 25, 2018
ee706d3
Merge branch 'master' into master
alain-charles Jul 26, 2018
5eb5821
feature/auth
alain-charles Jul 26, 2018
1a08225
Merge remote-tracking branch 'origin/master'
alain-charles Jul 26, 2018
1d697bc
feature/auth
alain-charles Jul 27, 2018
1c25956
Merge remote-tracking branch 'upstream/master'
alain-charles Jul 27, 2018
ff0e8d3
Merge remote-tracking branch 'upstream/master'
alain-charles Jul 27, 2018
e7af99e
Optimized getAccessTokenPayload() calls
alain-charles Jul 28, 2018
95632b1
Optimized getAccessTokenPayload() calls
alain-charles Jul 28, 2018
bc4001f
Merge branch 'master' into master
nnixaa Jul 30, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 50 additions & 30 deletions src/backend/app.js
Original file line number Diff line number Diff line change
@@ -8,9 +8,14 @@ const express = require('express');
const bodyParser = require('body-parser');
const jwt = require('jwt-simple');
const auth = require('./auth.js')();
const auth_helpers = require('./auth-helpers.js');
const users = require('./users.js');
const tokens = require('./token_helpers.js');
const wines = require('./wines.js');
const cfg = require('./config.js');
const app = express();
const moment = require('moment');


app.use(bodyParser.json());
app.use(auth.initialize());
@@ -34,6 +39,10 @@ app.get('/api/user', auth.authenticate(), function (req, res) {
});
});

app.get('/api/wines', auth.authenticate(), function (req,res) {
res.json(wines);
})

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

if (req.body.email && req.body.password) {
@@ -43,17 +52,11 @@ app.post('/api/auth/login', function (req, res) {
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({
data: {
message: 'Successfully logged in!',
token: token
}
token_type: 'Bearer',
access_token: tokens.createAccessToken(user),
expires_in: cfg.accessTokenExpiresIn,
refresh_token: 'eb4e15840117437cbfd7343f257c4aae',
});
}
}
@@ -65,25 +68,18 @@ 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',
access_token: tokens.createAccessToken(user),
expires_in: cfg.accessTokenExpiresIn,
refresh_token: tokens.createRefreshToken(user),
});
}
}
@@ -155,18 +151,42 @@ app.delete('/api/auth/logout', function (req, res) {
});

app.post('/api/auth/refresh-token', function (req, res) {
var payload = {
id: users[0].id,
email: users[0].email,
role: 'user',
};
var token = jwt.encode(payload, cfg.jwtSecret);
var token = req.body.refresh_token;

// token issued via email strategy
if (token === 'eb4e15840117437cbfd7343f257c4aae') {
return res.json({
data: {
message: 'Successfully refreshed token.',
token: token
}
token_type: 'Bearer',
access_token: tokens.createAccessToken(user[0]),
expires_in: cfg.accessTokenExpiresIn,
refresh_token: 'eb4e15840117437cbfd7343f257c4aae',
});
}

// token issued by oauth2 strategy
var parts = token.split('.');
if (parts.length !== 3) {
return res.status(401).json({
error: 'invalid_token',
error_description: 'Invalid refresh token'
});
}
var payload = JSON.parse(auth_helpers.urlBase64Decode(parts[1]));
var exp = payload.exp;
var userId = payload.sub;
var now = moment().unix();
if (now > exp) {
return res.status(401).json({
error: 'unauthorized',
error_description: 'Refresh Token expired.'
})
} else {
return res.json({
token_type: 'Bearer',
access_token: tokens.createAccessToken(users[userId - 1]),
expires_in: cfg.accessTokenExpiresIn,
});
}
});

app.listen(4400, function () {
46 changes: 46 additions & 0 deletions src/backend/auth-helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
function b64decode(str) {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
var output = '';

str = String(str).replace(/=+$/, '');

if (str.length % 4 === 1) {
console.error("'atob' failed: The string to be decoded is not correctly encoded.");
}

for (
// initialize result and counters
var bc=0, bs, buffer, idx= 0;
// get next character
buffer = str.charAt(idx++);
// character found in table? initialize bit storage and add its ascii value;
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
// and if not first of each 4 characters,
// convert the first 8 bits to one ascii character
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
) {
// try to find character in table (0-63, not found => -1)
buffer = chars.indexOf(buffer);
}
return output;
}

function b64DecodeUnicode(str) {
return decodeURIComponent(Array.prototype.map.call(b64decode(str), function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
}

module.exports.urlBase64Decode = function (str) {
var output = str.replace(/-/g, '+').replace(/_/g, '/');
switch (output.length % 4) {
case 0: { break; }
case 2: { output += '=='; break; }
case 3: { output += '='; break; }
default: {
throw new Error('Illegal base64url string!');
}
}
return b64DecodeUnicode(output);
}

4 changes: 2 additions & 2 deletions src/backend/auth.js
Original file line number Diff line number Diff line change
@@ -12,12 +12,12 @@ var ExtractJwt = passportJWT.ExtractJwt;
var Strategy = passportJWT.Strategy;
var params = {
secretOrKey: cfg.jwtSecret,
jwtFromRequest: ExtractJwt.fromAuthHeader()
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()
};

module.exports = function () {
var strategy = new Strategy(params, function (payload, done) {
var user = users[payload.id] || null;
var user = users[payload.sub -1 ] || null;
if (user) {
return done(null, {
id: user.id
4 changes: 3 additions & 1 deletion src/backend/config.js
Original file line number Diff line number Diff line change
@@ -8,5 +8,7 @@ module.exports = {
jwtSecret: 'MyS3cr3tK3Y',
jwtSession: {
session: false
}
},
accessTokenExpiresIn : 60,
refreshTokenExpiresIn: 120,
};
5 changes: 3 additions & 2 deletions src/backend/package.json
Original file line number Diff line number Diff line change
@@ -13,7 +13,8 @@
"body-parser": "^1.17.1",
"express": "^4.15.2",
"jwt-simple": "^0.5.1",
"passport": "^0.3.2",
"passport-jwt": "^2.2.1"
"passport": "^0.4.0",
"passport-jwt": "^4.0.0",
"moment": "^2.22.2"
}
}
32 changes: 32 additions & 0 deletions src/backend/token_helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
const moment = require('moment');
const jwt = require('jwt-simple');
const cfg = require('./config.js');


module.exports.createAccessToken = function(user) {
var payload = {
sub: user.id,
exp: moment().add(cfg.accessTokenExpiresIn, 'seconds').unix(),
iat: moment().unix(),
id: user.id,
email: user.email,
role: 'user',
};
var token = jwt.encode(payload, cfg.jwtSecret);
return token;
}

module.exports.createRefreshToken = function(user) {
var refreshPayload = {
sub: user.id,
exp: moment().add(cfg.refreshTokenExpiresIn, 'seconds').unix(),
iat: moment().unix(),
id: user.id,
email: user.email,
role: 'REFRESH_TOKEN',
};
var refreshToken = jwt.encode(refreshPayload, cfg.jwtSecret);
return refreshToken;
}

module.exports
22 changes: 22 additions & 0 deletions src/backend/wines.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
const wines = [
{
id: 1,
name: 'Pommard 1er cru',
region: 'Bourgogne',
year: 2012,
},
{
id: 2,
name: 'Aloxe Corton Grand cru',
region: 'Bourgogne',
year: 2008,
},
{
id: 3,
name: 'Meursault 1er cru',
region: 'Bourgogne',
year: 1997,
},
];

module.exports = wines;
53 changes: 49 additions & 4 deletions src/framework/auth/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ import { NbAuthStrategy } from '../strategies/auth-strategy';
import { NB_AUTH_STRATEGIES } from '../auth.options';
import { NbAuthResult } from './auth-result';
import { NbTokenService } from './token/token.service';
import { NbAuthToken } from './token/token';
import { isNbAuthRefreshableToken, NbAuthToken } from './token/token';

/**
* Common authentication service.
@@ -21,8 +21,31 @@ import { NbAuthToken } from './token/token';
@Injectable()
export class NbAuthService {

// Authorization server urls - a way to make them available in http_interceptor or anywhere else
authUrls: string[] = [];

constructor(protected tokenService: NbTokenService,
@Inject(NB_AUTH_STRATEGIES) protected strategies) {
for (const strategy of this.strategies) {
if (strategy.getActionEndpoint('login')) {
this.authUrls.push(strategy.getActionEndpoint('login'))
};
if (strategy.getActionEndpoint('register')) {
this.authUrls.push(strategy.getActionEndpoint('register'))
};
if (strategy.getActionEndpoint('request-pass')) {
this.authUrls.push(strategy.getActionEndpoint('request-pass'))
};
if (strategy.getActionEndpoint('logout')) {
this.authUrls.push(strategy.getActionEndpoint('logout'))
};
if (strategy.getActionEndpoint('token')) {
this.authUrls.push(strategy.getActionEndpoint('token'))
};
if (strategy.getActionEndpoint('refresh')) {
this.authUrls.push(strategy.getActionEndpoint('refresh'))
};
}
}

/**
@@ -34,12 +57,29 @@ export class NbAuthService {
}

/**
* Returns true if auth token is presented in the token storage
* Returns true if valid auth token is presented in the token storage.
* If not, calls the strategy refreshToken, and returns true if it get a new valid access token, false otherwise
* @returns {Observable<any>}
*/
isAuthenticated(): Observable<boolean> {
return this.getToken()
.pipe(map((token: NbAuthToken) => token.isValid()));
.pipe(
switchMap(token => {
if (!token.isValid() && (token.getStrategyName()) && (isNbAuthRefreshableToken(token)) ) {
return this.refreshToken(token.getStrategyName(), token)
.pipe(
switchMap(res => {
if (res.isSuccess()) {
return observableOf(res.getToken().isValid());
} else {
return observableOf(false);
}
}),
)
} else {
return observableOf(token.isValid());
}
}));
}

/**
@@ -162,14 +202,19 @@ export class NbAuthService {
* @returns {Observable<NbAuthResult>}
*/
refreshToken(strategyName: string, data?: any): Observable<NbAuthResult> {
return this.getStrategy(strategyName).refreshToken()
return this.getStrategy(strategyName).refreshToken(data)
.pipe(
switchMap((result: NbAuthResult) => {
return this.processResultToken(result);
}),
);
}

getAuthUrls(): string[] {
return this.authUrls;
}


/**
* Get registered strategy by name
*
8 changes: 4 additions & 4 deletions src/framework/auth/services/auth.spec.ts
Original file line number Diff line number Diff line change
@@ -28,8 +28,8 @@ describe('auth-service', () => {
const resp401 = new HttpResponse<Object>({body: {}, status: 401});
const resp200 = new HttpResponse<Object>({body: {}, status: 200});

const testToken = nbAuthCreateToken(NbAuthSimpleToken, testTokenValue);
const emptyToken = nbAuthCreateToken(NbAuthSimpleToken, null);
const testToken = nbAuthCreateToken(NbAuthSimpleToken, testTokenValue, 'strategy');
const emptyToken = nbAuthCreateToken(NbAuthSimpleToken, null, null);

const failResult = new NbAuthResult(false,
resp401,
@@ -112,7 +112,7 @@ describe('auth-service', () => {

authService.getToken().subscribe((val: NbAuthSimpleToken) => {
expect(spy).toHaveBeenCalled();
expect(val.getValue()).toEqual(testTokenValue);
expect(val.getAccessToken()).toEqual(testTokenValue);
});
},
);
@@ -150,7 +150,7 @@ describe('auth-service', () => {
.pipe(first())
.subscribe((token: NbAuthSimpleToken) => {
expect(spy).toHaveBeenCalled();
expect(token.getValue()).toEqual(testTokenValue);
expect(token.getAccessToken()).toEqual(testTokenValue);
done();
});
},
Loading