import { getUniqueScopes, createQueryParams, runPopup, parseQueryResult, encodeState, createRandomString, runIframe, sha256, bufferToBase64UrlEncoded, oauthToken, openPopup } from './utils'; import Cache from './cache'; import TransactionManager from './transaction-manager'; import AsyncTokenVerifier from './async-token-verifier'; import ClientStorage from './storage'; //this is necessary to export the type definitions used in this file import './global'; const DEFAULT_SCOPE = 'openid profile email'; export default class Auth0Login { cache: Cache; transactionManager: TransactionManager; tokenVerifier: AsyncTokenVerifier; storage: ClientStorage; constructor(private options: Auth0LoginOptions) { this.cache = new Cache(); this.transactionManager = new TransactionManager(); this.storage = new ClientStorage(); const { domain, client_id } = options; this.tokenVerifier = new AsyncTokenVerifier({ domain, client_id }); } private _url(path) { return `${this._domainUrl()}${path}`; } private _domainUrl() { return `https://${this.options.domain}`; } private _getParams( authorizeOptions: BaseLoginOptions, state: string, nonce: string, code_challenge: string, redirect_uri: string ): AuthorizeOptions { const { domain, ...withoutDomain } = this.options; return { ...withoutDomain, ...authorizeOptions, scope: getUniqueScopes( `${DEFAULT_SCOPE} ${this.options.scope || ''} ${authorizeOptions.scope || ''}` ), response_type: 'code', response_mode: 'query', state, nonce, redirect_uri, code_challenge, code_challenge_method: 'S256' }; } private _authorizeUrl(authorizeOptions: AuthorizeOptions) { return this._url(`/authorize?${createQueryParams(authorizeOptions)}`); } public async init() { if (!this.storage.get('auth0.is.authenticated')) { return; } try { await this.getTokenSilently({ audience: this.options.audience, scope: this.options.scope, ignoreCache: true }); } catch (error) { console.warn('could not fetch token', error); } } public async loginWithPopup(options: PopupLoginOptions) { const popup = await openPopup(); const { ...authorizeOptions } = options; const stateIn = encodeState(createRandomString()); const nonceIn = createRandomString(); const code_verifier = createRandomString(); const code_challengeBuffer = await sha256(code_verifier); const code_challenge = bufferToBase64UrlEncoded(code_challengeBuffer); const params = this._getParams( authorizeOptions, stateIn, nonceIn, code_challenge, window.location.origin ); const url = this._authorizeUrl({ ...params, response_mode: 'web_message' }); const codeResult = await runPopup(popup, url); if (stateIn !== codeResult.state) { throw new Error('Invalid state'); } const authResult = await oauthToken({ baseUrl: this._domainUrl(), audience: this.options.audience, client_id: this.options.client_id, code_verifier, code: codeResult.code }); const user = await this.tokenVerifier.verify({ id_token: authResult.id_token, nonce: nonceIn }); const cacheEntry = { ...authResult, user, scope: params.scope, audience: params.audience || 'default' }; this.cache.save(cacheEntry); this.storage.save('auth0.is.authenticated', true, { daysUntilExpire: 1 }); } public async getUser( options: GetUserOptions = { audience: this.options.audience || 'default', scope: this.options.scope || DEFAULT_SCOPE } ) { options.scope = getUniqueScopes(`${DEFAULT_SCOPE} ${options.scope || ''}`); const cache = this.cache.get(options); return cache && cache.user; } public async loginWithRedirect(options: RedirectLoginOptions) { const { redirect_uri, appState, ...authorizeOptions } = options; const stateIn = encodeState(createRandomString()); const nonceIn = createRandomString(); const code_verifier = createRandomString(); const code_challengeBuffer = await sha256(code_verifier); const code_challenge = bufferToBase64UrlEncoded(code_challengeBuffer); const params = this._getParams( authorizeOptions, stateIn, nonceIn, code_challenge, redirect_uri ); const url = this._authorizeUrl(params); this.transactionManager.create({ state: stateIn, nonce: nonceIn, code_verifier, code_challenge: code_challenge, appState, scope: params.scope, audience: params.audience || 'default' }); window.location.assign(url); } public async handleRedirectCallback() { if (!window.location.search) { throw new Error( 'There is no query params available at `window.location.search`.' ); } const codeResult = parseQueryResult(window.location.search.substr(1)); const transaction = this.transactionManager.get(codeResult.state); if (!transaction) { throw new Error('Invalid state'); } this.transactionManager.remove(codeResult.state); const authResult = await oauthToken({ baseUrl: this._domainUrl(), audience: this.options.audience, client_id: this.options.client_id, code_verifier: transaction.code_verifier, code: codeResult.code }); const user = await this.tokenVerifier.verify({ id_token: authResult.id_token, nonce: transaction.nonce }); const cacheEntry = { ...authResult, user, audience: transaction.audience, scope: transaction.scope }; this.cache.save(cacheEntry); this.storage.save('auth0.is.authenticated', true, { daysUntilExpire: 1 }); } public async getTokenSilently( options: GetTokenSilentlyOptions = { audience: this.options.audience, scope: this.options.scope || DEFAULT_SCOPE, ignoreCache: false } ) { options.scope = getUniqueScopes(`${DEFAULT_SCOPE} ${options.scope || ''}`); if (!options.ignoreCache) { const cache = this.cache.get({ scope: options.scope, audience: options.audience || 'default' }); if (cache) { return cache.access_token; } } const stateIn = encodeState(createRandomString()); const nonceIn = createRandomString(); const code_verifier = createRandomString(); const code_challengeBuffer = await sha256(code_verifier); const code_challenge = bufferToBase64UrlEncoded(code_challengeBuffer); const authorizeOptions = { ...this.options, audience: options.audience, scope: options.scope }; const params = this._getParams( authorizeOptions, stateIn, nonceIn, code_challenge, window.location.origin ); const url = this._authorizeUrl({ ...params, prompt: 'none', response_mode: 'web_message' }); const codeResult = await runIframe(url, this._domainUrl()); if (stateIn !== codeResult.state) { throw new Error('Invalid state'); } const authResult = await oauthToken({ baseUrl: this._domainUrl(), audience: this.options.audience, client_id: this.options.client_id, code_verifier, code: codeResult.code }); const user = await this.tokenVerifier.verify({ id_token: authResult.id_token, nonce: nonceIn }); const cacheEntry = { ...authResult, user, scope: params.scope, audience: params.audience || 'default' }; this.cache.save(cacheEntry); this.storage.save('auth0.is.authenticated', true, { daysUntilExpire: 1 }); return authResult.access_token; } public async getTokenWithPopup( options: GetTokenWithPopupOptions = { audience: this.options.audience, scope: this.options.scope || DEFAULT_SCOPE } ) { options.scope = getUniqueScopes( `${DEFAULT_SCOPE} ${this.options.scope || ''} ${options.scope || ''}` ); await this.loginWithPopup(options); const cache = this.cache.get({ scope: options.scope, audience: options.audience || 'default' }); return cache.access_token; } public async isAuthenticated() { const user = await this.getUser(); return !!user; } public logout(options: LogoutOptions = {}) { if (options.client_id !== null) { options.client_id = options.client_id || this.options.client_id; } else { delete options.client_id; } this.storage.remove('auth0.is.authenticated'); const url = this._url(`/v2/logout?${createQueryParams(options)}`); window.location.assign(url); } }