@eventuras/fides-auth 0.5.0 → 0.7.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.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -5
- package/dist/logger.js +100 -58
- package/dist/logger.js.map +1 -0
- package/dist/oauth-browser.js +52 -30
- package/dist/oauth-browser.js.map +1 -0
- package/dist/oauth-logging.d.ts +45 -0
- package/dist/oauth-logging.d.ts.map +1 -0
- package/dist/oauth-logging.js +50 -0
- package/dist/oauth-logging.js.map +1 -0
- package/dist/oauth.d.ts.map +1 -1
- package/dist/oauth.js +293 -133
- package/dist/oauth.js.map +1 -0
- package/dist/providers/vipps/index.js +162 -76
- package/dist/providers/vipps/index.js.map +1 -0
- package/dist/rate-limit.js +55 -17
- package/dist/rate-limit.js.map +1 -0
- package/dist/session-refresh.d.ts.map +1 -1
- package/dist/session-refresh.js +40 -19
- package/dist/session-refresh.js.map +1 -0
- package/dist/session-validation.js +77 -2
- package/dist/session-validation.js.map +1 -0
- package/dist/silent-login.js +133 -33
- package/dist/silent-login.js.map +1 -0
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils.d.ts +8 -0
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +196 -2
- package/dist/utils.js.map +1 -0
- package/package.json +6 -2
- package/dist/build-nL2--iMi.js +0 -1014
- package/dist/decode_jwt-1J26fl4I.js +0 -25
- package/dist/decrypt-Cahlu_6Y.js +0 -92
- package/dist/deflate-koSuX7FB.js +0 -1015
- package/dist/session-validation-BxObT3wC.js +0 -66
- package/dist/utils-ByMRF7b2.js +0 -379
|
@@ -1,120 +1,206 @@
|
|
|
1
|
-
import { createLogger
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { createLogger } from "../../logger.js";
|
|
2
|
+
import { buildAuthorizationUrl, buildPKCEOptions } from "../../oauth.js";
|
|
3
|
+
import * as openid from "openid-client";
|
|
4
4
|
//#region src/providers/vipps/types.ts
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Available Vipps Login scopes
|
|
7
|
+
*/
|
|
8
|
+
var VippsLoginScopes = {
|
|
9
|
+
/** Required: OpenID Connect base scope */
|
|
6
10
|
OpenId: "openid",
|
|
11
|
+
/** User's name (given_name, family_name, name) */
|
|
7
12
|
Name: "name",
|
|
13
|
+
/** Phone number */
|
|
8
14
|
PhoneNumber: "phoneNumber",
|
|
15
|
+
/** Physical address */
|
|
9
16
|
Address: "address",
|
|
17
|
+
/** Birth date */
|
|
10
18
|
BirthDate: "birthDate",
|
|
19
|
+
/** Email address */
|
|
11
20
|
Email: "email"
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Default scopes for Vipps Login
|
|
24
|
+
*/
|
|
25
|
+
var defaultVippsLoginScope = [
|
|
26
|
+
VippsLoginScopes.OpenId,
|
|
27
|
+
VippsLoginScopes.Name,
|
|
28
|
+
VippsLoginScopes.PhoneNumber,
|
|
29
|
+
VippsLoginScopes.Address,
|
|
30
|
+
VippsLoginScopes.Email
|
|
31
|
+
].join(" ");
|
|
32
|
+
/**
|
|
33
|
+
* Vipps API environments
|
|
34
|
+
*/
|
|
35
|
+
var VippsEnvironments = {
|
|
36
|
+
/** Test environment */
|
|
19
37
|
Test: "https://apitest.vipps.no",
|
|
38
|
+
/** Production environment */
|
|
20
39
|
Production: "https://api.vipps.no"
|
|
21
40
|
};
|
|
22
|
-
|
|
23
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Get Vipps OIDC issuer URL for environment
|
|
43
|
+
*/
|
|
44
|
+
function getVippsIssuer(environment) {
|
|
45
|
+
return `${environment}/access-management-1.0/access/`;
|
|
24
46
|
}
|
|
25
47
|
//#endregion
|
|
26
48
|
//#region src/providers/vipps/client.ts
|
|
27
|
-
|
|
49
|
+
/**
|
|
50
|
+
* Vipps Login Client
|
|
51
|
+
*
|
|
52
|
+
* Client for Vipps Login (OIDC) authentication flow.
|
|
53
|
+
* Implements OAuth/OIDC operations with PKCE for Vipps.
|
|
54
|
+
*/
|
|
55
|
+
var logger = createLogger({
|
|
28
56
|
namespace: "fides-auth:vipps",
|
|
29
57
|
context: { module: "VippsLoginClient" }
|
|
30
|
-
})
|
|
58
|
+
});
|
|
59
|
+
/**
|
|
60
|
+
* Vipps Login Client
|
|
61
|
+
*
|
|
62
|
+
* Handles Vipps Login (OIDC) authentication flow using PKCE.
|
|
63
|
+
*/
|
|
64
|
+
var VippsLoginClient = class {
|
|
31
65
|
config;
|
|
32
66
|
oauthConfig;
|
|
33
67
|
configPromise = null;
|
|
34
|
-
constructor(
|
|
68
|
+
constructor(config) {
|
|
35
69
|
this.config = {
|
|
36
|
-
...
|
|
37
|
-
scope:
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
70
|
+
...config,
|
|
71
|
+
scope: config.scope ?? defaultVippsLoginScope
|
|
72
|
+
};
|
|
73
|
+
this.oauthConfig = {
|
|
74
|
+
issuer: config.issuer,
|
|
75
|
+
clientId: config.clientId,
|
|
76
|
+
clientSecret: config.clientSecret,
|
|
77
|
+
redirect_uri: config.redirectUri,
|
|
43
78
|
scope: this.config.scope
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
|
|
79
|
+
};
|
|
80
|
+
logger.info({
|
|
81
|
+
issuer: config.issuer,
|
|
82
|
+
redirectUri: config.redirectUri,
|
|
47
83
|
scope: this.config.scope
|
|
48
84
|
}, "VippsLoginClient initialized");
|
|
49
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Get OpenID Configuration (cached)
|
|
88
|
+
* This ensures we use the same Configuration instance across all operations
|
|
89
|
+
*/
|
|
50
90
|
async getConfig() {
|
|
51
|
-
|
|
91
|
+
if (!this.configPromise) this.configPromise = openid.discovery(new URL(this.oauthConfig.issuer), this.oauthConfig.clientId, this.oauthConfig.clientSecret, openid.ClientSecretPost(this.oauthConfig.clientSecret));
|
|
92
|
+
return this.configPromise;
|
|
52
93
|
}
|
|
94
|
+
/**
|
|
95
|
+
* Build PKCE options for authorization flow
|
|
96
|
+
*
|
|
97
|
+
* @returns PKCE options including code_verifier, code_challenge, and state
|
|
98
|
+
*/
|
|
53
99
|
async buildPKCEOptions() {
|
|
54
|
-
|
|
100
|
+
logger.debug("Building PKCE options");
|
|
101
|
+
return buildPKCEOptions(this.oauthConfig);
|
|
55
102
|
}
|
|
56
|
-
|
|
57
|
-
|
|
103
|
+
/**
|
|
104
|
+
* Build authorization URL for Vipps Login
|
|
105
|
+
*
|
|
106
|
+
* @param pkceOptions - PKCE options from buildPKCEOptions()
|
|
107
|
+
* @returns Authorization URL to redirect user to
|
|
108
|
+
*/
|
|
109
|
+
async buildAuthorizationUrl(pkceOptions) {
|
|
110
|
+
logger.debug("Building authorization URL");
|
|
111
|
+
return buildAuthorizationUrl(await this.getConfig(), pkceOptions);
|
|
58
112
|
}
|
|
59
|
-
|
|
60
|
-
|
|
113
|
+
/**
|
|
114
|
+
* Exchange authorization code for tokens
|
|
115
|
+
*
|
|
116
|
+
* @param callbackUrl - The full callback URL with code parameter from Vipps
|
|
117
|
+
* @param codeVerifier - PKCE code verifier from initial request
|
|
118
|
+
* @param expectedState - Expected state value for validation (optional)
|
|
119
|
+
* @returns Token response with access_token, id_token, and refresh_token
|
|
120
|
+
*/
|
|
121
|
+
async exchangeCodeForTokens(callbackUrl, codeVerifier, expectedState) {
|
|
122
|
+
logger.debug("Exchanging authorization code for tokens");
|
|
61
123
|
try {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
124
|
+
const config = await this.getConfig();
|
|
125
|
+
const currentUrl = typeof callbackUrl === "string" ? new URL(callbackUrl) : callbackUrl;
|
|
126
|
+
const tokens = await openid.authorizationCodeGrant(config, currentUrl, {
|
|
127
|
+
pkceCodeVerifier: codeVerifier,
|
|
128
|
+
expectedState,
|
|
129
|
+
idTokenExpected: true
|
|
66
130
|
});
|
|
67
|
-
|
|
68
|
-
hasAccessToken: !!
|
|
69
|
-
hasRefreshToken: !!
|
|
70
|
-
hasIdToken: !!
|
|
71
|
-
expiresIn:
|
|
72
|
-
}, "Successfully exchanged code for tokens")
|
|
73
|
-
|
|
74
|
-
|
|
131
|
+
logger.info({
|
|
132
|
+
hasAccessToken: !!tokens.access_token,
|
|
133
|
+
hasRefreshToken: !!tokens.refresh_token,
|
|
134
|
+
hasIdToken: !!tokens.id_token,
|
|
135
|
+
expiresIn: tokens.expires_in
|
|
136
|
+
}, "Successfully exchanged code for tokens");
|
|
137
|
+
return tokens;
|
|
138
|
+
} catch (error) {
|
|
139
|
+
logger.error({ error }, "Failed to exchange code for tokens");
|
|
140
|
+
throw error;
|
|
75
141
|
}
|
|
76
142
|
}
|
|
77
|
-
|
|
78
|
-
|
|
143
|
+
/**
|
|
144
|
+
* Get user information from Vipps userinfo endpoint
|
|
145
|
+
*
|
|
146
|
+
* @param accessToken - Access token from token exchange
|
|
147
|
+
* @returns Vipps user information
|
|
148
|
+
*/
|
|
149
|
+
async getUserInfo(accessToken) {
|
|
150
|
+
logger.debug("Fetching user info from Vipps");
|
|
79
151
|
try {
|
|
80
|
-
|
|
81
|
-
if (!
|
|
82
|
-
|
|
83
|
-
Authorization: `Bearer ${
|
|
152
|
+
const userinfoUrl = (await this.getConfig()).serverMetadata().userinfo_endpoint;
|
|
153
|
+
if (!userinfoUrl) throw new Error("Userinfo endpoint not found in OpenID configuration");
|
|
154
|
+
const response = await fetch(userinfoUrl, { headers: {
|
|
155
|
+
Authorization: `Bearer ${accessToken}`,
|
|
84
156
|
...this.config.subscriptionKey && { "Ocp-Apim-Subscription-Key": this.config.subscriptionKey },
|
|
85
157
|
...this.config.merchantSerialNumber && { "Merchant-Serial-Number": this.config.merchantSerialNumber }
|
|
86
158
|
} });
|
|
87
|
-
if (!
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
status:
|
|
91
|
-
error:
|
|
92
|
-
}, "Failed to fetch user info")
|
|
159
|
+
if (!response.ok) {
|
|
160
|
+
const errorText = await response.text();
|
|
161
|
+
logger.error({
|
|
162
|
+
status: response.status,
|
|
163
|
+
error: errorText
|
|
164
|
+
}, "Failed to fetch user info");
|
|
165
|
+
throw new Error(`Failed to fetch user info: ${response.status} - ${errorText}`);
|
|
93
166
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
sub:
|
|
97
|
-
hasEmail: !!
|
|
98
|
-
hasPhone: !!
|
|
99
|
-
hasAddress: !!
|
|
100
|
-
}, "Successfully fetched user info")
|
|
101
|
-
|
|
102
|
-
|
|
167
|
+
const userInfo = await response.json();
|
|
168
|
+
logger.info({
|
|
169
|
+
sub: userInfo.sub,
|
|
170
|
+
hasEmail: !!userInfo.email,
|
|
171
|
+
hasPhone: !!userInfo.phone_number,
|
|
172
|
+
hasAddress: !!userInfo.addresses?.length
|
|
173
|
+
}, "Successfully fetched user info");
|
|
174
|
+
return userInfo;
|
|
175
|
+
} catch (error) {
|
|
176
|
+
logger.error({ error }, "Error fetching user info");
|
|
177
|
+
throw error;
|
|
103
178
|
}
|
|
104
179
|
}
|
|
105
|
-
|
|
106
|
-
|
|
180
|
+
/**
|
|
181
|
+
* Refresh access token using refresh token
|
|
182
|
+
*
|
|
183
|
+
* @param refreshToken - Refresh token from initial token exchange
|
|
184
|
+
* @returns New token response
|
|
185
|
+
*/
|
|
186
|
+
async refreshTokens(refreshToken) {
|
|
187
|
+
logger.debug("Refreshing access token");
|
|
107
188
|
try {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
189
|
+
const config = await this.getConfig();
|
|
190
|
+
const tokens = await openid.refreshTokenGrant(config, refreshToken);
|
|
191
|
+
logger.info({
|
|
192
|
+
hasAccessToken: !!tokens.access_token,
|
|
193
|
+
hasRefreshToken: !!tokens.refresh_token,
|
|
194
|
+
expiresIn: tokens.expires_in
|
|
195
|
+
}, "Successfully refreshed tokens");
|
|
196
|
+
return tokens;
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logger.error({ error }, "Failed to refresh tokens");
|
|
199
|
+
throw error;
|
|
116
200
|
}
|
|
117
201
|
}
|
|
118
202
|
};
|
|
119
203
|
//#endregion
|
|
120
|
-
export {
|
|
204
|
+
export { VippsEnvironments, VippsLoginClient, VippsLoginScopes, defaultVippsLoginScope, getVippsIssuer };
|
|
205
|
+
|
|
206
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/providers/vipps/types.ts","../../../src/providers/vipps/client.ts"],"sourcesContent":["/**\n * Vipps Login Provider Types\n *\n * Type definitions for Vipps Login (OIDC) integration.\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/\n */\n\n/**\n * Configuration for Vipps Login client\n */\nexport interface VippsLoginConfig {\n /** Vipps OIDC issuer URL (e.g., 'https://apitest.vipps.no/access-management-1.0/access') */\n issuer: string;\n /** OAuth Client ID from Vipps */\n clientId: string;\n /** OAuth Client Secret from Vipps */\n clientSecret: string;\n /** OAuth redirect URI (callback URL) */\n redirectUri: string;\n /** OpenID Connect scopes (default: 'openid name phoneNumber address email') */\n scope?: string;\n /** Vipps subscription key (Ocp-Apim-Subscription-Key) - required for userinfo endpoint */\n subscriptionKey?: string;\n /** Merchant serial number - required for userinfo endpoint */\n merchantSerialNumber?: string;\n}\n\n/**\n * Vipps address from userinfo endpoint\n */\nexport interface VippsAddress {\n /** Street address */\n street_address?: string;\n /** Postal code */\n postal_code?: string;\n /** Region/city */\n region?: string;\n /** Country code (e.g., 'NO') */\n country?: string;\n /** Formatted address string */\n formatted?: string;\n /** Address type (e.g., 'home', 'work', 'other') */\n address_type?: string;\n}\n\n/**\n * User information returned from Vipps Login userinfo endpoint\n *\n * @see https://developer.vippsmobilepay.com/docs/APIs/login-api/login-api-guide/#userinfo-endpoint\n */\nexport interface VippsUserInfo {\n /** Subject - unique Vipps user identifier (UUID) */\n sub: string;\n /** Email address */\n email?: string;\n /** Email verification status */\n email_verified?: boolean;\n /** Given name (first name) */\n given_name?: string;\n /** Family name (last name) */\n family_name?: string;\n /** Full name */\n name?: string;\n /** Phone number (with country code, e.g., '4712345678') */\n phone_number?: string;\n /** Phone number verification status */\n phone_number_verified?: boolean;\n /** Birth date (ISO 8601 format, e.g., '1990-01-15') */\n birthdate?: string;\n /** Physical addresses (array) */\n addresses?: VippsAddress[];\n /** Other claims (deprecated) */\n other_addresses?: VippsAddress[];\n}\n\n/**\n * Available Vipps Login scopes\n */\nexport const VippsLoginScopes = {\n /** Required: OpenID Connect base scope */\n OpenId: 'openid',\n /** User's name (given_name, family_name, name) */\n Name: 'name',\n /** Phone number */\n PhoneNumber: 'phoneNumber',\n /** Physical address */\n Address: 'address',\n /** Birth date */\n BirthDate: 'birthDate',\n /** Email address */\n Email: 'email',\n} as const;\n\n/**\n * Default scopes for Vipps Login\n */\nexport const defaultVippsLoginScope = [\n VippsLoginScopes.OpenId,\n VippsLoginScopes.Name,\n VippsLoginScopes.PhoneNumber,\n VippsLoginScopes.Address,\n VippsLoginScopes.Email,\n].join(' ');\n\n/**\n * Vipps API environments\n */\nexport const VippsEnvironments = {\n /** Test environment */\n Test: 'https://apitest.vipps.no',\n /** Production environment */\n Production: 'https://api.vipps.no',\n} as const;\n\n/**\n * Get Vipps OIDC issuer URL for environment\n */\nexport function getVippsIssuer(environment: string): string {\n // Note: Vipps metadata returns issuer with trailing slash\n return `${environment}/access-management-1.0/access/`;\n}\n","/**\n * Vipps Login Client\n *\n * Client for Vipps Login (OIDC) authentication flow.\n * Implements OAuth/OIDC operations with PKCE for Vipps.\n */\n\nimport * as openid from 'openid-client';\n\nimport { createLogger } from '../../logger';\nimport {\n buildAuthorizationUrl,\n buildPKCEOptions,\n type OAuthConfig,\n type PKCEOptions,\n} from '../../oauth';\nimport {\n defaultVippsLoginScope,\n type VippsLoginConfig,\n type VippsUserInfo,\n} from './types';\n\nconst logger = createLogger({\n namespace: 'fides-auth:vipps',\n context: { module: 'VippsLoginClient' },\n});\n\n/**\n * Vipps Login Client\n *\n * Handles Vipps Login (OIDC) authentication flow using PKCE.\n */\nexport class VippsLoginClient {\n private readonly config: VippsLoginConfig;\n private readonly oauthConfig: OAuthConfig;\n private configPromise: Promise<openid.Configuration> | null = null;\n\n constructor(config: VippsLoginConfig) {\n this.config = {\n ...config,\n scope: config.scope ?? defaultVippsLoginScope,\n };\n\n this.oauthConfig = {\n issuer: config.issuer,\n clientId: config.clientId,\n clientSecret: config.clientSecret,\n redirect_uri: config.redirectUri,\n scope: this.config.scope!,\n };\n\n logger.info(\n {\n issuer: config.issuer,\n redirectUri: config.redirectUri,\n scope: this.config.scope,\n },\n 'VippsLoginClient initialized'\n );\n }\n\n /**\n * Get OpenID Configuration (cached)\n * This ensures we use the same Configuration instance across all operations\n */\n private async getConfig(): Promise<openid.Configuration> {\n if (!this.configPromise) {\n this.configPromise = openid.discovery(\n new URL(this.oauthConfig.issuer),\n this.oauthConfig.clientId,\n this.oauthConfig.clientSecret,\n openid.ClientSecretPost(this.oauthConfig.clientSecret)\n );\n }\n return this.configPromise;\n }\n\n /**\n * Build PKCE options for authorization flow\n *\n * @returns PKCE options including code_verifier, code_challenge, and state\n */\n async buildPKCEOptions(): Promise<PKCEOptions> {\n logger.debug('Building PKCE options');\n return buildPKCEOptions(this.oauthConfig);\n }\n\n /**\n * Build authorization URL for Vipps Login\n *\n * @param pkceOptions - PKCE options from buildPKCEOptions()\n * @returns Authorization URL to redirect user to\n */\n async buildAuthorizationUrl(pkceOptions: PKCEOptions): Promise<URL> {\n logger.debug('Building authorization URL');\n const config = await this.getConfig();\n return buildAuthorizationUrl(config, pkceOptions);\n }\n\n /**\n * Exchange authorization code for tokens\n *\n * @param callbackUrl - The full callback URL with code parameter from Vipps\n * @param codeVerifier - PKCE code verifier from initial request\n * @param expectedState - Expected state value for validation (optional)\n * @returns Token response with access_token, id_token, and refresh_token\n */\n async exchangeCodeForTokens(\n callbackUrl: string | URL,\n codeVerifier: string,\n expectedState?: string\n ): Promise<openid.TokenEndpointResponse> {\n logger.debug('Exchanging authorization code for tokens');\n\n try {\n const config = await this.getConfig();\n\n // Convert to URL if string\n const currentUrl = typeof callbackUrl === 'string' ? new URL(callbackUrl) : callbackUrl;\n\n const tokens = await openid.authorizationCodeGrant(config, currentUrl, {\n pkceCodeVerifier: codeVerifier,\n expectedState, // Pass the expected state for validation\n idTokenExpected: true,\n });\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n hasIdToken: !!tokens.id_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully exchanged code for tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to exchange code for tokens');\n throw error;\n }\n }\n\n /**\n * Get user information from Vipps userinfo endpoint\n *\n * @param accessToken - Access token from token exchange\n * @returns Vipps user information\n */\n async getUserInfo(accessToken: string): Promise<VippsUserInfo> {\n logger.debug('Fetching user info from Vipps');\n\n try {\n const config = await this.getConfig();\n const userinfoUrl = config.serverMetadata().userinfo_endpoint;\n\n if (!userinfoUrl) {\n throw new Error('Userinfo endpoint not found in OpenID configuration');\n }\n\n const response = await fetch(userinfoUrl, {\n headers: {\n Authorization: `Bearer ${accessToken}`,\n ...(this.config.subscriptionKey && {\n 'Ocp-Apim-Subscription-Key': this.config.subscriptionKey,\n }),\n ...(this.config.merchantSerialNumber && {\n 'Merchant-Serial-Number': this.config.merchantSerialNumber,\n }),\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text();\n logger.error(\n {\n status: response.status,\n error: errorText,\n },\n 'Failed to fetch user info'\n );\n throw new Error(`Failed to fetch user info: ${response.status} - ${errorText}`);\n }\n\n const userInfo = (await response.json()) as VippsUserInfo;\n\n logger.info(\n {\n sub: userInfo.sub,\n hasEmail: !!userInfo.email,\n hasPhone: !!userInfo.phone_number,\n hasAddress: !!userInfo.addresses?.length,\n },\n 'Successfully fetched user info'\n );\n\n return userInfo;\n } catch (error) {\n logger.error({ error }, 'Error fetching user info');\n throw error;\n }\n }\n\n /**\n * Refresh access token using refresh token\n *\n * @param refreshToken - Refresh token from initial token exchange\n * @returns New token response\n */\n async refreshTokens(refreshToken: string): Promise<openid.TokenEndpointResponse> {\n logger.debug('Refreshing access token');\n\n try {\n const config = await this.getConfig();\n const tokens = await openid.refreshTokenGrant(config, refreshToken);\n\n logger.info(\n {\n hasAccessToken: !!tokens.access_token,\n hasRefreshToken: !!tokens.refresh_token,\n expiresIn: tokens.expires_in,\n },\n 'Successfully refreshed tokens'\n );\n\n return tokens;\n } catch (error) {\n logger.error({ error }, 'Failed to refresh tokens');\n throw error;\n }\n }\n}\n"],"mappings":"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"}
|
package/dist/rate-limit.js
CHANGED
|
@@ -1,26 +1,64 @@
|
|
|
1
1
|
//#region src/rate-limit.ts
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Token bucket rate limiter for protecting against brute-force attacks.
|
|
4
|
+
*
|
|
5
|
+
* Each key (e.g. IP address, user ID) gets its own bucket. Tokens refill
|
|
6
|
+
* over time at a fixed interval. When a bucket is empty, requests are denied.
|
|
7
|
+
*
|
|
8
|
+
* @typeParam _Key - The type of key used to identify rate-limited entities
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const limiter = new TokenBucket<string>(10, 60); // 10 tokens, refill 1/min
|
|
13
|
+
*
|
|
14
|
+
* if (!limiter.consume(ipAddress, 1)) {
|
|
15
|
+
* throw new Error('Rate limit exceeded');
|
|
16
|
+
* }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
var TokenBucket = class {
|
|
3
20
|
max;
|
|
4
21
|
refillIntervalSeconds;
|
|
5
|
-
constructor(
|
|
6
|
-
this.max =
|
|
22
|
+
constructor(max, refillIntervalSeconds) {
|
|
23
|
+
this.max = max;
|
|
24
|
+
this.refillIntervalSeconds = refillIntervalSeconds;
|
|
7
25
|
}
|
|
8
26
|
storage = /* @__PURE__ */ new Map();
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
27
|
+
/** Check whether the bucket has enough tokens without consuming any. */
|
|
28
|
+
check(key, cost) {
|
|
29
|
+
const bucket = this.storage.get(key) ?? null;
|
|
30
|
+
if (bucket === null) return true;
|
|
31
|
+
const refill = Math.floor((Date.now() - bucket.refilledAt) / (this.refillIntervalSeconds * 1e3));
|
|
32
|
+
if (refill > 0) return Math.min(bucket.count + refill, this.max) >= cost;
|
|
33
|
+
return bucket.count >= cost;
|
|
14
34
|
}
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
35
|
+
/** Consume tokens from the bucket. Returns `true` if allowed, `false` if rate-limited. */
|
|
36
|
+
consume(key, cost) {
|
|
37
|
+
let bucket = this.storage.get(key) ?? null;
|
|
38
|
+
const now = Date.now();
|
|
39
|
+
if (bucket === null) {
|
|
40
|
+
bucket = {
|
|
41
|
+
count: this.max - cost,
|
|
42
|
+
refilledAt: now
|
|
43
|
+
};
|
|
44
|
+
this.storage.set(key, bucket);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
const refill = Math.floor((now - bucket.refilledAt) / (this.refillIntervalSeconds * 1e3));
|
|
48
|
+
if (refill > 0) {
|
|
49
|
+
bucket.count = Math.min(bucket.count + refill, this.max);
|
|
50
|
+
bucket.refilledAt = now;
|
|
51
|
+
}
|
|
52
|
+
if (bucket.count < cost) {
|
|
53
|
+
this.storage.set(key, bucket);
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
bucket.count -= cost;
|
|
57
|
+
this.storage.set(key, bucket);
|
|
58
|
+
return true;
|
|
23
59
|
}
|
|
24
60
|
};
|
|
25
61
|
//#endregion
|
|
26
|
-
export {
|
|
62
|
+
export { TokenBucket };
|
|
63
|
+
|
|
64
|
+
//# sourceMappingURL=rate-limit.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rate-limit.js","names":[],"sources":["../src/rate-limit.ts"],"sourcesContent":["/**\n * Token bucket rate limiter for protecting against brute-force attacks.\n *\n * Each key (e.g. IP address, user ID) gets its own bucket. Tokens refill\n * over time at a fixed interval. When a bucket is empty, requests are denied.\n *\n * @typeParam _Key - The type of key used to identify rate-limited entities\n *\n * @example\n * ```typescript\n * const limiter = new TokenBucket<string>(10, 60); // 10 tokens, refill 1/min\n *\n * if (!limiter.consume(ipAddress, 1)) {\n * throw new Error('Rate limit exceeded');\n * }\n * ```\n */\nexport class TokenBucket<_Key> {\n public max: number;\n public refillIntervalSeconds: number;\n\n constructor(max: number, refillIntervalSeconds: number) {\n this.max = max;\n this.refillIntervalSeconds = refillIntervalSeconds;\n }\n\n private readonly storage = new Map<_Key, Bucket>();\n\n /** Check whether the bucket has enough tokens without consuming any. */\n public check(key: _Key, cost: number): boolean {\n const bucket = this.storage.get(key) ?? null;\n if (bucket === null) {\n return true;\n }\n const now = Date.now();\n const refill = Math.floor((now - bucket.refilledAt) / (this.refillIntervalSeconds * 1000));\n if (refill > 0) {\n return Math.min(bucket.count + refill, this.max) >= cost;\n }\n return bucket.count >= cost;\n }\n\n /** Consume tokens from the bucket. Returns `true` if allowed, `false` if rate-limited. */\n public consume(key: _Key, cost: number): boolean {\n let bucket = this.storage.get(key) ?? null;\n const now = Date.now();\n if (bucket === null) {\n bucket = {\n count: this.max - cost,\n refilledAt: now,\n };\n this.storage.set(key, bucket);\n return true;\n }\n const refill = Math.floor((now - bucket.refilledAt) / (this.refillIntervalSeconds * 1000));\n if (refill > 0) {\n bucket.count = Math.min(bucket.count + refill, this.max);\n bucket.refilledAt = now;\n }\n if (bucket.count < cost) {\n this.storage.set(key, bucket);\n return false;\n }\n bucket.count -= cost;\n this.storage.set(key, bucket);\n return true;\n }\n}\ninterface Bucket {\n count: number;\n refilledAt: number;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,IAAa,cAAb,MAA+B;CAC7B;CACA;CAEA,YAAY,KAAa,uBAA+B;AACtD,OAAK,MAAM;AACX,OAAK,wBAAwB;;CAG/B,0BAA2B,IAAI,KAAmB;;CAGlD,MAAa,KAAW,MAAuB;EAC7C,MAAM,SAAS,KAAK,QAAQ,IAAI,IAAI,IAAI;AACxC,MAAI,WAAW,KACb,QAAO;EAGT,MAAM,SAAS,KAAK,OADR,KAAK,KACU,GAAM,OAAO,eAAe,KAAK,wBAAwB,KAAM;AAC1F,MAAI,SAAS,EACX,QAAO,KAAK,IAAI,OAAO,QAAQ,QAAQ,KAAK,IAAI,IAAI;AAEtD,SAAO,OAAO,SAAS;;;CAIzB,QAAe,KAAW,MAAuB;EAC/C,IAAI,SAAS,KAAK,QAAQ,IAAI,IAAI,IAAI;EACtC,MAAM,MAAM,KAAK,KAAK;AACtB,MAAI,WAAW,MAAM;AACnB,YAAS;IACP,OAAO,KAAK,MAAM;IAClB,YAAY;IACb;AACD,QAAK,QAAQ,IAAI,KAAK,OAAO;AAC7B,UAAO;;EAET,MAAM,SAAS,KAAK,OAAO,MAAM,OAAO,eAAe,KAAK,wBAAwB,KAAM;AAC1F,MAAI,SAAS,GAAG;AACd,UAAO,QAAQ,KAAK,IAAI,OAAO,QAAQ,QAAQ,KAAK,IAAI;AACxD,UAAO,aAAa;;AAEtB,MAAI,OAAO,QAAQ,MAAM;AACvB,QAAK,QAAQ,IAAI,KAAK,OAAO;AAC7B,UAAO;;AAET,SAAO,SAAS;AAChB,OAAK,QAAQ,IAAI,KAAK,OAAO;AAC7B,SAAO"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session-refresh.d.ts","sourceRoot":"","sources":["../src/session-refresh.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAsB,MAAM,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"session-refresh.d.ts","sourceRoot":"","sources":["../src/session-refresh.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAsB,MAAM,SAAS,CAAC;AAE1D,OAAO,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAIxD;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,GACzB,iBAAiB,OAAO,EACxB,QAAQ,WAAW,EACnB,UAAS,oBAAyB,KACjC,OAAO,CAAC,OAAO,GAAG,IAAI,CA8CxB,CAAC"}
|
package/dist/session-refresh.js
CHANGED
|
@@ -1,27 +1,48 @@
|
|
|
1
|
-
import { createLogger
|
|
2
|
-
import {
|
|
1
|
+
import { createLogger } from "./logger.js";
|
|
2
|
+
import { getOAuthErrorLogContext } from "./oauth-logging.js";
|
|
3
|
+
import { refreshAccessToken } from "./oauth.js";
|
|
3
4
|
//#region src/session-refresh.ts
|
|
4
|
-
var
|
|
5
|
-
|
|
5
|
+
var logger = createLogger({ namespace: "fides-auth:session-refresh" });
|
|
6
|
+
/**
|
|
7
|
+
* Refreshes an existing session by exchanging its refresh token for new tokens.
|
|
8
|
+
*
|
|
9
|
+
* @param current_session - The current session containing a refresh token
|
|
10
|
+
* @param config - OAuth configuration for the identity provider
|
|
11
|
+
* @param options - Optional session creation options
|
|
12
|
+
* @returns The updated session with new tokens, or throws on failure
|
|
13
|
+
* @throws Error if no refresh token is available or the token exchange fails
|
|
14
|
+
*/
|
|
15
|
+
var refreshSession = async (current_session, config, options = {}) => {
|
|
16
|
+
logger.debug("Starting session refresh");
|
|
17
|
+
if (!current_session.tokens?.refreshToken) {
|
|
18
|
+
logger.error("No refresh token available in session");
|
|
19
|
+
throw new Error("No refresh token available");
|
|
20
|
+
}
|
|
6
21
|
try {
|
|
7
|
-
|
|
8
|
-
|
|
22
|
+
const newtokens = await refreshAccessToken(config, current_session.tokens.refreshToken);
|
|
23
|
+
const updatedSession = {
|
|
24
|
+
...current_session,
|
|
9
25
|
tokens: {
|
|
10
|
-
...
|
|
11
|
-
accessToken:
|
|
12
|
-
accessTokenExpiresAt:
|
|
13
|
-
refreshToken:
|
|
26
|
+
...current_session.tokens,
|
|
27
|
+
accessToken: newtokens.access_token,
|
|
28
|
+
accessTokenExpiresAt: newtokens.expires_in ? new Date(Date.now() + newtokens.expires_in * 1e3) : void 0,
|
|
29
|
+
refreshToken: newtokens.refresh_token ?? current_session.tokens?.refreshToken
|
|
14
30
|
}
|
|
15
31
|
};
|
|
16
|
-
|
|
17
|
-
hasNewAccessToken: !!
|
|
18
|
-
hasNewRefreshToken: !!
|
|
19
|
-
expiresIn:
|
|
20
|
-
}, "Session refreshed successfully")
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
logger.info({
|
|
33
|
+
hasNewAccessToken: !!updatedSession.tokens.accessToken,
|
|
34
|
+
hasNewRefreshToken: !!newtokens.refresh_token,
|
|
35
|
+
expiresIn: newtokens.expires_in
|
|
36
|
+
}, "Session refreshed successfully");
|
|
37
|
+
return updatedSession;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
const errorContext = getOAuthErrorLogContext(error);
|
|
40
|
+
if (errorContext.code === "OAUTH_RESPONSE_BODY_ERROR" && errorContext.error === "invalid_grant") logger.info("Session refresh failed - refresh token expired or invalid");
|
|
41
|
+
else logger.error({ error: errorContext }, "Session refresh failed");
|
|
42
|
+
throw error;
|
|
24
43
|
}
|
|
25
44
|
};
|
|
26
45
|
//#endregion
|
|
27
|
-
export {
|
|
46
|
+
export { refreshSession };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=session-refresh.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-refresh.js","names":[],"sources":["../src/session-refresh.ts"],"sourcesContent":["import { createLogger } from './logger';\nimport { OAuthConfig, refreshAccessToken } from \"./oauth\";\nimport { getOAuthErrorLogContext } from './oauth-logging';\nimport { CreateSessionOptions, Session } from \"./types\";\n\nconst logger = createLogger({ namespace: 'fides-auth:session-refresh' });\n\n/**\n * Refreshes an existing session by exchanging its refresh token for new tokens.\n *\n * @param current_session - The current session containing a refresh token\n * @param config - OAuth configuration for the identity provider\n * @param options - Optional session creation options\n * @returns The updated session with new tokens, or throws on failure\n * @throws Error if no refresh token is available or the token exchange fails\n */\nexport const refreshSession = async (\n current_session: Session,\n config: OAuthConfig,\n options: CreateSessionOptions = {}\n): Promise<Session | null> => {\n logger.debug('Starting session refresh');\n\n if (!current_session.tokens?.refreshToken) {\n logger.error('No refresh token available in session');\n throw new Error('No refresh token available');\n }\n\n try {\n const newtokens = await refreshAccessToken(config, current_session.tokens.refreshToken);\n\n const updatedSession = {\n ...current_session,\n tokens: {\n ...current_session.tokens,\n accessToken: newtokens.access_token,\n accessTokenExpiresAt: newtokens.expires_in\n ? new Date(Date.now() + newtokens.expires_in * 1000)\n : undefined,\n refreshToken: newtokens.refresh_token ?? current_session.tokens?.refreshToken,\n },\n };\n\n logger.info({\n hasNewAccessToken: !!updatedSession.tokens.accessToken,\n hasNewRefreshToken: !!newtokens.refresh_token,\n expiresIn: newtokens.expires_in,\n }, 'Session refreshed successfully');\n\n return updatedSession;\n } catch (error) {\n // Check if this is an expected invalid_grant error\n const errorContext = getOAuthErrorLogContext(error);\n const isInvalidGrant =\n errorContext.code === 'OAUTH_RESPONSE_BODY_ERROR' && errorContext.error === 'invalid_grant';\n\n if (isInvalidGrant) {\n // Expected error during logout/session expiry - log at info level\n logger.info('Session refresh failed - refresh token expired or invalid');\n } else {\n // Unexpected error - log at error level\n logger.error({ error: errorContext }, 'Session refresh failed');\n }\n\n throw error;\n }\n};\n\n"],"mappings":";;;;AAKA,IAAM,SAAS,aAAa,EAAE,WAAW,8BAA8B,CAAC;;;;;;;;;;AAWxE,IAAa,iBAAiB,OAC5B,iBACA,QACA,UAAgC,EAAE,KACN;AAC5B,QAAO,MAAM,2BAA2B;AAExC,KAAI,CAAC,gBAAgB,QAAQ,cAAc;AACzC,SAAO,MAAM,wCAAwC;AACrD,QAAM,IAAI,MAAM,6BAA6B;;AAG/C,KAAI;EACF,MAAM,YAAY,MAAM,mBAAmB,QAAQ,gBAAgB,OAAO,aAAa;EAEvF,MAAM,iBAAiB;GACrB,GAAG;GACH,QAAQ;IACN,GAAG,gBAAgB;IACnB,aAAa,UAAU;IACvB,sBAAsB,UAAU,aAC5B,IAAI,KAAK,KAAK,KAAK,GAAG,UAAU,aAAa,IAAK,GAClD,KAAA;IACJ,cAAc,UAAU,iBAAiB,gBAAgB,QAAQ;IAClE;GACF;AAED,SAAO,KAAK;GACV,mBAAmB,CAAC,CAAC,eAAe,OAAO;GAC3C,oBAAoB,CAAC,CAAC,UAAU;GAChC,WAAW,UAAU;GACtB,EAAE,iCAAiC;AAEpC,SAAO;UACA,OAAO;EAEd,MAAM,eAAe,wBAAwB,MAAM;AAInD,MAFE,aAAa,SAAS,+BAA+B,aAAa,UAAU,gBAI5E,QAAO,KAAK,4DAA4D;MAGxE,QAAO,MAAM,EAAE,OAAO,cAAc,EAAE,yBAAyB;AAGjE,QAAM"}
|
|
@@ -1,2 +1,77 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
1
|
+
import { createLogger } from "./logger.js";
|
|
2
|
+
import { hexToUint8Array } from "./utils.js";
|
|
3
|
+
import { decodeJwt, jwtDecrypt } from "jose";
|
|
4
|
+
//#region src/session-validation.ts
|
|
5
|
+
var logger = createLogger({ namespace: "fides-auth:session-validation" });
|
|
6
|
+
/**
|
|
7
|
+
* Validates an encrypted JWT and checks for expiration.
|
|
8
|
+
*
|
|
9
|
+
* @param encryptedJwt The encrypted JWT string to validate
|
|
10
|
+
* @param secret The decryption key as a hex string or Uint8Array (32 bytes for A256GCM)
|
|
11
|
+
* @returns Session and SessionValidationResult indicating whether it's valid or not,
|
|
12
|
+
* plus accessTokenExpiresIn if valid
|
|
13
|
+
*/
|
|
14
|
+
async function validateSessionJwt(encryptedJwt, secret) {
|
|
15
|
+
logger.debug("Starting session JWT validation");
|
|
16
|
+
const secretBytes = typeof secret === "string" ? hexToUint8Array(secret) : secret;
|
|
17
|
+
let payload;
|
|
18
|
+
try {
|
|
19
|
+
({payload} = await jwtDecrypt(encryptedJwt, secretBytes));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
const errorMessage = error instanceof Error ? error.message : "Failed decrypting JWT";
|
|
22
|
+
logger.warn({ reason: errorMessage }, "Session JWT decryption failed");
|
|
23
|
+
return {
|
|
24
|
+
status: "INVALID",
|
|
25
|
+
reason: `Decryption error: ${errorMessage}`
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (!isSession(payload)) {
|
|
29
|
+
logger.warn("JWT payload does not match Session structure");
|
|
30
|
+
return {
|
|
31
|
+
status: "INVALID",
|
|
32
|
+
reason: "jwt does not seem to include a session"
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
logger.debug("Session validated successfully");
|
|
36
|
+
const expiresIn = computeAccessTokenExpiresIn(payload);
|
|
37
|
+
if (expiresIn !== void 0 && expiresIn <= 0) {
|
|
38
|
+
logger.info({ accessTokenExpiresIn: expiresIn }, "Session access token has expired");
|
|
39
|
+
return {
|
|
40
|
+
session: payload,
|
|
41
|
+
status: "EXPIRED",
|
|
42
|
+
accessTokenExpiresIn: expiresIn,
|
|
43
|
+
reason: "Access token has expired"
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
session: payload,
|
|
48
|
+
status: "VALID",
|
|
49
|
+
accessTokenExpiresIn: expiresIn
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Computes the number of seconds until the access token expires.
|
|
54
|
+
* Returns undefined if no expiry can be determined.
|
|
55
|
+
*/
|
|
56
|
+
function computeAccessTokenExpiresIn(session) {
|
|
57
|
+
const accessToken = session.tokens?.accessToken;
|
|
58
|
+
if (!accessToken) return;
|
|
59
|
+
try {
|
|
60
|
+
const { exp } = decodeJwt(accessToken);
|
|
61
|
+
if (typeof exp !== "number") return;
|
|
62
|
+
return Math.floor(exp - Date.now() / 1e3);
|
|
63
|
+
} catch {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/** Type guard that checks whether an unknown value conforms to the {@link Session} shape. */
|
|
68
|
+
function isSession(object) {
|
|
69
|
+
if (typeof object !== "object" || object === null) return false;
|
|
70
|
+
if ("tokens" in object && typeof object.tokens !== "object") return false;
|
|
71
|
+
if ("user" in object && typeof object.user !== "object") return false;
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
//#endregion
|
|
75
|
+
export { isSession, validateSessionJwt };
|
|
76
|
+
|
|
77
|
+
//# sourceMappingURL=session-validation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session-validation.js","names":[],"sources":["../src/session-validation.ts"],"sourcesContent":["// session-validation.ts\n\nimport { jwtDecrypt, decodeJwt } from 'jose';\n\nimport { createLogger } from './logger';\nimport { hexToUint8Array } from './utils';\nimport { Session } from './types';\n\nconst logger = createLogger({ namespace: 'fides-auth:session-validation' });\n\n/** Result of validating an encrypted session JWT. */\nexport interface SessionValidationResult {\n /** The decoded session, if valid. */\n session?: Session;\n /** Validation status: VALID, INVALID (decryption/format error), or EXPIRED. */\n status: 'VALID' | 'INVALID' | 'EXPIRED';\n /** Seconds until the access token expires (negative means already expired). */\n accessTokenExpiresIn?: number;\n /** Human-readable reason when status is not VALID. */\n reason?: string;\n}\n\n/**\n * Validates an encrypted JWT and checks for expiration.\n *\n * @param encryptedJwt The encrypted JWT string to validate\n * @param secret The decryption key as a hex string or Uint8Array (32 bytes for A256GCM)\n * @returns Session and SessionValidationResult indicating whether it's valid or not,\n * plus accessTokenExpiresIn if valid\n */\nexport async function validateSessionJwt(\n encryptedJwt: string,\n secret: string | Uint8Array,\n): Promise<SessionValidationResult> {\n logger.debug('Starting session JWT validation');\n\n const secretBytes = typeof secret === 'string'\n ? hexToUint8Array(secret)\n : secret;\n\n let payload: unknown;\n try {\n ({ payload } = await jwtDecrypt(encryptedJwt, secretBytes));\n } catch (error) {\n const errorMessage = error instanceof Error ? error.message : 'Failed decrypting JWT';\n logger.warn({ reason: errorMessage }, 'Session JWT decryption failed');\n return {\n status: 'INVALID',\n reason: `Decryption error: ${errorMessage}`\n };\n }\n\n // Check if payload has the shape of a Session\n if (!isSession(payload)) {\n logger.warn('JWT payload does not match Session structure');\n return {\n status: 'INVALID',\n reason: 'jwt does not seem to include a session'\n };\n }\n\n logger.debug('Session validated successfully');\n\n // Check access token expiry if available\n const expiresIn = computeAccessTokenExpiresIn(payload);\n\n if (expiresIn !== undefined && expiresIn <= 0) {\n logger.info({ accessTokenExpiresIn: expiresIn }, 'Session access token has expired');\n return {\n session: payload,\n status: 'EXPIRED',\n accessTokenExpiresIn: expiresIn,\n reason: 'Access token has expired',\n };\n }\n\n return {\n session: payload,\n status: 'VALID',\n accessTokenExpiresIn: expiresIn,\n };\n}\n\n/**\n * Computes the number of seconds until the access token expires.\n * Returns undefined if no expiry can be determined.\n */\nfunction computeAccessTokenExpiresIn(session: Session): number | undefined {\n const accessToken = session.tokens?.accessToken;\n if (!accessToken) {\n return undefined;\n }\n\n try {\n const { exp } = decodeJwt(accessToken);\n if (typeof exp !== 'number') {\n return undefined;\n }\n return Math.floor(exp - Date.now() / 1000);\n } catch {\n // Not a valid JWT or can't decode — ignore\n return undefined;\n }\n}\n\n\n/** Type guard that checks whether an unknown value conforms to the {@link Session} shape. */\nexport function isSession(object: unknown): object is Session {\n if (typeof object !== 'object' || object === null) {\n return false;\n }\n\n if (\n 'tokens' in object &&\n typeof (object as any).tokens !== 'object'\n ) {\n return false;\n }\n\n if (\n 'user' in object &&\n typeof (object as any).user !== 'object'\n ) {\n return false;\n }\n\n // Seems to be a real session\n return true;\n}\n"],"mappings":";;;;AAQA,IAAM,SAAS,aAAa,EAAE,WAAW,iCAAiC,CAAC;;;;;;;;;AAsB3E,eAAsB,mBACpB,cACA,QACkC;AAClC,QAAO,MAAM,kCAAkC;CAE/C,MAAM,cAAc,OAAO,WAAW,WAClC,gBAAgB,OAAO,GACvB;CAEJ,IAAI;AACJ,KAAI;AACF,GAAC,CAAE,WAAY,MAAM,WAAW,cAAc,YAAY;UACnD,OAAO;EACd,MAAM,eAAe,iBAAiB,QAAQ,MAAM,UAAU;AAC9D,SAAO,KAAK,EAAE,QAAQ,cAAc,EAAE,gCAAgC;AACtE,SAAO;GACL,QAAQ;GACR,QAAQ,qBAAqB;GAC9B;;AAIH,KAAI,CAAC,UAAU,QAAQ,EAAE;AACvB,SAAO,KAAK,+CAA+C;AAC3D,SAAO;GACL,QAAQ;GACR,QAAQ;GACT;;AAGH,QAAO,MAAM,iCAAiC;CAG9C,MAAM,YAAY,4BAA4B,QAAQ;AAEtD,KAAI,cAAc,KAAA,KAAa,aAAa,GAAG;AAC7C,SAAO,KAAK,EAAE,sBAAsB,WAAW,EAAE,mCAAmC;AACpF,SAAO;GACL,SAAS;GACT,QAAQ;GACR,sBAAsB;GACtB,QAAQ;GACT;;AAGH,QAAO;EACL,SAAS;EACT,QAAQ;EACR,sBAAsB;EACvB;;;;;;AAOH,SAAS,4BAA4B,SAAsC;CACzE,MAAM,cAAc,QAAQ,QAAQ;AACpC,KAAI,CAAC,YACH;AAGF,KAAI;EACF,MAAM,EAAE,QAAQ,UAAU,YAAY;AACtC,MAAI,OAAO,QAAQ,SACjB;AAEF,SAAO,KAAK,MAAM,MAAM,KAAK,KAAK,GAAG,IAAK;SACpC;AAEN;;;;AAMJ,SAAgB,UAAU,QAAoC;AAC5D,KAAI,OAAO,WAAW,YAAY,WAAW,KAC3C,QAAO;AAGT,KACE,YAAY,UACZ,OAAQ,OAAe,WAAW,SAElC,QAAO;AAGT,KACE,UAAU,UACV,OAAQ,OAAe,SAAS,SAEhC,QAAO;AAIT,QAAO"}
|