@soapjs/soap-auth 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,7 +15,7 @@ npm install @soapjs/soap-auth @soapjs/soap
15
15
  ## Requirements
16
16
 
17
17
  - Node.js 24.17.0 or newer
18
- - `@soapjs/soap` 0.12 or newer
18
+ - `@soapjs/soap` 0.14 or newer
19
19
 
20
20
  ## Quick Start
21
21
 
@@ -69,6 +69,14 @@ Available recipes:
69
69
 
70
70
  Recipes are also available from `@soapjs/soap-auth/recipes`.
71
71
 
72
+ Type-only subpath imports are supported for TypeScript projects using classic
73
+ `moduleResolution: node`:
74
+
75
+ ```ts
76
+ import type { StorageContext } from "@soapjs/soap-auth/types";
77
+ import type { CookieOptions } from "@soapjs/soap-auth/recipes";
78
+ ```
79
+
72
80
  ## Factory Configuration
73
81
 
74
82
  `SoapAuth.create()` registers built-in strategies from config:
@@ -206,6 +214,26 @@ const auth = await SoapAuth.create({
206
214
 
207
215
  For providers without a `userinfo` endpoint, implement `user.fetchUser(accessToken)` and return your application user directly.
208
216
 
217
+ OAuth2 `state.persistence` and `nonce.persistence` callbacks receive the
218
+ current auth context:
219
+
220
+ ```ts
221
+ state: {
222
+ persistence: {
223
+ store: async (state, context, metadata) => {
224
+ await context.storeInCookie?.(state, { name: metadata?.key ?? "state" });
225
+ },
226
+ read: async (context, key) => context.getFromCookie?.(key ?? "state") ?? null,
227
+ remove: async (context, key) => {
228
+ await context.removeFromCookie?.(key ?? "state");
229
+ },
230
+ },
231
+ }
232
+ ```
233
+
234
+ This lets HTTP adapters such as `soap-express` persist OAuth state and nonce in
235
+ cookies, sessions, or request-scoped storage without global state.
236
+
209
237
  ## Configurable Hybrid OAuth2
210
238
 
211
239
  Hybrid OAuth2 tries existing JWT/session auth first, then falls back to OAuth2. This is useful when browser users log in through OAuth2 but API clients can keep using JWT.
@@ -24,8 +24,9 @@ class PKCEService {
24
24
  const cv = this.config.verifier.generate?.() || (0, tools_1.generateRandomString)();
25
25
  this.config.verifier.embed(context, cv);
26
26
  const expirationTime = Date.now() + (this.config.verifier.expiresIn ?? 300) * 1000;
27
- await this.config.verifier.persistence?.store(cv, {
27
+ await this.config.verifier.persistence?.store(cv, context, {
28
28
  expiration: expirationTime,
29
+ key: "code_verifier",
29
30
  });
30
31
  return cv;
31
32
  }
@@ -37,8 +38,9 @@ class PKCEService {
37
38
  this.defaultGenerateCodeChallenge(codeVerifier);
38
39
  this.config.challenge.embed(context, challenge);
39
40
  const expirationTime = Date.now() + (this.config.challenge.expiresIn ?? 300) * 1000;
40
- await this.config.challenge.persistence?.store?.(challenge, {
41
+ await this.config.challenge.persistence?.store?.(challenge, context, {
41
42
  expiration: expirationTime,
43
+ key: "code_challenge",
42
44
  });
43
45
  return challenge;
44
46
  }
@@ -49,7 +51,7 @@ class PKCEService {
49
51
  const codeVerifier = this.extractCodeVerifier(context);
50
52
  if (!codeVerifier)
51
53
  return true;
52
- const stored = await this.config.verifier?.persistence?.read?.(codeVerifier);
54
+ const stored = await this.config.verifier?.persistence?.read?.(context, codeVerifier);
53
55
  if (!stored || !stored.expiration)
54
56
  return true;
55
57
  return Date.now() > stored.expiration;
@@ -58,7 +60,7 @@ class PKCEService {
58
60
  const challenge = this.extractCodeChallenge(context);
59
61
  if (!challenge)
60
62
  return true;
61
- const stored = await this.config.challenge?.persistence?.read?.(challenge);
63
+ const stored = await this.config.challenge?.persistence?.read?.(context, challenge);
62
64
  if (!stored || !stored.expiration)
63
65
  return true;
64
66
  return Date.now() > stored.expiration;
@@ -66,14 +68,14 @@ class PKCEService {
66
68
  async clearCodeVerifier(context) {
67
69
  const cv = this.config.verifier.extract(context);
68
70
  if (cv) {
69
- await this.config.verifier.persistence?.remove?.(cv);
71
+ await this.config.verifier.persistence?.remove?.(context, cv);
70
72
  this.config.verifier.embed(context, "");
71
73
  }
72
74
  }
73
75
  async clearCodeChallenge(context) {
74
76
  const challenge = this.config.challenge.extract(context);
75
77
  if (challenge) {
76
- await this.config.challenge.persistence?.remove?.(challenge);
78
+ await this.config.challenge.persistence?.remove?.(context, challenge);
77
79
  this.config.challenge.embed(context, "");
78
80
  }
79
81
  }
@@ -13,8 +13,8 @@ export declare class JwtStrategy<TContext = Soap.HttpContext, TUser extends Soap
13
13
  protected verifyRefreshToken(token: string): Promise<any>;
14
14
  protected generateAccessToken(user: TUser, context: TContext): Promise<string>;
15
15
  protected generateRefreshToken(user: TUser, context: TContext): Promise<string>;
16
- protected storeAccessToken(token: string): Promise<void>;
17
- protected storeRefreshToken(token: string): Promise<void>;
16
+ protected storeAccessToken(token: string, context?: TContext): Promise<void>;
17
+ protected storeRefreshToken(token: string, context?: TContext): Promise<void>;
18
18
  protected embedAccessToken(token: string, context: TContext): void;
19
19
  protected embedRefreshToken(token: string, context: TContext): void;
20
20
  protected extractAccessToken(context: TContext): string | undefined;
@@ -104,14 +104,14 @@ class JwtStrategy extends token_auth_strategy_1.TokenAuthStrategy {
104
104
  const payload = this.buildAccessTokenPayload(user, context);
105
105
  return Promise.resolve(jwt_tools_1.JwtTools.generateRefreshToken(payload, this.refreshTokenConfig));
106
106
  }
107
- async storeAccessToken(token) {
107
+ async storeAccessToken(token, context) {
108
108
  if (this.accessTokenConfig.persistence?.store) {
109
- await this.accessTokenConfig.persistence.store(token, null, this.accessTokenConfig.issuer.options.expiresIn);
109
+ await this.accessTokenConfig.persistence.store(token, context ?? null, { expiresIn: this.accessTokenConfig.issuer.options.expiresIn });
110
110
  }
111
111
  }
112
- async storeRefreshToken(token) {
112
+ async storeRefreshToken(token, context) {
113
113
  if (this.refreshTokenConfig?.persistence?.store) {
114
- await this.refreshTokenConfig.persistence.store(token, null, this.refreshTokenConfig.issuer.options.expiresIn);
114
+ await this.refreshTokenConfig.persistence.store(token, context ?? null, { expiresIn: this.refreshTokenConfig.issuer.options.expiresIn });
115
115
  }
116
116
  }
117
117
  embedAccessToken(token, context) {
@@ -164,7 +164,7 @@ class JwtStrategy extends token_auth_strategy_1.TokenAuthStrategy {
164
164
  async invalidateRefreshToken(token, context) {
165
165
  const refreshToken = token || (await this.extractRefreshToken(context));
166
166
  if (refreshToken) {
167
- await this.refreshTokenConfig?.persistence?.remove?.(refreshToken);
167
+ await this.refreshTokenConfig?.persistence?.remove?.(context ?? null, refreshToken);
168
168
  if (context) {
169
169
  jwt_tools_1.JwtTools.clearDefaultJwtCookie(context);
170
170
  jwt_tools_1.JwtTools.clearDefaultJwtHeader(context);
@@ -175,7 +175,7 @@ class JwtStrategy extends token_auth_strategy_1.TokenAuthStrategy {
175
175
  async invalidateAccessToken(token, context) {
176
176
  const accessToken = token || (await this.extractAccessToken(context));
177
177
  if (accessToken) {
178
- await this.accessTokenConfig.persistence?.remove?.(accessToken);
178
+ await this.accessTokenConfig.persistence?.remove?.(context ?? null, accessToken);
179
179
  if (context) {
180
180
  jwt_tools_1.JwtTools.clearDefaultJwtCookie(context);
181
181
  jwt_tools_1.JwtTools.clearDefaultJwtHeader(context);
@@ -59,7 +59,7 @@ class HybridOAuth2Strategy extends oauth2_strategy_1.OAuth2Strategy {
59
59
  idToken = exchangedTokens.idToken;
60
60
  }
61
61
  const user = idToken
62
- ? await this.verifyIdToken(idToken)
62
+ ? await this.verifyIdToken(idToken, context)
63
63
  : await this.fetchUser(accessToken);
64
64
  if (!user)
65
65
  throw new errors_1.UserNotFoundError();
@@ -55,7 +55,7 @@ export declare abstract class OAuth2Strategy<TContext = Soap.HttpContext, TUser
55
55
  idToken?: string;
56
56
  }>;
57
57
  protected handleTokenRefreshFailure(context: TContext): Promise<void>;
58
- protected verifyIdToken(idToken: string): Promise<TUser | null>;
58
+ protected verifyIdToken(idToken: string, context?: TContext): Promise<TUser | null>;
59
59
  revokeToken(token: string): Promise<void>;
60
60
  protected isTokenExpired(token: string): boolean;
61
61
  }
@@ -161,11 +161,15 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
161
161
  let nonce;
162
162
  if (this.config.state) {
163
163
  state = await oauth2_tools_1.OAuth2Tools.generateState(this.config);
164
- await this.config.state.persistence?.store?.(state);
164
+ await this.config.state.persistence?.store?.(state, context, {
165
+ key: "state",
166
+ });
165
167
  }
166
168
  if (this.config.nonce) {
167
169
  nonce = await oauth2_tools_1.OAuth2Tools.generateNonce(this.config);
168
- await this.config.nonce.persistence?.store?.(nonce);
170
+ await this.config.nonce.persistence?.store?.(nonce, context, {
171
+ key: "nonce",
172
+ });
169
173
  }
170
174
  const authUrl = await this.buildAuthorizationUrl(context, state, nonce);
171
175
  this.redirectUser(context, authUrl);
@@ -175,14 +179,14 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
175
179
  return;
176
180
  }
177
181
  const returnedState = oauth2_tools_1.OAuth2Tools.extractState(context);
178
- const storedState = (await this.config.state.persistence?.read?.());
182
+ const storedState = (await this.config.state.persistence?.read?.(context, "state"));
179
183
  const valid = this.config.state.validateState
180
184
  ? await this.config.state.validateState(storedState, returnedState)
181
185
  : !!storedState && storedState === returnedState;
182
186
  if (!valid) {
183
187
  throw new oauth2_errors_1.InvalidStateError();
184
188
  }
185
- await this.config.state.persistence?.remove?.();
189
+ await this.config.state.persistence?.remove?.(context, "state");
186
190
  }
187
191
  async buildAuthorizationUrl(context, state, nonce) {
188
192
  const params = new URLSearchParams({
@@ -363,8 +367,8 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
363
367
  this.logger?.error(e);
364
368
  }
365
369
  }
366
- async verifyIdToken(idToken) {
367
- const storedNonce = await this.config?.nonce?.persistence?.read?.();
370
+ async verifyIdToken(idToken, context) {
371
+ const storedNonce = await this.config?.nonce?.persistence?.read?.(context, "nonce");
368
372
  if (storedNonce) {
369
373
  const decodedToken = await this.jwks.verify(idToken);
370
374
  if (decodedToken.nonce) {
@@ -9,13 +9,13 @@ export interface OAuth2Endpoints {
9
9
  logoutUrl?: string;
10
10
  }
11
11
  export interface OAuth2StateConfig<TContext = unknown, TData = any> {
12
- persistence?: PersistenceConfig;
12
+ persistence?: PersistenceConfig<TData, TContext>;
13
13
  context?: ContextOperationConfig<TContext, TData>;
14
14
  generateState?: () => string | Promise<string>;
15
15
  validateState?: (storedState: TContext, returnedState: string) => boolean | Promise<boolean>;
16
16
  }
17
17
  export interface OAuth2NonceConfig<TContext = unknown, TData = any> {
18
- persistence?: PersistenceConfig;
18
+ persistence?: PersistenceConfig<TData, TContext>;
19
19
  context?: ContextOperationConfig<TContext, TData>;
20
20
  generateNonce?: () => string | Promise<string>;
21
21
  validateNonce?: (storedNonce: string | null, returnedNonce: string) => boolean | Promise<boolean>;
@@ -47,7 +47,7 @@ export interface OAuth2StrategyConfig<TContext = unknown, TUser = unknown> exten
47
47
  [key: string]: AuthRouteConfig;
48
48
  };
49
49
  state?: OAuth2StateConfig<TContext>;
50
- nonce?: OAuth2NonceConfig;
50
+ nonce?: OAuth2NonceConfig<TContext>;
51
51
  jwks?: JwksConfig;
52
52
  }
53
53
  export interface OAuth2ProviderConfig<TContext = unknown, TUser = unknown> extends Omit<OAuth2StrategyConfig<TContext, TUser>, "endpoints" | "grantType" | "routes"> {
@@ -13,8 +13,8 @@ export declare abstract class TokenAuthStrategy<TContext = Soap.HttpContext, TUs
13
13
  protected abstract verifyRefreshToken(token: string): Promise<any>;
14
14
  protected abstract generateAccessToken(data: TUser, context: TContext): Promise<string>;
15
15
  protected abstract generateRefreshToken(data: TUser, context: TContext): Promise<string>;
16
- protected abstract storeAccessToken(token: string): Promise<void>;
17
- protected abstract storeRefreshToken(token: string): Promise<void>;
16
+ protected abstract storeAccessToken(token: string, context?: TContext): Promise<void>;
17
+ protected abstract storeRefreshToken(token: string, context?: TContext): Promise<void>;
18
18
  protected abstract invalidateAccessToken(token: string, context?: TContext): Promise<void>;
19
19
  protected abstract invalidateRefreshToken(token: string, context?: TContext): Promise<void>;
20
20
  protected abstract embedAccessToken(token: string, context: TContext): void;
@@ -153,10 +153,10 @@ class TokenAuthStrategy extends base_auth_strategy_1.BaseAuthStrategy {
153
153
  refreshToken = await this.generateRefreshToken(user, context);
154
154
  }
155
155
  }
156
- await this.storeAccessToken(accessToken);
156
+ await this.storeAccessToken(accessToken, context);
157
157
  this.embedAccessToken(accessToken, context);
158
158
  if (refreshToken) {
159
- await this.storeRefreshToken(refreshToken);
159
+ await this.storeRefreshToken(refreshToken, context);
160
160
  this.embedRefreshToken(refreshToken, context);
161
161
  }
162
162
  this.logger?.info(`JWT issued successfully`);
package/build/types.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as Soap from "@soapjs/soap";
2
2
  export type AuthResult<TUser extends Soap.AuthUser = Soap.AuthUser> = Soap.AuthResult<TUser>;
3
- export type AuthStrategy<TUser extends Soap.AuthUser = Soap.AuthUser> = Soap.AuthStrategy<TUser>;
3
+ export type AuthStrategy<TUser extends Soap.AuthUser = Soap.AuthUser, TContext extends Soap.HttpContext = Soap.HttpContext> = Soap.AuthStrategy<TUser, TContext>;
4
4
  import { LocalStrategyConfig } from "./strategies/local/local.types";
5
5
  import { OAuth2ProviderConfig } from "./strategies/oauth2/oauth2.types";
6
6
  import { ApiKeyStrategyConfig } from "./strategies/api-key/api-key.types";
@@ -75,13 +75,13 @@ export interface MfaConfig<TUser = unknown, TContext = unknown> {
75
75
  }
76
76
  export type PasswordType = "default" | "one-time" | "temporary";
77
77
  export type NewPasswordOptions = {
78
- expiresIn?: number;
78
+ expiresIn?: string | number;
79
79
  type: PasswordType;
80
80
  additional?: Record<string, unknown>;
81
81
  };
82
82
  export type PasswordInfo = {
83
83
  type: PasswordType;
84
- expiresIn?: number;
84
+ expiresIn?: string | number;
85
85
  lastChangeDate?: Date;
86
86
  };
87
87
  export interface PasswordPolicyConfig {
@@ -259,10 +259,17 @@ export interface TokenVerifierConfig {
259
259
  };
260
260
  verify?: (token: string) => Promise<any>;
261
261
  }
262
- export interface PersistenceConfig<T = any> {
263
- store: (data: any, ...args: any[]) => Promise<void>;
264
- read: (...args: any[]) => Promise<T | null>;
265
- remove: (...args: any[]) => Promise<void>;
262
+ export interface PersistenceMetadata {
263
+ key?: string;
264
+ name?: string;
265
+ expiration?: number;
266
+ expiresIn?: string | number;
267
+ [key: string]: unknown;
268
+ }
269
+ export interface PersistenceConfig<T = any, TContext = any> {
270
+ store: (data: any, context?: TContext | null, metadata?: PersistenceMetadata, ...args: any[]) => Promise<void> | void;
271
+ read: (context?: TContext | null, key?: string, ...args: any[]) => Promise<T | null> | T | null;
272
+ remove: (context?: TContext | null, key?: string, ...args: any[]) => Promise<void> | void;
266
273
  }
267
274
  export interface ContextOperationConfig<TContext = any, TData = any> {
268
275
  embed?: (context: TContext, data: TData) => void;
@@ -272,7 +279,7 @@ export interface TokenConfig<TContext = any, TUser = any> extends ContextOperati
272
279
  rotation?: TokenRotationConfig<TContext, TUser>;
273
280
  issuer?: TokenIssuerConfig<TContext>;
274
281
  verifier?: TokenVerifierConfig;
275
- persistence?: PersistenceConfig;
282
+ persistence?: PersistenceConfig<string, TContext>;
276
283
  additional?: Record<string, unknown>;
277
284
  }
278
285
  export interface RefreshTokenConfig<TContext = any, TUser = any> extends TokenConfig<TContext, TUser> {
@@ -298,13 +305,13 @@ export interface PKCEConfig<TContext> {
298
305
  generate?: (codeVerifier: string) => string;
299
306
  embed?: (context: TContext, challenge: string) => void;
300
307
  extract?: (context: TContext) => string | null;
301
- persistence?: PersistenceConfig;
308
+ persistence?: PersistenceConfig<any, TContext>;
302
309
  };
303
310
  verifier: {
304
311
  expiresIn?: number;
305
312
  generate?: () => string;
306
313
  embed?: (context: TContext, codeVerifier: string) => void;
307
314
  extract?: (context: TContext) => string | null;
308
- persistence?: PersistenceConfig;
315
+ persistence?: PersistenceConfig<any, TContext>;
309
316
  };
310
317
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soapjs/soap-auth",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "Authentication strategies, sessions, MFA, and token helpers for the SoapJS ecosystem.",
5
5
  "homepage": "https://docs.soapjs.com",
6
6
  "repository": {
@@ -72,6 +72,52 @@
72
72
  },
73
73
  "./package.json": "./package.json"
74
74
  },
75
+ "typesVersions": {
76
+ "*": {
77
+ "session": [
78
+ "./build/session/index.d.ts"
79
+ ],
80
+ "session/*": [
81
+ "./build/session/*"
82
+ ],
83
+ "services": [
84
+ "./build/services/index.d.ts"
85
+ ],
86
+ "services/*": [
87
+ "./build/services/*"
88
+ ],
89
+ "strategies": [
90
+ "./build/strategies/index.d.ts"
91
+ ],
92
+ "strategies/*": [
93
+ "./build/strategies/*"
94
+ ],
95
+ "tools": [
96
+ "./build/tools/index.d.ts"
97
+ ],
98
+ "tools/*": [
99
+ "./build/tools/*"
100
+ ],
101
+ "recipes": [
102
+ "./build/recipes/index.d.ts"
103
+ ],
104
+ "recipes/*": [
105
+ "./build/recipes/*"
106
+ ],
107
+ "errors": [
108
+ "./build/errors.d.ts"
109
+ ],
110
+ "types": [
111
+ "./build/types.d.ts"
112
+ ],
113
+ "soap-auth": [
114
+ "./build/soap-auth.d.ts"
115
+ ],
116
+ "utils/validation": [
117
+ "./build/utils/validation.d.ts"
118
+ ]
119
+ }
120
+ },
75
121
  "files": [
76
122
  "build",
77
123
  "README.md",
@@ -93,14 +139,14 @@
93
139
  "access": "public"
94
140
  },
95
141
  "devDependencies": {
96
- "@soapjs/soap": "^0.12.1",
142
+ "@soapjs/soap": "^0.14.0",
97
143
  "@types/jest": "^29.5.14",
98
144
  "jest": "^29.7.0",
99
145
  "ts-jest": "^29.4.11",
100
146
  "typescript": "^4.8.2"
101
147
  },
102
148
  "peerDependencies": {
103
- "@soapjs/soap": ">=0.12.0"
149
+ "@soapjs/soap": ">=0.14.0"
104
150
  },
105
151
  "engines": {
106
152
  "node": ">=24.17.0"