@shware/security 0.4.9 → 1.1.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/auth/index.cjs +103 -23
- package/dist/auth/index.cjs.map +1 -1
- package/dist/auth/index.d.cts +10 -3
- package/dist/auth/index.d.ts +10 -3
- package/dist/auth/index.mjs +104 -24
- package/dist/auth/index.mjs.map +1 -1
- package/dist/auth/types.cjs.map +1 -1
- package/dist/auth/types.d.cts +11 -3
- package/dist/auth/types.d.ts +11 -3
- package/dist/index.cjs +4 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/oauth2/client.cjs +40 -6
- package/dist/oauth2/client.cjs.map +1 -1
- package/dist/oauth2/client.d.cts +29 -6
- package/dist/oauth2/client.d.ts +29 -6
- package/dist/oauth2/client.mjs +40 -6
- package/dist/oauth2/client.mjs.map +1 -1
- package/dist/oauth2/error.cjs.map +1 -1
- package/dist/oauth2/error.d.cts +1 -1
- package/dist/oauth2/error.d.ts +1 -1
- package/dist/oauth2/error.mjs.map +1 -1
- package/dist/oauth2/provider/apple.cjs +21 -5
- package/dist/oauth2/provider/apple.cjs.map +1 -1
- package/dist/oauth2/provider/apple.d.cts +12 -4
- package/dist/oauth2/provider/apple.d.ts +12 -4
- package/dist/oauth2/provider/apple.mjs +19 -4
- package/dist/oauth2/provider/apple.mjs.map +1 -1
- package/dist/oauth2/provider/common.cjs +5 -13
- package/dist/oauth2/provider/common.cjs.map +1 -1
- package/dist/oauth2/provider/common.d.cts +6 -11
- package/dist/oauth2/provider/common.d.ts +6 -11
- package/dist/oauth2/provider/common.mjs +5 -13
- package/dist/oauth2/provider/common.mjs.map +1 -1
- package/dist/oauth2/provider/facebook.cjs +19 -3
- package/dist/oauth2/provider/facebook.cjs.map +1 -1
- package/dist/oauth2/provider/facebook.d.cts +17 -3
- package/dist/oauth2/provider/facebook.d.ts +17 -3
- package/dist/oauth2/provider/facebook.mjs +18 -3
- package/dist/oauth2/provider/facebook.mjs.map +1 -1
- package/dist/oauth2/provider/github.cjs +6 -3
- package/dist/oauth2/provider/github.cjs.map +1 -1
- package/dist/oauth2/provider/github.d.cts +11 -3
- package/dist/oauth2/provider/github.d.ts +11 -3
- package/dist/oauth2/provider/github.mjs +5 -3
- package/dist/oauth2/provider/github.mjs.map +1 -1
- package/dist/oauth2/provider/google.cjs +17 -1
- package/dist/oauth2/provider/google.cjs.map +1 -1
- package/dist/oauth2/provider/google.d.cts +11 -4
- package/dist/oauth2/provider/google.d.ts +11 -4
- package/dist/oauth2/provider/google.mjs +16 -1
- package/dist/oauth2/provider/google.mjs.map +1 -1
- package/dist/oauth2/provider/x.cjs +16 -4
- package/dist/oauth2/provider/x.cjs.map +1 -1
- package/dist/oauth2/provider/x.d.cts +11 -3
- package/dist/oauth2/provider/x.d.ts +11 -3
- package/dist/oauth2/provider/x.mjs +15 -4
- package/dist/oauth2/provider/x.mjs.map +1 -1
- package/dist/oauth2/types.cjs +20 -0
- package/dist/oauth2/types.cjs.map +1 -1
- package/dist/oauth2/types.d.cts +73 -12
- package/dist/oauth2/types.d.ts +73 -12
- package/dist/oauth2/types.mjs +13 -0
- package/dist/oauth2/types.mjs.map +1 -1
- package/dist/session/__test__/{redis-session.test.cjs → redis.test.cjs} +7 -7
- package/dist/session/__test__/redis.test.cjs.map +1 -0
- package/dist/session/__test__/{redis-session.test.mjs → redis.test.mjs} +4 -4
- package/dist/session/__test__/redis.test.mjs.map +1 -0
- package/dist/session/index.cjs +3 -3
- package/dist/session/index.cjs.map +1 -1
- package/dist/session/index.d.cts +1 -1
- package/dist/session/index.d.ts +1 -1
- package/dist/session/index.mjs +5 -2
- package/dist/session/index.mjs.map +1 -1
- package/dist/session/{redis-session.cjs → redis.cjs} +29 -8
- package/dist/session/redis.cjs.map +1 -0
- package/dist/session/{redis-session.d.cts → redis.d.cts} +6 -2
- package/dist/session/{redis-session.d.ts → redis.d.ts} +6 -2
- package/dist/session/{redis-session.mjs → redis.mjs} +25 -4
- package/dist/session/redis.mjs.map +1 -0
- package/dist/session/types.cjs.map +1 -1
- package/dist/session/types.d.cts +3 -0
- package/dist/session/types.d.ts +3 -0
- package/dist/utils/timing.cjs +73 -0
- package/dist/utils/timing.cjs.map +1 -0
- package/dist/utils/timing.d.cts +13 -0
- package/dist/utils/timing.d.ts +13 -0
- package/dist/utils/timing.mjs +48 -0
- package/dist/utils/timing.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/oidc/index.cjs +0 -39
- package/dist/oidc/index.cjs.map +0 -1
- package/dist/oidc/index.d.cts +0 -10
- package/dist/oidc/index.d.ts +0 -10
- package/dist/oidc/index.mjs +0 -14
- package/dist/oidc/index.mjs.map +0 -1
- package/dist/session/__test__/redis-session.test.cjs.map +0 -1
- package/dist/session/__test__/redis-session.test.mjs.map +0 -1
- package/dist/session/redis-session.cjs.map +0 -1
- package/dist/session/redis-session.mjs.map +0 -1
- /package/dist/session/__test__/{redis-session.test.d.cts → redis.test.d.cts} +0 -0
- /package/dist/session/__test__/{redis-session.test.d.ts → redis.test.d.ts} +0 -0
package/dist/auth/index.cjs
CHANGED
|
@@ -38,7 +38,9 @@ var import_tiny_invariant = __toESM(require("tiny-invariant"), 1);
|
|
|
38
38
|
var import_session = require("../session/index.cjs");
|
|
39
39
|
var import_http = require("../utils/http.cjs");
|
|
40
40
|
var import_client = require("../oauth2/client.cjs");
|
|
41
|
+
var import_timing = require("../utils/timing.cjs");
|
|
41
42
|
var Auth = class {
|
|
43
|
+
timing;
|
|
42
44
|
cookieName;
|
|
43
45
|
cookieOptions;
|
|
44
46
|
repository;
|
|
@@ -46,10 +48,13 @@ var Auth = class {
|
|
|
46
48
|
PATH_CSRF = "/csrf";
|
|
47
49
|
PATH_LOGOUT = "/logout";
|
|
48
50
|
PATH_LOGGED = "/logged";
|
|
51
|
+
PATH_OAUTH2_STATE = "/oauth2/state/:registrationId";
|
|
49
52
|
PATH_OAUTH2_AUTHORIZATION = "/oauth2/authorization/:registrationId";
|
|
50
|
-
|
|
53
|
+
PATH_LOGIN_OAUTH2_CODE = "/login/oauth2/code/:registrationId";
|
|
54
|
+
PATH_LOGIN_OAUTH2_NATIVE = "/login/oauth2/native/:registrationId";
|
|
51
55
|
ATTR_OAUTH2_AUTHORIZATION_REQUEST = "oauth2AuthorizationRequest";
|
|
52
|
-
constructor({ repository, oauth2, cookie }) {
|
|
56
|
+
constructor({ repository, oauth2, cookie, timing: timing2 }) {
|
|
57
|
+
this.timing = timing2 ?? false;
|
|
53
58
|
this.repository = repository;
|
|
54
59
|
const { name, ...cookieOptions } = cookie ?? {};
|
|
55
60
|
this.cookieName = name ?? "SESSION";
|
|
@@ -83,45 +88,77 @@ var Auth = class {
|
|
|
83
88
|
(0, import_http.setCookie)(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
|
|
84
89
|
return response;
|
|
85
90
|
};
|
|
91
|
+
createPkceParameters() {
|
|
92
|
+
const code_verifier = (0, import_crypto.randomUUID)();
|
|
93
|
+
const code_challenge = (0, import_crypto.createHash)("sha256").update(code_verifier).digest().toString("base64url");
|
|
94
|
+
return { code_verifier, code_challenge, code_challenge_method: "S256" };
|
|
95
|
+
}
|
|
96
|
+
getOauth2StateKey(state) {
|
|
97
|
+
return `oauth2:state:${state}`;
|
|
98
|
+
}
|
|
99
|
+
oauth2State = async (request) => {
|
|
100
|
+
const { registrationId } = (0, import_http.param)(request, this.PATH_OAUTH2_STATE);
|
|
101
|
+
(0, import_tiny_invariant.default)(this.oauth2Client, "oauth2Client is not initialized");
|
|
102
|
+
const state = (0, import_crypto.randomUUID)();
|
|
103
|
+
const nonce = (0, import_crypto.randomUUID)();
|
|
104
|
+
const pkce = this.createPkceParameters();
|
|
105
|
+
const value = { state, nonce, registrationId, ...pkce };
|
|
106
|
+
await this.repository.setItem(this.getOauth2StateKey(state), value, 10 * 60);
|
|
107
|
+
return Response.json({
|
|
108
|
+
state,
|
|
109
|
+
nonce,
|
|
110
|
+
code_challenge: pkce.code_challenge,
|
|
111
|
+
code_challenge_method: pkce.code_challenge_method
|
|
112
|
+
});
|
|
113
|
+
};
|
|
86
114
|
oauth2Authorization = async (request) => {
|
|
87
115
|
(0, import_tiny_invariant.default)(this.oauth2Client, "oauth2Client is not initialized");
|
|
116
|
+
const { mark, setTiming } = (0, import_timing.timing)({ enabled: this.timing });
|
|
88
117
|
const { registrationId } = (0, import_http.param)(request, this.PATH_OAUTH2_AUTHORIZATION);
|
|
89
118
|
const state = (0, import_crypto.randomUUID)();
|
|
90
|
-
const
|
|
91
|
-
const uri = this.oauth2Client.createAuthorizationUri(registrationId, state,
|
|
119
|
+
const pkce = this.createPkceParameters();
|
|
120
|
+
const uri = this.oauth2Client.createAuthorizationUri({ registrationId, state, pkce });
|
|
92
121
|
const sessionId = (0, import_http.getCookie)(request, this.cookieName);
|
|
93
122
|
const session = sessionId ? await this.repository.findById(sessionId) ?? this.repository.createSession() : this.repository.createSession();
|
|
123
|
+
mark("fetch_session");
|
|
94
124
|
const authorizationRequest = {
|
|
95
125
|
state,
|
|
96
|
-
codeVerifier,
|
|
97
126
|
registrationId,
|
|
98
|
-
authorizationRequestUri: uri.href
|
|
127
|
+
authorizationRequestUri: uri.href,
|
|
128
|
+
additionalParameters: pkce
|
|
99
129
|
};
|
|
100
130
|
const value = JSON.stringify(authorizationRequest);
|
|
101
131
|
session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);
|
|
102
132
|
await this.repository.save(session);
|
|
133
|
+
mark("save_session");
|
|
103
134
|
const response = new Response(null, { status: 302, headers: { location: uri.href } });
|
|
104
135
|
const maxAge = session.getMaxInactiveInterval();
|
|
105
136
|
(0, import_http.setCookie)(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
|
|
137
|
+
setTiming(response);
|
|
106
138
|
return response;
|
|
107
139
|
};
|
|
108
|
-
redirect = (error) => {
|
|
140
|
+
redirect = (error, description) => {
|
|
109
141
|
(0, import_tiny_invariant.default)(this.oauth2Client, "oauth2Client is not initialized");
|
|
110
|
-
|
|
142
|
+
const uri = new URL(this.oauth2Client.errorUri);
|
|
143
|
+
uri.searchParams.set("error", error);
|
|
144
|
+
if (description) uri.searchParams.set("error_description", description);
|
|
145
|
+
return Response.redirect(uri.href, 302);
|
|
111
146
|
};
|
|
112
|
-
|
|
147
|
+
loginOAuth2Code = async (request, onAuthorized) => {
|
|
113
148
|
(0, import_tiny_invariant.default)(this.oauth2Client, "oauth2Client is not initialized");
|
|
149
|
+
const { mark, setTiming } = (0, import_timing.timing)({ enabled: this.timing });
|
|
114
150
|
const sessionId = (0, import_http.getCookie)(request, this.cookieName);
|
|
115
151
|
const session = sessionId ? await this.repository.findById(sessionId) : null;
|
|
116
152
|
if (!session) {
|
|
117
|
-
return this.redirect("
|
|
153
|
+
return this.redirect("invalid_request", "session not found");
|
|
118
154
|
}
|
|
155
|
+
mark("load_session");
|
|
119
156
|
const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
|
|
120
157
|
if (!json) {
|
|
121
|
-
return this.redirect("
|
|
158
|
+
return this.redirect("invalid_request", "authorization request not found");
|
|
122
159
|
}
|
|
123
160
|
const cached = JSON.parse(json);
|
|
124
|
-
const { registrationId } = (0, import_http.param)(request, this.
|
|
161
|
+
const { registrationId } = (0, import_http.param)(request, this.PATH_LOGIN_OAUTH2_CODE);
|
|
125
162
|
let data;
|
|
126
163
|
if (request.method === "POST" && request.headers.get("content-type")?.toLowerCase() === "application/x-www-form-urlencoded") {
|
|
127
164
|
const params = new URLSearchParams(await request.text());
|
|
@@ -131,37 +168,80 @@ var Auth = class {
|
|
|
131
168
|
}
|
|
132
169
|
const parsed = import_client.oauth2RedirectQuerySchema.safeParse(data);
|
|
133
170
|
if (!parsed.success) {
|
|
134
|
-
return this.redirect("
|
|
171
|
+
return this.redirect("invalid_request", "invalid redirect query");
|
|
135
172
|
}
|
|
136
173
|
if (!parsed.data.code || !parsed.data.state) {
|
|
137
|
-
|
|
138
|
-
return this.redirect("OAUTH2_AUTHORIZATION_ERROR");
|
|
174
|
+
return this.redirect("invalid_request", "invalid redirect query");
|
|
139
175
|
}
|
|
140
176
|
if (parsed.data.state !== cached.state) {
|
|
141
|
-
return this.redirect("
|
|
177
|
+
return this.redirect("invalid_request", "redirect state mismatch");
|
|
142
178
|
}
|
|
143
179
|
if (registrationId !== cached.registrationId) {
|
|
144
|
-
return this.redirect("
|
|
180
|
+
return this.redirect("invalid_request", "redirect registration mismatch");
|
|
145
181
|
}
|
|
146
182
|
const { code } = parsed.data;
|
|
147
|
-
const
|
|
148
|
-
const token = await this.oauth2Client.exchangeAuthorizationCode(
|
|
183
|
+
const token = await this.oauth2Client.exchangeAuthorizationCode({
|
|
149
184
|
registrationId,
|
|
150
185
|
code,
|
|
151
|
-
|
|
152
|
-
);
|
|
153
|
-
const userInfo = await this.oauth2Client.getUserInfo(registrationId, token);
|
|
186
|
+
pkce: cached.additionalParameters
|
|
187
|
+
});
|
|
188
|
+
const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });
|
|
189
|
+
mark("validate_session");
|
|
154
190
|
const principal = await onAuthorized(request, registrationId, userInfo, token);
|
|
191
|
+
mark("save_principal");
|
|
155
192
|
session.setLastAccessedTime(Date.now());
|
|
156
193
|
session.removeAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
|
|
157
194
|
session.setAttribute(import_session.PRINCIPAL_NAME_INDEX_NAME, principal.name);
|
|
158
195
|
await this.repository.save(session);
|
|
196
|
+
mark("update_session");
|
|
159
197
|
const response = new Response(null, {
|
|
160
198
|
status: 302,
|
|
161
|
-
headers: { location: this.oauth2Client.
|
|
199
|
+
headers: { location: this.oauth2Client.successUri }
|
|
200
|
+
});
|
|
201
|
+
const maxAge = session.getMaxInactiveInterval();
|
|
202
|
+
(0, import_http.setCookie)(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
|
|
203
|
+
setTiming(response);
|
|
204
|
+
return response;
|
|
205
|
+
};
|
|
206
|
+
loginOAuth2Native = async (request, onAuthorized) => {
|
|
207
|
+
(0, import_tiny_invariant.default)(this.oauth2Client, "oauth2Client is not initialized");
|
|
208
|
+
const { mark, setTiming } = (0, import_timing.timing)({ enabled: this.timing });
|
|
209
|
+
const { registrationId } = (0, import_http.param)(request, this.PATH_LOGIN_OAUTH2_NATIVE);
|
|
210
|
+
const credentials = await request.json();
|
|
211
|
+
const key = this.getOauth2StateKey(credentials.state);
|
|
212
|
+
const cached = await this.repository.getItem(key);
|
|
213
|
+
if (!cached) {
|
|
214
|
+
const json = { error: "invalid_request", error_description: "oauth2 state not found" };
|
|
215
|
+
return Response.json(json, { status: 400 });
|
|
216
|
+
}
|
|
217
|
+
if (cached.registrationId !== registrationId) {
|
|
218
|
+
const json = { error: "invalid_request", error_description: "registration mismatch" };
|
|
219
|
+
return Response.json(json, { status: 400 });
|
|
220
|
+
}
|
|
221
|
+
mark("validate_state");
|
|
222
|
+
const pkce = {
|
|
223
|
+
code_verifier: cached.code_verifier,
|
|
224
|
+
code_challenge: cached.code_challenge,
|
|
225
|
+
code_challenge_method: cached.code_challenge_method
|
|
226
|
+
};
|
|
227
|
+
const { userInfo, token } = await this.oauth2Client.loginOAuth2Native({
|
|
228
|
+
registrationId,
|
|
229
|
+
credentials,
|
|
230
|
+
pkce
|
|
231
|
+
});
|
|
232
|
+
mark("login_oauth2_native");
|
|
233
|
+
const principal = await onAuthorized(request, registrationId, userInfo, token);
|
|
234
|
+
mark("save_principal");
|
|
235
|
+
const session = this.repository.createSession();
|
|
236
|
+
session.setAttribute(import_session.PRINCIPAL_NAME_INDEX_NAME, principal.name);
|
|
237
|
+
await this.repository.save(session);
|
|
238
|
+
const response = new Response(JSON.stringify(principal), {
|
|
239
|
+
status: 200,
|
|
240
|
+
headers: { "Content-Type": "application/json" }
|
|
162
241
|
});
|
|
163
242
|
const maxAge = session.getMaxInactiveInterval();
|
|
164
243
|
(0, import_http.setCookie)(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
|
|
244
|
+
setTiming(response);
|
|
165
245
|
return response;
|
|
166
246
|
};
|
|
167
247
|
};
|
package/dist/auth/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/index.ts"],"sourcesContent":["import { randomUUID } from 'crypto';\nimport invariant from 'tiny-invariant';\nimport { PRINCIPAL_NAME_INDEX_NAME, type SessionRepository } from '../session/index';\nimport {\n param,\n query,\n getCookie,\n setCookie,\n deleteCookie,\n type CookieOptions,\n} from '../utils/http';\nimport { OAuth2Client, oauth2RedirectQuerySchema } from '../oauth2/client';\nimport type { OAuth2AuthorizationRequest } from '../oauth2/types';\nimport type { AuthConfig, AuthService, LoggedHandler, OAuth2AuthorizedHandler } from './types';\n\nexport class Auth implements AuthService {\n private readonly cookieName;\n private readonly cookieOptions: CookieOptions;\n private readonly repository: SessionRepository;\n private readonly oauth2Client: OAuth2Client | null;\n\n public readonly PATH_CSRF = '/csrf' as const;\n public readonly PATH_LOGOUT = '/logout' as const;\n public readonly PATH_LOGGED = '/logged' as const;\n\n public readonly PATH_OAUTH2_AUTHORIZATION = '/oauth2/authorization/:registrationId' as const;\n public readonly PATH_OAUTH2_REDIRECT = '/login/oauth2/code/:registrationId' as const;\n\n public readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({ repository, oauth2, cookie }: AuthConfig) {\n this.repository = repository;\n const { name, ...cookieOptions } = cookie ?? {};\n this.cookieName = name ?? 'SESSION';\n this.cookieOptions = {\n path: '/',\n sameSite: 'none',\n secure: true,\n httpOnly: true,\n ...cookieOptions,\n };\n this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;\n }\n\n logout = async (request: Request): Promise<Response> => {\n const sessionId = getCookie(request, this.cookieName);\n if (sessionId) await this.repository.deleteById(sessionId);\n const response = Response.json({});\n deleteCookie(response, this.cookieName);\n return response;\n };\n\n logged = async (request: Request, onLogged?: LoggedHandler): Promise<Response> => {\n // 1. get session from cookie\n const sessionId = getCookie(request, this.cookieName);\n if (!sessionId) return Response.json({ data: false });\n const session = await this.repository.findById(sessionId);\n if (!session) return Response.json({ data: false });\n\n // 2. get principal name from session\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n // 3. call onLogged\n if (name !== null) await onLogged?.({ name: String(name) });\n\n // 4. renew cookie\n const response = Response.json({ data: name !== null });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n\n return response;\n };\n\n oauth2Authorization = async (request: Request): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { registrationId } = param(request, this.PATH_OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const codeVerifier = randomUUID();\n const uri = this.oauth2Client.createAuthorizationUri(registrationId, state, codeVerifier);\n const sessionId = getCookie(request, this.cookieName);\n const session = sessionId\n ? ((await this.repository.findById(sessionId)) ?? this.repository.createSession())\n : this.repository.createSession();\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n codeVerifier,\n registrationId,\n authorizationRequestUri: uri.href,\n };\n\n const value = JSON.stringify(authorizationRequest);\n session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);\n await this.repository.save(session);\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n return response;\n };\n\n private redirect = (error: string): Response => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n return Response.redirect(`${this.oauth2Client.errorUri}?error=${error}`, 302);\n };\n\n loginOAuth2 = async (\n request: Request,\n onAuthorized: OAuth2AuthorizedHandler\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n\n // 1. get session from cookie\n const sessionId = getCookie(request, this.cookieName);\n const session = sessionId ? await this.repository.findById(sessionId) : null;\n if (!session) {\n return this.redirect('OAUTH2_AUTHORIZATION_SESSION_NOT_FOUND');\n }\n\n // 2. get cached authorization request from session\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) {\n return this.redirect('OAUTH2_AUTHORIZATION_REQUEST_NOT_FOUND');\n }\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n\n // 3. validate redirect query/formdata\n const { registrationId } = param(request, this.PATH_OAUTH2_REDIRECT);\n let data: Record<string, string>;\n // apple redirect: response_mode=form_post\n if (\n request.method === 'POST' &&\n request.headers.get('content-type')?.toLowerCase() === 'application/x-www-form-urlencoded'\n ) {\n const params = new URLSearchParams(await request.text());\n data = Object.fromEntries(params.entries());\n } else {\n data = query(request);\n }\n\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) {\n return this.redirect('INVALID_OAUTH2_REDIRECT_QUERY');\n }\n if (!parsed.data.code || !parsed.data.state) {\n console.error('oauth2 redirect error:', parsed.data);\n return this.redirect('OAUTH2_AUTHORIZATION_ERROR');\n }\n if (parsed.data.state !== cached.state) {\n return this.redirect('INVALID_OAUTH2_REDIRECT_STATE');\n }\n if (registrationId !== cached.registrationId) {\n return this.redirect('INVALID_OAUTH2_REDIRECT_REGISTRATION_ID');\n }\n\n // 4. exchange authorization code for token and get user info\n const { code } = parsed.data;\n const { codeVerifier } = cached;\n const token = await this.oauth2Client.exchangeAuthorizationCode(\n registrationId,\n code,\n codeVerifier\n );\n const userInfo = await this.oauth2Client.getUserInfo(registrationId, token);\n\n // 5. create or update principal\n const principal = await onAuthorized(request, registrationId, userInfo, token);\n\n // 6. update session\n session.setLastAccessedTime(Date.now());\n session.removeAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n\n const response = new Response(null, {\n status: 302,\n headers: { location: this.oauth2Client.baseUri },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n return response;\n };\n}\n"],"mappings":"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","names":["invariant"]}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/index.ts"],"sourcesContent":["import { createHash, randomUUID } from 'crypto';\nimport invariant from 'tiny-invariant';\nimport { PRINCIPAL_NAME_INDEX_NAME, type SessionRepository } from '../session/index';\nimport {\n param,\n query,\n getCookie,\n setCookie,\n deleteCookie,\n type CookieOptions,\n} from '../utils/http';\nimport { OAuth2Client, oauth2RedirectQuerySchema } from '../oauth2/client';\nimport { OAuth2ErrorType } from '../oauth2/error';\nimport type {\n NativeCredentials,\n OAuth2AuthorizationRequest,\n PkceParameters,\n} from '../oauth2/types';\nimport type {\n AuthConfig,\n AuthService,\n LoggedHandler,\n OAuth2AuthorizedHandler,\n OAuth2State,\n} from './types';\nimport { timing } from '../utils/timing';\n\nexport class Auth implements AuthService {\n private readonly timing: boolean;\n\n private readonly cookieName;\n private readonly cookieOptions: CookieOptions;\n private readonly repository: SessionRepository;\n private readonly oauth2Client: OAuth2Client | null;\n\n public readonly PATH_CSRF = '/csrf' as const;\n public readonly PATH_LOGOUT = '/logout' as const;\n public readonly PATH_LOGGED = '/logged' as const;\n\n public readonly PATH_OAUTH2_STATE = '/oauth2/state/:registrationId' as const;\n public readonly PATH_OAUTH2_AUTHORIZATION = '/oauth2/authorization/:registrationId' as const;\n public readonly PATH_LOGIN_OAUTH2_CODE = '/login/oauth2/code/:registrationId' as const;\n public readonly PATH_LOGIN_OAUTH2_NATIVE = '/login/oauth2/native/:registrationId' as const;\n\n public readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({ repository, oauth2, cookie, timing }: AuthConfig) {\n this.timing = timing ?? false;\n this.repository = repository;\n const { name, ...cookieOptions } = cookie ?? {};\n this.cookieName = name ?? 'SESSION';\n this.cookieOptions = {\n path: '/',\n sameSite: 'none',\n secure: true,\n httpOnly: true,\n ...cookieOptions,\n };\n this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;\n }\n\n logout = async (request: Request): Promise<Response> => {\n const sessionId = getCookie(request, this.cookieName);\n if (sessionId) await this.repository.deleteById(sessionId);\n const response = Response.json({});\n deleteCookie(response, this.cookieName);\n return response;\n };\n\n logged = async (request: Request, onLogged?: LoggedHandler): Promise<Response> => {\n // 1. get session from cookie\n const sessionId = getCookie(request, this.cookieName);\n if (!sessionId) return Response.json({ data: false });\n const session = await this.repository.findById(sessionId);\n if (!session) return Response.json({ data: false });\n\n // 2. get principal name from session\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n // 3. call onLogged\n if (name !== null) await onLogged?.({ name: String(name) });\n\n // 4. renew cookie\n const response = Response.json({ data: name !== null });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n return response;\n };\n\n private createPkceParameters(): PkceParameters {\n const code_verifier = randomUUID();\n const code_challenge = createHash('sha256')\n .update(code_verifier)\n .digest()\n .toString('base64url');\n\n return { code_verifier, code_challenge, code_challenge_method: 'S256' };\n }\n\n private getOauth2StateKey(state: string) {\n return `oauth2:state:${state}`;\n }\n\n oauth2State = async (request: Request): Promise<Response> => {\n const { registrationId } = param(request, this.PATH_OAUTH2_STATE);\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const state = randomUUID();\n const nonce = randomUUID();\n const pkce = this.createPkceParameters();\n const value: OAuth2State = { state, nonce, registrationId, ...pkce };\n await this.repository.setItem(this.getOauth2StateKey(state), value, 10 * 60);\n return Response.json({\n state,\n nonce,\n code_challenge: pkce.code_challenge,\n code_challenge_method: pkce.code_challenge_method,\n });\n };\n\n oauth2Authorization = async (request: Request): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { registrationId } = param(request, this.PATH_OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n const uri = this.oauth2Client.createAuthorizationUri({ registrationId, state, pkce });\n const sessionId = getCookie(request, this.cookieName);\n const session = sessionId\n ? ((await this.repository.findById(sessionId)) ?? this.repository.createSession())\n : this.repository.createSession();\n mark('fetch_session');\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n registrationId,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n };\n\n const value = JSON.stringify(authorizationRequest);\n session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);\n await this.repository.save(session);\n mark('save_session');\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n private redirect = (error: OAuth2ErrorType, description?: string): Response => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const uri = new URL(this.oauth2Client.errorUri);\n uri.searchParams.set('error', error);\n if (description) uri.searchParams.set('error_description', description);\n return Response.redirect(uri.href, 302);\n };\n\n loginOAuth2Code = async (\n request: Request,\n onAuthorized: OAuth2AuthorizedHandler\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // 1. get session from cookie\n const sessionId = getCookie(request, this.cookieName);\n const session = sessionId ? await this.repository.findById(sessionId) : null;\n if (!session) {\n return this.redirect('invalid_request', 'session not found');\n }\n mark('load_session');\n\n // 2. get cached authorization request from session\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) {\n return this.redirect('invalid_request', 'authorization request not found');\n }\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n\n // 3. validate redirect query/formdata\n const { registrationId } = param(request, this.PATH_LOGIN_OAUTH2_CODE);\n let data: Record<string, string>;\n // apple redirect: response_mode=form_post\n if (\n request.method === 'POST' &&\n request.headers.get('content-type')?.toLowerCase() === 'application/x-www-form-urlencoded'\n ) {\n const params = new URLSearchParams(await request.text());\n data = Object.fromEntries(params.entries());\n } else {\n data = query(request);\n }\n\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) {\n return this.redirect('invalid_request', 'invalid redirect query');\n }\n if (!parsed.data.code || !parsed.data.state) {\n return this.redirect('invalid_request', 'invalid redirect query');\n }\n if (parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', 'redirect state mismatch');\n }\n if (registrationId !== cached.registrationId) {\n return this.redirect('invalid_request', 'redirect registration mismatch');\n }\n\n // 4. exchange authorization code for token and get user info\n const { code } = parsed.data;\n\n const token = await this.oauth2Client.exchangeAuthorizationCode({\n registrationId,\n code,\n pkce: cached.additionalParameters,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });\n mark('validate_session');\n\n // 5. create or update principal\n const principal = await onAuthorized(request, registrationId, userInfo, token);\n mark('save_principal');\n\n // 6. update session\n session.setLastAccessedTime(Date.now());\n session.removeAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n mark('update_session');\n\n const response = new Response(null, {\n status: 302,\n headers: { location: this.oauth2Client.successUri },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n loginOAuth2Native = async (\n request: Request,\n onAuthorized: OAuth2AuthorizedHandler\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { registrationId } = param(request, this.PATH_LOGIN_OAUTH2_NATIVE);\n const credentials = (await request.json()) as NativeCredentials;\n const key = this.getOauth2StateKey(credentials.state);\n const cached = await this.repository.getItem<OAuth2State>(key);\n if (!cached) {\n const json = { error: 'invalid_request', error_description: 'oauth2 state not found' };\n return Response.json(json, { status: 400 });\n }\n if (cached.registrationId !== registrationId) {\n const json = { error: 'invalid_request', error_description: 'registration mismatch' };\n return Response.json(json, { status: 400 });\n }\n mark('validate_state');\n\n const pkce = {\n code_verifier: cached.code_verifier,\n code_challenge: cached.code_challenge,\n code_challenge_method: cached.code_challenge_method,\n };\n\n const { userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n registrationId,\n credentials,\n pkce,\n });\n mark('login_oauth2_native');\n\n const principal = await onAuthorized(request, registrationId, userInfo, token);\n mark('save_principal');\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n}\n"],"mappings":"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","names":["timing","invariant"]}
|
package/dist/auth/index.d.cts
CHANGED
|
@@ -6,6 +6,7 @@ import '../utils/http.cjs';
|
|
|
6
6
|
import 'cookie';
|
|
7
7
|
|
|
8
8
|
declare class Auth implements AuthService {
|
|
9
|
+
private readonly timing;
|
|
9
10
|
private readonly cookieName;
|
|
10
11
|
private readonly cookieOptions;
|
|
11
12
|
private readonly repository;
|
|
@@ -13,15 +14,21 @@ declare class Auth implements AuthService {
|
|
|
13
14
|
readonly PATH_CSRF: "/csrf";
|
|
14
15
|
readonly PATH_LOGOUT: "/logout";
|
|
15
16
|
readonly PATH_LOGGED: "/logged";
|
|
17
|
+
readonly PATH_OAUTH2_STATE: "/oauth2/state/:registrationId";
|
|
16
18
|
readonly PATH_OAUTH2_AUTHORIZATION: "/oauth2/authorization/:registrationId";
|
|
17
|
-
readonly
|
|
19
|
+
readonly PATH_LOGIN_OAUTH2_CODE: "/login/oauth2/code/:registrationId";
|
|
20
|
+
readonly PATH_LOGIN_OAUTH2_NATIVE: "/login/oauth2/native/:registrationId";
|
|
18
21
|
readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = "oauth2AuthorizationRequest";
|
|
19
|
-
constructor({ repository, oauth2, cookie }: AuthConfig);
|
|
22
|
+
constructor({ repository, oauth2, cookie, timing }: AuthConfig);
|
|
20
23
|
logout: (request: Request) => Promise<Response>;
|
|
21
24
|
logged: (request: Request, onLogged?: LoggedHandler) => Promise<Response>;
|
|
25
|
+
private createPkceParameters;
|
|
26
|
+
private getOauth2StateKey;
|
|
27
|
+
oauth2State: (request: Request) => Promise<Response>;
|
|
22
28
|
oauth2Authorization: (request: Request) => Promise<Response>;
|
|
23
29
|
private redirect;
|
|
24
|
-
|
|
30
|
+
loginOAuth2Code: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
|
|
31
|
+
loginOAuth2Native: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export { Auth };
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import '../utils/http.js';
|
|
|
6
6
|
import 'cookie';
|
|
7
7
|
|
|
8
8
|
declare class Auth implements AuthService {
|
|
9
|
+
private readonly timing;
|
|
9
10
|
private readonly cookieName;
|
|
10
11
|
private readonly cookieOptions;
|
|
11
12
|
private readonly repository;
|
|
@@ -13,15 +14,21 @@ declare class Auth implements AuthService {
|
|
|
13
14
|
readonly PATH_CSRF: "/csrf";
|
|
14
15
|
readonly PATH_LOGOUT: "/logout";
|
|
15
16
|
readonly PATH_LOGGED: "/logged";
|
|
17
|
+
readonly PATH_OAUTH2_STATE: "/oauth2/state/:registrationId";
|
|
16
18
|
readonly PATH_OAUTH2_AUTHORIZATION: "/oauth2/authorization/:registrationId";
|
|
17
|
-
readonly
|
|
19
|
+
readonly PATH_LOGIN_OAUTH2_CODE: "/login/oauth2/code/:registrationId";
|
|
20
|
+
readonly PATH_LOGIN_OAUTH2_NATIVE: "/login/oauth2/native/:registrationId";
|
|
18
21
|
readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = "oauth2AuthorizationRequest";
|
|
19
|
-
constructor({ repository, oauth2, cookie }: AuthConfig);
|
|
22
|
+
constructor({ repository, oauth2, cookie, timing }: AuthConfig);
|
|
20
23
|
logout: (request: Request) => Promise<Response>;
|
|
21
24
|
logged: (request: Request, onLogged?: LoggedHandler) => Promise<Response>;
|
|
25
|
+
private createPkceParameters;
|
|
26
|
+
private getOauth2StateKey;
|
|
27
|
+
oauth2State: (request: Request) => Promise<Response>;
|
|
22
28
|
oauth2Authorization: (request: Request) => Promise<Response>;
|
|
23
29
|
private redirect;
|
|
24
|
-
|
|
30
|
+
loginOAuth2Code: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
|
|
31
|
+
loginOAuth2Native: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
|
|
25
32
|
}
|
|
26
33
|
|
|
27
34
|
export { Auth };
|
package/dist/auth/index.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/auth/index.ts
|
|
2
|
-
import { randomUUID } from "crypto";
|
|
2
|
+
import { createHash, randomUUID } from "crypto";
|
|
3
3
|
import invariant from "tiny-invariant";
|
|
4
4
|
import { PRINCIPAL_NAME_INDEX_NAME } from "../session/index.mjs";
|
|
5
5
|
import {
|
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
deleteCookie
|
|
11
11
|
} from "../utils/http.mjs";
|
|
12
12
|
import { OAuth2Client, oauth2RedirectQuerySchema } from "../oauth2/client.mjs";
|
|
13
|
+
import { timing } from "../utils/timing.mjs";
|
|
13
14
|
var Auth = class {
|
|
15
|
+
timing;
|
|
14
16
|
cookieName;
|
|
15
17
|
cookieOptions;
|
|
16
18
|
repository;
|
|
@@ -18,10 +20,13 @@ var Auth = class {
|
|
|
18
20
|
PATH_CSRF = "/csrf";
|
|
19
21
|
PATH_LOGOUT = "/logout";
|
|
20
22
|
PATH_LOGGED = "/logged";
|
|
23
|
+
PATH_OAUTH2_STATE = "/oauth2/state/:registrationId";
|
|
21
24
|
PATH_OAUTH2_AUTHORIZATION = "/oauth2/authorization/:registrationId";
|
|
22
|
-
|
|
25
|
+
PATH_LOGIN_OAUTH2_CODE = "/login/oauth2/code/:registrationId";
|
|
26
|
+
PATH_LOGIN_OAUTH2_NATIVE = "/login/oauth2/native/:registrationId";
|
|
23
27
|
ATTR_OAUTH2_AUTHORIZATION_REQUEST = "oauth2AuthorizationRequest";
|
|
24
|
-
constructor({ repository, oauth2, cookie }) {
|
|
28
|
+
constructor({ repository, oauth2, cookie, timing: timing2 }) {
|
|
29
|
+
this.timing = timing2 ?? false;
|
|
25
30
|
this.repository = repository;
|
|
26
31
|
const { name, ...cookieOptions } = cookie ?? {};
|
|
27
32
|
this.cookieName = name ?? "SESSION";
|
|
@@ -55,45 +60,77 @@ var Auth = class {
|
|
|
55
60
|
setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
|
|
56
61
|
return response;
|
|
57
62
|
};
|
|
63
|
+
createPkceParameters() {
|
|
64
|
+
const code_verifier = randomUUID();
|
|
65
|
+
const code_challenge = createHash("sha256").update(code_verifier).digest().toString("base64url");
|
|
66
|
+
return { code_verifier, code_challenge, code_challenge_method: "S256" };
|
|
67
|
+
}
|
|
68
|
+
getOauth2StateKey(state) {
|
|
69
|
+
return `oauth2:state:${state}`;
|
|
70
|
+
}
|
|
71
|
+
oauth2State = async (request) => {
|
|
72
|
+
const { registrationId } = param(request, this.PATH_OAUTH2_STATE);
|
|
73
|
+
invariant(this.oauth2Client, "oauth2Client is not initialized");
|
|
74
|
+
const state = randomUUID();
|
|
75
|
+
const nonce = randomUUID();
|
|
76
|
+
const pkce = this.createPkceParameters();
|
|
77
|
+
const value = { state, nonce, registrationId, ...pkce };
|
|
78
|
+
await this.repository.setItem(this.getOauth2StateKey(state), value, 10 * 60);
|
|
79
|
+
return Response.json({
|
|
80
|
+
state,
|
|
81
|
+
nonce,
|
|
82
|
+
code_challenge: pkce.code_challenge,
|
|
83
|
+
code_challenge_method: pkce.code_challenge_method
|
|
84
|
+
});
|
|
85
|
+
};
|
|
58
86
|
oauth2Authorization = async (request) => {
|
|
59
87
|
invariant(this.oauth2Client, "oauth2Client is not initialized");
|
|
88
|
+
const { mark, setTiming } = timing({ enabled: this.timing });
|
|
60
89
|
const { registrationId } = param(request, this.PATH_OAUTH2_AUTHORIZATION);
|
|
61
90
|
const state = randomUUID();
|
|
62
|
-
const
|
|
63
|
-
const uri = this.oauth2Client.createAuthorizationUri(registrationId, state,
|
|
91
|
+
const pkce = this.createPkceParameters();
|
|
92
|
+
const uri = this.oauth2Client.createAuthorizationUri({ registrationId, state, pkce });
|
|
64
93
|
const sessionId = getCookie(request, this.cookieName);
|
|
65
94
|
const session = sessionId ? await this.repository.findById(sessionId) ?? this.repository.createSession() : this.repository.createSession();
|
|
95
|
+
mark("fetch_session");
|
|
66
96
|
const authorizationRequest = {
|
|
67
97
|
state,
|
|
68
|
-
codeVerifier,
|
|
69
98
|
registrationId,
|
|
70
|
-
authorizationRequestUri: uri.href
|
|
99
|
+
authorizationRequestUri: uri.href,
|
|
100
|
+
additionalParameters: pkce
|
|
71
101
|
};
|
|
72
102
|
const value = JSON.stringify(authorizationRequest);
|
|
73
103
|
session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);
|
|
74
104
|
await this.repository.save(session);
|
|
105
|
+
mark("save_session");
|
|
75
106
|
const response = new Response(null, { status: 302, headers: { location: uri.href } });
|
|
76
107
|
const maxAge = session.getMaxInactiveInterval();
|
|
77
108
|
setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
|
|
109
|
+
setTiming(response);
|
|
78
110
|
return response;
|
|
79
111
|
};
|
|
80
|
-
redirect = (error) => {
|
|
112
|
+
redirect = (error, description) => {
|
|
81
113
|
invariant(this.oauth2Client, "oauth2Client is not initialized");
|
|
82
|
-
|
|
114
|
+
const uri = new URL(this.oauth2Client.errorUri);
|
|
115
|
+
uri.searchParams.set("error", error);
|
|
116
|
+
if (description) uri.searchParams.set("error_description", description);
|
|
117
|
+
return Response.redirect(uri.href, 302);
|
|
83
118
|
};
|
|
84
|
-
|
|
119
|
+
loginOAuth2Code = async (request, onAuthorized) => {
|
|
85
120
|
invariant(this.oauth2Client, "oauth2Client is not initialized");
|
|
121
|
+
const { mark, setTiming } = timing({ enabled: this.timing });
|
|
86
122
|
const sessionId = getCookie(request, this.cookieName);
|
|
87
123
|
const session = sessionId ? await this.repository.findById(sessionId) : null;
|
|
88
124
|
if (!session) {
|
|
89
|
-
return this.redirect("
|
|
125
|
+
return this.redirect("invalid_request", "session not found");
|
|
90
126
|
}
|
|
127
|
+
mark("load_session");
|
|
91
128
|
const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
|
|
92
129
|
if (!json) {
|
|
93
|
-
return this.redirect("
|
|
130
|
+
return this.redirect("invalid_request", "authorization request not found");
|
|
94
131
|
}
|
|
95
132
|
const cached = JSON.parse(json);
|
|
96
|
-
const { registrationId } = param(request, this.
|
|
133
|
+
const { registrationId } = param(request, this.PATH_LOGIN_OAUTH2_CODE);
|
|
97
134
|
let data;
|
|
98
135
|
if (request.method === "POST" && request.headers.get("content-type")?.toLowerCase() === "application/x-www-form-urlencoded") {
|
|
99
136
|
const params = new URLSearchParams(await request.text());
|
|
@@ -103,37 +140,80 @@ var Auth = class {
|
|
|
103
140
|
}
|
|
104
141
|
const parsed = oauth2RedirectQuerySchema.safeParse(data);
|
|
105
142
|
if (!parsed.success) {
|
|
106
|
-
return this.redirect("
|
|
143
|
+
return this.redirect("invalid_request", "invalid redirect query");
|
|
107
144
|
}
|
|
108
145
|
if (!parsed.data.code || !parsed.data.state) {
|
|
109
|
-
|
|
110
|
-
return this.redirect("OAUTH2_AUTHORIZATION_ERROR");
|
|
146
|
+
return this.redirect("invalid_request", "invalid redirect query");
|
|
111
147
|
}
|
|
112
148
|
if (parsed.data.state !== cached.state) {
|
|
113
|
-
return this.redirect("
|
|
149
|
+
return this.redirect("invalid_request", "redirect state mismatch");
|
|
114
150
|
}
|
|
115
151
|
if (registrationId !== cached.registrationId) {
|
|
116
|
-
return this.redirect("
|
|
152
|
+
return this.redirect("invalid_request", "redirect registration mismatch");
|
|
117
153
|
}
|
|
118
154
|
const { code } = parsed.data;
|
|
119
|
-
const
|
|
120
|
-
const token = await this.oauth2Client.exchangeAuthorizationCode(
|
|
155
|
+
const token = await this.oauth2Client.exchangeAuthorizationCode({
|
|
121
156
|
registrationId,
|
|
122
157
|
code,
|
|
123
|
-
|
|
124
|
-
);
|
|
125
|
-
const userInfo = await this.oauth2Client.getUserInfo(registrationId, token);
|
|
158
|
+
pkce: cached.additionalParameters
|
|
159
|
+
});
|
|
160
|
+
const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });
|
|
161
|
+
mark("validate_session");
|
|
126
162
|
const principal = await onAuthorized(request, registrationId, userInfo, token);
|
|
163
|
+
mark("save_principal");
|
|
127
164
|
session.setLastAccessedTime(Date.now());
|
|
128
165
|
session.removeAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
|
|
129
166
|
session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);
|
|
130
167
|
await this.repository.save(session);
|
|
168
|
+
mark("update_session");
|
|
131
169
|
const response = new Response(null, {
|
|
132
170
|
status: 302,
|
|
133
|
-
headers: { location: this.oauth2Client.
|
|
171
|
+
headers: { location: this.oauth2Client.successUri }
|
|
172
|
+
});
|
|
173
|
+
const maxAge = session.getMaxInactiveInterval();
|
|
174
|
+
setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
|
|
175
|
+
setTiming(response);
|
|
176
|
+
return response;
|
|
177
|
+
};
|
|
178
|
+
loginOAuth2Native = async (request, onAuthorized) => {
|
|
179
|
+
invariant(this.oauth2Client, "oauth2Client is not initialized");
|
|
180
|
+
const { mark, setTiming } = timing({ enabled: this.timing });
|
|
181
|
+
const { registrationId } = param(request, this.PATH_LOGIN_OAUTH2_NATIVE);
|
|
182
|
+
const credentials = await request.json();
|
|
183
|
+
const key = this.getOauth2StateKey(credentials.state);
|
|
184
|
+
const cached = await this.repository.getItem(key);
|
|
185
|
+
if (!cached) {
|
|
186
|
+
const json = { error: "invalid_request", error_description: "oauth2 state not found" };
|
|
187
|
+
return Response.json(json, { status: 400 });
|
|
188
|
+
}
|
|
189
|
+
if (cached.registrationId !== registrationId) {
|
|
190
|
+
const json = { error: "invalid_request", error_description: "registration mismatch" };
|
|
191
|
+
return Response.json(json, { status: 400 });
|
|
192
|
+
}
|
|
193
|
+
mark("validate_state");
|
|
194
|
+
const pkce = {
|
|
195
|
+
code_verifier: cached.code_verifier,
|
|
196
|
+
code_challenge: cached.code_challenge,
|
|
197
|
+
code_challenge_method: cached.code_challenge_method
|
|
198
|
+
};
|
|
199
|
+
const { userInfo, token } = await this.oauth2Client.loginOAuth2Native({
|
|
200
|
+
registrationId,
|
|
201
|
+
credentials,
|
|
202
|
+
pkce
|
|
203
|
+
});
|
|
204
|
+
mark("login_oauth2_native");
|
|
205
|
+
const principal = await onAuthorized(request, registrationId, userInfo, token);
|
|
206
|
+
mark("save_principal");
|
|
207
|
+
const session = this.repository.createSession();
|
|
208
|
+
session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);
|
|
209
|
+
await this.repository.save(session);
|
|
210
|
+
const response = new Response(JSON.stringify(principal), {
|
|
211
|
+
status: 200,
|
|
212
|
+
headers: { "Content-Type": "application/json" }
|
|
134
213
|
});
|
|
135
214
|
const maxAge = session.getMaxInactiveInterval();
|
|
136
215
|
setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
|
|
216
|
+
setTiming(response);
|
|
137
217
|
return response;
|
|
138
218
|
};
|
|
139
219
|
};
|
package/dist/auth/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/index.ts"],"sourcesContent":["import { randomUUID } from 'crypto';\nimport invariant from 'tiny-invariant';\nimport { PRINCIPAL_NAME_INDEX_NAME, type SessionRepository } from '../session/index';\nimport {\n param,\n query,\n getCookie,\n setCookie,\n deleteCookie,\n type CookieOptions,\n} from '../utils/http';\nimport { OAuth2Client, oauth2RedirectQuerySchema } from '../oauth2/client';\nimport type { OAuth2AuthorizationRequest } from '../oauth2/types';\nimport type { AuthConfig, AuthService, LoggedHandler, OAuth2AuthorizedHandler } from './types';\n\nexport class Auth implements AuthService {\n private readonly cookieName;\n private readonly cookieOptions: CookieOptions;\n private readonly repository: SessionRepository;\n private readonly oauth2Client: OAuth2Client | null;\n\n public readonly PATH_CSRF = '/csrf' as const;\n public readonly PATH_LOGOUT = '/logout' as const;\n public readonly PATH_LOGGED = '/logged' as const;\n\n public readonly PATH_OAUTH2_AUTHORIZATION = '/oauth2/authorization/:registrationId' as const;\n public readonly PATH_OAUTH2_REDIRECT = '/login/oauth2/code/:registrationId' as const;\n\n public readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({ repository, oauth2, cookie }: AuthConfig) {\n this.repository = repository;\n const { name, ...cookieOptions } = cookie ?? {};\n this.cookieName = name ?? 'SESSION';\n this.cookieOptions = {\n path: '/',\n sameSite: 'none',\n secure: true,\n httpOnly: true,\n ...cookieOptions,\n };\n this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;\n }\n\n logout = async (request: Request): Promise<Response> => {\n const sessionId = getCookie(request, this.cookieName);\n if (sessionId) await this.repository.deleteById(sessionId);\n const response = Response.json({});\n deleteCookie(response, this.cookieName);\n return response;\n };\n\n logged = async (request: Request, onLogged?: LoggedHandler): Promise<Response> => {\n // 1. get session from cookie\n const sessionId = getCookie(request, this.cookieName);\n if (!sessionId) return Response.json({ data: false });\n const session = await this.repository.findById(sessionId);\n if (!session) return Response.json({ data: false });\n\n // 2. get principal name from session\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n // 3. call onLogged\n if (name !== null) await onLogged?.({ name: String(name) });\n\n // 4. renew cookie\n const response = Response.json({ data: name !== null });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n\n return response;\n };\n\n oauth2Authorization = async (request: Request): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { registrationId } = param(request, this.PATH_OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const codeVerifier = randomUUID();\n const uri = this.oauth2Client.createAuthorizationUri(registrationId, state, codeVerifier);\n const sessionId = getCookie(request, this.cookieName);\n const session = sessionId\n ? ((await this.repository.findById(sessionId)) ?? this.repository.createSession())\n : this.repository.createSession();\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n codeVerifier,\n registrationId,\n authorizationRequestUri: uri.href,\n };\n\n const value = JSON.stringify(authorizationRequest);\n session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);\n await this.repository.save(session);\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n return response;\n };\n\n private redirect = (error: string): Response => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n return Response.redirect(`${this.oauth2Client.errorUri}?error=${error}`, 302);\n };\n\n loginOAuth2 = async (\n request: Request,\n onAuthorized: OAuth2AuthorizedHandler\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n\n // 1. get session from cookie\n const sessionId = getCookie(request, this.cookieName);\n const session = sessionId ? await this.repository.findById(sessionId) : null;\n if (!session) {\n return this.redirect('OAUTH2_AUTHORIZATION_SESSION_NOT_FOUND');\n }\n\n // 2. get cached authorization request from session\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) {\n return this.redirect('OAUTH2_AUTHORIZATION_REQUEST_NOT_FOUND');\n }\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n\n // 3. validate redirect query/formdata\n const { registrationId } = param(request, this.PATH_OAUTH2_REDIRECT);\n let data: Record<string, string>;\n // apple redirect: response_mode=form_post\n if (\n request.method === 'POST' &&\n request.headers.get('content-type')?.toLowerCase() === 'application/x-www-form-urlencoded'\n ) {\n const params = new URLSearchParams(await request.text());\n data = Object.fromEntries(params.entries());\n } else {\n data = query(request);\n }\n\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) {\n return this.redirect('INVALID_OAUTH2_REDIRECT_QUERY');\n }\n if (!parsed.data.code || !parsed.data.state) {\n console.error('oauth2 redirect error:', parsed.data);\n return this.redirect('OAUTH2_AUTHORIZATION_ERROR');\n }\n if (parsed.data.state !== cached.state) {\n return this.redirect('INVALID_OAUTH2_REDIRECT_STATE');\n }\n if (registrationId !== cached.registrationId) {\n return this.redirect('INVALID_OAUTH2_REDIRECT_REGISTRATION_ID');\n }\n\n // 4. exchange authorization code for token and get user info\n const { code } = parsed.data;\n const { codeVerifier } = cached;\n const token = await this.oauth2Client.exchangeAuthorizationCode(\n registrationId,\n code,\n codeVerifier\n );\n const userInfo = await this.oauth2Client.getUserInfo(registrationId, token);\n\n // 5. create or update principal\n const principal = await onAuthorized(request, registrationId, userInfo, token);\n\n // 6. update session\n session.setLastAccessedTime(Date.now());\n session.removeAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n\n const response = new Response(null, {\n status: 302,\n headers: { location: this.oauth2Client.baseUri },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n return response;\n };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../../src/auth/index.ts"],"sourcesContent":["import { createHash, randomUUID } from 'crypto';\nimport invariant from 'tiny-invariant';\nimport { PRINCIPAL_NAME_INDEX_NAME, type SessionRepository } from '../session/index';\nimport {\n param,\n query,\n getCookie,\n setCookie,\n deleteCookie,\n type CookieOptions,\n} from '../utils/http';\nimport { OAuth2Client, oauth2RedirectQuerySchema } from '../oauth2/client';\nimport { OAuth2ErrorType } from '../oauth2/error';\nimport type {\n NativeCredentials,\n OAuth2AuthorizationRequest,\n PkceParameters,\n} from '../oauth2/types';\nimport type {\n AuthConfig,\n AuthService,\n LoggedHandler,\n OAuth2AuthorizedHandler,\n OAuth2State,\n} from './types';\nimport { timing } from '../utils/timing';\n\nexport class Auth implements AuthService {\n private readonly timing: boolean;\n\n private readonly cookieName;\n private readonly cookieOptions: CookieOptions;\n private readonly repository: SessionRepository;\n private readonly oauth2Client: OAuth2Client | null;\n\n public readonly PATH_CSRF = '/csrf' as const;\n public readonly PATH_LOGOUT = '/logout' as const;\n public readonly PATH_LOGGED = '/logged' as const;\n\n public readonly PATH_OAUTH2_STATE = '/oauth2/state/:registrationId' as const;\n public readonly PATH_OAUTH2_AUTHORIZATION = '/oauth2/authorization/:registrationId' as const;\n public readonly PATH_LOGIN_OAUTH2_CODE = '/login/oauth2/code/:registrationId' as const;\n public readonly PATH_LOGIN_OAUTH2_NATIVE = '/login/oauth2/native/:registrationId' as const;\n\n public readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = 'oauth2AuthorizationRequest';\n\n constructor({ repository, oauth2, cookie, timing }: AuthConfig) {\n this.timing = timing ?? false;\n this.repository = repository;\n const { name, ...cookieOptions } = cookie ?? {};\n this.cookieName = name ?? 'SESSION';\n this.cookieOptions = {\n path: '/',\n sameSite: 'none',\n secure: true,\n httpOnly: true,\n ...cookieOptions,\n };\n this.oauth2Client = oauth2?.client ? new OAuth2Client(oauth2.client) : null;\n }\n\n logout = async (request: Request): Promise<Response> => {\n const sessionId = getCookie(request, this.cookieName);\n if (sessionId) await this.repository.deleteById(sessionId);\n const response = Response.json({});\n deleteCookie(response, this.cookieName);\n return response;\n };\n\n logged = async (request: Request, onLogged?: LoggedHandler): Promise<Response> => {\n // 1. get session from cookie\n const sessionId = getCookie(request, this.cookieName);\n if (!sessionId) return Response.json({ data: false });\n const session = await this.repository.findById(sessionId);\n if (!session) return Response.json({ data: false });\n\n // 2. get principal name from session\n const name = session.getAttribute(PRINCIPAL_NAME_INDEX_NAME);\n session.setLastAccessedTime(Date.now());\n await this.repository.save(session);\n\n // 3. call onLogged\n if (name !== null) await onLogged?.({ name: String(name) });\n\n // 4. renew cookie\n const response = Response.json({ data: name !== null });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n return response;\n };\n\n private createPkceParameters(): PkceParameters {\n const code_verifier = randomUUID();\n const code_challenge = createHash('sha256')\n .update(code_verifier)\n .digest()\n .toString('base64url');\n\n return { code_verifier, code_challenge, code_challenge_method: 'S256' };\n }\n\n private getOauth2StateKey(state: string) {\n return `oauth2:state:${state}`;\n }\n\n oauth2State = async (request: Request): Promise<Response> => {\n const { registrationId } = param(request, this.PATH_OAUTH2_STATE);\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const state = randomUUID();\n const nonce = randomUUID();\n const pkce = this.createPkceParameters();\n const value: OAuth2State = { state, nonce, registrationId, ...pkce };\n await this.repository.setItem(this.getOauth2StateKey(state), value, 10 * 60);\n return Response.json({\n state,\n nonce,\n code_challenge: pkce.code_challenge,\n code_challenge_method: pkce.code_challenge_method,\n });\n };\n\n oauth2Authorization = async (request: Request): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { registrationId } = param(request, this.PATH_OAUTH2_AUTHORIZATION);\n const state = randomUUID();\n const pkce = this.createPkceParameters();\n const uri = this.oauth2Client.createAuthorizationUri({ registrationId, state, pkce });\n const sessionId = getCookie(request, this.cookieName);\n const session = sessionId\n ? ((await this.repository.findById(sessionId)) ?? this.repository.createSession())\n : this.repository.createSession();\n mark('fetch_session');\n\n const authorizationRequest: OAuth2AuthorizationRequest = {\n state,\n registrationId,\n authorizationRequestUri: uri.href,\n additionalParameters: pkce,\n };\n\n const value = JSON.stringify(authorizationRequest);\n session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);\n await this.repository.save(session);\n mark('save_session');\n\n const response = new Response(null, { status: 302, headers: { location: uri.href } });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n private redirect = (error: OAuth2ErrorType, description?: string): Response => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const uri = new URL(this.oauth2Client.errorUri);\n uri.searchParams.set('error', error);\n if (description) uri.searchParams.set('error_description', description);\n return Response.redirect(uri.href, 302);\n };\n\n loginOAuth2Code = async (\n request: Request,\n onAuthorized: OAuth2AuthorizedHandler\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n\n // 1. get session from cookie\n const sessionId = getCookie(request, this.cookieName);\n const session = sessionId ? await this.repository.findById(sessionId) : null;\n if (!session) {\n return this.redirect('invalid_request', 'session not found');\n }\n mark('load_session');\n\n // 2. get cached authorization request from session\n const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST) as string | null;\n if (!json) {\n return this.redirect('invalid_request', 'authorization request not found');\n }\n const cached: OAuth2AuthorizationRequest = JSON.parse(json);\n\n // 3. validate redirect query/formdata\n const { registrationId } = param(request, this.PATH_LOGIN_OAUTH2_CODE);\n let data: Record<string, string>;\n // apple redirect: response_mode=form_post\n if (\n request.method === 'POST' &&\n request.headers.get('content-type')?.toLowerCase() === 'application/x-www-form-urlencoded'\n ) {\n const params = new URLSearchParams(await request.text());\n data = Object.fromEntries(params.entries());\n } else {\n data = query(request);\n }\n\n const parsed = oauth2RedirectQuerySchema.safeParse(data);\n if (!parsed.success) {\n return this.redirect('invalid_request', 'invalid redirect query');\n }\n if (!parsed.data.code || !parsed.data.state) {\n return this.redirect('invalid_request', 'invalid redirect query');\n }\n if (parsed.data.state !== cached.state) {\n return this.redirect('invalid_request', 'redirect state mismatch');\n }\n if (registrationId !== cached.registrationId) {\n return this.redirect('invalid_request', 'redirect registration mismatch');\n }\n\n // 4. exchange authorization code for token and get user info\n const { code } = parsed.data;\n\n const token = await this.oauth2Client.exchangeAuthorizationCode({\n registrationId,\n code,\n pkce: cached.additionalParameters,\n });\n const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });\n mark('validate_session');\n\n // 5. create or update principal\n const principal = await onAuthorized(request, registrationId, userInfo, token);\n mark('save_principal');\n\n // 6. update session\n session.setLastAccessedTime(Date.now());\n session.removeAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n mark('update_session');\n\n const response = new Response(null, {\n status: 302,\n headers: { location: this.oauth2Client.successUri },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n\n loginOAuth2Native = async (\n request: Request,\n onAuthorized: OAuth2AuthorizedHandler\n ): Promise<Response> => {\n invariant(this.oauth2Client, 'oauth2Client is not initialized');\n const { mark, setTiming } = timing({ enabled: this.timing });\n const { registrationId } = param(request, this.PATH_LOGIN_OAUTH2_NATIVE);\n const credentials = (await request.json()) as NativeCredentials;\n const key = this.getOauth2StateKey(credentials.state);\n const cached = await this.repository.getItem<OAuth2State>(key);\n if (!cached) {\n const json = { error: 'invalid_request', error_description: 'oauth2 state not found' };\n return Response.json(json, { status: 400 });\n }\n if (cached.registrationId !== registrationId) {\n const json = { error: 'invalid_request', error_description: 'registration mismatch' };\n return Response.json(json, { status: 400 });\n }\n mark('validate_state');\n\n const pkce = {\n code_verifier: cached.code_verifier,\n code_challenge: cached.code_challenge,\n code_challenge_method: cached.code_challenge_method,\n };\n\n const { userInfo, token } = await this.oauth2Client.loginOAuth2Native({\n registrationId,\n credentials,\n pkce,\n });\n mark('login_oauth2_native');\n\n const principal = await onAuthorized(request, registrationId, userInfo, token);\n mark('save_principal');\n\n const session = this.repository.createSession();\n session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);\n await this.repository.save(session);\n const response = new Response(JSON.stringify(principal), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\n setTiming(response);\n\n return response;\n };\n}\n"],"mappings":"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","names":["timing"]}
|
package/dist/auth/types.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/auth/types.ts"],"sourcesContent":["import { Principal } from '../core';\nimport { OAuth2ClientConfig, UserInfo } from '../oauth2/types';\nimport
|
|
1
|
+
{"version":3,"sources":["../../src/auth/types.ts"],"sourcesContent":["import type { Principal } from '../core';\nimport type { OAuth2Token, OAuth2ClientConfig, PkceParameters, UserInfo } from '../oauth2/types';\nimport type { SessionRepository } from '../session';\nimport type { CookieOptions } from '../utils/http';\n\nexport type LoggedHandler = (principal: Principal) => Promise<void>;\nexport type OAuth2AuthorizedHandler = (\n request: Request,\n registrationId: string,\n userInfo: UserInfo,\n token: OAuth2Token\n) => Promise<Principal>;\n\nexport interface OAuth2State extends PkceParameters {\n state: string;\n nonce: string;\n registrationId: string;\n}\n\nexport interface AuthConfig {\n timing?: boolean;\n repository: SessionRepository;\n cookie?: CookieOptions & { name?: string };\n oauth2?: {\n client?: OAuth2ClientConfig;\n };\n}\n\nexport interface AuthService {\n logout: (request: Request) => Promise<Response>;\n logged: (request: Request, onLogged?: LoggedHandler) => Promise<Response>;\n\n oauth2State: (request: Request) => Promise<Response>;\n oauth2Authorization: (request: Request) => Promise<Response>;\n loginOAuth2Code: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;\n loginOAuth2Native: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
|