UNPKG

@glamtime/oauth-oidc-client

Version:

Secure your Angular app using the latest standards for OpenID Connect & OAuth2. Provides support for token refresh, all modern OIDC Identity Providers and more.

421 lines (411 loc) 18 kB
import * as i0 from '@angular/core'; import { InjectionToken, Injectable, Inject, PLATFORM_ID, NgModule } from '@angular/core'; import { DOCUMENT } from '@angular/common'; import { from, BehaviorSubject, map, switchMap, tap, of } from 'rxjs'; import * as i1 from '@angular/common/http'; import { HttpHeaders, HttpParams } from '@angular/common/http'; class OAuthConfig { constructor() { // The URL for your Okta organization or an Okta authentication server. // This option is required this.issuer = 'https://id.glamtime.com.cn'; // Client id pre-registered for the OIDC authentication flow. this.clientId = ''; // Used in authorization and interaction code flows by server-side web applications to obtain OAuth tokens. // In a production application, this value should never be visible on the client side. this.clientSecret = ''; // The url that is redirected to when using token.getWithRedirect. // If no redirectUri is provided, defaults to the current origin (window.location.origin). this.redirectUri = window.location.origin; // The url that is redirected to when using token.getWithRedirect. // If no redirectUri is provided, defaults to the current origin (window.location.origin). this.postLogoutRedirectUri = window.location.origin; // Specify what information to make available in the returned id_token or access_token. // For OIDC, you must include openid as one of the scopes. // Defaults to ['openid', 'email']. this.scopes = ['openid', 'profile', 'email']; // Array of secure URLs on which the token should be sent if the interceptor is added to the `HTTP_INTERCEPTORS`. this.resourceServers = []; } } const OAUTH_CONFIG = new InjectionToken('OAUTH_CONFIG'); class OAuthStorageService { constructor() { this._storage = localStorage; } get state() { return this._getItem('state'); } set state(code) { if (code) { this._setItem('state', code); } else { this._removeItem('state'); } } get codeVerifier() { return this._getItem('codeVerifier'); } set codeVerifier(code) { if (code) { this._setItem('codeVerifier', code); } else { this._removeItem('codeVerifier'); } } get accessToken() { return this._getItem('accessToken'); } set accessToken(token) { if (token) { this._setItem('accessToken', token); } else { this._removeItem('accessToken'); } } get idToken() { return this._getItem('idToken'); } set idToken(token) { if (token) { this._setItem('idToken', token); } else { this._removeItem('idToken'); } } get refreshToken() { return this._getItem('refreshToken'); } set refreshToken(token) { if (token) { this._setItem('refreshToken', token); } else { this._removeItem('refreshToken'); } } resetAuthState() { this._removeItem('state'); this._removeItem('codeVerifier'); this._removeItem('accessToken'); this._removeItem('idToken'); this._removeItem('refreshToken'); this._removeItem('jwtKeys'); } _getItem(key) { return this._storage.getItem(key); } _setItem(key, value) { this._storage.setItem(key, value); } _removeItem(key) { this._storage.removeItem(key); } _clear() { this._storage.clear(); } } OAuthStorageService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthStorageService, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); OAuthStorageService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthStorageService }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthStorageService, decorators: [{ type: Injectable }], ctorParameters: function () { return []; } }); class OAuthCryptoService { constructor(_document) { this._document = _document; } generateState() { let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const crypto = this._getCrypto(); if (crypto) { let bytes = new Uint8Array(32); crypto.getRandomValues(bytes); result = Array.from(bytes, (v) => { return characters[v % characters.length]; }).join(''); } return result; } /** * Generate code verifier * See also: https://tools.ietf.org/html/rfc7636#section-4.1 */ generatePKCECodeVerifier() { let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~'; const crypto = this._getCrypto(); if (crypto) { let bytes = new Uint8Array(32); crypto.getRandomValues(bytes); result = Array.from(bytes, (v) => { return characters[v % characters.length]; }).join(''); } return this._base64UrlEncode(result); } generatePKCECodeChallenge(codeVerifier) { const crypto = this._getCrypto(); return from(new Promise((resolve, reject) => { if (crypto && codeVerifier) { const bytes = new Uint8Array(codeVerifier.length); for (let i = 0; i < codeVerifier.length; i++) { bytes[i] = codeVerifier.charCodeAt(i); } crypto.subtle.digest('SHA-256', bytes).then((buffer) => { const bytesArray = new Uint8Array(buffer); const s = Array.from(bytesArray, (v) => { return String.fromCharCode(v); }).join(''); resolve(this._base64UrlEncode(s)); }); } else { reject(); } })); } _getCrypto() { // support for IE, (window.crypto || window.msCrypto) return this._document.defaultView.crypto || this._document.defaultView.msCrypto; } _base64UrlEncode(str) { const base64 = self.btoa(str); return base64 .replace(/\+/g, '-') .replace(/\//g, '_') .replace(/=/g, ''); } } OAuthCryptoService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthCryptoService, deps: [{ token: DOCUMENT }], target: i0.ɵɵFactoryTarget.Injectable }); OAuthCryptoService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthCryptoService }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthCryptoService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: undefined, decorators: [{ type: Inject, args: [DOCUMENT] }] }]; } }); const OAUTH_WELL_KNOWN_SUFFIX = `/.well-known/openid-configuration`; class OAuthService { constructor(_zone, _httpClient, _storageService, _cryptoService, _document, _platformId, config) { this._zone = _zone; this._httpClient = _httpClient; this._storageService = _storageService; this._cryptoService = _cryptoService; this._document = _document; this._platformId = _platformId; this._config = new OAuthConfig(); this._openIDConfiguration = null; this._jwtKeys = { keys: [] }; this._userInfo = null; this.authenticated$ = new BehaviorSubject(false); this._config = { ...(new OAuthConfig()), ...config }; } get accessToken() { return this._storageService.accessToken; } get resourceServers() { return this._config.resourceServers; } checkAuth() { return this._fetchOpenIdConfiguration().pipe(map(() => !!(this._storageService.refreshToken && this._storageService.accessToken)), switchMap((result) => { if (result) { this.authenticated$.next(true); return this._fetchRefreshToken().pipe(switchMap(() => this._fetchUserProfile())); } else { const currentUrl = this._document.defaultView.location.toString(); const parsedUrl = new URL(currentUrl); const urlParams = new URLSearchParams(parsedUrl.search); if (urlParams.has('code') && urlParams.has('state')) { const authorizationCode = urlParams.get('code'); const state = urlParams.get('state'); if (state === this._storageService.state) { return this._fetchToken(authorizationCode).pipe(switchMap(() => this._fetchUserProfile()), tap(() => { this.authenticated$.next(true); })); } else { throw new Error(''); } } } this.authenticated$.next(false); return of(false); })); } login() { this._storageService.resetAuthState(); return this._fetchOpenIdConfiguration().pipe(map(() => { this._storageService.state = this._cryptoService.generateState(); this._storageService.codeVerifier = this._cryptoService.generatePKCECodeVerifier(); return this._storageService.codeVerifier; }), switchMap((codeVerifier) => this._cryptoService.generatePKCECodeChallenge(codeVerifier)), map((codeChallenge) => { this._redirectTo(this._createLoginUrl(codeChallenge)); return true; })); } logout() { this._storageService.resetAuthState(); return this._fetchOpenIdConfiguration().pipe(switchMap(() => { const url = this._openIDConfiguration?.revocation_endpoint; let headers = new HttpHeaders(); headers = headers.set('Content-Type', 'application/x-www-form-urlencoded'); headers = headers.set('Authorization', this._encodeClientCredentials()); let params = new HttpParams() .set('token', this._storageService.accessToken); return this._httpClient.post(url, params, { headers: headers }); }), tap(() => this.authenticated$.next(false))); } // Fetch the metadata from openid-configuration _fetchOpenIdConfiguration() { if (this._openIDConfiguration) { return of(this._openIDConfiguration); } const url = this._config.issuer + OAUTH_WELL_KNOWN_SUFFIX; return this._httpClient.get(url).pipe(tap((data) => { this._openIDConfiguration = data; }), switchMap(_ => { return this._httpClient.get(this._openIDConfiguration.jwks_uri).pipe(tap((data) => { this._jwtKeys = data; })); }), map(_ => { return this._openIDConfiguration; })); } _fetchToken(authorizationCode) { const url = this._openIDConfiguration.token_endpoint; let headers = new HttpHeaders(); headers = headers.set('Content-Type', 'application/x-www-form-urlencoded'); headers = headers.set('Authorization', this._encodeClientCredentials()); let params = new HttpParams() .set('grant_type', 'authorization_code') .set('code', authorizationCode) .set('redirect_uri', this._config.redirectUri); if (this._storageService.codeVerifier) { params = params.set('code_verifier', this._storageService.codeVerifier); } return this._httpClient.post(url, params, { headers: headers }).pipe(tap((value) => { this._storageService.accessToken = value.access_token; this._storageService.refreshToken = value.refresh_token; this._storageService.idToken = value.id_token; })); } _fetchRefreshToken() { const url = this._openIDConfiguration.token_endpoint; let headers = new HttpHeaders(); headers = headers.set('Content-Type', 'application/x-www-form-urlencoded'); headers = headers.set('Authorization', this._encodeClientCredentials()); let params = new HttpParams() .set('grant_type', 'refresh_token') .set('refresh_token', this._storageService.refreshToken); return this._httpClient.post(url, params, { headers: headers }).pipe(tap((value) => { this._storageService.accessToken = value.access_token; this._storageService.refreshToken = value.refresh_token; this._storageService.idToken = value.id_token; })); } _fetchUserProfile() { const headers = new HttpHeaders().set('Authorization', 'Bearer ' + this._storageService.accessToken); return this._httpClient.get(this._openIDConfiguration.userinfo_endpoint, { headers: headers }).pipe(tap((value) => { this._userInfo = value; })); } _createLoginUrl(codeChallenge) { return this._openIDConfiguration.authorization_endpoint + '?' + 'response_type=' + encodeURIComponent('code') + '&client_id=' + encodeURIComponent(this._config.clientId) + '&state=' + encodeURIComponent(this._storageService.state) + '&scope=' + encodeURIComponent(this._config.scopes.join(' ')) + '&code_challenge=' + codeChallenge + '&code_challenge_method=S256' + '&redirect_uri=' + encodeURIComponent(this._config.redirectUri); } _redirectTo(url) { this._document.location.href = url; } _encodeClientCredentials() { return 'Basic ' + self.btoa(`${this._config.clientId}:${this._config.clientSecret}`); } } OAuthService.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthService, deps: [{ token: i0.NgZone }, { token: i1.HttpClient }, { token: OAuthStorageService }, { token: OAuthCryptoService }, { token: DOCUMENT }, { token: PLATFORM_ID }, { token: OAUTH_CONFIG }], target: i0.ɵɵFactoryTarget.Injectable }); OAuthService.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthService }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthService, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: i0.NgZone }, { type: i1.HttpClient }, { type: OAuthStorageService }, { type: OAuthCryptoService }, { type: undefined, decorators: [{ type: Inject, args: [DOCUMENT] }] }, { type: undefined, decorators: [{ type: Inject, args: [PLATFORM_ID] }] }, { type: undefined, decorators: [{ type: Inject, args: [OAUTH_CONFIG] }] }]; } }); class OAuthInterceptor { constructor(_oauthService) { this._oauthService = _oauthService; } intercept(request, next) { for (const url of this._oauthService.resourceServers) { if (request.url.startsWith(url)) { const token = this._oauthService.accessToken; if (token) { request = request.clone({ headers: request.headers.set('Authorization', 'Bearer ' + token) }); } break; } } return next.handle(request); } } OAuthInterceptor.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthInterceptor, deps: [{ token: OAuthService }], target: i0.ɵɵFactoryTarget.Injectable }); OAuthInterceptor.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthInterceptor }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthInterceptor, decorators: [{ type: Injectable }], ctorParameters: function () { return [{ type: OAuthService }]; } }); class OAuthModule { static forRoot(config) { return { ngModule: OAuthModule, providers: [ { provide: OAUTH_CONFIG, useValue: config }, OAuthStorageService, OAuthCryptoService, OAuthService, ] }; } } OAuthModule.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); OAuthModule.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "14.2.8", ngImport: i0, type: OAuthModule }); OAuthModule.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthModule }); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "14.2.8", ngImport: i0, type: OAuthModule, decorators: [{ type: NgModule, args: [{ declarations: [], imports: [], exports: [] }] }] }); /* * Public API Surface of oauth-oidc-client */ /** * Generated bundle index. Do not edit. */ export { OAUTH_CONFIG, OAUTH_WELL_KNOWN_SUFFIX, OAuthConfig, OAuthCryptoService, OAuthInterceptor, OAuthModule, OAuthService, OAuthStorageService }; //# sourceMappingURL=glamtime-oauth-oidc-client.mjs.map