Skip to content

Commit b4fc624

Browse files
alain-charlesnnixaa
authored andcommitted
feat(playground): automatic refresh token (#658)
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', ...} ```
1 parent 3a708dd commit b4fc624

File tree

11 files changed

+299
-35
lines changed

11 files changed

+299
-35
lines changed

src/backend/app.js

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,14 @@ const express = require('express');
88
const bodyParser = require('body-parser');
99
const jwt = require('jwt-simple');
1010
const auth = require('./auth.js')();
11+
const auth_helpers = require('./auth_helpers.js');
1112
const users = require('./users.js');
13+
const tokens = require('./token_helpers.js');
14+
const wines = require('./wines.js');
1215
const cfg = require('./config.js');
1316
const app = express();
17+
const moment = require('moment');
18+
1419

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

42+
app.get('/api/wines', auth.authenticate(), function (req,res) {
43+
res.json(wines);
44+
})
45+
3746
app.post('/api/auth/login', function (req, res) {
3847

3948
if (req.body.email && req.body.password) {
@@ -43,16 +52,10 @@ app.post('/api/auth/login', function (req, res) {
4352
return u.email === email && u.password === password;
4453
});
4554
if (user) {
46-
var payload = {
47-
id: user.id,
48-
email: user.email,
49-
role: 'user',
50-
};
51-
var token = jwt.encode(payload, cfg.jwtSecret);
5255
return res.json({
5356
data: {
5457
message: 'Successfully logged in!',
55-
token: token
58+
token: tokens.createAccessToken(user),
5659
}
5760
});
5861
}
@@ -73,17 +76,11 @@ app.post('/api/auth/token', function (req, res) {
7376
return u.email === email && u.password === password;
7477
});
7578
if (user) {
76-
var payload = {
77-
id: user.id,
78-
email: user.email,
79-
role: 'user',
80-
};
81-
var token = jwt.encode(payload, cfg.jwtSecret);
8279
return res.json({
8380
token_type: 'Bearer',
84-
access_token: token,
85-
expires_in: 3600,
86-
refresh_token: 'eb4e1584-0117-437c-bfd7-343f257c4aae',
81+
access_token: tokens.createAccessToken(user),
82+
expires_in: cfg.accessTokenExpiresIn,
83+
refresh_token: tokens.createRefreshToken(user),
8784
});
8885
}
8986
}
@@ -155,18 +152,45 @@ app.delete('/api/auth/logout', function (req, res) {
155152
});
156153

157154
app.post('/api/auth/refresh-token', function (req, res) {
158-
var payload = {
159-
id: users[0].id,
160-
email: users[0].email,
161-
role: 'user',
162-
};
163-
var token = jwt.encode(payload, cfg.jwtSecret);
155+
156+
// token issued by oauth2 strategy
157+
if (req.body.refresh_token) {
158+
var token = req.body.refresh_token;
159+
var parts = token.split('.');
160+
if (parts.length !== 3) {
161+
return res.status(401).json({
162+
error: 'invalid_token',
163+
error_description: 'Invalid refresh token'
164+
});
165+
}
166+
var payload = JSON.parse(auth_helpers.urlBase64Decode(parts[1]));
167+
var exp = payload.exp;
168+
var userId = payload.sub;
169+
var now = moment().unix();
170+
if (now > exp) {
171+
return res.status(401).json({
172+
error: 'unauthorized',
173+
error_description: 'Refresh Token expired.'
174+
})
175+
} else {
176+
return res.json({
177+
token_type: 'Bearer',
178+
access_token: tokens.createAccessToken(users[userId - 1]),
179+
expires_in: cfg.accessTokenExpiresIn,
180+
});
181+
}
182+
}
183+
184+
// token issued via email strategy
185+
if (req.body.token) {
164186
return res.json({
165-
data: {
166-
message: 'Successfully refreshed token.',
167-
token: token
168-
}
169-
});
187+
data: {
188+
message: 'Successfully refreshed token!',
189+
token: tokens.createAccessToken(users[0]),
190+
}
191+
});
192+
};
193+
170194
});
171195

172196
app.listen(4400, function () {

src/backend/auth.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,12 @@ var ExtractJwt = passportJWT.ExtractJwt;
1212
var Strategy = passportJWT.Strategy;
1313
var params = {
1414
secretOrKey: cfg.jwtSecret,
15-
jwtFromRequest: ExtractJwt.fromAuthHeader()
15+
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken()
1616
};
1717

1818
module.exports = function () {
1919
var strategy = new Strategy(params, function (payload, done) {
20-
var user = users[payload.id] || null;
20+
var user = users[payload.sub -1 ] || null;
2121
if (user) {
2222
return done(null, {
2323
id: user.id

src/backend/auth_helpers.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
function b64decode(str) {
2+
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
3+
var output = '';
4+
5+
str = String(str).replace(/=+$/, '');
6+
7+
if (str.length % 4 === 1) {
8+
console.error("'atob' failed: The string to be decoded is not correctly encoded.");
9+
}
10+
11+
for (
12+
// initialize result and counters
13+
var bc=0, bs, buffer, idx= 0;
14+
// get next character
15+
buffer = str.charAt(idx++);
16+
// character found in table? initialize bit storage and add its ascii value;
17+
~buffer && (bs = bc % 4 ? bs * 64 + buffer : buffer,
18+
// and if not first of each 4 characters,
19+
// convert the first 8 bits to one ascii character
20+
bc++ % 4) ? output += String.fromCharCode(255 & bs >> (-2 * bc & 6)) : 0
21+
) {
22+
// try to find character in table (0-63, not found => -1)
23+
buffer = chars.indexOf(buffer);
24+
}
25+
return output;
26+
}
27+
28+
function b64DecodeUnicode(str) {
29+
return decodeURIComponent(Array.prototype.map.call(b64decode(str), function(c) {
30+
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
31+
}).join(''));
32+
}
33+
34+
module.exports.urlBase64Decode = function (str) {
35+
var output = str.replace(/-/g, '+').replace(/_/g, '/');
36+
switch (output.length % 4) {
37+
case 0: { break; }
38+
case 2: { output += '=='; break; }
39+
case 3: { output += '='; break; }
40+
default: {
41+
throw new Error('Illegal base64url string!');
42+
}
43+
}
44+
return b64DecodeUnicode(output);
45+
}
46+

src/backend/config.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,7 @@ module.exports = {
88
jwtSecret: 'MyS3cr3tK3Y',
99
jwtSession: {
1010
session: false
11-
}
11+
},
12+
accessTokenExpiresIn : 60,
13+
refreshTokenExpiresIn: 120
1214
};

src/backend/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@
1313
"body-parser": "^1.17.1",
1414
"express": "^4.15.2",
1515
"jwt-simple": "^0.5.1",
16+
"moment": "^2.22.2",
1617
"passport": "^0.3.2",
17-
"passport-jwt": "^2.2.1"
18+
"passport-jwt": "^4.0.0"
1819
}
1920
}

src/backend/token_helpers.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const moment = require('moment');
2+
const jwt = require('jwt-simple');
3+
const cfg = require('./config.js');
4+
5+
module.exports.permanentRefreshToken = 'eb4e15840117437cbfd7343f257c4aae';
6+
7+
module.exports.createAccessToken = function(user) {
8+
var payload = {
9+
sub: user.id,
10+
exp: moment().add(cfg.accessTokenExpiresIn, 'seconds').unix(),
11+
iat: moment().unix(),
12+
id: user.id,
13+
email: user.email,
14+
role: 'user',
15+
};
16+
var token = jwt.encode(payload, cfg.jwtSecret);
17+
return token;
18+
}
19+
20+
module.exports.createRefreshToken = function(user) {
21+
var refreshPayload = {
22+
sub: user.id,
23+
exp: moment().add(cfg.refreshTokenExpiresIn, 'seconds').unix(),
24+
iat: moment().unix(),
25+
id: user.id,
26+
email: user.email,
27+
role: 'REFRESH_TOKEN',
28+
};
29+
var refreshToken = jwt.encode(refreshPayload, cfg.jwtSecret);
30+
return refreshToken;
31+
}
32+
33+
module.exports

src/backend/wines.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
const wines = [
2+
{
3+
id: 1,
4+
name: 'Pommard 1er cru',
5+
region: 'Bourgogne',
6+
year: 2012,
7+
},
8+
{
9+
id: 2,
10+
name: 'Aloxe Corton Grand cru',
11+
region: 'Bourgogne',
12+
year: 2008,
13+
},
14+
{
15+
id: 3,
16+
name: 'Meursault 1er cru',
17+
region: 'Bourgogne',
18+
year: 1997,
19+
},
20+
];
21+
22+
module.exports = wines;
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* @license
3+
* Copyright Akveo. All Rights Reserved.
4+
* Licensed under the MIT License. See License.txt in the project root for license information.
5+
*/
6+
7+
import { Component, Inject } from '@angular/core';
8+
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
9+
import { Router } from '@angular/router';
10+
import { Observable, of as observableOf } from 'rxjs';
11+
import { catchError, delay } from 'rxjs/operators';
12+
import { NbAuthResult, NbAuthService, NbAuthToken } from '../../../framework/auth/services';
13+
import { NB_AUTH_OPTIONS } from '../../../framework/auth/auth.options';
14+
import { getDeepFromObject } from '../../../framework/auth/helpers';
15+
import { Wine } from './wine';
16+
17+
@Component({
18+
selector: 'nb-playground-api-calls',
19+
template: `
20+
<router-outlet></router-outlet>
21+
<nb-layout>
22+
<nb-layout-column>
23+
<nb-card>
24+
<nb-card-body>
25+
<h2>You are authenticated</h2>
26+
<p>You can call the secured API</p>
27+
<button nbButton status="primary" (click)="loadWines()">Call API</button>
28+
<button nbButton status="primary" (click)="logout()">Sign out</button>
29+
</nb-card-body>
30+
</nb-card>
31+
<nb-card *ngIf="(wines$ | async)?.length">
32+
<nb-card-header>
33+
Alain'wines
34+
</nb-card-header>
35+
<nb-list>
36+
<nb-list-item *ngFor="let wine of wines$ | async">
37+
{{ wine.region }}, {{ wine.name }} ({{ wine.year }})
38+
</nb-list-item>
39+
</nb-list>
40+
</nb-card>
41+
</nb-layout-column>
42+
</nb-layout>
43+
`,
44+
})
45+
46+
export class NbPlaygroundApiCallsComponent {
47+
48+
token: NbAuthToken;
49+
wines$: Observable<Wine[]>;
50+
redirectDelay: number = 0;
51+
strategy: string = '';
52+
53+
constructor(private authService: NbAuthService,
54+
private http: HttpClient,
55+
private router: Router,
56+
@Inject(NB_AUTH_OPTIONS) protected options = {}) {
57+
58+
this.redirectDelay = this.getConfigValue('forms.logout.redirectDelay');
59+
this.strategy = this.getConfigValue('forms.logout.strategy');
60+
61+
this.authService.onTokenChange()
62+
.subscribe((token: NbAuthToken) => {
63+
this.token = null;
64+
if (token && token.isValid()) {
65+
this.token = token;
66+
}
67+
});
68+
}
69+
70+
logout() {
71+
this.authService.logout(this.strategy)
72+
.pipe(
73+
delay(this.redirectDelay),
74+
)
75+
.subscribe((result: NbAuthResult) => this.router.navigate(['/auth/login']));
76+
}
77+
78+
loadWines() {
79+
this.wines$ = this.http.get<Wine[]>('http://localhost:4400/api/wines')
80+
.pipe(
81+
catchError(err => {
82+
if (err instanceof HttpErrorResponse && err.status === 401) {
83+
this.router.navigate(['/auth/login']);
84+
}
85+
return observableOf([]);
86+
}),
87+
);
88+
}
89+
90+
getConfigValue(key: string): any {
91+
return getDeepFromObject(this.options, key, null);
92+
}
93+
}

src/playground/auth/api-calls/wine.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export interface Wine {
2+
id: number;
3+
name: string;
4+
region: string;
5+
year: number
6+
}

src/playground/auth/auth-routing.module.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from '@nebular/auth';
1919
import { NbAclTestComponent } from './acl/acl-test.component';
2020
import { NbAuthGuard } from './auth-guard.service';
21+
import { NbPlaygroundApiCallsComponent } from './api-calls/api-calls.component';
2122

2223

2324
export const routes: Routes = [
@@ -66,6 +67,11 @@ export const routes: Routes = [
6667
canActivate: [NbAuthGuard],
6768
component: NbAuthPlaygroundComponent,
6869
},
70+
{
71+
path: 'auth/api-calls.component',
72+
canActivate: [NbAuthGuard],
73+
component: NbPlaygroundApiCallsComponent,
74+
},
6975
];
7076

7177
@NgModule({

0 commit comments

Comments
 (0)