From 24a46cfa70339d5e3ee30fe2a66df4c22d6c457e Mon Sep 17 00:00:00 2001 From: Marcachips Date: Sun, 14 May 2023 17:09:06 +0000 Subject: [PATCH] chore(release): v1.1.0 --- CHANGELOG.md | 8 ++ dist/cjs/events/expired.d.ts | 8 ++ dist/cjs/events/expired.js | 12 ++ dist/cjs/events/login.d.ts | 10 ++ dist/cjs/events/login.js | 13 +++ dist/cjs/events/logout.d.ts | 4 + dist/cjs/events/logout.js | 7 ++ dist/cjs/index.d.ts | 38 +++++++ dist/cjs/index.js | 37 ++++++ dist/cjs/services/jwt.d.ts | 46 ++++++++ dist/cjs/services/jwt.js | 211 +++++++++++++++++++++++++++++++++++ dist/esm/events/expired.d.ts | 8 ++ dist/esm/events/expired.js | 9 ++ dist/esm/events/login.d.ts | 10 ++ dist/esm/events/login.js | 10 ++ dist/esm/events/logout.d.ts | 4 + dist/esm/events/logout.js | 4 + dist/esm/index.d.ts | 38 +++++++ dist/esm/index.js | 30 +++++ dist/esm/services/jwt.d.ts | 46 ++++++++ dist/esm/services/jwt.js | 206 ++++++++++++++++++++++++++++++++++ package.json | 2 +- 22 files changed, 760 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md create mode 100644 dist/cjs/events/expired.d.ts create mode 100644 dist/cjs/events/expired.js create mode 100644 dist/cjs/events/login.d.ts create mode 100644 dist/cjs/events/login.js create mode 100644 dist/cjs/events/logout.d.ts create mode 100644 dist/cjs/events/logout.js create mode 100644 dist/cjs/index.d.ts create mode 100644 dist/cjs/index.js create mode 100644 dist/cjs/services/jwt.d.ts create mode 100644 dist/cjs/services/jwt.js create mode 100644 dist/esm/events/expired.d.ts create mode 100644 dist/esm/events/expired.js create mode 100644 dist/esm/events/login.d.ts create mode 100644 dist/esm/events/login.js create mode 100644 dist/esm/events/logout.d.ts create mode 100644 dist/esm/events/logout.js create mode 100644 dist/esm/index.d.ts create mode 100644 dist/esm/index.js create mode 100644 dist/esm/services/jwt.d.ts create mode 100644 dist/esm/services/jwt.js diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b9bb59c --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,8 @@ +# Changelog + +# 1.1.0 (2023-05-14) + + +### Features + +* initialize klient jwt extension ([e2d8eab](https://github.com/klientjs/jwt/commit/e2d8eab20af7f2872ccc7cd8207c5eeedb0a024c)) \ No newline at end of file diff --git a/dist/cjs/events/expired.d.ts b/dist/cjs/events/expired.d.ts new file mode 100644 index 0000000..57bdd48 --- /dev/null +++ b/dist/cjs/events/expired.d.ts @@ -0,0 +1,8 @@ +import { Event, RequestEvent } from '@klient/core'; +import type { AxiosError } from 'axios'; +export default class CredentialsExpiredEvent extends Event { + relatedEvent: RequestEvent; + error: Error | AxiosError; + static NAME: string; + constructor(relatedEvent: RequestEvent, error: Error | AxiosError); +} diff --git a/dist/cjs/events/expired.js b/dist/cjs/events/expired.js new file mode 100644 index 0000000..ce2232e --- /dev/null +++ b/dist/cjs/events/expired.js @@ -0,0 +1,12 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const core_1 = require("@klient/core"); +class CredentialsExpiredEvent extends core_1.Event { + constructor(relatedEvent, error) { + super(); + this.relatedEvent = relatedEvent; + this.error = error; + } +} +exports.default = CredentialsExpiredEvent; +CredentialsExpiredEvent.NAME = 'jwt:expired'; diff --git a/dist/cjs/events/login.d.ts b/dist/cjs/events/login.d.ts new file mode 100644 index 0000000..f7ad6e5 --- /dev/null +++ b/dist/cjs/events/login.d.ts @@ -0,0 +1,10 @@ +import { Event } from '@klient/core'; +import type { AxiosResponse } from 'axios'; +import type { AuthenticationState } from '../services/jwt'; +export default class LoginEvent extends Event { + response: AxiosResponse; + state: AuthenticationState; + decodedToken: unknown; + static NAME: string; + constructor(response: AxiosResponse, state: AuthenticationState, decodedToken: unknown); +} diff --git a/dist/cjs/events/login.js b/dist/cjs/events/login.js new file mode 100644 index 0000000..94b3a9c --- /dev/null +++ b/dist/cjs/events/login.js @@ -0,0 +1,13 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const core_1 = require("@klient/core"); +class LoginEvent extends core_1.Event { + constructor(response, state, decodedToken) { + super(); + this.response = response; + this.state = state; + this.decodedToken = decodedToken; + } +} +exports.default = LoginEvent; +LoginEvent.NAME = 'jwt:login'; diff --git a/dist/cjs/events/logout.d.ts b/dist/cjs/events/logout.d.ts new file mode 100644 index 0000000..4861a50 --- /dev/null +++ b/dist/cjs/events/logout.d.ts @@ -0,0 +1,4 @@ +import { Event } from '@klient/core'; +export default class LogoutEvent extends Event { + static NAME: string; +} diff --git a/dist/cjs/events/logout.js b/dist/cjs/events/logout.js new file mode 100644 index 0000000..8058fcf --- /dev/null +++ b/dist/cjs/events/logout.js @@ -0,0 +1,7 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const core_1 = require("@klient/core"); +class LogoutEvent extends core_1.Event { +} +exports.default = LogoutEvent; +LogoutEvent.NAME = 'jwt:logout'; diff --git a/dist/cjs/index.d.ts b/dist/cjs/index.d.ts new file mode 100644 index 0000000..2c6180a --- /dev/null +++ b/dist/cjs/index.d.ts @@ -0,0 +1,38 @@ +import Klient, { Request } from '@klient/core'; +import type { JwtDecodeOptions } from 'jwt-decode'; +import type { AxiosResponse, AxiosRequestConfig } from 'axios'; +import type { KlientRequestConfig, Parameters as KlientParameters } from '@klient/core'; +import type { StorageOptions } from '@klient/storage'; +import JwtSecurity from './services/jwt'; +import type { AuthenticationState } from './services/jwt'; +export { default as LoginEvent } from './events/login'; +export { default as ExpiredEvent } from './events/expired'; +export { default as LogoutEvent } from './events/logout'; +export { default as JwtSecurity } from './services/jwt'; +export declare type Authenticate = (config: KlientRequestConfig, jwt: JwtSecurity) => void; +export interface ConfigurableStep extends AxiosRequestConfig { + configure?: (credentials: T, config: KlientRequestConfig, jwt: JwtSecurity) => void; + map?: (response: AxiosResponse, config: KlientRequestConfig, jwt: JwtSecurity) => AuthenticationState; +} +export interface Parameters extends KlientParameters { + jwt?: { + login?: ConfigurableStep; + refresh?: ConfigurableStep; + authenticate?: Authenticate; + storage?: { + type: 'cookie' | 'static' | 'localStorage' | string; + options?: StorageOptions; + }; + decode_options?: JwtDecodeOptions; + }; +} +export declare const defaultParameters: Parameters; +export interface KlientExtended extends Klient { + jwt: JwtSecurity; + login: (data: unknown) => Request; + logout: () => Promise; +} +export declare const extension: { + name: string; + initialize: (klient: Klient) => void; +}; diff --git a/dist/cjs/index.js b/dist/cjs/index.js new file mode 100644 index 0000000..1f0ec45 --- /dev/null +++ b/dist/cjs/index.js @@ -0,0 +1,37 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.extension = exports.defaultParameters = exports.JwtSecurity = exports.LogoutEvent = exports.ExpiredEvent = exports.LoginEvent = void 0; +const core_1 = require("@klient/core"); +const jwt_1 = require("./services/jwt"); +var login_1 = require("./events/login"); +Object.defineProperty(exports, "LoginEvent", { enumerable: true, get: function () { return login_1.default; } }); +var expired_1 = require("./events/expired"); +Object.defineProperty(exports, "ExpiredEvent", { enumerable: true, get: function () { return expired_1.default; } }); +var logout_1 = require("./events/logout"); +Object.defineProperty(exports, "LogoutEvent", { enumerable: true, get: function () { return logout_1.default; } }); +var jwt_2 = require("./services/jwt"); +Object.defineProperty(exports, "JwtSecurity", { enumerable: true, get: function () { return jwt_2.default; } }); +exports.defaultParameters = { + jwt: { + login: undefined, + refresh: undefined, + authenticate: undefined, + storage: undefined, + decode_options: undefined + } +}; +exports.extension = { + name: '@klient/jwt', + initialize: (klient) => { + klient.parameters.merge(exports.defaultParameters, { + jwt: klient.parameters.get('jwt') || {} + }); + const jwt = new jwt_1.default(klient); + klient.services.set('jwt', jwt); + klient + .extends('login', jwt.login.bind(jwt)) + .extends('logout', jwt.logout.bind(jwt)) + .extends('jwt', jwt); + } +}; +core_1.Extensions.push(exports.extension); diff --git a/dist/cjs/services/jwt.d.ts b/dist/cjs/services/jwt.d.ts new file mode 100644 index 0000000..0a9dd93 --- /dev/null +++ b/dist/cjs/services/jwt.d.ts @@ -0,0 +1,46 @@ +import { JwtDecodeOptions } from 'jwt-decode'; +import Klient, { RequestEvent, KlientRequestConfig, Request } from '@klient/core'; +import { Storage } from '@klient/storage'; +import type { AxiosError, AxiosResponse } from 'axios'; +import type { JwtPayload } from 'jwt-decode'; +export declare type AnyObject = { + [prop: string]: unknown; +}; +export interface AuthenticationState extends AnyObject { + token: string; + tokenExp?: number; + refreshToken?: string; + refreshTokenExp?: number; + date?: number; +} +export interface DecodedToken { + exp: number; +} +export declare const unixTimestamp: () => number; +export default class JwtSecurity { + protected readonly klient: Klient; + static readonly ACTION_LOGIN = "jwt:login"; + static readonly ACTION_REFRESH_CREDENTIALS = "jwt:refresh"; + readonly storage: Storage | undefined; + state: AuthenticationState | undefined; + constructor(klient: Klient); + login(credentials: unknown): Promise>; + logout(): Promise; + refresh(event?: RequestEvent): Promise>; + setupRequest(request: Request): void; + decode(token: string, options?: JwtDecodeOptions): JwtPayload; + setState(nextState?: AuthenticationState): void; + protected refreshCredentials(event: RequestEvent): Promise> | undefined; + protected handleCredentialsExpired(event: RequestEvent, err: AxiosError | Error): Promise; + protected mapLoginResponseToState(response: AxiosResponse, request: KlientRequestConfig, isRefreshTokenResponse?: boolean): Promise; + protected getSecurityParameter(key: string, def?: unknown): unknown; + get isAuthenticated(): boolean; + get isTokenExpired(): boolean; + get isRefreshTokenExpired(): boolean; + get isCredentialsExpired(): boolean | null; + get token(): string | undefined; + get tokenExp(): number | undefined; + get refreshToken(): string | undefined; + get refreshTokenExp(): number | undefined; + get authenticationDate(): Date | undefined; +} diff --git a/dist/cjs/services/jwt.js b/dist/cjs/services/jwt.js new file mode 100644 index 0000000..fadf044 --- /dev/null +++ b/dist/cjs/services/jwt.js @@ -0,0 +1,211 @@ +"use strict"; +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.unixTimestamp = void 0; +const jwt_decode_1 = require("jwt-decode"); +const core_1 = require("@klient/core"); +const storage_1 = require("@klient/storage"); +const logout_1 = require("../events/logout"); +const expired_1 = require("../events/expired"); +const login_1 = require("../events/login"); +const unixTimestamp = () => Math.floor(Date.now() / 1000); +exports.unixTimestamp = unixTimestamp; +class JwtSecurity { + constructor(klient) { + var _a; + this.klient = klient; + const storageConfig = this.getSecurityParameter('storage'); + if (storageConfig === null || storageConfig === void 0 ? void 0 : storageConfig.type) { + this.storage = storage_1.default.create(storageConfig.type, storageConfig.options); + } + this.state = (_a = this.storage) === null || _a === void 0 ? void 0 : _a.read(); + klient + .on(core_1.RequestEvent.NAME, (e) => this.refreshCredentials(e), 102) + .on(core_1.RequestEvent.NAME, (e) => this.setupRequest(e.request), 100) + .on(expired_1.default.NAME, this.logout.bind(this), -100); + } + login(credentials) { + const _a = this.getSecurityParameter('login', {}), { configure, map } = _a, requestConfig = __rest(_a, ["configure", "map"]); + const config = Object.assign({ context: { + action: JwtSecurity.ACTION_LOGIN, + authenticate: false + } }, requestConfig); + if (configure) { + configure(credentials, config, this); + } + else if (!config.method || config.method === 'GET') { + config.params = credentials; + } + else { + config.data = credentials; + } + return this.klient.request(config).then((response) => __awaiter(this, void 0, void 0, function* () { + yield this.mapLoginResponseToState(response, config); + return response; + })); + } + logout() { + return this.klient.dispatcher.dispatch(new logout_1.default(), false).finally(() => { + this.setState(undefined); + }); + } + refresh(event) { + const _a = this.getSecurityParameter('refresh', {}), { configure, map } = _a, refreshConfig = __rest(_a, ["configure", "map"]); + const config = Object.assign({ context: { + action: JwtSecurity.ACTION_REFRESH_CREDENTIALS, + authenticate: false + } }, refreshConfig); + if (configure) { + configure(this.refreshToken, config, this); + } + else { + config.method = 'POST'; + config.data = { refresh_token: this.refreshToken }; + } + return this.klient + .request(config) + .then((response) => __awaiter(this, void 0, void 0, function* () { + yield this.mapLoginResponseToState(response, config, true); + return response; + })) + .catch((err) => __awaiter(this, void 0, void 0, function* () { + if (event) { + yield this.handleCredentialsExpired(event, err); + } + throw err; + })); + } + setupRequest(request) { + const { config, context } = request; + if (context.authenticate === false || !this.isAuthenticated) { + return; + } + const authenticate = this.getSecurityParameter('authenticate'); + if (authenticate) { + authenticate(config, this); + } + else { + config.headers = config.headers || {}; + config.headers.Authorization = `Bearer ${this.token}`; + } + context.isAuthenticated = true; + } + decode(token, options) { + const paramOptions = this.getSecurityParameter('decode_options', {}); + return (0, jwt_decode_1.default)(token, Object.assign(Object.assign({}, paramOptions), options)); + } + setState(nextState) { + var _a; + this.state = nextState; + (_a = this.storage) === null || _a === void 0 ? void 0 : _a.write(nextState); + } + refreshCredentials(event) { + const _a = this.getSecurityParameter('refresh', {}), { configure, map } = _a, refreshConfig = __rest(_a, ["configure", "map"]); + if (event.context.authenticate === false || !this.isAuthenticated || (!refreshConfig.url && !configure)) { + return; + } + return this.isTokenExpired && this.isRefreshTokenExpired + ? this.handleCredentialsExpired(event, new Error('Unable to refresh credentials')) + : this.refresh(event); + } + handleCredentialsExpired(event, err) { + return this.klient.dispatcher.dispatch(new expired_1.default(event, err), false).then(() => { + throw err; + }); + } + mapLoginResponseToState(response, request, isRefreshTokenResponse = false) { + let { map } = this.getSecurityParameter('login', {}); + if (isRefreshTokenResponse) { + map = this.getSecurityParameter('refresh', {}).map; + } + let nextState; + if (map) { + nextState = map(response, request, this); + } + else { + const { token, refresh_token } = response.data; + const tokenDecoded = this.decode(token); + nextState = { + token, + tokenExp: tokenDecoded.exp, + refreshToken: refresh_token, + refreshTokenExp: undefined, + date: (0, exports.unixTimestamp)() + }; + if (refresh_token) { + nextState.refreshTokenExp = this.decode(refresh_token).exp; + } + } + this.setState(nextState); + return this.klient.dispatcher.dispatch(new login_1.default(response, this.state, this.decode(this.token)), false); + } + getSecurityParameter(key, def = undefined) { + const value = this.klient.parameters.get(`jwt.${key}`); + if (value === undefined) { + return def; + } + return this.klient.parameters.get(`jwt.${key}`); + } + get isAuthenticated() { + return typeof this.token === 'string'; + } + get isTokenExpired() { + const { token, tokenExp } = this; + return !token || (typeof tokenExp === 'number' && tokenExp <= (0, exports.unixTimestamp)()); + } + get isRefreshTokenExpired() { + const { refreshToken, refreshTokenExp } = this; + return !refreshToken || (typeof refreshTokenExp === 'number' && refreshTokenExp <= (0, exports.unixTimestamp)()); + } + get isCredentialsExpired() { + if (!this.token) { + return null; + } + return this.isTokenExpired && this.isRefreshTokenExpired; + } + get token() { + var _a; + return (_a = this.state) === null || _a === void 0 ? void 0 : _a.token; + } + get tokenExp() { + var _a; + return (_a = this.state) === null || _a === void 0 ? void 0 : _a.tokenExp; + } + get refreshToken() { + var _a; + return (_a = this.state) === null || _a === void 0 ? void 0 : _a.refreshToken; + } + get refreshTokenExp() { + var _a; + return (_a = this.state) === null || _a === void 0 ? void 0 : _a.refreshTokenExp; + } + get authenticationDate() { + var _a; + const time = (_a = this.state) === null || _a === void 0 ? void 0 : _a.date; + if (time) { + return new Date(time * 1000); + } + } +} +exports.default = JwtSecurity; +JwtSecurity.ACTION_LOGIN = 'jwt:login'; +JwtSecurity.ACTION_REFRESH_CREDENTIALS = 'jwt:refresh'; diff --git a/dist/esm/events/expired.d.ts b/dist/esm/events/expired.d.ts new file mode 100644 index 0000000..57bdd48 --- /dev/null +++ b/dist/esm/events/expired.d.ts @@ -0,0 +1,8 @@ +import { Event, RequestEvent } from '@klient/core'; +import type { AxiosError } from 'axios'; +export default class CredentialsExpiredEvent extends Event { + relatedEvent: RequestEvent; + error: Error | AxiosError; + static NAME: string; + constructor(relatedEvent: RequestEvent, error: Error | AxiosError); +} diff --git a/dist/esm/events/expired.js b/dist/esm/events/expired.js new file mode 100644 index 0000000..1669728 --- /dev/null +++ b/dist/esm/events/expired.js @@ -0,0 +1,9 @@ +import { Event } from '@klient/core'; +export default class CredentialsExpiredEvent extends Event { + constructor(relatedEvent, error) { + super(); + this.relatedEvent = relatedEvent; + this.error = error; + } +} +CredentialsExpiredEvent.NAME = 'jwt:expired'; diff --git a/dist/esm/events/login.d.ts b/dist/esm/events/login.d.ts new file mode 100644 index 0000000..f7ad6e5 --- /dev/null +++ b/dist/esm/events/login.d.ts @@ -0,0 +1,10 @@ +import { Event } from '@klient/core'; +import type { AxiosResponse } from 'axios'; +import type { AuthenticationState } from '../services/jwt'; +export default class LoginEvent extends Event { + response: AxiosResponse; + state: AuthenticationState; + decodedToken: unknown; + static NAME: string; + constructor(response: AxiosResponse, state: AuthenticationState, decodedToken: unknown); +} diff --git a/dist/esm/events/login.js b/dist/esm/events/login.js new file mode 100644 index 0000000..55f95d7 --- /dev/null +++ b/dist/esm/events/login.js @@ -0,0 +1,10 @@ +import { Event } from '@klient/core'; +export default class LoginEvent extends Event { + constructor(response, state, decodedToken) { + super(); + this.response = response; + this.state = state; + this.decodedToken = decodedToken; + } +} +LoginEvent.NAME = 'jwt:login'; diff --git a/dist/esm/events/logout.d.ts b/dist/esm/events/logout.d.ts new file mode 100644 index 0000000..4861a50 --- /dev/null +++ b/dist/esm/events/logout.d.ts @@ -0,0 +1,4 @@ +import { Event } from '@klient/core'; +export default class LogoutEvent extends Event { + static NAME: string; +} diff --git a/dist/esm/events/logout.js b/dist/esm/events/logout.js new file mode 100644 index 0000000..7068347 --- /dev/null +++ b/dist/esm/events/logout.js @@ -0,0 +1,4 @@ +import { Event } from '@klient/core'; +export default class LogoutEvent extends Event { +} +LogoutEvent.NAME = 'jwt:logout'; diff --git a/dist/esm/index.d.ts b/dist/esm/index.d.ts new file mode 100644 index 0000000..2c6180a --- /dev/null +++ b/dist/esm/index.d.ts @@ -0,0 +1,38 @@ +import Klient, { Request } from '@klient/core'; +import type { JwtDecodeOptions } from 'jwt-decode'; +import type { AxiosResponse, AxiosRequestConfig } from 'axios'; +import type { KlientRequestConfig, Parameters as KlientParameters } from '@klient/core'; +import type { StorageOptions } from '@klient/storage'; +import JwtSecurity from './services/jwt'; +import type { AuthenticationState } from './services/jwt'; +export { default as LoginEvent } from './events/login'; +export { default as ExpiredEvent } from './events/expired'; +export { default as LogoutEvent } from './events/logout'; +export { default as JwtSecurity } from './services/jwt'; +export declare type Authenticate = (config: KlientRequestConfig, jwt: JwtSecurity) => void; +export interface ConfigurableStep extends AxiosRequestConfig { + configure?: (credentials: T, config: KlientRequestConfig, jwt: JwtSecurity) => void; + map?: (response: AxiosResponse, config: KlientRequestConfig, jwt: JwtSecurity) => AuthenticationState; +} +export interface Parameters extends KlientParameters { + jwt?: { + login?: ConfigurableStep; + refresh?: ConfigurableStep; + authenticate?: Authenticate; + storage?: { + type: 'cookie' | 'static' | 'localStorage' | string; + options?: StorageOptions; + }; + decode_options?: JwtDecodeOptions; + }; +} +export declare const defaultParameters: Parameters; +export interface KlientExtended extends Klient { + jwt: JwtSecurity; + login: (data: unknown) => Request; + logout: () => Promise; +} +export declare const extension: { + name: string; + initialize: (klient: Klient) => void; +}; diff --git a/dist/esm/index.js b/dist/esm/index.js new file mode 100644 index 0000000..794cda5 --- /dev/null +++ b/dist/esm/index.js @@ -0,0 +1,30 @@ +import { Extensions } from '@klient/core'; +import JwtSecurity from './services/jwt'; +export { default as LoginEvent } from './events/login'; +export { default as ExpiredEvent } from './events/expired'; +export { default as LogoutEvent } from './events/logout'; +export { default as JwtSecurity } from './services/jwt'; +export const defaultParameters = { + jwt: { + login: undefined, + refresh: undefined, + authenticate: undefined, + storage: undefined, + decode_options: undefined + } +}; +export const extension = { + name: '@klient/jwt', + initialize: (klient) => { + klient.parameters.merge(defaultParameters, { + jwt: klient.parameters.get('jwt') || {} + }); + const jwt = new JwtSecurity(klient); + klient.services.set('jwt', jwt); + klient + .extends('login', jwt.login.bind(jwt)) + .extends('logout', jwt.logout.bind(jwt)) + .extends('jwt', jwt); + } +}; +Extensions.push(extension); diff --git a/dist/esm/services/jwt.d.ts b/dist/esm/services/jwt.d.ts new file mode 100644 index 0000000..0a9dd93 --- /dev/null +++ b/dist/esm/services/jwt.d.ts @@ -0,0 +1,46 @@ +import { JwtDecodeOptions } from 'jwt-decode'; +import Klient, { RequestEvent, KlientRequestConfig, Request } from '@klient/core'; +import { Storage } from '@klient/storage'; +import type { AxiosError, AxiosResponse } from 'axios'; +import type { JwtPayload } from 'jwt-decode'; +export declare type AnyObject = { + [prop: string]: unknown; +}; +export interface AuthenticationState extends AnyObject { + token: string; + tokenExp?: number; + refreshToken?: string; + refreshTokenExp?: number; + date?: number; +} +export interface DecodedToken { + exp: number; +} +export declare const unixTimestamp: () => number; +export default class JwtSecurity { + protected readonly klient: Klient; + static readonly ACTION_LOGIN = "jwt:login"; + static readonly ACTION_REFRESH_CREDENTIALS = "jwt:refresh"; + readonly storage: Storage | undefined; + state: AuthenticationState | undefined; + constructor(klient: Klient); + login(credentials: unknown): Promise>; + logout(): Promise; + refresh(event?: RequestEvent): Promise>; + setupRequest(request: Request): void; + decode(token: string, options?: JwtDecodeOptions): JwtPayload; + setState(nextState?: AuthenticationState): void; + protected refreshCredentials(event: RequestEvent): Promise> | undefined; + protected handleCredentialsExpired(event: RequestEvent, err: AxiosError | Error): Promise; + protected mapLoginResponseToState(response: AxiosResponse, request: KlientRequestConfig, isRefreshTokenResponse?: boolean): Promise; + protected getSecurityParameter(key: string, def?: unknown): unknown; + get isAuthenticated(): boolean; + get isTokenExpired(): boolean; + get isRefreshTokenExpired(): boolean; + get isCredentialsExpired(): boolean | null; + get token(): string | undefined; + get tokenExp(): number | undefined; + get refreshToken(): string | undefined; + get refreshTokenExp(): number | undefined; + get authenticationDate(): Date | undefined; +} diff --git a/dist/esm/services/jwt.js b/dist/esm/services/jwt.js new file mode 100644 index 0000000..eb1d212 --- /dev/null +++ b/dist/esm/services/jwt.js @@ -0,0 +1,206 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __rest = (this && this.__rest) || function (s, e) { + var t = {}; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0) + t[p] = s[p]; + if (s != null && typeof Object.getOwnPropertySymbols === "function") + for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) { + if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i])) + t[p[i]] = s[p[i]]; + } + return t; +}; +import jwtDecode from 'jwt-decode'; +import { RequestEvent } from '@klient/core'; +import StorageFactory from '@klient/storage'; +import LogoutEvent from '../events/logout'; +import CredentialsExpiredEvent from '../events/expired'; +import LoginEvent from '../events/login'; +export const unixTimestamp = () => Math.floor(Date.now() / 1000); +export default class JwtSecurity { + constructor(klient) { + var _a; + this.klient = klient; + const storageConfig = this.getSecurityParameter('storage'); + if (storageConfig === null || storageConfig === void 0 ? void 0 : storageConfig.type) { + this.storage = StorageFactory.create(storageConfig.type, storageConfig.options); + } + this.state = (_a = this.storage) === null || _a === void 0 ? void 0 : _a.read(); + klient + .on(RequestEvent.NAME, (e) => this.refreshCredentials(e), 102) + .on(RequestEvent.NAME, (e) => this.setupRequest(e.request), 100) + .on(CredentialsExpiredEvent.NAME, this.logout.bind(this), -100); + } + login(credentials) { + const _a = this.getSecurityParameter('login', {}), { configure, map } = _a, requestConfig = __rest(_a, ["configure", "map"]); + const config = Object.assign({ context: { + action: JwtSecurity.ACTION_LOGIN, + authenticate: false + } }, requestConfig); + if (configure) { + configure(credentials, config, this); + } + else if (!config.method || config.method === 'GET') { + config.params = credentials; + } + else { + config.data = credentials; + } + return this.klient.request(config).then((response) => __awaiter(this, void 0, void 0, function* () { + yield this.mapLoginResponseToState(response, config); + return response; + })); + } + logout() { + return this.klient.dispatcher.dispatch(new LogoutEvent(), false).finally(() => { + this.setState(undefined); + }); + } + refresh(event) { + const _a = this.getSecurityParameter('refresh', {}), { configure, map } = _a, refreshConfig = __rest(_a, ["configure", "map"]); + const config = Object.assign({ context: { + action: JwtSecurity.ACTION_REFRESH_CREDENTIALS, + authenticate: false + } }, refreshConfig); + if (configure) { + configure(this.refreshToken, config, this); + } + else { + config.method = 'POST'; + config.data = { refresh_token: this.refreshToken }; + } + return this.klient + .request(config) + .then((response) => __awaiter(this, void 0, void 0, function* () { + yield this.mapLoginResponseToState(response, config, true); + return response; + })) + .catch((err) => __awaiter(this, void 0, void 0, function* () { + if (event) { + yield this.handleCredentialsExpired(event, err); + } + throw err; + })); + } + setupRequest(request) { + const { config, context } = request; + if (context.authenticate === false || !this.isAuthenticated) { + return; + } + const authenticate = this.getSecurityParameter('authenticate'); + if (authenticate) { + authenticate(config, this); + } + else { + config.headers = config.headers || {}; + config.headers.Authorization = `Bearer ${this.token}`; + } + context.isAuthenticated = true; + } + decode(token, options) { + const paramOptions = this.getSecurityParameter('decode_options', {}); + return jwtDecode(token, Object.assign(Object.assign({}, paramOptions), options)); + } + setState(nextState) { + var _a; + this.state = nextState; + (_a = this.storage) === null || _a === void 0 ? void 0 : _a.write(nextState); + } + refreshCredentials(event) { + const _a = this.getSecurityParameter('refresh', {}), { configure, map } = _a, refreshConfig = __rest(_a, ["configure", "map"]); + if (event.context.authenticate === false || !this.isAuthenticated || (!refreshConfig.url && !configure)) { + return; + } + return this.isTokenExpired && this.isRefreshTokenExpired + ? this.handleCredentialsExpired(event, new Error('Unable to refresh credentials')) + : this.refresh(event); + } + handleCredentialsExpired(event, err) { + return this.klient.dispatcher.dispatch(new CredentialsExpiredEvent(event, err), false).then(() => { + throw err; + }); + } + mapLoginResponseToState(response, request, isRefreshTokenResponse = false) { + let { map } = this.getSecurityParameter('login', {}); + if (isRefreshTokenResponse) { + map = this.getSecurityParameter('refresh', {}).map; + } + let nextState; + if (map) { + nextState = map(response, request, this); + } + else { + const { token, refresh_token } = response.data; + const tokenDecoded = this.decode(token); + nextState = { + token, + tokenExp: tokenDecoded.exp, + refreshToken: refresh_token, + refreshTokenExp: undefined, + date: unixTimestamp() + }; + if (refresh_token) { + nextState.refreshTokenExp = this.decode(refresh_token).exp; + } + } + this.setState(nextState); + return this.klient.dispatcher.dispatch(new LoginEvent(response, this.state, this.decode(this.token)), false); + } + getSecurityParameter(key, def = undefined) { + const value = this.klient.parameters.get(`jwt.${key}`); + if (value === undefined) { + return def; + } + return this.klient.parameters.get(`jwt.${key}`); + } + get isAuthenticated() { + return typeof this.token === 'string'; + } + get isTokenExpired() { + const { token, tokenExp } = this; + return !token || (typeof tokenExp === 'number' && tokenExp <= unixTimestamp()); + } + get isRefreshTokenExpired() { + const { refreshToken, refreshTokenExp } = this; + return !refreshToken || (typeof refreshTokenExp === 'number' && refreshTokenExp <= unixTimestamp()); + } + get isCredentialsExpired() { + if (!this.token) { + return null; + } + return this.isTokenExpired && this.isRefreshTokenExpired; + } + get token() { + var _a; + return (_a = this.state) === null || _a === void 0 ? void 0 : _a.token; + } + get tokenExp() { + var _a; + return (_a = this.state) === null || _a === void 0 ? void 0 : _a.tokenExp; + } + get refreshToken() { + var _a; + return (_a = this.state) === null || _a === void 0 ? void 0 : _a.refreshToken; + } + get refreshTokenExp() { + var _a; + return (_a = this.state) === null || _a === void 0 ? void 0 : _a.refreshTokenExp; + } + get authenticationDate() { + var _a; + const time = (_a = this.state) === null || _a === void 0 ? void 0 : _a.date; + if (time) { + return new Date(time * 1000); + } + } +} +JwtSecurity.ACTION_LOGIN = 'jwt:login'; +JwtSecurity.ACTION_REFRESH_CREDENTIALS = 'jwt:refresh'; diff --git a/package.json b/package.json index 6f07366..629b482 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "token", "authorization" ], - "version": "1.0.0", + "version": "1.1.0", "license": "MIT", "main": "./dist/cjs/index.js", "types": "./dist/cjs/index.d.ts",