Skip to content

Commit

Permalink
feat(playground): automatic refresh token (#658)
Browse files Browse the repository at this point in the history
This can be played in the playground with `NbAuthPasswordStrategy` as well, just replace in the ** playground**  `auth-module.ts`:
```
login: {
          strategy: 'password',
...}
```
by 
```
login: {
          strategy: 'email',
...}
```
  • Loading branch information
alain-charles authored and nnixaa committed Aug 23, 2018
1 parent 3a708dd commit b4fc624
Show file tree
Hide file tree
Showing 11 changed files with 299 additions and 35 deletions.
78 changes: 51 additions & 27 deletions src/backend/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -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) {
Expand All @@ -43,16 +52,10 @@ 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: tokens.createAccessToken(user),
}
});
}
Expand All @@ -73,17 +76,11 @@ app.post('/api/auth/token', 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({
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),
});
}
}
Expand Down Expand Up @@ -155,18 +152,45 @@ 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);

// token issued by oauth2 strategy
if (req.body.refresh_token) {
var token = req.body.refresh_token;
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,
});
}
}

// token issued via email strategy
if (req.body.token) {
return res.json({
data: {
message: 'Successfully refreshed token.',
token: token
}
});
data: {
message: 'Successfully refreshed token!',
token: tokens.createAccessToken(users[0]),
}
});
};

});

app.listen(4400, function () {
Expand Down
4 changes: 2 additions & 2 deletions src/backend/auth.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
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: 3 additions & 1 deletion src/backend/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,7 @@ module.exports = {
jwtSecret: 'MyS3cr3tK3Y',
jwtSession: {
session: false
}
},
accessTokenExpiresIn : 60,
refreshTokenExpiresIn: 120
};
3 changes: 2 additions & 1 deletion src/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@
"body-parser": "^1.17.1",
"express": "^4.15.2",
"jwt-simple": "^0.5.1",
"moment": "^2.22.2",
"passport": "^0.3.2",
"passport-jwt": "^2.2.1"
"passport-jwt": "^4.0.0"
}
}
33 changes: 33 additions & 0 deletions src/backend/token_helpers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
const moment = require('moment');
const jwt = require('jwt-simple');
const cfg = require('./config.js');

module.exports.permanentRefreshToken = 'eb4e15840117437cbfd7343f257c4aae';

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;
93 changes: 93 additions & 0 deletions src/playground/auth/api-calls/api-calls.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* @license
* Copyright Akveo. All Rights Reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import { Component, Inject } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Router } from '@angular/router';
import { Observable, of as observableOf } from 'rxjs';
import { catchError, delay } from 'rxjs/operators';
import { NbAuthResult, NbAuthService, NbAuthToken } from '../../../framework/auth/services';
import { NB_AUTH_OPTIONS } from '../../../framework/auth/auth.options';
import { getDeepFromObject } from '../../../framework/auth/helpers';
import { Wine } from './wine';

@Component({
selector: 'nb-playground-api-calls',
template: `
<router-outlet></router-outlet>
<nb-layout>
<nb-layout-column>
<nb-card>
<nb-card-body>
<h2>You are authenticated</h2>
<p>You can call the secured API</p>
<button nbButton status="primary" (click)="loadWines()">Call API</button>
<button nbButton status="primary" (click)="logout()">Sign out</button>
</nb-card-body>
</nb-card>
<nb-card *ngIf="(wines$ | async)?.length">
<nb-card-header>
Alain'wines
</nb-card-header>
<nb-list>
<nb-list-item *ngFor="let wine of wines$ | async">
{{ wine.region }}, {{ wine.name }} ({{ wine.year }})
</nb-list-item>
</nb-list>
</nb-card>
</nb-layout-column>
</nb-layout>
`,
})

export class NbPlaygroundApiCallsComponent {

token: NbAuthToken;
wines$: Observable<Wine[]>;
redirectDelay: number = 0;
strategy: string = '';

constructor(private authService: NbAuthService,
private http: HttpClient,
private router: Router,
@Inject(NB_AUTH_OPTIONS) protected options = {}) {

this.redirectDelay = this.getConfigValue('forms.logout.redirectDelay');
this.strategy = this.getConfigValue('forms.logout.strategy');

this.authService.onTokenChange()
.subscribe((token: NbAuthToken) => {
this.token = null;
if (token && token.isValid()) {
this.token = token;
}
});
}

logout() {
this.authService.logout(this.strategy)
.pipe(
delay(this.redirectDelay),
)
.subscribe((result: NbAuthResult) => this.router.navigate(['/auth/login']));
}

loadWines() {
this.wines$ = this.http.get<Wine[]>('http://localhost:4400/api/wines')
.pipe(
catchError(err => {
if (err instanceof HttpErrorResponse && err.status === 401) {
this.router.navigate(['/auth/login']);
}
return observableOf([]);
}),
);
}

getConfigValue(key: string): any {
return getDeepFromObject(this.options, key, null);
}
}
6 changes: 6 additions & 0 deletions src/playground/auth/api-calls/wine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export interface Wine {
id: number;
name: string;
region: string;
year: number
}
6 changes: 6 additions & 0 deletions src/playground/auth/auth-routing.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from '@nebular/auth';
import { NbAclTestComponent } from './acl/acl-test.component';
import { NbAuthGuard } from './auth-guard.service';
import { NbPlaygroundApiCallsComponent } from './api-calls/api-calls.component';


export const routes: Routes = [
Expand Down Expand Up @@ -66,6 +67,11 @@ export const routes: Routes = [
canActivate: [NbAuthGuard],
component: NbAuthPlaygroundComponent,
},
{
path: 'auth/api-calls.component',
canActivate: [NbAuthGuard],
component: NbPlaygroundApiCallsComponent,
},
];

@NgModule({
Expand Down
Loading

0 comments on commit b4fc624

Please sign in to comment.