Skip to content

Commit 14ed108

Browse files
committed
add refresh token support
1 parent 693dc57 commit 14ed108

File tree

7 files changed

+145
-34
lines changed

7 files changed

+145
-34
lines changed

samples/VanillaJS/public/code-identityserver-sample.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
</div>
2323
<div>
2424
<button id='popupSignin'>signin with popup</button>
25-
<button id='iframeSignin'>signin with iframe</button>
25+
<button id='iframeSignin'>signin silent/renew access token</button>
2626
</div>
2727
<div>
2828
<button id='startSignoutMainWindow'>start signout main window</button>

samples/VanillaJS/public/code-identityserver-sample.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ var settings = {
3636
post_logout_redirect_uri: url + '/code-identityserver-sample.html',
3737
response_type: 'code',
3838
//response_mode: 'fragment',
39-
scope: 'openid profile',
39+
scope: 'openid profile api offline_access',
4040

4141
popup_redirect_uri: url + '/code-identityserver-sample-popup-signin.html',
4242
popup_post_logout_redirect_uri: url + '/code-identityserver-sample-popup-signout.html',
@@ -46,7 +46,8 @@ var settings = {
4646
//silentRequestTimeout:10000,
4747

4848
filterProtocolClaims: true,
49-
loadUserInfo: true
49+
loadUserInfo: true,
50+
revokeAccessTokenOnSignout : true
5051
};
5152
var mgr = new Oidc.UserManager(settings);
5253

src/TokenClient.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ export class TokenClient {
1919

2020
exchangeCode(args = {}) {
2121
args.grant_type = args.grant_type || "authorization_code";
22+
args.client_id = args.client_id || this._settings.client_id;
23+
args.redirect_uri = args.redirect_uri || this._settings.redirect_uri;
2224

2325
if (!args.code) {
2426
Log.error("TokenClient.exchangeCode: No code passed");
@@ -41,7 +43,30 @@ export class TokenClient {
4143
Log.debug("TokenClient.exchangeCode: Received token endpoint");
4244

4345
return this._jsonService.postForm(url, args).then(response => {
44-
Log.debug("TokenClient.exchangeCode: response received", response);
46+
Log.debug("TokenClient.exchangeCode: response received");
47+
return response;
48+
});
49+
});
50+
}
51+
52+
exchangeRefreshToken(args = {}) {
53+
args.grant_type = args.grant_type || "refresh_token";
54+
args.client_id = args.client_id || this._settings.client_id;
55+
56+
if (!args.refresh_token) {
57+
Log.error("TokenClient.exchangeRefreshToken: No refresh_token passed");
58+
return Promise.reject(new Error("A refresh_token is required"));
59+
}
60+
if (!args.client_id) {
61+
Log.error("TokenClient.exchangeRefreshToken: No client_id passed");
62+
return Promise.reject(new Error("A client_id is required"));
63+
}
64+
65+
return this._metadataService.getTokenEndpoint(false).then(url => {
66+
Log.debug("TokenClient.exchangeRefreshToken: Received token endpoint");
67+
68+
return this._jsonService.postForm(url, args).then(response => {
69+
Log.debug("TokenClient.exchangeRefreshToken: response received");
4570
return response;
4671
});
4772
});

src/TokenRevocationClient.js

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { MetadataService } from './MetadataService';
66
import { Global } from './Global';
77

88
const AccessTokenTypeHint = "access_token";
9+
const RefreshTokenTypeHint = "refresh_token";
910

1011
export class TokenRevocationClient {
1112
constructor(settings, XMLHttpRequestCtor = Global.XMLHttpRequest, MetadataServiceCtor = MetadataService) {
@@ -19,10 +20,15 @@ export class TokenRevocationClient {
1920
this._metadataService = new MetadataServiceCtor(this._settings);
2021
}
2122

22-
revoke(accessToken, required) {
23-
if (!accessToken) {
24-
Log.error("TokenRevocationClient.revoke: No accessToken provided");
25-
throw new Error("No accessToken provided.");
23+
revoke(token, required, type = "access_token") {
24+
if (!token) {
25+
Log.error("TokenRevocationClient.revoke: No token provided");
26+
throw new Error("No token provided.");
27+
}
28+
29+
if (type !== AccessTokenTypeHint && type != RefreshTokenTypeHint) {
30+
Log.error("TokenRevocationClient.revoke: Invalid token type");
31+
throw new Error("Invalid token type.");
2632
}
2733

2834
return this._metadataService.getRevocationEndpoint().then(url => {
@@ -36,14 +42,14 @@ export class TokenRevocationClient {
3642
return;
3743
}
3844

39-
Log.error("TokenRevocationClient.revoke: Revoking access token");
45+
Log.debug("TokenRevocationClient.revoke: Revoking " + type);
4046
var client_id = this._settings.client_id;
4147
var client_secret = this._settings.client_secret;
42-
return this._revoke(url, client_id, client_secret, accessToken);
48+
return this._revoke(url, client_id, client_secret, token, type);
4349
});
4450
}
4551

46-
_revoke(url, client_id, client_secret, accessToken) {
52+
_revoke(url, client_id, client_secret, token, type) {
4753

4854
return new Promise((resolve, reject) => {
4955

@@ -69,8 +75,8 @@ export class TokenRevocationClient {
6975
if (client_secret) {
7076
body += "&client_secret=" + encodeURIComponent(client_secret);
7177
}
72-
body += "&token_type_hint=" + encodeURIComponent(AccessTokenTypeHint);
73-
body += "&token=" + encodeURIComponent(accessToken);
78+
body += "&token_type_hint=" + encodeURIComponent(type);
79+
body += "&token=" + encodeURIComponent(token);
7480

7581
xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
7682
xhr.send(body);

src/User.js

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
import { Log } from './Log';
55

66
export class User {
7-
constructor({id_token, session_state, access_token, token_type, scope, profile, expires_at, state}) {
7+
constructor({id_token, session_state, access_token, refresh_token, token_type, scope, profile, expires_at, state}) {
88
this.id_token = id_token;
99
this.session_state = session_state;
1010
this.access_token = access_token;
11+
this.refresh_token = refresh_token;
1112
this.token_type = token_type;
1213
this.scope = scope;
1314
this.profile = profile;
@@ -22,6 +23,13 @@ export class User {
2223
}
2324
return undefined;
2425
}
26+
set expires_in(value) {
27+
let expires_in = parseInt(value);
28+
if (typeof expires_in === 'number' && expires_in > 0) {
29+
let now = parseInt(Date.now() / 1000);
30+
this.expires_at = now + expires_in;
31+
}
32+
}
2533

2634
get expired() {
2735
let expires_in = this.expires_in;
@@ -41,6 +49,7 @@ export class User {
4149
id_token: this.id_token,
4250
session_state: this.session_state,
4351
access_token: this.access_token,
52+
refresh_token: this.refresh_token,
4453
token_type: this.token_type,
4554
scope: this.scope,
4655
profile: this.profile,

src/UserManager.js

Lines changed: 82 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ import { UserManagerEvents } from './UserManagerEvents';
99
import { SilentRenewService } from './SilentRenewService';
1010
import { SessionMonitor } from './SessionMonitor';
1111
import { TokenRevocationClient } from './TokenRevocationClient';
12+
import { TokenClient } from './TokenClient';
1213

1314
export class UserManager extends OidcClient {
1415
constructor(settings = {},
1516
SilentRenewServiceCtor = SilentRenewService,
1617
SessionMonitorCtor = SessionMonitor,
17-
TokenRevocationClientCtor = TokenRevocationClient
18+
TokenRevocationClientCtor = TokenRevocationClient,
19+
TokenClientCtor = TokenClient
1820
) {
1921

2022
if (!(settings instanceof UserManagerSettings)) {
@@ -37,6 +39,7 @@ export class UserManager extends OidcClient {
3739
}
3840

3941
this._tokenRevocationClient = new TokenRevocationClientCtor(this._settings);
42+
this._tokenClient = new TokenClientCtor(this._settings);
4043
}
4144

4245
get _redirectNavigator() {
@@ -144,6 +147,51 @@ export class UserManager extends OidcClient {
144147
}
145148

146149
signinSilent(args = {}) {
150+
// first determine if we have a refresh token, or need to use iframe
151+
return this._loadUser().then(user => {
152+
if (user && user.refresh_token) {
153+
args.refresh_token = user.refresh_token;
154+
return this._useRefreshToken(args);
155+
}
156+
else {
157+
args.id_token_hint = args.id_token_hint || (this.settings.includeIdTokenInSilentRenew && user.id_token);
158+
return this._signinSilentIframe(args);
159+
}
160+
});
161+
}
162+
163+
_useRefreshToken(args = {}) {
164+
return this._tokenClient.exchangeRefreshToken(args).then(result => {
165+
if (!result) {
166+
Log.error("UserManager._useRefreshToken: No response returned from token endpoint");
167+
return Promise.reject("No response returned from token endpoint");
168+
}
169+
if (!result.access_token) {
170+
Log.error("UserManager._useRefreshToken: No access token returned from token endpoint");
171+
return Promise.reject("No access token returned from token endpoint");
172+
}
173+
174+
Log.debug("UserManager._useRefreshToken: refresh token response success");
175+
176+
return this._loadUser().then(user => {
177+
if (user) {
178+
user.access_token = result.access_token;
179+
user.refresh_token = result.refresh_token || user.refresh_token;
180+
user.expires_in = result.expires_in;
181+
182+
return this.storeUser(user).then(()=>{
183+
this._events.load(user);
184+
return user;
185+
});
186+
}
187+
else {
188+
return null;
189+
}
190+
});
191+
});;
192+
}
193+
194+
_signinSilentIframe(args = {}) {
147195
let url = args.redirect_uri || this.settings.silent_redirect_uri;
148196
if (!url) {
149197
Log.error("UserManager.signinSilent: No silent_redirect_uri configured");
@@ -153,21 +201,9 @@ export class UserManager extends OidcClient {
153201
args.redirect_uri = url;
154202
args.prompt = args.prompt || "none";
155203

156-
let setIdToken;
157-
if (args.id_token_hint || !this.settings.includeIdTokenInSilentRenew) {
158-
setIdToken = Promise.resolve();
159-
}
160-
else {
161-
setIdToken = this._loadUser().then(user => {
162-
args.id_token_hint = user && user.id_token;
163-
});
164-
}
165-
166-
return setIdToken.then(() => {
167-
return this._signin(args, this._iframeNavigator, {
168-
startUrl: url,
169-
silentRequestTimeout: args.silentRequestTimeout || this.settings.silentRequestTimeout
170-
});
204+
return this._signin(args, this._iframeNavigator, {
205+
startUrl: url,
206+
silentRequestTimeout: args.silentRequestTimeout || this.settings.silentRequestTimeout
171207
}).then(user => {
172208
if (user) {
173209
if (user.profile && user.profile.sub) {
@@ -181,6 +217,7 @@ export class UserManager extends OidcClient {
181217
return user;
182218
});
183219
}
220+
184221
signinSilentCallback(url) {
185222
return this._signinCallback(url, this._iframeNavigator).then(user => {
186223
if (user) {
@@ -384,6 +421,7 @@ export class UserManager extends OidcClient {
384421
Log.debug("UserManager.revokeAccessToken: removing token properties from user and re-storing");
385422

386423
user.access_token = null;
424+
user.refresh_token = null;
387425
user.expires_at = null;
388426
user.token_type = null;
389427

@@ -399,17 +437,43 @@ export class UserManager extends OidcClient {
399437
}
400438

401439
_revokeInternal(user, required) {
402-
var access_token = user && user.access_token;
440+
if (user) {
441+
var access_token = user.access_token;
442+
var refresh_token = user.refresh_token;
443+
444+
return this._revokeAccessTokenInternal(access_token, require)
445+
.then(atSuccess => {
446+
return this._revokeRefreshTokenInternal(refresh_token, required)
447+
.then(rtSuccess => {
448+
if (!atSuccess && !rtSuccess) {
449+
Log.debug("UserManager.revokeAccessToken: no need to revoke due to no token(s), or JWT format");
450+
}
451+
452+
return atSuccess || rtSuccess;
453+
});
454+
});
455+
}
456+
457+
return Promise.resolve(false);
458+
}
403459

460+
_revokeAccessTokenInternal(access_token, required) {
404461
// check for JWT vs. reference token
405462
if (!access_token || access_token.indexOf('.') >= 0) {
406-
Log.debug("UserManager.revokeAccessToken: no need to revoke due to no user, token, or JWT format");
407463
return Promise.resolve(false);
408464
}
409465

410466
return this._tokenRevocationClient.revoke(access_token, required).then(() => true);
411467
}
412468

469+
_revokeRefreshTokenInternal(refresh_token, required) {
470+
if (!refresh_token) {
471+
return Promise.resolve(false);
472+
}
473+
474+
return this._tokenRevocationClient.revoke(refresh_token, required, "refresh_token").then(() => true);
475+
}
476+
413477
startSilentRenew() {
414478
this._silentRenewService.start();
415479
}

test/unit/UserManager.spec.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ describe("UserManager", function () {
3333
Global._testing();
3434

3535
Log.logger = console;
36-
Log.level = Log.NONE;
36+
Log.level = Log.DEBUG;
3737

3838
stubNavigator = {};
3939
stubUserStore = new StubStateStore();
@@ -95,7 +95,9 @@ describe("UserManager", function () {
9595

9696
describe("signinSilent", function(){
9797

98-
it("should pass silentRequestTimeout from settings", function(done){
98+
it("should pass silentRequestTimeout from settings", function(done) {
99+
100+
stubUserStore.item = new User({id_token:"id_token"}).toStorageString();
99101

100102
settings.silentRequestTimeout = 123;
101103
settings.silent_redirect_uri = "http://client/silent_callback";
@@ -112,6 +114,8 @@ describe("UserManager", function () {
112114

113115
it("should pass silentRequestTimeout from params", function(done){
114116

117+
stubUserStore.item = new User({id_token:"id_token"}).toStorageString();
118+
115119
settings.silent_redirect_uri = "http://client/silent_callback";
116120
subject = new UserManager(settings);
117121

@@ -124,6 +128,8 @@ describe("UserManager", function () {
124128

125129
it("should pass prompt from params", function(done){
126130

131+
stubUserStore.item = new User({id_token:"id_token"}).toStorageString();
132+
127133
settings.silent_redirect_uri = "http://client/silent_callback";
128134
subject = new UserManager(settings);
129135

0 commit comments

Comments
 (0)