@soapjs/soap-auth 0.4.4 → 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 +286 -125
- package/build/index.d.ts +1 -0
- package/build/index.js +1 -0
- package/build/recipes/auth-config.recipes.d.ts +40 -0
- package/build/recipes/auth-config.recipes.js +135 -0
- package/build/recipes/http-context.helpers.d.ts +13 -0
- package/build/recipes/http-context.helpers.js +64 -0
- package/build/recipes/index.d.ts +3 -0
- package/build/recipes/index.js +19 -0
- package/build/recipes/oauth2-presets.d.ts +20 -0
- package/build/recipes/oauth2-presets.js +74 -0
- package/build/services/pkce.service.js +8 -6
- package/build/soap-auth.js +62 -0
- package/build/strategies/jwt/jwt.strategy.d.ts +2 -2
- package/build/strategies/jwt/jwt.strategy.js +6 -6
- package/build/strategies/oauth2/hybrid.oauth2.strategy.js +2 -2
- package/build/strategies/oauth2/oauth2.strategy.d.ts +1 -1
- package/build/strategies/oauth2/oauth2.strategy.js +12 -8
- package/build/strategies/oauth2/oauth2.types.d.ts +8 -3
- package/build/strategies/oauth2/providers/configurable-hybrid-oauth2.strategy.d.ts +19 -0
- package/build/strategies/oauth2/providers/configurable-hybrid-oauth2.strategy.js +85 -0
- package/build/strategies/oauth2/providers/configurable-oauth2.strategy.d.ts +11 -0
- package/build/strategies/oauth2/providers/configurable-oauth2.strategy.js +46 -0
- package/build/strategies/oauth2/providers/index.d.ts +2 -0
- package/build/strategies/oauth2/providers/index.js +2 -0
- package/build/strategies/oauth2/providers/provider.types.d.ts +3 -0
- package/build/strategies/token-auth.strategy.d.ts +2 -2
- package/build/strategies/token-auth.strategy.js +2 -2
- package/build/types.d.ts +22 -12
- package/package.json +137 -13
- package/.claude/settings.local.json +0 -20
- package/jest.config.unit.json +0 -10
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.redirect = exports.setCookie = exports.getCookie = exports.setBearerToken = exports.extractBearerToken = void 0;
|
|
4
|
+
function extractBearerToken(context) {
|
|
5
|
+
const header = context?.req?.headers?.authorization ??
|
|
6
|
+
context?.request?.headers?.authorization ??
|
|
7
|
+
context?.headers?.authorization;
|
|
8
|
+
if (typeof header === "string" && header.toLowerCase().startsWith("bearer ")) {
|
|
9
|
+
return header.slice(7);
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
exports.extractBearerToken = extractBearerToken;
|
|
14
|
+
function setBearerToken(context, token) {
|
|
15
|
+
const value = `Bearer ${token}`;
|
|
16
|
+
if (typeof context?.res?.setHeader === "function") {
|
|
17
|
+
context.res.setHeader("Authorization", value);
|
|
18
|
+
}
|
|
19
|
+
else if (typeof context?.response?.setHeader === "function") {
|
|
20
|
+
context.response.setHeader("Authorization", value);
|
|
21
|
+
}
|
|
22
|
+
else if (typeof context?.setHeader === "function") {
|
|
23
|
+
context.setHeader("Authorization", value);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
exports.setBearerToken = setBearerToken;
|
|
27
|
+
function getCookie(context, name) {
|
|
28
|
+
return (context?.req?.cookies?.[name] ??
|
|
29
|
+
context?.request?.cookies?.[name] ??
|
|
30
|
+
context?.cookies?.[name]);
|
|
31
|
+
}
|
|
32
|
+
exports.getCookie = getCookie;
|
|
33
|
+
function setCookie(context, name, value, options = {}) {
|
|
34
|
+
if (typeof context?.res?.cookie === "function") {
|
|
35
|
+
context.res.cookie(name, value, options);
|
|
36
|
+
}
|
|
37
|
+
else if (typeof context?.response?.cookie === "function") {
|
|
38
|
+
context.response.cookie(name, value, options);
|
|
39
|
+
}
|
|
40
|
+
else if (typeof context?.cookie === "function") {
|
|
41
|
+
context.cookie(name, value, options);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
exports.setCookie = setCookie;
|
|
45
|
+
function redirect(context, url, status = 302) {
|
|
46
|
+
if (typeof context?.res?.redirect === "function") {
|
|
47
|
+
context.res.redirect(url);
|
|
48
|
+
}
|
|
49
|
+
else if (typeof context?.response?.redirect === "function") {
|
|
50
|
+
context.response.redirect(url);
|
|
51
|
+
}
|
|
52
|
+
else if (typeof context?.redirect === "function") {
|
|
53
|
+
context.redirect(url);
|
|
54
|
+
}
|
|
55
|
+
else if (typeof context?.res?.setHeader === "function") {
|
|
56
|
+
context.res.setHeader("Location", url);
|
|
57
|
+
context.res.status?.(status);
|
|
58
|
+
}
|
|
59
|
+
else if (typeof context?.setHeader === "function") {
|
|
60
|
+
context.setHeader("Location", url);
|
|
61
|
+
context.status?.(status);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
exports.redirect = redirect;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./auth-config.recipes"), exports);
|
|
18
|
+
__exportStar(require("./http-context.helpers"), exports);
|
|
19
|
+
__exportStar(require("./oauth2-presets"), exports);
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { OAuth2Endpoints } from "../strategies/oauth2/oauth2.types";
|
|
2
|
+
export type OAuth2ProviderPreset = "auth0" | "keycloak" | "discord" | "google" | "github" | "facebook";
|
|
3
|
+
export interface Auth0PresetOptions {
|
|
4
|
+
domain: string;
|
|
5
|
+
}
|
|
6
|
+
export interface KeycloakPresetOptions {
|
|
7
|
+
baseUrl: string;
|
|
8
|
+
realm: string;
|
|
9
|
+
}
|
|
10
|
+
export interface FacebookPresetOptions {
|
|
11
|
+
version?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const oauth2ProviderEndpoints: {
|
|
14
|
+
auth0(options: Auth0PresetOptions): OAuth2Endpoints;
|
|
15
|
+
keycloak(options: KeycloakPresetOptions): OAuth2Endpoints;
|
|
16
|
+
discord(): OAuth2Endpoints;
|
|
17
|
+
google(): OAuth2Endpoints;
|
|
18
|
+
github(): OAuth2Endpoints;
|
|
19
|
+
facebook(options?: FacebookPresetOptions): OAuth2Endpoints;
|
|
20
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.oauth2ProviderEndpoints = void 0;
|
|
4
|
+
function trimTrailingSlash(value) {
|
|
5
|
+
return value.replace(/\/+$/, "");
|
|
6
|
+
}
|
|
7
|
+
function ensureHttpsUrl(value) {
|
|
8
|
+
if (/^https?:\/\//i.test(value)) {
|
|
9
|
+
return trimTrailingSlash(value);
|
|
10
|
+
}
|
|
11
|
+
return `https://${trimTrailingSlash(value)}`;
|
|
12
|
+
}
|
|
13
|
+
exports.oauth2ProviderEndpoints = {
|
|
14
|
+
auth0(options) {
|
|
15
|
+
if (!options?.domain) {
|
|
16
|
+
throw new Error("Auth0 OAuth2 preset requires domain.");
|
|
17
|
+
}
|
|
18
|
+
const baseUrl = ensureHttpsUrl(options.domain);
|
|
19
|
+
return {
|
|
20
|
+
authorizationUrl: `${baseUrl}/authorize`,
|
|
21
|
+
tokenUrl: `${baseUrl}/oauth/token`,
|
|
22
|
+
userInfoUrl: `${baseUrl}/userinfo`,
|
|
23
|
+
revocationUrl: `${baseUrl}/oauth/revoke`,
|
|
24
|
+
logoutUrl: `${baseUrl}/v2/logout`,
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
keycloak(options) {
|
|
28
|
+
if (!options?.baseUrl || !options?.realm) {
|
|
29
|
+
throw new Error("Keycloak OAuth2 preset requires baseUrl and realm.");
|
|
30
|
+
}
|
|
31
|
+
const baseUrl = ensureHttpsUrl(options.baseUrl);
|
|
32
|
+
const realmUrl = `${baseUrl}/realms/${encodeURIComponent(options.realm)}`;
|
|
33
|
+
return {
|
|
34
|
+
authorizationUrl: `${realmUrl}/protocol/openid-connect/auth`,
|
|
35
|
+
tokenUrl: `${realmUrl}/protocol/openid-connect/token`,
|
|
36
|
+
userInfoUrl: `${realmUrl}/protocol/openid-connect/userinfo`,
|
|
37
|
+
revocationUrl: `${realmUrl}/protocol/openid-connect/revoke`,
|
|
38
|
+
logoutUrl: `${realmUrl}/protocol/openid-connect/logout`,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
discord() {
|
|
42
|
+
return {
|
|
43
|
+
authorizationUrl: "https://discord.com/oauth2/authorize",
|
|
44
|
+
tokenUrl: "https://discord.com/api/oauth2/token",
|
|
45
|
+
userInfoUrl: "https://discord.com/api/users/@me",
|
|
46
|
+
revocationUrl: "https://discord.com/api/oauth2/token/revoke",
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
google() {
|
|
50
|
+
return {
|
|
51
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
52
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
53
|
+
userInfoUrl: "https://openidconnect.googleapis.com/v1/userinfo",
|
|
54
|
+
revocationUrl: "https://oauth2.googleapis.com/revoke",
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
github() {
|
|
58
|
+
return {
|
|
59
|
+
authorizationUrl: "https://github.com/login/oauth/authorize",
|
|
60
|
+
tokenUrl: "https://github.com/login/oauth/access_token",
|
|
61
|
+
userInfoUrl: "https://api.github.com/user",
|
|
62
|
+
revocationUrl: "https://api.github.com/applications/{client_id}/token",
|
|
63
|
+
};
|
|
64
|
+
},
|
|
65
|
+
facebook(options = {}) {
|
|
66
|
+
const version = options.version ?? "v19.0";
|
|
67
|
+
return {
|
|
68
|
+
authorizationUrl: "https://www.facebook.com/dialog/oauth",
|
|
69
|
+
tokenUrl: `https://graph.facebook.com/${version}/oauth/access_token`,
|
|
70
|
+
userInfoUrl: `https://graph.facebook.com/${version}/me`,
|
|
71
|
+
revocationUrl: `https://graph.facebook.com/${version}/me/permissions`,
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -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
|
}
|
package/build/soap-auth.js
CHANGED
|
@@ -7,6 +7,7 @@ const jwt_strategy_1 = require("./strategies/jwt/jwt.strategy");
|
|
|
7
7
|
const local_strategy_1 = require("./strategies/local/local.strategy");
|
|
8
8
|
const basic_strategy_1 = require("./strategies/basic/basic.strategy");
|
|
9
9
|
const api_key_strategy_1 = require("./strategies/api-key/api-key.strategy");
|
|
10
|
+
const providers_1 = require("./strategies/oauth2/providers");
|
|
10
11
|
class SoapAuth {
|
|
11
12
|
requiredStrategyMethods = ["authenticate"];
|
|
12
13
|
strategies = new Map();
|
|
@@ -219,6 +220,67 @@ class SoapAuth {
|
|
|
219
220
|
if (sharedJwt) {
|
|
220
221
|
auth.addStrategy(sharedJwt, "jwt", "http");
|
|
221
222
|
}
|
|
223
|
+
if (config.http.oauth2) {
|
|
224
|
+
for (const [provider, providerConfig] of Object.entries(config.http.oauth2)) {
|
|
225
|
+
switch (provider) {
|
|
226
|
+
case "google":
|
|
227
|
+
auth.addStrategy(new providers_1.GoogleStrategy(providerConfig, sessionHandler, sharedJwt, logger), "google", "http");
|
|
228
|
+
break;
|
|
229
|
+
case "github":
|
|
230
|
+
auth.addStrategy(new providers_1.GitHubStrategy(providerConfig, sessionHandler, sharedJwt, logger), "github", "http");
|
|
231
|
+
break;
|
|
232
|
+
case "facebook":
|
|
233
|
+
auth.addStrategy(new providers_1.FacebookStrategy(providerConfig, sessionHandler, sharedJwt, logger), "facebook", "http");
|
|
234
|
+
break;
|
|
235
|
+
default:
|
|
236
|
+
if (providerConfig.endpoints?.authorizationUrl &&
|
|
237
|
+
providerConfig.endpoints?.tokenUrl) {
|
|
238
|
+
auth.addStrategy(new providers_1.ConfigurableOAuth2Strategy({
|
|
239
|
+
...providerConfig,
|
|
240
|
+
name: provider,
|
|
241
|
+
grantType: providerConfig.grantType ?? "authorization_code",
|
|
242
|
+
routes: {
|
|
243
|
+
login: {
|
|
244
|
+
path: `/auth/${provider}`,
|
|
245
|
+
method: "GET",
|
|
246
|
+
},
|
|
247
|
+
callback: {
|
|
248
|
+
path: `/auth/${provider}/callback`,
|
|
249
|
+
method: "GET",
|
|
250
|
+
},
|
|
251
|
+
...providerConfig.routes,
|
|
252
|
+
},
|
|
253
|
+
}, sessionHandler, sharedJwt, logger), provider, "http");
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
throw new Error(`OAuth2 provider "${provider}" requires endpoints.authorizationUrl and endpoints.tokenUrl, or a custom strategy via http.custom.`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (config.http.hybridOAuth2) {
|
|
261
|
+
for (const [provider, providerConfig] of Object.entries(config.http.hybridOAuth2)) {
|
|
262
|
+
if (!providerConfig.endpoints?.authorizationUrl ||
|
|
263
|
+
!providerConfig.endpoints?.tokenUrl) {
|
|
264
|
+
throw new Error(`Hybrid OAuth2 provider "${provider}" requires endpoints.authorizationUrl and endpoints.tokenUrl, or a custom strategy via http.custom.`);
|
|
265
|
+
}
|
|
266
|
+
auth.addStrategy(new providers_1.ConfigurableHybridOAuth2Strategy({
|
|
267
|
+
...providerConfig,
|
|
268
|
+
name: provider,
|
|
269
|
+
grantType: providerConfig.grantType ?? "authorization_code",
|
|
270
|
+
routes: {
|
|
271
|
+
login: {
|
|
272
|
+
path: `/auth/${provider}`,
|
|
273
|
+
method: "GET",
|
|
274
|
+
},
|
|
275
|
+
callback: {
|
|
276
|
+
path: `/auth/${provider}/callback`,
|
|
277
|
+
method: "GET",
|
|
278
|
+
},
|
|
279
|
+
...providerConfig.routes,
|
|
280
|
+
},
|
|
281
|
+
}, sessionHandler, sharedJwt, logger), provider, "http");
|
|
282
|
+
}
|
|
283
|
+
}
|
|
222
284
|
if (config.http.custom) {
|
|
223
285
|
for (const [name, strategy] of Object.entries(config.http.custom)) {
|
|
224
286
|
auth.addStrategy(strategy, name, "http");
|
|
@@ -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);
|
|
@@ -49,7 +49,7 @@ class HybridOAuth2Strategy extends oauth2_strategy_1.OAuth2Strategy {
|
|
|
49
49
|
if (!accessToken) {
|
|
50
50
|
const code = this.extractAuthorizationCode(context);
|
|
51
51
|
if (!code) {
|
|
52
|
-
this.login(context);
|
|
52
|
+
await this.login(context);
|
|
53
53
|
throw new errors_1.MissingAuthorizationCodeError();
|
|
54
54
|
}
|
|
55
55
|
await this.validateState(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({
|
|
@@ -202,7 +206,7 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
|
|
|
202
206
|
let authorizationUrl = `${this.config.endpoints.authorizationUrl}?${params.toString()}`;
|
|
203
207
|
if (this.pkce) {
|
|
204
208
|
const codeVerifier = await this.pkce.generateCodeVerifier(context);
|
|
205
|
-
const codeChallenge = this.pkce.generateCodeChallenge(codeVerifier, context);
|
|
209
|
+
const codeChallenge = await this.pkce.generateCodeChallenge(codeVerifier, context);
|
|
206
210
|
authorizationUrl += `&code_challenge=${codeChallenge}&code_challenge_method=S256`;
|
|
207
211
|
}
|
|
208
212
|
return authorizationUrl;
|
|
@@ -212,7 +216,7 @@ class OAuth2Strategy extends base_auth_strategy_1.BaseAuthStrategy {
|
|
|
212
216
|
grant_type: "authorization_code",
|
|
213
217
|
client_id: this.config.clientId,
|
|
214
218
|
code,
|
|
215
|
-
|
|
219
|
+
redirect_uri: this.config.redirectUri,
|
|
216
220
|
};
|
|
217
221
|
if (this.pkce) {
|
|
218
222
|
const codeVerifier = this.pkce.extractCodeVerifier(context);
|
|
@@ -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,6 +47,11 @@ 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
|
+
export interface OAuth2ProviderConfig<TContext = unknown, TUser = unknown> extends Omit<OAuth2StrategyConfig<TContext, TUser>, "endpoints" | "grantType" | "routes"> {
|
|
54
|
+
grantType?: "authorization_code";
|
|
55
|
+
endpoints?: Partial<OAuth2Endpoints>;
|
|
56
|
+
routes?: OAuth2StrategyConfig<TContext, TUser>["routes"];
|
|
57
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import * as Soap from "@soapjs/soap";
|
|
2
|
+
import { SessionHandler } from "../../../session/session-handler";
|
|
3
|
+
import { JwtStrategy } from "../../jwt/jwt.strategy";
|
|
4
|
+
import { HybridOAuth2Strategy } from "../hybrid.oauth2.strategy";
|
|
5
|
+
import { ConfigurableOAuth2StrategyConfig } from "./provider.types";
|
|
6
|
+
export declare class ConfigurableHybridOAuth2Strategy<TUser extends Soap.AuthUser = Soap.AuthUser> extends HybridOAuth2Strategy<Soap.HttpContext, TUser> {
|
|
7
|
+
protected config: ConfigurableOAuth2StrategyConfig<TUser>;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
constructor(config: ConfigurableOAuth2StrategyConfig<TUser>, session?: SessionHandler, jwt?: JwtStrategy<Soap.HttpContext, TUser>, logger?: Soap.Logger);
|
|
10
|
+
protected extractAccessToken(ctx: Soap.HttpContext): Promise<string | undefined>;
|
|
11
|
+
protected extractRefreshToken(ctx: Soap.HttpContext): Promise<string | undefined>;
|
|
12
|
+
protected storeAccessToken(_token: string, _ctx: Soap.HttpContext): Promise<void>;
|
|
13
|
+
protected storeRefreshToken(_token: string, _ctx: Soap.HttpContext): Promise<void>;
|
|
14
|
+
protected embedAccessToken(token: string, ctx: Soap.HttpContext): void;
|
|
15
|
+
protected embedRefreshToken(token: string, ctx: Soap.HttpContext): void;
|
|
16
|
+
protected extractAuthorizationCode(ctx: Soap.HttpContext): string | null;
|
|
17
|
+
protected redirectUser(ctx: Soap.HttpContext, authUrl: string): void;
|
|
18
|
+
protected fetchUser(accessToken: string): Promise<TUser | null>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConfigurableHybridOAuth2Strategy = void 0;
|
|
4
|
+
const errors_1 = require("../../../errors");
|
|
5
|
+
const hybrid_oauth2_strategy_1 = require("../hybrid.oauth2.strategy");
|
|
6
|
+
class ConfigurableHybridOAuth2Strategy extends hybrid_oauth2_strategy_1.HybridOAuth2Strategy {
|
|
7
|
+
config;
|
|
8
|
+
name;
|
|
9
|
+
constructor(config, session, jwt, logger) {
|
|
10
|
+
super(config, session, jwt, logger);
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.name = config.name;
|
|
13
|
+
}
|
|
14
|
+
extractAccessToken(ctx) {
|
|
15
|
+
const auth = ctx.req.headers?.authorization;
|
|
16
|
+
if (typeof auth === "string" && auth.startsWith("Bearer ")) {
|
|
17
|
+
return Promise.resolve(auth.slice(7));
|
|
18
|
+
}
|
|
19
|
+
return Promise.resolve(ctx.req.cookies?.access_token);
|
|
20
|
+
}
|
|
21
|
+
extractRefreshToken(ctx) {
|
|
22
|
+
return Promise.resolve(ctx.req.cookies?.refresh_token);
|
|
23
|
+
}
|
|
24
|
+
storeAccessToken(_token, _ctx) {
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
}
|
|
27
|
+
storeRefreshToken(_token, _ctx) {
|
|
28
|
+
return Promise.resolve();
|
|
29
|
+
}
|
|
30
|
+
embedAccessToken(token, ctx) {
|
|
31
|
+
ctx.res.setHeader("Authorization", `Bearer ${token}`);
|
|
32
|
+
}
|
|
33
|
+
embedRefreshToken(token, ctx) {
|
|
34
|
+
ctx.res.cookie("refresh_token", token, {
|
|
35
|
+
httpOnly: true,
|
|
36
|
+
secure: true,
|
|
37
|
+
sameSite: "lax",
|
|
38
|
+
maxAge: 30 * 24 * 60 * 60 * 1000,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
extractAuthorizationCode(ctx) {
|
|
42
|
+
return ctx.req.query?.code ?? null;
|
|
43
|
+
}
|
|
44
|
+
redirectUser(ctx, authUrl) {
|
|
45
|
+
if (typeof ctx.res.redirect === "function") {
|
|
46
|
+
ctx.res.redirect(authUrl);
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
ctx.res.setHeader("Location", authUrl);
|
|
50
|
+
ctx.res.status(302).json({ message: "Redirecting", location: authUrl });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async fetchUser(accessToken) {
|
|
54
|
+
try {
|
|
55
|
+
if (!this.config.endpoints.userInfoUrl) {
|
|
56
|
+
if (this.config.user?.fetchUser) {
|
|
57
|
+
return this.config.user.fetchUser(accessToken);
|
|
58
|
+
}
|
|
59
|
+
throw new errors_1.MissingConfigError("userInfoUrl");
|
|
60
|
+
}
|
|
61
|
+
const response = await fetch(this.config.endpoints.userInfoUrl, {
|
|
62
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const profile = await response.json();
|
|
68
|
+
if (this.config.user?.validateUser) {
|
|
69
|
+
return this.config.user.validateUser(profile);
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
id: profile.sub ?? profile.id,
|
|
73
|
+
email: profile.email,
|
|
74
|
+
username: profile.preferred_username ?? profile.username ?? profile.name,
|
|
75
|
+
name: profile.name,
|
|
76
|
+
picture: profile.picture ?? profile.avatar_url,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
this.logger?.error("ConfigurableHybridOAuth2Strategy.fetchUser failed:", error);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.ConfigurableHybridOAuth2Strategy = ConfigurableHybridOAuth2Strategy;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import * as Soap from "@soapjs/soap";
|
|
2
|
+
import { SessionHandler } from "../../../session/session-handler";
|
|
3
|
+
import { JwtStrategy } from "../../jwt/jwt.strategy";
|
|
4
|
+
import { HttpOAuth2Strategy } from "./http-oauth2.strategy";
|
|
5
|
+
import { ConfigurableOAuth2StrategyConfig } from "./provider.types";
|
|
6
|
+
export declare class ConfigurableOAuth2Strategy<TUser extends Soap.AuthUser = Soap.AuthUser> extends HttpOAuth2Strategy<TUser> {
|
|
7
|
+
protected config: ConfigurableOAuth2StrategyConfig<TUser>;
|
|
8
|
+
readonly name: string;
|
|
9
|
+
constructor(config: ConfigurableOAuth2StrategyConfig<TUser>, session?: SessionHandler, jwt?: JwtStrategy<Soap.HttpContext, TUser>, logger?: Soap.Logger);
|
|
10
|
+
protected fetchUser(accessToken: string): Promise<TUser | null>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConfigurableOAuth2Strategy = void 0;
|
|
4
|
+
const http_oauth2_strategy_1 = require("./http-oauth2.strategy");
|
|
5
|
+
const errors_1 = require("../../../errors");
|
|
6
|
+
class ConfigurableOAuth2Strategy extends http_oauth2_strategy_1.HttpOAuth2Strategy {
|
|
7
|
+
config;
|
|
8
|
+
name;
|
|
9
|
+
constructor(config, session, jwt, logger) {
|
|
10
|
+
super(config, session, jwt, logger);
|
|
11
|
+
this.config = config;
|
|
12
|
+
this.name = config.name;
|
|
13
|
+
}
|
|
14
|
+
async fetchUser(accessToken) {
|
|
15
|
+
try {
|
|
16
|
+
if (!this.config.endpoints.userInfoUrl) {
|
|
17
|
+
if (this.config.user?.fetchUser) {
|
|
18
|
+
return this.config.user.fetchUser(accessToken);
|
|
19
|
+
}
|
|
20
|
+
throw new errors_1.MissingConfigError("userInfoUrl");
|
|
21
|
+
}
|
|
22
|
+
const response = await fetch(this.config.endpoints.userInfoUrl, {
|
|
23
|
+
headers: { Authorization: `Bearer ${accessToken}` },
|
|
24
|
+
});
|
|
25
|
+
if (!response.ok) {
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
const profile = await response.json();
|
|
29
|
+
if (this.config.user?.validateUser) {
|
|
30
|
+
return this.config.user.validateUser(profile);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
id: profile.sub ?? profile.id,
|
|
34
|
+
email: profile.email,
|
|
35
|
+
username: profile.preferred_username ?? profile.username ?? profile.name,
|
|
36
|
+
name: profile.name,
|
|
37
|
+
picture: profile.picture ?? profile.avatar_url,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
this.logger?.error("ConfigurableOAuth2Strategy.fetchUser failed:", error);
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
exports.ConfigurableOAuth2Strategy = ConfigurableOAuth2Strategy;
|