Skip to content

Commit 51e3197

Browse files
Add support for code flow silent-refresh and popup
1 parent a1652dc commit 51e3197

File tree

3 files changed

+66
-39
lines changed

3 files changed

+66
-39
lines changed

docs-src/silent-refresh.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ This file is loaded into the hidden iframe after getting new tokens. Its only ta
6666
<html>
6767
<body>
6868
<script>
69-
parent.postMessage(location.hash, location.origin);
69+
window.parent.postMessage(location.hash || ('#' + location.search), location.origin);
7070
</script>
7171
</body>
7272
</html>

projects/lib/src/oauth-service.ts

Lines changed: 49 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -827,14 +827,7 @@ export class OAuthService extends AuthConfig implements OnDestroy {
827827
this.tryLogin({
828828
customHashFragment: message,
829829
preventClearHashAfterLogin: true,
830-
onLoginError: err => {
831-
this.eventsSubject.next(
832-
new OAuthErrorEvent('silent_refresh_error', err)
833-
);
834-
},
835-
onTokenReceived: () => {
836-
this.eventsSubject.next(new OAuthSuccessEvent('silently_refreshed'));
837-
}
830+
customRedirectUri: this.silentRefreshRedirectUri || this.redirectUri
838831
}).catch(err => this.debug('tryLogin during silent refresh failed', err));
839832
};
840833

@@ -896,7 +889,7 @@ export class OAuthService extends AuthConfig implements OnDestroy {
896889
first()
897890
);
898891
const success = this.events.pipe(
899-
filter(e => e.type === 'silently_refreshed'),
892+
filter(e => e.type === 'token_received'),
900893
first()
901894
);
902895
const timeout = of(
@@ -905,22 +898,35 @@ export class OAuthService extends AuthConfig implements OnDestroy {
905898

906899
return race([errors, success, timeout])
907900
.pipe(
908-
tap(e => {
909-
if (e.type === 'silent_refresh_timeout') {
910-
this.eventsSubject.next(e);
911-
}
912-
}),
913901
map(e => {
914902
if (e instanceof OAuthErrorEvent) {
903+
if (e.type === 'silent_refresh_timeout') {
904+
this.eventsSubject.next(e);
905+
} else {
906+
e = new OAuthErrorEvent('silent_refresh_error', e);
907+
this.eventsSubject.next(e);
908+
}
915909
throw e;
910+
} else if (e.type === 'token_received') {
911+
e = new OAuthSuccessEvent('silently_refreshed');
912+
this.eventsSubject.next(e);
916913
}
917914
return e;
918915
})
919916
)
920917
.toPromise();
921918
}
922919

920+
/**
921+
* This method exists for backwards compatibility.
922+
* {@link OAuthService#initLoginFlowInPopup} handles both code
923+
* and implicit flows.
924+
*/
923925
public initImplicitFlowInPopup(options?: { height?: number, width?: number }) {
926+
return this.initLoginFlowInPopup(options);
927+
}
928+
929+
public initLoginFlowInPopup(options?: { height?: number, width?: number }) {
924930
options = options || {};
925931
return this.createLoginUrl(null, null, this.silentRefreshRedirectUri, false, {
926932
display: 'popup'
@@ -940,6 +946,7 @@ export class OAuthService extends AuthConfig implements OnDestroy {
940946
this.tryLogin({
941947
customHashFragment: message,
942948
preventClearHashAfterLogin: true,
949+
customRedirectUri: this.silentRefreshRedirectUri
943950
}).then(() => {
944951
cleanup();
945952
resolve();
@@ -1264,7 +1271,7 @@ export class OAuthService extends AuthConfig implements OnDestroy {
12641271
}
12651272

12661273
return url;
1267-
1274+
12681275
}
12691276

12701277
initImplicitFlowInternal(
@@ -1373,7 +1380,7 @@ export class OAuthService extends AuthConfig implements OnDestroy {
13731380
*/
13741381
public tryLogin(options: LoginOptions = null): Promise<boolean> {
13751382
if (this.config.responseType === 'code') {
1376-
return this.tryLoginCodeFlow().then(_ => true);
1383+
return this.tryLoginCodeFlow(options).then(_ => true);
13771384
}
13781385
else {
13791386
return this.tryLoginImplicitFlow(options);
@@ -1395,20 +1402,27 @@ export class OAuthService extends AuthConfig implements OnDestroy {
13951402

13961403
}
13971404

1398-
public tryLoginCodeFlow(): Promise<void> {
1405+
public tryLoginCodeFlow(options: LoginOptions = null): Promise<void> {
1406+
options = options || {};
13991407

1400-
const parts = this.parseQueryString(window.location.search)
1408+
const querySource = options.customHashFragment ?
1409+
options.customHashFragment.substring(1) :
1410+
window.location.search;
1411+
1412+
const parts = this.parseQueryString(querySource)
14011413

14021414
const code = parts['code'];
14031415
const state = parts['state'];
14041416

1405-
const href = location.href
1406-
.replace(/[&\?]code=[^&\$]*/, '')
1407-
.replace(/[&\?]scope=[^&\$]*/, '')
1408-
.replace(/[&\?]state=[^&\$]*/, '')
1409-
.replace(/[&\?]session_state=[^&\$]*/, '');
1417+
if (!options.preventClearHashAfterLogin) {
1418+
const href = location.href
1419+
.replace(/[&\?]code=[^&\$]*/, '')
1420+
.replace(/[&\?]scope=[^&\$]*/, '')
1421+
.replace(/[&\?]state=[^&\$]*/, '')
1422+
.replace(/[&\?]session_state=[^&\$]*/, '');
14101423

1411-
history.replaceState(null, window.name, href);
1424+
history.replaceState(null, window.name, href);
1425+
}
14121426

14131427
let [nonceInState, userState] = this.parseState(state);
14141428
this.state = userState;
@@ -1434,7 +1448,7 @@ export class OAuthService extends AuthConfig implements OnDestroy {
14341448

14351449
if (code) {
14361450
return new Promise((resolve, reject) => {
1437-
this.getTokenFromCode(code).then(result => {
1451+
this.getTokenFromCode(code, options).then(result => {
14381452
resolve();
14391453
}).catch(err => {
14401454
reject(err);
@@ -1448,11 +1462,11 @@ export class OAuthService extends AuthConfig implements OnDestroy {
14481462
/**
14491463
* Get token using an intermediate code. Works for the Authorization Code flow.
14501464
*/
1451-
private getTokenFromCode(code: string): Promise<object> {
1465+
private getTokenFromCode(code: string, options: LoginOptions): Promise<object> {
14521466
let params = new HttpParams()
14531467
.set('grant_type', 'authorization_code')
14541468
.set('code', code)
1455-
.set('redirect_uri', this.redirectUri);
1469+
.set('redirect_uri', options.customRedirectUri || this.redirectUri);
14561470

14571471
if (!this.disablePKCE) {
14581472
const pkciVerifier = this._storage.getItem('PKCI_verifier');
@@ -1503,32 +1517,32 @@ export class OAuthService extends AuthConfig implements OnDestroy {
15031517
(tokenResponse) => {
15041518
this.debug('refresh tokenResponse', tokenResponse);
15051519
this.storeAccessTokenResponse(
1506-
tokenResponse.access_token,
1507-
tokenResponse.refresh_token,
1520+
tokenResponse.access_token,
1521+
tokenResponse.refresh_token,
15081522
tokenResponse.expires_in,
15091523
tokenResponse.scope);
15101524

15111525
if (this.oidc && tokenResponse.id_token) {
1512-
this.processIdToken(tokenResponse.id_token, tokenResponse.access_token).
1526+
this.processIdToken(tokenResponse.id_token, tokenResponse.access_token).
15131527
then(result => {
15141528
this.storeIdToken(result);
1515-
1529+
15161530
this.eventsSubject.next(new OAuthSuccessEvent('token_received'));
15171531
this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed'));
1518-
1532+
15191533
resolve(tokenResponse);
15201534
})
15211535
.catch(reason => {
15221536
this.eventsSubject.next(new OAuthErrorEvent('token_validation_error', reason));
15231537
console.error('Error validating tokens');
15241538
console.error(reason);
1525-
1539+
15261540
reject(reason);
15271541
});
15281542
} else {
15291543
this.eventsSubject.next(new OAuthSuccessEvent('token_received'));
15301544
this.eventsSubject.next(new OAuthSuccessEvent('token_refreshed'));
1531-
1545+
15321546
resolve(tokenResponse);
15331547
}
15341548
},
@@ -1688,7 +1702,7 @@ export class OAuthService extends AuthConfig implements OnDestroy {
16881702
): boolean {
16891703
const savedNonce = this._storage.getItem('nonce');
16901704
if (savedNonce !== nonceInState) {
1691-
1705+
16921706
const err = 'Validating access_token failed, wrong state/nonce.';
16931707
console.error(err, savedNonce, nonceInState);
16941708
return false;

projects/lib/src/types.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Additional options that can be passt to tryLogin.
2+
* Additional options that can be passed to tryLogin.
33
*/
44
export class LoginOptions {
55
/**
@@ -28,7 +28,12 @@ export class LoginOptions {
2828
/**
2929
* A custom hash fragment to be used instead of the
3030
* actual one. This is used for silent refreshes, to
31-
* pass the iframes hash fragment to this method.
31+
* pass the iframes hash fragment to this method, and
32+
* is also used by popup flows in the same manner.
33+
* This can be used with code flow, where is must be set
34+
* to a hash symbol followed by the querystring. The
35+
* question mark is optional, but may be present following
36+
* the hash symbol.
3237
*/
3338
customHashFragment?: string;
3439

@@ -45,9 +50,17 @@ export class LoginOptions {
4550
/**
4651
* Normally, you want to clear your hash fragment after
4752
* the lib read the token(s) so that they are not displayed
48-
* anymore in the url. If not, set this to true.
53+
* anymore in the url. If not, set this to true. For code flow
54+
* this controls removing query string values.
4955
*/
5056
preventClearHashAfterLogin? = false;
57+
58+
/**
59+
* Set this for code flow if you used a custom redirect Uri
60+
* when retrieving the code. This is used internally for silent
61+
* refresh and popup flows.
62+
*/
63+
customRedirectUri?: string;
5164
}
5265

5366
/**

0 commit comments

Comments
 (0)