Skip to content

Commit a04f202

Browse files
committed
Handle refresh token expiration
1 parent ff63ef0 commit a04f202

11 files changed

+57
-45
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## Angular SPA Web API Changelog
22

3+
<a name="Sep 11, 2017"></a>
4+
### Sep 11, 2017
5+
* Handle refresh token expiration.
6+
37
<a name="Aug 21, 2017"></a>
48
### Aug 21, 2017
59
* Create `BrowserStorage` class & store user's info.

EXPLANATION.md

Lines changed: 11 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,11 @@ public static IEnumerable<Client> GetClients()
2525
"WebAPI",
2626
"roles"
2727
},
28-
AllowOfflineAccess = true // For refresh token.
28+
AllowOfflineAccess = true, // For refresh token.
29+
RefreshTokenUsage = TokenUsage.OneTimeOnly,
30+
AbsoluteRefreshTokenLifetime = 86400,
31+
SlidingRefreshTokenLifetime = 1800,
32+
RefreshTokenExpiration = TokenExpiration.Sliding
2933
}
3034
};
3135
}
@@ -36,7 +40,8 @@ Our Angular app, identified as _AngularSPA_:
3640
- doesn't use a _secret_ key: in a client application it would be useless because visible;
3741
- has an _access token_ for 15 minutes, then need to refresh the token;
3842
- can access to the _scopes_: in this case our Web API, called _WebAPI_, and user roles;
39-
- has _OfflineAccess_ for refresh token.
43+
- has _OfflineAccess_ for _refresh token_;
44+
- _refresh token_ has a sliding lifetime of 30 minutes and a maximum lifetime of 1 day: the _refresh token_ has a longer lifetime than the _access token_ to allow the user to remain authenticated, but for a maximum of one day.
4045

4146
The following are the resources:
4247
```C#
@@ -296,7 +301,7 @@ public getUserInfo(): Observable<any> {
296301
In this example, we use a scheduler to request a new _access token_ before it expires through the _refresh token_:
297302
```TypeScript
298303
/**
299-
* Optional strategy for refresh token through a scheduler.
304+
* Strategy for refresh token through a scheduler.
300305
* Will schedule a refresh at the appropriate time.
301306
*/
302307
public scheduleRefresh(): void {
@@ -312,8 +317,7 @@ public scheduleRefresh(): void {
312317
// Scheduler works.
313318
},
314319
(error: any) => {
315-
// Need to handle this error.
316-
console.log(error);
320+
this.handleRefreshTokenError();
317321
}
318322
);
319323
});
@@ -325,7 +329,7 @@ public scheduleRefresh(): void {
325329
public startupTokenRefresh(): void {
326330
// If the user is authenticated, uses the token stream
327331
// provided by angular2-jwt and flatMap the token.
328-
if (this.signinStatus.getValue()) {
332+
if (this.tokenNotExpired()) {
329333
const source = this.authHttp.tokenStream.flatMap(
330334
(token: string) => {
331335
const now: number = new Date().valueOf();
@@ -343,24 +347,10 @@ public startupTokenRefresh(): void {
343347
this.scheduleRefresh();
344348
},
345349
(error: any) => {
346-
// Need to handle this error.
347-
console.log(error);
350+
this.handleRefreshTokenError();
348351
}
349352
);
350353
});
351-
} else {
352-
// Revokes tokens.
353-
this.revokeToken();
354-
this.revokeRefreshToken();
355-
}
356-
}
357-
358-
/**
359-
* Unsubscribes from the scheduling of the refresh token.
360-
*/
361-
public unscheduleRefresh(): void {
362-
if (this.refreshSubscription) {
363-
this.refreshSubscription.unsubscribe();
364354
}
365355
}
366356

src/AngularSPAWebAPI/Config.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ public static IEnumerable<Client> GetClients()
5050
"roles",
5151
"WebAPI"
5252
},
53-
AllowOfflineAccess = true // For refresh token.
53+
AllowOfflineAccess = true, // For refresh token.
54+
RefreshTokenUsage = TokenUsage.OneTimeOnly,
55+
AbsoluteRefreshTokenLifetime = 86400,
56+
SlidingRefreshTokenLifetime = 1800,
57+
RefreshTokenExpiration = TokenExpiration.Sliding
5458
}
5559
};
5660
}

src/AngularSPAWebAPI/app/account/signin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export class Signin {
1919
this.authenticationService.signin(this.model.username, this.model.password)
2020
.subscribe(
2121
() => {
22-
// Optional strategy for refresh token through a scheduler.
22+
// Strategy for refresh token through a scheduler.
2323
this.authenticationService.scheduleRefresh();
2424

2525
// Gets user's data.

src/AngularSPAWebAPI/app/app.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class AppComponent implements OnInit {
3939
this.isAdmin = this.authenticationService.isInRole("administrator");
4040
});
4141

42-
// Optional strategy for refresh token through a scheduler.
42+
// Strategy for refresh token through a scheduler.
4343
this.authenticationService.startupTokenRefresh();
4444
}
4545

src/AngularSPAWebAPI/app/services/authentication.service.ts

Lines changed: 30 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Injectable } from '@angular/core';
22
import { Http, Headers, RequestOptions, Response } from '@angular/http';
3+
import { Router } from '@angular/router';
34
import { Observable } from 'rxjs/Observable';
45
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
56
import 'rxjs/add/operator/map';
@@ -26,7 +27,6 @@ import { BrowserStorage } from './browser-storage.service';
2627

2728
/**
2829
* Behavior subjects of the user's status & data.
29-
* https://netbasal.com/angular-2-persist-your-login-status-with-behaviorsubject-45da9ec43243#.14rltx9dh
3030
*/
3131
private signinStatus = new BehaviorSubject<boolean>(this.tokenNotExpired());
3232
private user = new BehaviorSubject<User>(this.getUser());
@@ -50,10 +50,23 @@ import { BrowserStorage } from './browser-storage.service';
5050
private headers: Headers;
5151
private options: RequestOptions;
5252

53-
constructor(private http: Http, private authHttp: AuthHttp, private browserStorage: BrowserStorage) {
53+
constructor(
54+
private router: Router,
55+
private http: Http,
56+
private authHttp: AuthHttp,
57+
private browserStorage: BrowserStorage
58+
) {
5459
// Creates header for post requests.
5560
this.headers = new Headers({ 'Content-Type': 'application/x-www-form-urlencoded' });
5661
this.options = new RequestOptions({ headers: this.headers });
62+
63+
if (!this.tokenNotExpired()) {
64+
// Removes user's info.
65+
this.browserStorage.remove("user_info");
66+
// Revokes tokens.
67+
this.revokeToken();
68+
this.revokeRefreshToken();
69+
}
5770
}
5871

5972
public signin(username: string, password: string): Observable<any> {
@@ -86,7 +99,7 @@ import { BrowserStorage } from './browser-storage.service';
8699
}
87100

88101
/**
89-
* Optional strategy for refresh token through a scheduler.
102+
* Strategy for refresh token through a scheduler.
90103
* Will schedule a refresh at the appropriate time.
91104
*/
92105
public scheduleRefresh(): void {
@@ -102,8 +115,7 @@ import { BrowserStorage } from './browser-storage.service';
102115
// Scheduler works.
103116
},
104117
(error: any) => {
105-
// Need to handle this error.
106-
console.log(error);
118+
this.handleRefreshTokenError();
107119
}
108120
);
109121
});
@@ -115,7 +127,7 @@ import { BrowserStorage } from './browser-storage.service';
115127
public startupTokenRefresh(): void {
116128
// If the user is authenticated, uses the token stream
117129
// provided by angular2-jwt and flatMap the token.
118-
if (this.signinStatus.getValue()) {
130+
if (this.tokenNotExpired()) {
119131
const source = this.authHttp.tokenStream.flatMap(
120132
(token: string) => {
121133
const now: number = new Date().valueOf();
@@ -133,15 +145,10 @@ import { BrowserStorage } from './browser-storage.service';
133145
this.scheduleRefresh();
134146
},
135147
(error: any) => {
136-
// Need to handle this error.
137-
console.log(error);
148+
this.handleRefreshTokenError();
138149
}
139150
);
140151
});
141-
} else {
142-
// Revokes tokens.
143-
this.revokeToken();
144-
this.revokeRefreshToken();
145152
}
146153
}
147154

@@ -154,6 +161,15 @@ import { BrowserStorage } from './browser-storage.service';
154161
}
155162
}
156163

164+
/**
165+
* Handles errors on refresh token, like expiration.
166+
*/
167+
public handleRefreshTokenError(): void {
168+
// In this sample, the user is forced to sign out.
169+
this.signout();
170+
this.router.navigate(['/home']);
171+
}
172+
157173
/**
158174
* Tries to get a new token using refresh token.
159175
*/
@@ -321,11 +337,9 @@ import { BrowserStorage } from './browser-storage.service';
321337
}
322338

323339
private getUser(): User {
324-
if (this.tokenNotExpired() && this.browserStorage.get("user_info")) {
325-
return JSON.parse(this.browserStorage.get("user_info"));;
340+
if (this.tokenNotExpired()) {
341+
return JSON.parse(this.browserStorage.get("user_info"));
326342
}
327-
// Removes user's info if the token is expired.
328-
this.browserStorage.remove("user_info");
329343
return new User();
330344
}
331345

src/AngularSPAWebAPI/wwwroot/dist/0.d98b3366a9f569342f20.chunk.js renamed to src/AngularSPAWebAPI/wwwroot/dist/0.c791ea99e0c0d6ecd2de.chunk.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/AngularSPAWebAPI/wwwroot/dist/1.d98b3366a9f569342f20.chunk.js renamed to src/AngularSPAWebAPI/wwwroot/dist/1.c791ea99e0c0d6ecd2de.chunk.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)