UNPKG

@salte-auth/salte-auth

Version:
245 lines (206 loc) 7.29 kB
import { AccessToken, Common, GUID, Interceptors } from '../utils'; import { Provider } from './core/provider'; import { SalteAuthError } from './core/salte-auth-error'; export class OAuth2Provider extends Provider { public accessToken?: AccessToken; public constructor(config?: OAuth2Provider.Config) { super(config); this.sync(); } public connected(): void { this.required('clientID', 'responseType'); } public async secure(request: Interceptors.XHR.ExtendedXMLHttpRequest | Request): Promise<'login' | boolean> { if (this.config.responseType === 'token') { if (this.accessToken.expired) { return 'login'; } if (request) { if (request instanceof Request) { request.headers.set('Authorization', `Bearer ${this.accessToken.raw}`); } else if (request instanceof XMLHttpRequest) { request.setRequestHeader('Authorization', `Bearer ${this.accessToken.raw}`); } else { throw new SalteAuthError({ code: 'unknown_request', message: `Unknown request type. (${request})`, }); } } } return true; } public $validate(options: OAuth2Provider.Validation): void { try { if (!options) { throw new SalteAuthError({ code: 'empty_response', message: `The response provided was empty, this is most likely due to the configured handler not providing it.` }); } if (options.error) { throw new SalteAuthError({ code: options.error, message: `${options.error_description ? options.error_description : options.error}${options.error_uri ? ` (${options.error_uri})` : ''}`, }); } const { code, access_token, state, expires_in, token_type } = options; if (this.validation('state') && this.storage.get('state') !== state) { throw new SalteAuthError({ code: 'invalid_state', message: 'State provided by identity provider did not match local state.', }); } const types = this.storage.get('response-type', '').split(' '); if (Common.includes(types, 'code')) { if (!code) { throw new SalteAuthError({ code: 'invalid_code', message: 'Expected a code to be returned by the Provider.', }); } } else if (Common.includes(types, 'token')) { if (!access_token) { throw new SalteAuthError({ code: 'invalid_access_token', message: 'Expected an access token to be returned by the Provider.', }); } } if (code) { this.storage.set('code.raw', code); this.storage.delete('access-token.raw'); this.storage.delete('access-token.expiration'); this.storage.delete('access-token.type'); } else if (access_token) { this.storage.set('access-token.raw', access_token); this.storage.set('access-token.expiration', Date.now() + (Number(expires_in) * 1000)); this.storage.set('access-token.type', token_type); this.storage.delete('code.raw'); } } finally { this.storage.delete('state'); } } public validate(options: OAuth2Provider.Validation): void { this.logger.trace('[validate] (options): ', options); try { this.$validate(options); } catch (error) { this.emit('login', error); throw error; } finally { this.sync(); } this.emit('login', null, this.code || this.accessToken); } public get code(): string { return this.storage.get('code.raw'); } public $login(options: OAuth2Provider.OverrideOptions = {}): string { const state = GUID.state(this.$name); const responseType = options.responseType || this.config.responseType; this.storage.set('state', state); this.storage.set('response-type', responseType); return this.url(this.login, { ...this.config.queryParams && this.config.queryParams('login'), client_id: this.config.clientID, response_type: responseType, redirect_uri: this.redirectUrl('login'), scope: this.config.scope, state, }); } public sync(): void { this.logger.trace('[sync] updating access token'); this.accessToken = new AccessToken( this.storage.get('access-token.raw'), this.storage.get('access-token.expiration'), this.storage.get('access-token.type') ); } } export interface OAuth2Provider { config: OAuth2Provider.Config; on(name: 'login', listener: (error?: Error, accessToken?: AccessToken) => void): void; on(name: 'login', listener: (error?: Error, code?: string) => void): void; on(name: 'logout', listener: (error?: Error) => void): void; } export declare namespace OAuth2Provider { export interface Config extends Provider.Config { // TODO: Need to figure how to fix this, might require separating OpenIDProvider from OAuth2Provider... /* eslint-disable tsdoc/syntax */ /** * Determines whether a authorization code (server) or access token (client) should be returned. * @type {('code'|'token')} */ responseType?: string; /* eslint-enable tsdoc/syntax */ /** * A list of space-delimited claims used to determine what user information is provided and what access is given. */ scope?: string; /** * The client id of your identity provider */ clientID: string; validation?: boolean | ValidationOptions; } export interface OverrideOptions { // TODO: Need to figure how to fix this, might require separating OpenIDProvider from OAuth2Provider... /* eslint-disable tsdoc/syntax */ /** * Determines whether a authorization code (server) or access token (client) should be returned. * @type {('code'|'token')} */ responseType?: string; /* eslint-enable tsdoc/syntax */ } export interface ValidationOptions extends Provider.ValidationOptions { /** * Disables cross-site forgery validation via "state". */ state: boolean; } export interface Validation { /** * An error code sent from the Provider */ error: ('unauthorized_client'|'access_denied'|'unsupported_response_type'|'invalid_scope'|'server_error'|'temporarily_unavailable'); /** * Human-readable message sent back by the Provider. */ error_description?: string; /** * A URI to a human-readable web page with information about the error. */ error_uri?: string; /** * A value sent back by the server to the client. * * Used to prevent cross-site request forgery. */ state: string; /** * The authorization code generated by the Provider. * * Generally used by a backend server to generate an access token. */ code: string; /** * The access token issued by the Provider. */ access_token: string; /** * The type of the token issued. */ token_type: ('bearer'|'mac'); /** * The lifetime (in seconds) of the access_token. * * For example, the value "3600" denotes that the access token will * expire in one hour from the time the response was generated. */ expires_in: string; } }