Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for code flow silent-refresh and popup #609

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs-src/silent-refresh.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ This file is loaded into the hidden iframe after getting new tokens. Its only ta
<html>
<body>
<script>
parent.postMessage(location.hash, location.origin);
window.parent.postMessage(location.hash || ('#' + location.search), location.origin);
</script>
</body>
</html>
Expand Down
89 changes: 62 additions & 27 deletions projects/lib/src/oauth-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,8 @@ export class OAuthService extends AuthConfig implements OnDestroy {
}

protected refreshInternal(params, noPrompt): Promise<TokenResponse|OAuthEvent> {
if (this.responseType === 'code') {

if (!this.silentRefreshRedirectUri && this.responseType === 'code') {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KevinCathcart wouldn't it also be possible to check if there is a refreshToken available, instead of checking if the silentRefreshRedirectUri ist configured?

return this.refreshToken();
} else {
return this.silentRefresh(params, noPrompt);
Expand Down Expand Up @@ -833,14 +834,7 @@ export class OAuthService extends AuthConfig implements OnDestroy {
this.tryLogin({
customHashFragment: message,
preventClearHashAfterLogin: true,
onLoginError: err => {
this.eventsSubject.next(
new OAuthErrorEvent('silent_refresh_error', err)
);
},
onTokenReceived: () => {
this.eventsSubject.next(new OAuthSuccessEvent('silently_refreshed'));
}
customRedirectUri: this.silentRefreshRedirectUri || this.redirectUri
}).catch(err => this.debug('tryLogin during silent refresh failed', err));
};

Expand Down Expand Up @@ -900,7 +894,7 @@ export class OAuthService extends AuthConfig implements OnDestroy {
first()
);
const success = this.events.pipe(
filter(e => e.type === 'silently_refreshed'),
filter(e => e.type === 'token_received'),
first()
);
const timeout = of(
Expand All @@ -909,22 +903,35 @@ export class OAuthService extends AuthConfig implements OnDestroy {

return race([errors, success, timeout])
.pipe(
tap(e => {
if (e.type === 'silent_refresh_timeout') {
this.eventsSubject.next(e);
}
}),
map(e => {
if (e instanceof OAuthErrorEvent) {
if (e.type === 'silent_refresh_timeout') {
this.eventsSubject.next(e);
} else {
e = new OAuthErrorEvent('silent_refresh_error', e);
this.eventsSubject.next(e);
}
throw e;
} else if (e.type === 'token_received') {
e = new OAuthSuccessEvent('silently_refreshed');
this.eventsSubject.next(e);
}
return e;
})
)
.toPromise();
}

/**
* This method exists for backwards compatibility.
* {@link OAuthService#initLoginFlowInPopup} handles both code
* and implicit flows.
*/
public initImplicitFlowInPopup(options?: { height?: number, width?: number }) {
return this.initLoginFlowInPopup(options);
}

public initLoginFlowInPopup(options?: { height?: number, width?: number }) {
options = options || {};
return this.createLoginUrl(null, null, this.silentRefreshRedirectUri, false, {
display: 'popup'
Expand Down Expand Up @@ -959,10 +966,12 @@ export class OAuthService extends AuthConfig implements OnDestroy {

const listener = (e: MessageEvent) => {
const message = this.processMessageEventMessage(e);

if (message && message !== null) {
this.tryLogin({
customHashFragment: message,
preventClearHashAfterLogin: true,
customRedirectUri: this.silentRefreshRedirectUri,
}).then(() => {
cleanup();
resolve();
Expand All @@ -973,6 +982,7 @@ export class OAuthService extends AuthConfig implements OnDestroy {
} else {
console.log('false event firing');
}

};

window.addEventListener('message', listener);
Expand Down Expand Up @@ -1402,25 +1412,50 @@ export class OAuthService extends AuthConfig implements OnDestroy {
*/
public tryLogin(options: LoginOptions = null): Promise<boolean> {
if (this.config.responseType === 'code') {
return this.tryLoginCodeFlow().then(_ => true);
} else {
return this.tryLoginCodeFlow(options).then(_ => true);
}
else {
return this.tryLoginImplicitFlow(options);
}
}

public tryLoginCodeFlow(): Promise<void> {


private parseQueryString(queryString: string): object {
if (!queryString || queryString.length === 0) {
return {};
}

if (queryString.charAt(0) === '?') {
queryString = queryString.substr(1);
}

return this.urlHelper.parseQueryString(queryString);


}

public tryLoginCodeFlow(options: LoginOptions = null): Promise<void> {
options = options || {};

const querySource = options.customHashFragment ?
options.customHashFragment.substring(1) :
window.location.search;

const parts = this.getCodePartsFromUrl(window.location.search);

const code = parts['code'];
const state = parts['state'];

const href = location.href
.replace(/[&\?]code=[^&\$]*/, '')
.replace(/[&\?]scope=[^&\$]*/, '')
.replace(/[&\?]state=[^&\$]*/, '')
.replace(/[&\?]session_state=[^&\$]*/, '');
if (!options.preventClearHashAfterLogin) {
const href = location.href
.replace(/[&\?]code=[^&\$]*/, '')
.replace(/[&\?]scope=[^&\$]*/, '')
.replace(/[&\?]state=[^&\$]*/, '')
.replace(/[&\?]session_state=[^&\$]*/, '');

history.replaceState(null, window.name, href);
history.replaceState(null, window.name, href);
}

let [nonceInState, userState] = this.parseState(state);
this.state = userState;
Expand All @@ -1446,7 +1481,7 @@ export class OAuthService extends AuthConfig implements OnDestroy {

if (code) {
return new Promise((resolve, reject) => {
this.getTokenFromCode(code).then(result => {
this.getTokenFromCode(code, options).then(result => {
resolve();
}).catch(err => {
reject(err);
Expand Down Expand Up @@ -1477,11 +1512,11 @@ export class OAuthService extends AuthConfig implements OnDestroy {
/**
* Get token using an intermediate code. Works for the Authorization Code flow.
*/
private getTokenFromCode(code: string): Promise<TokenResponse> {
private getTokenFromCode(code: string, options: LoginOptions): Promise<object> {
let params = new HttpParams()
.set('grant_type', 'authorization_code')
.set('code', code)
.set('redirect_uri', this.redirectUri);
.set('redirect_uri', options.customRedirectUri || this.redirectUri);

if (!this.disablePKCE) {
const pkciVerifier = this._storage.getItem('PKCI_verifier');
Expand Down
19 changes: 16 additions & 3 deletions projects/lib/src/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* Additional options that can be passt to tryLogin.
* Additional options that can be passed to tryLogin.
*/
export class LoginOptions {
/**
Expand Down Expand Up @@ -28,7 +28,12 @@ export class LoginOptions {
/**
* A custom hash fragment to be used instead of the
* actual one. This is used for silent refreshes, to
* pass the iframes hash fragment to this method.
* pass the iframes hash fragment to this method, and
* is also used by popup flows in the same manner.
* This can be used with code flow, where is must be set

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: is instead of it

* to a hash symbol followed by the querystring. The
* question mark is optional, but may be present following
* the hash symbol.
*/
customHashFragment?: string;

Expand All @@ -45,9 +50,17 @@ export class LoginOptions {
/**
* Normally, you want to clear your hash fragment after
* the lib read the token(s) so that they are not displayed
* anymore in the url. If not, set this to true.
* anymore in the url. If not, set this to true. For code flow
* this controls removing query string values.
*/
preventClearHashAfterLogin? = false;

/**
* Set this for code flow if you used a custom redirect Uri
* when retrieving the code. This is used internally for silent
* refresh and popup flows.
*/
customRedirectUri?: string;
}

/**
Expand Down