diff --git a/package-lock.json b/package-lock.json index ffb8c75..749e2b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "react-oauth2-pkce", - "version": "2.0.6", + "version": "2.0.8", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 4acbd24..c6c16e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "react-oauth2-pkce", - "version": "2.0.7", + "version": "2.0.8", "description": "Authenticate against generic OAuth2 using PKCE", "author": "Gardner Bickford ", "license": "MIT", diff --git a/src/AuthService.ts b/src/AuthService.ts index d4d1d8c..43dbdec 100644 --- a/src/AuthService.ts +++ b/src/AuthService.ts @@ -18,6 +18,7 @@ export interface AuthServiceProps { scopes: string[] autoRefresh?: boolean refreshSlack?: number + onRedirectCallback?: (state?: string | null) => void; } export interface AuthTokens { @@ -46,6 +47,10 @@ export interface TokenRequestBody { codeVerifier?: string } +export interface AuthOptions { + state?: string; +} + export class AuthService { props: AuthServiceProps timeout?: number @@ -67,6 +72,20 @@ export class AuthService { } else if (this.props.autoRefresh) { this.startTimer() } + + this.tryInvokeRedirectCallback(); + } + + private tryInvokeRedirectCallback() { + + // ensure we only call redirect after successful authorization + const postAuthRedirect = window.localStorage.getItem('postAuthRedirect'); + if (this.props.onRedirectCallback && postAuthRedirect && this.isAuthenticated() === true) { + const state = window.localStorage.getItem('postAuthState') + window.localStorage.removeItem('postAuthState') + window.localStorage.removeItem('postAuthRedirect') + this.props.onRedirectCallback(state) + } } getUser(): {} { @@ -77,6 +96,10 @@ export class AuthService { } getCodeFromLocation(location: Location): string | null { + return this.getValueFromLocation(location, 'code'); + } + + getValueFromLocation(location: Location, name: string): string | null { const split = location.toString().split('?') if (split.length < 2) { return null @@ -84,13 +107,14 @@ export class AuthService { const pairs = split[1].split('&') for (const pair of pairs) { const [key, value] = pair.split('=') - if (key === 'code') { + if (key === name) { return decodeURIComponent(value || '') } } return null } + removeCodeFromLocation(): void { const [base, search] = window.location.href.split('?') if (!search) { @@ -165,12 +189,12 @@ export class AuthService { } } - async login(): Promise { - this.authorize() + async login(options?: AuthOptions): Promise { + this.authorize(options) } // this will do a full page reload and to to the OAuth2 provider's login page and then redirect back to redirectUri - authorize(): boolean { + authorize(options?: AuthOptions): boolean { const { clientId, provider, authorizeEndpoint, redirectUri, scopes, audience } = this.props const pkce = createPKCECodes() @@ -186,7 +210,8 @@ export class AuthService { redirectUri, ...(audience && { audience }), codeChallenge, - codeChallengeMethod: 'S256' + codeChallengeMethod: 'S256', + ...(options && { ...options }) } // Responds with a 302 redirect const url = `${authorizeEndpoint || `${provider}/authorize`}?${toUrlEncoded(query)}` @@ -297,6 +322,16 @@ export class AuthService { window.localStorage.removeItem('preAuthUri') console.log({ uri }) if (uri !== null) { + + const state = this.getValueFromLocation(location, 'state'); + if (state) { + window.localStorage.setItem('postAuthState', state); + } + + if (this.props.onRedirectCallback) { + window.localStorage.setItem('postAuthRedirect', "true"); + } + window.location.replace(uri) } this.removeCodeFromLocation()