@soapjs/soap-auth 0.3.3 → 0.4.0

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.
Files changed (77) hide show
  1. package/.claude/settings.local.json +20 -0
  2. package/build/__tests__/soap-auth.test.js +94 -0
  3. package/build/errors.d.ts +1 -1
  4. package/build/errors.js +2 -2
  5. package/build/index.d.ts +1 -0
  6. package/build/index.js +1 -0
  7. package/build/services/__tests__/password.service.test.js +25 -18
  8. package/build/services/__tests__/totp.service.test.d.ts +1 -0
  9. package/build/services/__tests__/totp.service.test.js +120 -0
  10. package/build/services/auth-throttle.service.d.ts +2 -2
  11. package/build/services/auth-throttle.service.js +2 -2
  12. package/build/services/index.d.ts +1 -0
  13. package/build/services/index.js +1 -0
  14. package/build/services/password.service.d.ts +7 -5
  15. package/build/services/password.service.js +76 -18
  16. package/build/services/totp.service.d.ts +16 -0
  17. package/build/services/totp.service.js +96 -0
  18. package/build/session/__tests__/session-handler.test.js +22 -14
  19. package/build/session/session-handler.d.ts +1 -0
  20. package/build/session/session-handler.js +39 -6
  21. package/build/soap-auth.d.ts +13 -6
  22. package/build/soap-auth.js +132 -5
  23. package/build/strategies/__tests__/base-auth.strategy.test.d.ts +3 -2
  24. package/build/strategies/__tests__/base-auth.strategy.test.js +1 -0
  25. package/build/strategies/__tests__/credential-auth.strategy.test.d.ts +1 -0
  26. package/build/strategies/__tests__/credential-auth.strategy.test.js +18 -17
  27. package/build/strategies/__tests__/token-auth.strategy.test.d.ts +1 -0
  28. package/build/strategies/__tests__/token-auth.strategy.test.js +1 -0
  29. package/build/strategies/api-key/api-key.strategy.d.ts +4 -4
  30. package/build/strategies/api-key/api-key.strategy.js +3 -2
  31. package/build/strategies/base-auth.strategy.d.ts +5 -4
  32. package/build/strategies/basic/basic.strategy.d.ts +2 -1
  33. package/build/strategies/basic/basic.strategy.js +1 -0
  34. package/build/strategies/credential-auth.strategy.d.ts +7 -7
  35. package/build/strategies/credential-auth.strategy.js +9 -14
  36. package/build/strategies/index.d.ts +1 -0
  37. package/build/strategies/index.js +1 -0
  38. package/build/strategies/jwt/__tests__/jwt.strategy.test.js +2 -2
  39. package/build/strategies/jwt/jwt.strategy.d.ts +3 -1
  40. package/build/strategies/jwt/jwt.strategy.js +35 -6
  41. package/build/strategies/jwt/jwt.tools.js +8 -8
  42. package/build/strategies/local/__tests__/local.strategy.test.js +8 -2
  43. package/build/strategies/local/local.strategy.d.ts +4 -1
  44. package/build/strategies/local/local.strategy.js +81 -0
  45. package/build/strategies/oauth2/__tests__/oauth2.strategy.test.d.ts +1 -0
  46. package/build/strategies/oauth2/__tests__/oauth2.strategy.test.js +239 -0
  47. package/build/strategies/oauth2/hybrid.oauth2.strategy.d.ts +3 -3
  48. package/build/strategies/oauth2/hybrid.oauth2.strategy.js +1 -6
  49. package/build/strategies/oauth2/oauth2.errors.d.ts +1 -0
  50. package/build/strategies/oauth2/oauth2.errors.js +4 -0
  51. package/build/strategies/oauth2/oauth2.strategy.d.ts +6 -4
  52. package/build/strategies/oauth2/oauth2.strategy.js +114 -46
  53. package/build/strategies/oauth2/oauth2.tools.js +2 -2
  54. package/build/strategies/oauth2/oauth2.types.d.ts +2 -2
  55. package/build/strategies/oauth2/providers/__tests__/social-providers.test.d.ts +1 -0
  56. package/build/strategies/oauth2/providers/__tests__/social-providers.test.js +201 -0
  57. package/build/strategies/oauth2/providers/facebook.strategy.d.ts +11 -0
  58. package/build/strategies/oauth2/providers/facebook.strategy.js +58 -0
  59. package/build/strategies/oauth2/providers/github.strategy.d.ts +11 -0
  60. package/build/strategies/oauth2/providers/github.strategy.js +56 -0
  61. package/build/strategies/oauth2/providers/google.strategy.d.ts +11 -0
  62. package/build/strategies/oauth2/providers/google.strategy.js +52 -0
  63. package/build/strategies/oauth2/providers/http-oauth2.strategy.d.ts +16 -0
  64. package/build/strategies/oauth2/providers/http-oauth2.strategy.js +49 -0
  65. package/build/strategies/oauth2/providers/index.d.ts +5 -0
  66. package/build/strategies/oauth2/providers/index.js +21 -0
  67. package/build/strategies/oauth2/providers/provider.types.d.ts +7 -0
  68. package/build/strategies/oauth2/providers/provider.types.js +2 -0
  69. package/build/strategies/token-auth.strategy.d.ts +4 -4
  70. package/build/strategies/token-auth.strategy.js +2 -3
  71. package/build/tools/tools.js +1 -2
  72. package/build/types.d.ts +31 -32
  73. package/build/utils/__tests__/validation.test.d.ts +1 -0
  74. package/build/utils/__tests__/validation.test.js +181 -0
  75. package/build/utils/validation.d.ts +23 -0
  76. package/build/utils/validation.js +139 -0
  77. package/package.json +8 -7
@@ -0,0 +1,239 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const oauth2_strategy_1 = require("../oauth2.strategy");
4
+ const oauth2_errors_1 = require("../oauth2.errors");
5
+ const errors_1 = require("../../../errors");
6
+ const mockFetch = jest.fn();
7
+ global.fetch = mockFetch;
8
+ function mockFetchOk(body) {
9
+ mockFetch.mockResolvedValueOnce({
10
+ ok: true,
11
+ status: 200,
12
+ statusText: "OK",
13
+ json: async () => body,
14
+ });
15
+ }
16
+ function mockFetchFail(status = 400) {
17
+ mockFetch.mockResolvedValueOnce({
18
+ ok: false,
19
+ status,
20
+ statusText: "Bad Request",
21
+ json: async () => ({}),
22
+ });
23
+ }
24
+ class TestOAuth2Strategy extends oauth2_strategy_1.OAuth2Strategy {
25
+ name = "test-oauth2";
26
+ async extractAccessToken(ctx) {
27
+ return ctx.tokens.access;
28
+ }
29
+ async extractRefreshToken(ctx) {
30
+ return ctx.tokens.refresh;
31
+ }
32
+ async storeAccessToken(token, ctx) {
33
+ ctx.tokens.access = token;
34
+ }
35
+ async storeRefreshToken(token, ctx) {
36
+ ctx.tokens.refresh = token;
37
+ }
38
+ embedAccessToken(token, ctx) {
39
+ ctx.tokens.access = token;
40
+ }
41
+ embedRefreshToken(token, ctx) {
42
+ ctx.tokens.refresh = token;
43
+ }
44
+ extractAuthorizationCode(ctx) {
45
+ return ctx.query.code ?? null;
46
+ }
47
+ redirectUser(ctx, url) {
48
+ ctx.redirectTo = url;
49
+ }
50
+ }
51
+ const baseConfig = {
52
+ clientId: "client-id",
53
+ clientSecret: "client-secret",
54
+ redirectUri: "https://example.com/callback",
55
+ grantType: "authorization_code",
56
+ scope: ["openid", "email"],
57
+ endpoints: {
58
+ authorizationUrl: "https://provider.example/auth",
59
+ tokenUrl: "https://provider.example/token",
60
+ userInfoUrl: "https://provider.example/userinfo",
61
+ },
62
+ routes: {
63
+ login: { path: "/login", method: "GET" },
64
+ callback: { path: "/callback", method: "GET" },
65
+ },
66
+ user: {
67
+ fetchUser: jest.fn(),
68
+ validateUser: jest.fn(async (p) => ({ id: p.sub ?? p.id, email: p.email })),
69
+ },
70
+ };
71
+ function makeCtx(overrides = {}) {
72
+ return { tokens: {}, query: {}, ...overrides };
73
+ }
74
+ describe("OAuth2Strategy — state / CSRF", () => {
75
+ afterEach(() => jest.clearAllMocks());
76
+ it("validateState passes when stored === returned state", async () => {
77
+ let stored = "csrf-abc";
78
+ const config = {
79
+ ...baseConfig,
80
+ state: {
81
+ persistence: {
82
+ store: jest.fn(async (s) => { stored = s; }),
83
+ read: jest.fn(async () => stored),
84
+ remove: jest.fn(async () => { stored = null; }),
85
+ },
86
+ },
87
+ };
88
+ const strategy = new TestOAuth2Strategy(config);
89
+ const ctx = makeCtx({ query: { code: "auth-code", state: "csrf-abc" } });
90
+ await expect(strategy.validateState(ctx)).resolves.toBeUndefined();
91
+ expect(config.state.persistence.remove).toHaveBeenCalled();
92
+ });
93
+ it("validateState throws InvalidStateError on mismatch", async () => {
94
+ const config = {
95
+ ...baseConfig,
96
+ state: {
97
+ persistence: {
98
+ read: jest.fn(async () => "stored-state"),
99
+ remove: jest.fn(),
100
+ },
101
+ },
102
+ };
103
+ const strategy = new TestOAuth2Strategy(config);
104
+ const ctx = makeCtx({ query: { state: "wrong-state" } });
105
+ await expect(strategy.validateState(ctx)).rejects.toThrow(oauth2_errors_1.InvalidStateError);
106
+ });
107
+ it("validateState is a no-op when config.state is absent", async () => {
108
+ const strategy = new TestOAuth2Strategy({ ...baseConfig, state: undefined });
109
+ await expect(strategy.validateState(makeCtx())).resolves.toBeUndefined();
110
+ });
111
+ it("startAuthorizationFlow generates state, stores it, and redirects", async () => {
112
+ const stored = [];
113
+ const config = {
114
+ ...baseConfig,
115
+ state: {
116
+ generateState: jest.fn(async () => "gen-state"),
117
+ persistence: {
118
+ store: jest.fn(async (s) => stored.push(s)),
119
+ },
120
+ },
121
+ };
122
+ const strategy = new TestOAuth2Strategy(config);
123
+ const ctx = makeCtx();
124
+ await strategy.startAuthorizationFlow(ctx);
125
+ expect(stored).toContain("gen-state");
126
+ expect(ctx.redirectTo).toContain("https://provider.example/auth");
127
+ expect(ctx.redirectTo).toContain("state=gen-state");
128
+ });
129
+ });
130
+ describe("OAuth2Strategy — buildAuthorizationUrl", () => {
131
+ afterEach(() => jest.clearAllMocks());
132
+ it("includes client_id, redirect_uri, response_type, scope", async () => {
133
+ const strategy = new TestOAuth2Strategy(baseConfig);
134
+ const url = await strategy.buildAuthorizationUrl(makeCtx());
135
+ expect(url).toContain("client_id=client-id");
136
+ expect(url).toContain("redirect_uri=");
137
+ expect(url).toContain("response_type=code");
138
+ expect(url).toContain("scope=openid+email");
139
+ });
140
+ it("embeds state when provided", async () => {
141
+ const strategy = new TestOAuth2Strategy(baseConfig);
142
+ const url = await strategy.buildAuthorizationUrl(makeCtx(), "my-state");
143
+ expect(url).toContain("state=my-state");
144
+ });
145
+ it("embeds nonce when provided", async () => {
146
+ const strategy = new TestOAuth2Strategy(baseConfig);
147
+ const url = await strategy.buildAuthorizationUrl(makeCtx(), undefined, "my-nonce");
148
+ expect(url).toContain("nonce=my-nonce");
149
+ });
150
+ });
151
+ describe("OAuth2Strategy — verifyAuthorizationCode", () => {
152
+ afterEach(() => jest.clearAllMocks());
153
+ it("does nothing when code is present", async () => {
154
+ const strategy = new TestOAuth2Strategy(baseConfig);
155
+ const ctx = makeCtx();
156
+ await expect(strategy.verifyAuthorizationCode(ctx, "valid-code")).resolves.toBeUndefined();
157
+ });
158
+ it("redirects and throws MissingAuthorizationCodeError when code is absent", async () => {
159
+ const strategy = new TestOAuth2Strategy(baseConfig);
160
+ const ctx = makeCtx();
161
+ await expect(strategy.verifyAuthorizationCode(ctx, null)).rejects.toThrow(errors_1.MissingAuthorizationCodeError);
162
+ expect(ctx.redirectTo).toBeDefined();
163
+ });
164
+ });
165
+ describe("OAuth2Strategy — exchangeCodeForToken", () => {
166
+ afterEach(() => jest.clearAllMocks());
167
+ it("sends correct POST body and returns tokens", async () => {
168
+ mockFetchOk({ access_token: "at-123", refresh_token: "rt-456" });
169
+ const strategy = new TestOAuth2Strategy(baseConfig);
170
+ const result = await strategy.exchangeCodeForToken(makeCtx(), "auth-code");
171
+ expect(result.accessToken).toBe("at-123");
172
+ expect(result.refreshToken).toBe("rt-456");
173
+ expect(mockFetch).toHaveBeenCalledWith("https://provider.example/token", expect.objectContaining({ method: "POST" }));
174
+ });
175
+ it("throws when token endpoint returns non-ok status", async () => {
176
+ mockFetchFail(401);
177
+ const strategy = new TestOAuth2Strategy(baseConfig);
178
+ await expect(strategy.exchangeCodeForToken(makeCtx(), "bad-code")).rejects.toThrow();
179
+ });
180
+ });
181
+ describe("OAuth2Strategy — isTokenExpired", () => {
182
+ afterEach(() => jest.clearAllMocks());
183
+ it("returns false for non-JWT tokens", () => {
184
+ const strategy = new TestOAuth2Strategy(baseConfig);
185
+ expect(strategy.isTokenExpired("opaque-token")).toBe(false);
186
+ });
187
+ it("returns true for an expired JWT", () => {
188
+ const strategy = new TestOAuth2Strategy(baseConfig);
189
+ const pastExp = Math.floor(Date.now() / 1000) - 3600;
190
+ const payload = Buffer.from(JSON.stringify({ exp: pastExp })).toString("base64");
191
+ const jwt = `header.${payload}.sig`;
192
+ expect(strategy.isTokenExpired(jwt)).toBe(true);
193
+ });
194
+ it("returns false for a non-expired JWT", () => {
195
+ const strategy = new TestOAuth2Strategy(baseConfig);
196
+ const futureExp = Math.floor(Date.now() / 1000) + 3600;
197
+ const payload = Buffer.from(JSON.stringify({ exp: futureExp })).toString("base64");
198
+ const jwt = `header.${payload}.sig`;
199
+ expect(strategy.isTokenExpired(jwt)).toBe(false);
200
+ });
201
+ it("returns false when JWT payload has no exp", () => {
202
+ const strategy = new TestOAuth2Strategy(baseConfig);
203
+ const payload = Buffer.from(JSON.stringify({ sub: "user" })).toString("base64");
204
+ const jwt = `header.${payload}.sig`;
205
+ expect(strategy.isTokenExpired(jwt)).toBe(false);
206
+ });
207
+ });
208
+ describe("OAuth2Strategy — authenticate", () => {
209
+ afterEach(() => jest.clearAllMocks());
210
+ it("exchanges code and returns AuthResult when no token in context", async () => {
211
+ let stored = "state-xyz";
212
+ const config = {
213
+ ...baseConfig,
214
+ state: {
215
+ persistence: {
216
+ read: jest.fn(async () => stored),
217
+ remove: jest.fn(async () => { stored = null; }),
218
+ },
219
+ },
220
+ };
221
+ const strategy = new TestOAuth2Strategy(config);
222
+ mockFetchOk({ access_token: "at-new", refresh_token: "rt-new" });
223
+ mockFetchOk({ sub: "u1", email: "user@example.com" });
224
+ const ctx = makeCtx({ query: { code: "auth-code", state: "state-xyz" } });
225
+ const result = await strategy.authenticate(ctx);
226
+ expect(result?.user).toMatchObject({ id: "u1", email: "user@example.com" });
227
+ expect(result?.tokens?.accessToken).toBe("at-new");
228
+ });
229
+ it("fetches user when valid access token already in context", async () => {
230
+ const strategy = new TestOAuth2Strategy(baseConfig);
231
+ const futureExp = Math.floor(Date.now() / 1000) + 3600;
232
+ const payload = Buffer.from(JSON.stringify({ exp: futureExp })).toString("base64");
233
+ const validJwt = `h.${payload}.s`;
234
+ mockFetchOk({ sub: "u2", email: "other@example.com" });
235
+ const ctx = makeCtx({ tokens: { access: validJwt } });
236
+ const result = await strategy.authenticate(ctx);
237
+ expect(result?.user).toMatchObject({ id: "u2" });
238
+ });
239
+ });
@@ -1,5 +1,5 @@
1
- import { AuthResult } from "../../types";
1
+ import * as Soap from "@soapjs/soap";
2
2
  import { OAuth2Strategy } from "./oauth2.strategy";
3
- export declare abstract class HybridOAuth2Strategy<TContext = unknown, TUser = unknown> extends OAuth2Strategy<TContext, TUser> {
4
- authenticate(context: TContext): Promise<AuthResult<TUser>>;
3
+ export declare abstract class HybridOAuth2Strategy<TContext = Soap.HttpContext, TUser extends Soap.AuthUser = Soap.AuthUser> extends OAuth2Strategy<TContext, TUser> {
4
+ authenticate(context: TContext): Promise<Soap.AuthResult<TUser> | null>;
5
5
  }
@@ -3,8 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.HybridOAuth2Strategy = void 0;
4
4
  const errors_1 = require("../../errors");
5
5
  const oauth2_strategy_1 = require("./oauth2.strategy");
6
- const oauth2_errors_1 = require("./oauth2.errors");
7
- const oauth2_tools_1 = require("./oauth2.tools");
8
6
  class HybridOAuth2Strategy extends oauth2_strategy_1.OAuth2Strategy {
9
7
  async authenticate(context) {
10
8
  try {
@@ -54,10 +52,7 @@ class HybridOAuth2Strategy extends oauth2_strategy_1.OAuth2Strategy {
54
52
  this.login(context);
55
53
  throw new errors_1.MissingAuthorizationCodeError();
56
54
  }
57
- const returnedState = oauth2_tools_1.OAuth2Tools.extractState(context);
58
- if (!(await this.config.state?.validateState(context, returnedState))) {
59
- throw new oauth2_errors_1.InvalidStateError();
60
- }
55
+ await this.validateState(context);
61
56
  const exchangedTokens = await this.exchangeCodeForToken(context, code);
62
57
  accessToken = exchangedTokens.accessToken;
63
58
  refreshToken = exchangedTokens.refreshToken;
@@ -1,6 +1,7 @@
1
1
  export declare class InvalidNonceError extends Error {
2
2
  }
3
3
  export declare class InvalidStateError extends Error {
4
+ constructor(message?: string);
4
5
  }
5
6
  export declare class InvalidIdTokenError extends Error {
6
7
  }
@@ -5,6 +5,10 @@ class InvalidNonceError extends Error {
5
5
  }
6
6
  exports.InvalidNonceError = InvalidNonceError;
7
7
  class InvalidStateError extends Error {
8
+ constructor(message = "OAuth2 state mismatch (possible CSRF).") {
9
+ super(message);
10
+ this.name = "InvalidStateError";
11
+ }
8
12
  }
9
13
  exports.InvalidStateError = InvalidStateError;
10
14
  class InvalidIdTokenError extends Error {
@@ -1,16 +1,16 @@
1
1
  import * as Soap from "@soapjs/soap";
2
- import { AuthResult } from "../../types";
3
2
  import { OAuth2StrategyConfig } from "./oauth2.types";
4
3
  import { SessionHandler } from "../../session/session-handler";
5
4
  import { BaseAuthStrategy } from "../base-auth.strategy";
6
5
  import { JwtStrategy } from "../jwt/jwt.strategy";
7
6
  import { JwtService } from "../../services/jwks.service";
8
7
  import { PKCEService } from "../../services/pkce.service";
9
- export declare abstract class OAuth2Strategy<TContext = unknown, TUser = unknown> extends BaseAuthStrategy<TContext, TUser> {
8
+ export declare abstract class OAuth2Strategy<TContext = Soap.HttpContext, TUser extends Soap.AuthUser = Soap.AuthUser> extends BaseAuthStrategy<TContext, TUser> {
10
9
  protected config: OAuth2StrategyConfig<TContext, TUser>;
11
10
  protected session?: SessionHandler;
12
11
  protected jwt?: JwtStrategy<TContext, TUser>;
13
12
  protected logger?: Soap.Logger;
13
+ abstract readonly name: string;
14
14
  protected jwks: JwtService;
15
15
  protected pkce: PKCEService<TContext>;
16
16
  protected abstract extractAccessToken(context: TContext): Promise<string | undefined>;
@@ -27,13 +27,15 @@ export declare abstract class OAuth2Strategy<TContext = unknown, TUser = unknown
27
27
  identifier: string;
28
28
  password: string;
29
29
  }>;
30
- authenticate(context: TContext): Promise<AuthResult<TUser>>;
30
+ authenticate(context: TContext): Promise<Soap.AuthResult<TUser> | null>;
31
31
  protected processOAuthFlow(context: TContext): Promise<{
32
32
  accessToken: string;
33
33
  refreshToken?: string;
34
34
  }>;
35
35
  protected verifyAuthorizationCode(context: TContext, code: string): Promise<void>;
36
- protected buildAuthorizationUrl(context: TContext): Promise<string>;
36
+ protected startAuthorizationFlow(context: TContext): Promise<void>;
37
+ protected validateState(context: TContext): Promise<void>;
38
+ protected buildAuthorizationUrl(context: TContext, state?: string, nonce?: string): Promise<string>;
37
39
  protected exchangeCodeForToken(context: TContext, code: string): Promise<{
38
40
  accessToken: string;
39
41
  idToken?: string;
@@ -1,10 +1,6 @@
1
1
  "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
2
  Object.defineProperty(exports, "__esModule", { value: true });
6
3
  exports.OAuth2Strategy = void 0;
7
- const axios_1 = __importDefault(require("axios"));
8
4
  const errors_1 = require("../../errors");
9
5
  const oauth2_tools_1 = require("./oauth2.tools");
10
6
  const base_auth_strategy_1 = require("../base-auth.strategy");
@@ -118,7 +114,7 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
118
114
  if (this.session) {
119
115
  session = await this.session.issueSession(user, context);
120
116
  }
121
- await this.role.isAuthorized(user);
117
+ await this.role?.isAuthorized(user);
122
118
  return { user: user, tokens: { accessToken, refreshToken }, session };
123
119
  }
124
120
  catch (error) {
@@ -133,6 +129,7 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
133
129
  if (this.config.grantType === "authorization_code") {
134
130
  const authorizationCode = this.extractAuthorizationCode(context);
135
131
  await this.verifyAuthorizationCode(context, authorizationCode);
132
+ await this.validateState(context);
136
133
  const tokenResult = await this.exchangeCodeForToken(context, authorizationCode);
137
134
  return tokenResult;
138
135
  }
@@ -155,13 +152,54 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
155
152
  async verifyAuthorizationCode(context, code) {
156
153
  if (!code) {
157
154
  this.logger?.warn("Authorization code missing, redirecting user.");
158
- const authUrl = await this.buildAuthorizationUrl(context);
159
- this.redirectUser(context, authUrl);
155
+ await this.startAuthorizationFlow(context);
160
156
  throw new errors_1.MissingAuthorizationCodeError();
161
157
  }
162
158
  }
163
- async buildAuthorizationUrl(context) {
164
- let authorizationUrl = `${this.config.endpoints.authorizationUrl}?client_id=${this.config.clientId}&redirect_uri=${encodeURIComponent(this.config.redirectUri)}&response_type=code&scope=${this.config.scope ?? ""}`;
159
+ async startAuthorizationFlow(context) {
160
+ let state;
161
+ let nonce;
162
+ if (this.config.state) {
163
+ state = await oauth2_tools_1.OAuth2Tools.generateState(this.config);
164
+ await this.config.state.persistence?.store?.(state);
165
+ }
166
+ if (this.config.nonce) {
167
+ nonce = await oauth2_tools_1.OAuth2Tools.generateNonce(this.config);
168
+ await this.config.nonce.persistence?.store?.(nonce);
169
+ }
170
+ const authUrl = await this.buildAuthorizationUrl(context, state, nonce);
171
+ this.redirectUser(context, authUrl);
172
+ }
173
+ async validateState(context) {
174
+ if (!this.config.state) {
175
+ return;
176
+ }
177
+ const returnedState = oauth2_tools_1.OAuth2Tools.extractState(context);
178
+ const storedState = (await this.config.state.persistence?.read?.());
179
+ const valid = this.config.state.validateState
180
+ ? await this.config.state.validateState(storedState, returnedState)
181
+ : !!storedState && storedState === returnedState;
182
+ if (!valid) {
183
+ throw new oauth2_errors_1.InvalidStateError();
184
+ }
185
+ await this.config.state.persistence?.remove?.();
186
+ }
187
+ async buildAuthorizationUrl(context, state, nonce) {
188
+ const params = new URLSearchParams({
189
+ client_id: this.config.clientId,
190
+ redirect_uri: this.config.redirectUri,
191
+ response_type: "code",
192
+ scope: Array.isArray(this.config.scope)
193
+ ? this.config.scope.join(" ")
194
+ : this.config.scope ?? "",
195
+ });
196
+ if (state) {
197
+ params.set("state", state);
198
+ }
199
+ if (nonce) {
200
+ params.set("nonce", nonce);
201
+ }
202
+ let authorizationUrl = `${this.config.endpoints.authorizationUrl}?${params.toString()}`;
165
203
  if (this.pkce) {
166
204
  const codeVerifier = await this.pkce.generateCodeVerifier(context);
167
205
  const codeChallenge = this.pkce.generateCodeChallenge(codeVerifier, context);
@@ -186,11 +224,15 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
186
224
  else if (this.config.clientSecret) {
187
225
  data.client_secret = this.config.clientSecret;
188
226
  }
189
- const response = await axios_1.default.post(this.config.endpoints.tokenUrl, {
227
+ const response = await fetch(this.config.endpoints.tokenUrl, {
228
+ method: "POST",
190
229
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
191
230
  body: new URLSearchParams(data).toString(),
192
231
  });
193
- const tokenData = await response.data();
232
+ if (!response.ok) {
233
+ throw new Error(`Token exchange failed: ${response.status} ${response.statusText}`);
234
+ }
235
+ const tokenData = await response.json();
194
236
  return {
195
237
  accessToken: tokenData.access_token,
196
238
  refreshToken: tokenData.refresh_token,
@@ -199,12 +241,7 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
199
241
  }
200
242
  async login(context) {
201
243
  try {
202
- const state = await oauth2_tools_1.OAuth2Tools.generateState(this.config);
203
- const nonce = await oauth2_tools_1.OAuth2Tools.generateNonce(this.config);
204
- await this.config.state?.persistence?.store?.(state);
205
- await this.config.nonce?.persistence?.store?.(nonce);
206
- const authUrl = await this.buildAuthorizationUrl(context);
207
- this.redirectUser(context, authUrl);
244
+ await this.startAuthorizationFlow(context);
208
245
  }
209
246
  catch (error) {
210
247
  await this.onFailure("login", {
@@ -219,10 +256,13 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
219
256
  if (!this.config.endpoints.userInfoUrl) {
220
257
  throw new errors_1.MissingConfigError("userInfoUrl");
221
258
  }
222
- const response = await axios_1.default.get(this.config.endpoints.userInfoUrl, {
259
+ const response = await fetch(this.config.endpoints.userInfoUrl, {
223
260
  headers: { Authorization: `Bearer ${accessToken}` },
224
261
  });
225
- return (await this.config.user.validateUser(response.data)) || null;
262
+ if (!response.ok) {
263
+ throw new Error(`User info request failed: ${response.status}`);
264
+ }
265
+ return (await this.config.user.validateUser(await response.json())) || null;
226
266
  }
227
267
  catch (error) {
228
268
  this.logger?.error("Failed to fetch user information:", error);
@@ -230,25 +270,41 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
230
270
  }
231
271
  }
232
272
  async exchangeClientCredentials() {
233
- const response = await axios_1.default.post(this.config.endpoints.tokenUrl, {
234
- grant_type: "client_credentials",
235
- client_id: this.config.clientId,
236
- client_secret: this.config.clientSecret,
273
+ const response = await fetch(this.config.endpoints.tokenUrl, {
274
+ method: "POST",
275
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
276
+ body: new URLSearchParams({
277
+ grant_type: "client_credentials",
278
+ client_id: this.config.clientId,
279
+ client_secret: this.config.clientSecret,
280
+ }).toString(),
237
281
  });
238
- return { accessToken: response.data.access_token };
282
+ if (!response.ok) {
283
+ throw new Error(`Client credentials exchange failed: ${response.status}`);
284
+ }
285
+ const data = await response.json();
286
+ return { accessToken: data.access_token };
239
287
  }
240
288
  async exchangePasswordGrant(username, password) {
241
289
  if (!username || !password) {
242
290
  throw new errors_1.MissingCredentialsError();
243
291
  }
244
- const response = await axios_1.default.post(this.config.endpoints.tokenUrl, {
245
- grant_type: "password",
246
- client_id: this.config.clientId,
247
- client_secret: this.config.clientSecret,
248
- username,
249
- password,
292
+ const response = await fetch(this.config.endpoints.tokenUrl, {
293
+ method: "POST",
294
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
295
+ body: new URLSearchParams({
296
+ grant_type: "password",
297
+ client_id: this.config.clientId,
298
+ client_secret: this.config.clientSecret,
299
+ username,
300
+ password,
301
+ }).toString(),
250
302
  });
251
- return { accessToken: response.data.access_token };
303
+ if (!response.ok) {
304
+ throw new Error(`Password grant failed: ${response.status}`);
305
+ }
306
+ const data = await response.json();
307
+ return { accessToken: data.access_token };
252
308
  }
253
309
  async refreshAccessToken(context) {
254
310
  try {
@@ -256,22 +312,27 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
256
312
  if (!refreshToken) {
257
313
  throw new errors_1.MissingTokenError("Refresh");
258
314
  }
259
- const response = await axios_1.default.post(this.config.endpoints.tokenUrl, {
260
- grant_type: "refresh_token",
261
- client_id: this.config.clientId,
262
- client_secret: this.config.clientSecret,
263
- refresh_token: refreshToken,
315
+ const response = await fetch(this.config.endpoints.tokenUrl, {
316
+ method: "POST",
317
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
318
+ body: new URLSearchParams({
319
+ grant_type: "refresh_token",
320
+ client_id: this.config.clientId,
321
+ client_secret: this.config.clientSecret,
322
+ refresh_token: refreshToken,
323
+ }).toString(),
264
324
  });
265
- if (response.data.error) {
266
- this.logger?.warn(`OAuth2 error: ${response.data.error_description || response.data.error}`);
325
+ const responseData = await response.json();
326
+ if (responseData.error) {
327
+ this.logger?.warn(`OAuth2 error: ${responseData.error_description || responseData.error}`);
267
328
  }
268
- if (response.status !== 200 || !response.data.access_token) {
329
+ if (!response.ok || !responseData.access_token) {
269
330
  throw new Error("Failed to refresh token");
270
331
  }
271
332
  const refreshedTokens = {
272
- accessToken: response.data.access_token,
273
- refreshToken: response.data.refresh_token,
274
- idToken: response.data.id_token,
333
+ accessToken: responseData.access_token,
334
+ refreshToken: responseData.refresh_token,
335
+ idToken: responseData.id_token,
275
336
  };
276
337
  await this.storeAccessToken(refreshedTokens.accessToken, context);
277
338
  await this.embedAccessToken(refreshedTokens.accessToken, context);
@@ -322,11 +383,18 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
322
383
  throw new errors_1.MissingConfigError("revocationUrl");
323
384
  }
324
385
  try {
325
- await axios_1.default.post(this.config.endpoints.revocationUrl, {
326
- token,
327
- client_id: this.config.clientId,
328
- client_secret: this.config.clientSecret,
386
+ const response = await fetch(this.config.endpoints.revocationUrl, {
387
+ method: "POST",
388
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
389
+ body: new URLSearchParams({
390
+ token,
391
+ client_id: this.config.clientId,
392
+ client_secret: this.config.clientSecret,
393
+ }).toString(),
329
394
  });
395
+ if (!response.ok) {
396
+ throw new Error(`Token revocation failed: ${response.status}`);
397
+ }
330
398
  this.logger?.info("Token revoked successfully.");
331
399
  }
332
400
  catch (error) {
@@ -104,14 +104,14 @@ const prepareOAuth2Config = (config) => {
104
104
  : undefined,
105
105
  state: config.state
106
106
  ? {
107
- generateState: () => Math.random().toString(36).substring(2, 15),
107
+ generateState: () => (0, tools_1.generateRandomString)(),
108
108
  validateState: (storedState, returnedState) => storedState === returnedState,
109
109
  ...config.state,
110
110
  }
111
111
  : undefined,
112
112
  nonce: config.nonce
113
113
  ? {
114
- generateNonce: () => Math.random().toString(36).substring(2, 15),
114
+ generateNonce: () => (0, tools_1.generateRandomString)(),
115
115
  validateNonce: (storedNonce, returnedNonce) => storedNonce === returnedNonce,
116
116
  ...config.nonce,
117
117
  }
@@ -1,5 +1,5 @@
1
1
  import { Algorithm } from "jsonwebtoken";
2
- import { CredentailsConfig, PKCEConfig, AuthRouteConfig, TokenAuthStrategyConfig, PersistenceConfig, ContextOperationConfig } from "../../types";
2
+ import { CredentialsConfig, PKCEConfig, AuthRouteConfig, TokenAuthStrategyConfig, PersistenceConfig, ContextOperationConfig } from "../../types";
3
3
  export interface OAuth2Endpoints {
4
4
  authorizationUrl: string;
5
5
  tokenUrl: string;
@@ -36,7 +36,7 @@ export interface OAuth2StrategyConfig<TContext = unknown, TUser = unknown> exten
36
36
  responseType?: "code" | "token" | "id_token" | string;
37
37
  grantType: "authorization_code" | "client_credentials" | "password" | "refresh_token" | string;
38
38
  endpoints: OAuth2Endpoints;
39
- credentials?: CredentailsConfig<TContext>;
39
+ credentials?: CredentialsConfig<TContext>;
40
40
  pkce?: PKCEConfig<TContext>;
41
41
  routes: {
42
42
  login: AuthRouteConfig;