@devlearning/jwt-auth
Version:
Jwt Angular Authentication manager with automatic Refresh Token management.
526 lines (506 loc) • 22.1 kB
JavaScript
import * as i0 from '@angular/core';
import { InjectionToken, Inject, Injectable, APP_INITIALIZER, NgModule } from '@angular/core';
import * as i1 from '@angular/common/http';
import { HttpErrorResponse, HttpResponse, HTTP_INTERCEPTORS } from '@angular/common/http';
import { BehaviorSubject, of, throwError } from 'rxjs';
import { exhaustMap, catchError, tap, switchMap, filter, take, finalize, map, mergeMap } from 'rxjs/operators';
import * as i2 from '@devlearning/mutex-fast-lock';
import { MutexFastLockModule } from '@devlearning/mutex-fast-lock';
class JwtTokenBase {
}
class JwtResponseError extends Error {
get message() { return this._message; }
get detailedMessage() { return this._detailedMessage; }
get status() { return this._status; }
get isUnhautorized() { return this._status == 401; }
constructor(message, detailedMessage, status = null) {
super(message);
this._message = message;
this._detailedMessage = detailedMessage;
this._status = status;
Object.setPrototypeOf(this, JwtResponseError.prototype);
}
}
var StorageType;
(function (StorageType) {
StorageType[StorageType["SESSION_STORAGE"] = 1] = "SESSION_STORAGE";
StorageType[StorageType["LOCAL_STORAGE"] = 2] = "LOCAL_STORAGE";
})(StorageType || (StorageType = {}));
class JwtAuthConfig {
}
var JwtAuthLogLevel;
(function (JwtAuthLogLevel) {
JwtAuthLogLevel[JwtAuthLogLevel["VERBOSE"] = 1] = "VERBOSE";
JwtAuthLogLevel[JwtAuthLogLevel["INFO"] = 2] = "INFO";
JwtAuthLogLevel[JwtAuthLogLevel["WARNING"] = 3] = "WARNING";
JwtAuthLogLevel[JwtAuthLogLevel["ERROR"] = 4] = "ERROR";
JwtAuthLogLevel[JwtAuthLogLevel["NONE"] = 5] = "NONE";
})(JwtAuthLogLevel || (JwtAuthLogLevel = {}));
class TokenRequest {
}
class RefreshTokenRequest {
}
const JWT_AUTH_CONFIG = new InjectionToken('JWT_AUTH_CONFIG');
const ERROR_INVALID_TOKEN = 'invalid_token';
const ERROR_EXPIRED_REFRESH_TOKEN = 'expired_refresh_token';
class WWWAuthenticateMessage {
constructor(message, error, description) {
this.scheme = "Bearer";
this.message = message;
this.error = error;
this.description = description;
}
}
class WWWAuthenticateMessageFactory {
static create(content) {
if (content == null)
return null;
if (!content.toLowerCase().startsWith("bearer"))
throw new Error("Unmanaged scheme authentication");
//Bearer error="invalid_token", error_description="The token expired at '07/25/2023 08:05:20'"
let message = content;
const parts = content.substring("bearer ".length).split(',');
let error = null;
let description = null;
parts.forEach(part => {
part = part.trim();
if (part.startsWith("error=")) {
error = part.split('=')[1].replaceAll("\"", '').replaceAll(',', '');
}
else if (part.startsWith("error_description=")) {
description = part.split('=')[1].replaceAll("\"", '').replaceAll(',', '');
}
});
return new WWWAuthenticateMessage(message, error, description);
}
}
/*
https://www.rfc-editor.org/rfc/rfc6750#section-3
invalid_request
The request is missing a required parameter, includes an
unsupported parameter or parameter value, repeats the same
parameter, uses more than one method for including an access
token, or is otherwise malformed. The resource server SHOULD
respond with the HTTP 400 (Bad Request) status code.
invalid_token
The access token provided is expired, revoked, malformed, or
invalid for other reasons. The resource SHOULD respond with
the HTTP 401 (Unauthorized) status code. The client MAY
request a new access token and retry the protected resource
request.
insufficient_scope
The request requires higher privileges than provided by the
access token. The resource server SHOULD respond with the HTTP
403 (Forbidden) status code and MAY include the "scope"
attribute with the scope necessary to access the protected
resource.
*/
const JWT_AUTH_KEY_STORAGE = "jwt-auth";
const TOKEN_KEY_STORAGE = JWT_AUTH_KEY_STORAGE + "-token";
const REFRESHING_KEY_STORAGE = JWT_AUTH_KEY_STORAGE + "-refreshing";
const REFRESHING_EVENT_CHANGED = JWT_AUTH_KEY_STORAGE + "-refreshing-changed";
class JwtAuthService {
get isLoggedIn$() {
return this._isLoggedInSubject.asObservable();
}
get jwtToken$() {
return this._jwtTokenSubject.asObservable();
}
get refreshingToken$() {
return this._isRefreshingTokenSubject.asObservable();
}
get isLoggedIn() { return this._isLoggedInSubject.value; }
get jwtToken() { return this._jwtTokenSubject.value; }
constructor(_config, _http, _mutexFastLock) {
this._config = _config;
this._http = _http;
this._mutexFastLock = _mutexFastLock;
this._isLocalStorageSupported = false;
this._isLoggedInSubject = new BehaviorSubject(false);
this._isRefreshingTokenSubject = new BehaviorSubject(false);
this._jwtTokenSubject = new BehaviorSubject(null);
this._refreshTokenSubject = new BehaviorSubject(null);
if (this._config.storageType == StorageType.SESSION_STORAGE) {
this._storage = sessionStorage;
}
else {
this._storage = localStorage;
}
this._isLocalStorageSupported = this._checkStorageIsSupported();
this._getLocalStorageSupported();
this.setRefreshingToken(false);
var that = this;
window.addEventListener('storage', function (ev) {
if (ev.key === TOKEN_KEY_STORAGE) {
if (that._config.logLevel <= JwtAuthLogLevel.VERBOSE)
console.debug("JwtAuth - eventListener storage token changed");
let token = JSON.parse(ev.newValue);
if (token?.accessToken != that.jwtToken.accessToken) {
that._setToken(token);
that._refreshTokenSubject.next(token);
}
}
});
}
init() {
return of(this._getJwtToken())
.pipe(exhaustMap(jwtToken => {
if (jwtToken != null && jwtToken.accessToken != null) {
this._jwtTokenSubject.next(jwtToken);
if (!this.isTokenExpired()) {
if (this._config.logLevel <= JwtAuthLogLevel.VERBOSE)
console.debug("JwtAuth - init - isTokenExpired: false");
this._setToken(jwtToken);
return of(jwtToken);
}
else if (!this.isRefreshTokenExpired()) {
if (this._config.logLevel <= JwtAuthLogLevel.VERBOSE)
console.debug("JwtAuth - init - isRefreshTokenExpired: false");
return this.refreshToken()
.pipe(catchError((err, obs) => {
this.logout();
return this._handleError(err);
}));
}
else {
if (this._config.logLevel <= JwtAuthLogLevel.VERBOSE)
console.debug("JwtAuth - init - token and refresh token is expired");
this.logout();
return of(null);
}
}
else {
if (this._config.logLevel <= JwtAuthLogLevel.VERBOSE)
console.debug("JwtAuth - init - token is null");
this.logout();
return of(null);
}
}));
}
token(request) {
return this._http.post(this._config.tokenUrl, request).pipe(tap(x => {
this._setToken(x);
}), catchError(err => {
this._cleanToken();
return this._handleError(err);
}));
}
refreshToken() {
if (this._jwtTokenSubject.value == null || this._jwtTokenSubject.value.accessToken == null) {
if (this._config.logLevel <= JwtAuthLogLevel.ERROR)
console.error("JwtAuth - refreshToken this._jwtTokenSubject.value was null");
return throwError(() => new Error("User is logged out"));
}
if (this._config.logLevel <= JwtAuthLogLevel.VERBOSE)
console.debug("JWT refreshToken");
return this._mutexFastLock.lock(REFRESHING_KEY_STORAGE, 100)
.pipe(switchMap(x => {
if (!this.getIsRefreshingToken()) {
this.setRefreshingToken(true);
return this._mutexFastLock.lock(TOKEN_KEY_STORAGE)
.pipe(tap(x => this._refreshTokenSubject.next(null)), tap(x => this._isRefreshingTokenSubject.next(true)), switchMap(x => {
let jwtToken = this._getJwtToken();
let request = new RefreshTokenRequest();
request.username = jwtToken.username;
request.refreshToken = jwtToken.refreshToken;
return this._http.post(this._config.refreshUrl, request)
.pipe(tap(x => {
this._setToken(x);
this._refreshTokenSubject.next(x);
}), catchError(err => {
if (this._config.logLevel <= JwtAuthLogLevel.VERBOSE)
console.debug("JWT refreshToken err " + err);
if (err.message && err.message.indexOf("Lock could not be acquired") >= 0) {
return this._refreshTokenSubject
.pipe(filter(x => x != null), filter(x => !this._checkTokenIsExpired(x)), take(1));
}
else {
if (err.status == 468) {
return throwError(() => new Error("Refresh token expired"));
}
else if (err.status == 401) {
const wwwAuthenticate = WWWAuthenticateMessageFactory.create(err.headers.get('WWW-Authenticate'));
if (wwwAuthenticate.error == ERROR_EXPIRED_REFRESH_TOKEN) {
this._cleanToken();
return throwError(() => new Error("Refresh token expired"));
}
}
else {
this._cleanToken();
return this._handleError(err);
}
}
}), finalize(() => {
this.setRefreshingToken(false);
this._mutexFastLock.release(TOKEN_KEY_STORAGE);
this._mutexFastLock.release(REFRESHING_KEY_STORAGE);
this._isRefreshingTokenSubject.next(false);
}));
}));
}
else {
return this._refreshTokenSubject
.pipe(filter(x => x != null), filter(x => !this._checkTokenIsExpired(x)), take(1));
}
}), catchError((err) => {
this._mutexFastLock.release(REFRESHING_KEY_STORAGE);
if (err.message && err.message.indexOf("Lock could not be acquired") >= 0) {
return this._refreshTokenSubject
.pipe(filter(x => x != null), filter(x => !this._checkTokenIsExpired(x)), take(1));
}
else {
return throwError(() => new Error(err));
}
}));
}
logout() {
this._cleanToken();
}
isAuthenticationUrl(url) {
return url == this._config.tokenUrl || url == this._config.refreshUrl;
}
isRefreshTokenExpired() {
return new Date().getTime() > this._jwtTokenSubject.value.refreshTokenExpiresIn;
}
isTokenExpired() {
return this._checkTokenIsExpired(this._jwtTokenSubject.value);
}
setTokenUrl(url) {
this._config.tokenUrl = url;
}
setRefreshUrl(url) {
this._config.refreshUrl = url;
}
getIsRefreshingToken() {
return this._storage.getItem(REFRESHING_KEY_STORAGE) == 'true';
}
setRefreshingToken(refreshing) {
this._storage.setItem(REFRESHING_KEY_STORAGE, '' + refreshing);
}
setToken(jwtToken) {
this._setToken(jwtToken);
}
_setToken(jwtToken) {
if (jwtToken != null) {
this._saveJwtToken(jwtToken);
this._isLoggedInSubject.next(true);
this._jwtTokenSubject.next(jwtToken);
}
else {
this._cleanToken();
}
}
_cleanToken() {
this._deleteJwtToken();
this._isLoggedInSubject.next(false);
this._jwtTokenSubject.next(null);
}
_checkStorageIsSupported() {
try {
this._storage.setItem(JWT_AUTH_KEY_STORAGE + '-test-storage', "test");
this._storage.removeItem(JWT_AUTH_KEY_STORAGE + '-test-storage');
return true;
}
catch (e) {
return false;
}
}
_getLocalStorageSupported() {
if (!this._isLocalStorageSupported) {
if (this._config.logLevel <= JwtAuthLogLevel.ERROR)
console.error("LocalStorage is not supported");
}
return this._isLocalStorageSupported;
}
_saveJwtToken(jwtToken) {
if (!this._isLocalStorageSupported)
return;
this._storage.setItem(TOKEN_KEY_STORAGE, JSON.stringify(jwtToken));
}
_getJwtToken() {
if (!this._isLocalStorageSupported)
return undefined;
return JSON.parse(this._storage.getItem(TOKEN_KEY_STORAGE));
}
_deleteJwtToken() {
this._storage.removeItem(TOKEN_KEY_STORAGE);
}
_checkTokenIsExpired(token) {
return new Date().getTime() > token.expiresIn;
}
_handleError(error) {
let message;
let detailedMessage;
if (error.error instanceof Error) {
if (this._config.logLevel <= JwtAuthLogLevel.ERROR)
console.error('An error occurred:', error.error.message);
message = error.error.message;
}
else if (error instanceof HttpErrorResponse) {
if (error.status == 500) {
message = error.error?.message;
detailedMessage = error.error?.detailedMessage;
}
else {
console.error(`Backend returned code ${error.status}, body was: ${error.message}`);
message = error.message;
}
}
let jwtResponse = new JwtResponseError(message, detailedMessage, error.status);
return throwError(() => jwtResponse);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: JwtAuthService, deps: [{ token: JWT_AUTH_CONFIG }, { token: i1.HttpClient }, { token: i2.MutexFastLockService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: JwtAuthService, providedIn: 'root' }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: JwtAuthService, decorators: [{
type: Injectable,
args: [{
providedIn: 'root'
}]
}], ctorParameters: () => [{ type: JwtAuthConfig, decorators: [{
type: Inject,
args: [JWT_AUTH_CONFIG]
}] }, { type: i1.HttpClient }, { type: i2.MutexFastLockService }] });
class JwtAuthInterceptor {
constructor(_config, _jwtAuth) {
this._config = _config;
this._jwtAuth = _jwtAuth;
}
intercept(request, next) {
if (this._config.logLevel <= JwtAuthLogLevel.VERBOSE)
console.debug("JwtAuthInterceptor " + request.url);
if (this._jwtAuth.isAuthenticationUrl(request.url)) {
return next.handle(request);
}
return next.handle(this.applyCredentials(request, this._jwtAuth.jwtToken?.accessToken))
.pipe(map((event) => {
if (event instanceof HttpResponse) {
return event;
}
}), catchError((error) => {
if (error instanceof HttpErrorResponse) {
switch (error.status) {
// case 400:
// return this.handle400Error(error, request, next);
case 401:
if (this._jwtAuth.jwtToken != null && this._jwtAuth.isLoggedIn) {
return this.handle401Error(error, request, next);
}
else {
return throwError(error);
}
// case 500:
// console.error("500 error");
// console.error(error);
// return throwError(error);
default:
// console.error(error);
return throwError(error);
}
}
else {
return throwError(error);
}
}));
}
applyCredentials(request, token) {
if (token == null || token == '')
return request;
return request.clone({
setHeaders: {
Authorization: `Bearer ${token}`
}
});
}
// private handle400Error(errorResponse: HttpErrorResponse, request: HttpRequest<any>, next: HttpHandler) {
// console.debug(errorResponse.error);
// if (errorResponse.error.error && errorResponse.error.error == "invalid_grant") {
// this._jwtAuth.logout();
// return throwError(errorResponse.error);
// }
// }
handle401Error(errorResponse, request, next) {
const wwwAuthenticate = WWWAuthenticateMessageFactory.create(errorResponse.headers.get('WWW-Authenticate'));
if (wwwAuthenticate.error == ERROR_INVALID_TOKEN) {
return this._jwtAuth.refreshToken()
.pipe(switchMap(x => next.handle(this.applyCredentials(request, x.accessToken))));
}
else {
return throwError(() => new Error(wwwAuthenticate.description + ' ' + wwwAuthenticate.description));
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: JwtAuthInterceptor, deps: [{ token: JWT_AUTH_CONFIG }, { token: JwtAuthService }], target: i0.ɵɵFactoryTarget.Injectable }); }
static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: JwtAuthInterceptor }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: JwtAuthInterceptor, decorators: [{
type: Injectable
}], ctorParameters: () => [{ type: JwtAuthConfig, decorators: [{
type: Inject,
args: [JWT_AUTH_CONFIG]
}] }, { type: JwtAuthService }] });
class JwtAuthGuard {
constructor(_jwtAuthService) {
this._jwtAuthService = _jwtAuthService;
}
canActivateBase(route, state) {
if (this._jwtAuthService.jwtToken == null
|| this._jwtAuthService.isLoggedIn == null
|| !this._jwtAuthService.isLoggedIn
|| this._jwtAuthService.isRefreshTokenExpired()) {
return of(false);
}
else if (this._jwtAuthService.isTokenExpired()) {
return this._jwtAuthService.refreshToken()
.pipe(mergeMap(x => of(true)), catchError(err => of(false)));
}
else {
return of(true);
}
}
}
class JwtAuthModule {
static forRoot(jwtAuthConfig) {
if (!jwtAuthConfig.useManualInitialization) {
return ({
ngModule: JwtAuthModule,
providers: [
{ provide: JWT_AUTH_CONFIG, useValue: jwtAuthConfig },
{ provide: HTTP_INTERCEPTORS, useClass: JwtAuthInterceptor, multi: true },
{ provide: APP_INITIALIZER, useFactory: init, deps: [JwtAuthService], multi: true }
]
});
}
else {
return ({
ngModule: JwtAuthModule,
providers: [
{ provide: JWT_AUTH_CONFIG, useValue: jwtAuthConfig },
{ provide: HTTP_INTERCEPTORS, useClass: JwtAuthInterceptor, multi: true }
]
});
}
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: JwtAuthModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); }
static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.1", ngImport: i0, type: JwtAuthModule, imports: [MutexFastLockModule] }); }
static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: JwtAuthModule, imports: [MutexFastLockModule] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.1", ngImport: i0, type: JwtAuthModule, decorators: [{
type: NgModule,
args: [{
imports: [
MutexFastLockModule
],
}]
}] });
function init(jwtAuth) {
const x = async () => { await jwtAuth.init(); };
return x;
}
/*
* Public API Surface of jwt-auth
*/
/**
* Generated bundle index. Do not edit.
*/
export { JwtAuthConfig, JwtAuthGuard, JwtAuthInterceptor, JwtAuthLogLevel, JwtAuthModule, JwtAuthService, JwtResponseError, JwtTokenBase, StorageType, TokenRequest, init };
//# sourceMappingURL=devlearning-jwt-auth.mjs.map