import { Injectable, Provider } from '@angular/core'; import { Http, Headers, Request, RequestOptions, RequestOptionsArgs, RequestMethod, Response } from '@angular/http'; import { Observable } from 'rxjs/Observable'; import 'rxjs/add/observable/fromPromise'; import 'rxjs/add/operator/mergeMap'; export interface IAuthConfig { globalHeaders: Array; headerName: string; headerPrefix: string; noJwtError: boolean; noTokenScheme?: boolean; tokenGetter: () => string | Promise; tokenName: string; } /** * Sets up the authentication configuration. */ export class AuthConfig { public globalHeaders: Array; public headerName: string; public headerPrefix: string; public noJwtError: boolean; public noTokenScheme: boolean; public tokenGetter: () => string | Promise; public tokenName: string; constructor(config: any = {}) { this.globalHeaders = config.globalHeaders || []; this.headerName = config.headerName || 'Authorization'; if (config.headerPrefix) { this.headerPrefix = config.headerPrefix + ' '; } else if (config.noTokenScheme) { this.headerPrefix = ''; } else { this.headerPrefix = 'Bearer '; } this.noJwtError = config.noJwtError || false; this.noTokenScheme = config.noTokenScheme || false; this.tokenGetter = config.tokenGetter || (() => localStorage.getItem(this.tokenName) as string); this.tokenName = config.tokenName || 'id_token'; } public getConfig(): IAuthConfig { return { globalHeaders: this.globalHeaders, headerName: this.headerName, headerPrefix: this.headerPrefix, noJwtError: this.noJwtError, noTokenScheme: this.noTokenScheme, tokenGetter: this.tokenGetter, tokenName: this.tokenName }; } } export class AuthHttpError extends Error { } /** * Allows for explicit authenticated HTTP requests. */ @Injectable() export class AuthHttp { private config: IAuthConfig; public tokenStream: Observable; constructor(options: AuthConfig, private http: Http, private defOpts?: RequestOptions) { this.config = options.getConfig(); this.tokenStream = new Observable((obs: any) => { obs.next(this.config.tokenGetter()); }); } private mergeOptions(providedOpts: RequestOptionsArgs, defaultOpts?: RequestOptions) { let newOptions = defaultOpts || new RequestOptions(); if (this.config.globalHeaders) { this.setGlobalHeaders(this.config.globalHeaders, providedOpts); } newOptions = newOptions.merge(new RequestOptions(providedOpts)); return newOptions; } private requestHelper(requestArgs: RequestOptionsArgs, additionalOptions?: RequestOptionsArgs): Observable { let options = new RequestOptions(requestArgs); if (additionalOptions) { options = options.merge(additionalOptions); } return this.request(new Request(this.mergeOptions(options, this.defOpts))); } private requestWithToken(req: Request, token: string): Observable { if (!tokenNotExpired(undefined, token)) { if (!this.config.noJwtError) { return new Observable((obs: any) => { obs.error(new AuthHttpError('No JWT present or has expired')); }); } } else { req.headers.set(this.config.headerName, this.config.headerPrefix + token); } return this.http.request(req); } public setGlobalHeaders(headers: Array, request: Request | RequestOptionsArgs) { if (!request.headers) { request.headers = new Headers(); } headers.forEach((header: Object) => { let key: string = Object.keys(header)[0]; let headerValue: string = (header as any)[key]; (request.headers as Headers).set(key, headerValue); }); } public request(url: string | Request, options?: RequestOptionsArgs): Observable { if (typeof url === 'string') { return this.get(url, options); // Recursion: transform url from String to Request } // else if ( ! url instanceof Request ) { // throw new Error('First argument must be a url string or Request instance.'); // } // from this point url is always an instance of Request; let req: Request = url as Request; let token: string | Promise = this.config.tokenGetter(); if (token instanceof Promise) { return Observable.fromPromise(token).mergeMap((jwtToken: string) => this.requestWithToken(req, jwtToken)); } else { return this.requestWithToken(req, token); } } public get(url: string, options?: RequestOptionsArgs): Observable { return this.requestHelper({ body: '', method: RequestMethod.Get, url: url }, options); } public post(url: string, body: any, options?: RequestOptionsArgs): Observable { return this.requestHelper({ body: body, method: RequestMethod.Post, url: url }, options); } public put(url: string, body: any, options?: RequestOptionsArgs): Observable { return this.requestHelper({ body: body, method: RequestMethod.Put, url: url }, options); } public delete(url: string, options?: RequestOptionsArgs): Observable { return this.requestHelper({ body: '', method: RequestMethod.Delete, url: url }, options); } public patch(url: string, body: any, options?: RequestOptionsArgs): Observable { return this.requestHelper({ body: body, method: RequestMethod.Patch, url: url }, options); } public head(url: string, options?: RequestOptionsArgs): Observable { return this.requestHelper({ body: '', method: RequestMethod.Head, url: url }, options); } public options(url: string, options?: RequestOptionsArgs): Observable { return this.requestHelper({ body: '', method: RequestMethod.Options, url: url }, options); } } /** * Helper class to decode and find JWT expiration. */ export class JwtHelper { public urlBase64Decode(str: string): string { let output = str.replace(/-/g, '+').replace(/_/g, '/'); switch (output.length % 4) { case 0: { break; } case 2: { output += '=='; break; } case 3: { output += '='; break; } default: { throw 'Illegal base64url string!'; } } return decodeURIComponent(encodeURI(typeof window === 'undefined' ? atob(output) : window.atob(output))); } public decodeToken(token: string): any { let parts = token.split('.'); if (parts.length !== 3) { throw new Error('JWT must have 3 parts'); } let decoded = this.urlBase64Decode(parts[1]); if (!decoded) { throw new Error('Cannot decode the token'); } return JSON.parse(decoded); } public getTokenExpirationDate(token: string): Date { let decoded: any; decoded = this.decodeToken(token); if (!decoded.hasOwnProperty('exp')) { return null; } let date = new Date(0); // The 0 here is the key, which sets the date to the epoch date.setUTCSeconds(decoded.exp); return date; } public isTokenExpired(token: string, offsetSeconds?: number): boolean { let date = this.getTokenExpirationDate(token); offsetSeconds = offsetSeconds || 0; if (date == null) { return false; } // Token expired? return !(date.valueOf() > (new Date().valueOf() + (offsetSeconds * 1000))); } } /** * Checks for presence of token and that token hasn't expired. * For use with the @CanActivate router decorator and NgIf */ export function tokenNotExpired(tokenName = 'id_token', jwt?: string): boolean { const token: string = jwt || localStorage.getItem(tokenName); const jwtHelper = new JwtHelper(); return token != null && !jwtHelper.isTokenExpired(token); } export const AUTH_PROVIDERS: Provider[] = [ { provide: AuthHttp, deps: [Http, RequestOptions], useFactory: (http: Http, options: RequestOptions) => { return new AuthHttp(new AuthConfig(), http, options); } } ]; export function provideAuth(config = {}): Provider[] { return [ { provide: AuthHttp, deps: [Http, RequestOptions], useFactory: (http: Http, options: RequestOptions) => { return new AuthHttp(new AuthConfig(config), http, options); } } ]; }