@shware/security 0.4.9 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/dist/auth/index.cjs +84 -22
  2. package/dist/auth/index.cjs.map +1 -1
  3. package/dist/auth/index.d.cts +8 -2
  4. package/dist/auth/index.d.ts +8 -2
  5. package/dist/auth/index.mjs +85 -23
  6. package/dist/auth/index.mjs.map +1 -1
  7. package/dist/auth/types.cjs.map +1 -1
  8. package/dist/auth/types.d.cts +10 -3
  9. package/dist/auth/types.d.ts +10 -3
  10. package/dist/index.cjs.map +1 -1
  11. package/dist/index.d.cts +1 -1
  12. package/dist/index.d.ts +1 -1
  13. package/dist/index.mjs.map +1 -1
  14. package/dist/oauth2/client.cjs +40 -6
  15. package/dist/oauth2/client.cjs.map +1 -1
  16. package/dist/oauth2/client.d.cts +29 -6
  17. package/dist/oauth2/client.d.ts +29 -6
  18. package/dist/oauth2/client.mjs +40 -6
  19. package/dist/oauth2/client.mjs.map +1 -1
  20. package/dist/oauth2/error.cjs.map +1 -1
  21. package/dist/oauth2/error.d.cts +1 -1
  22. package/dist/oauth2/error.d.ts +1 -1
  23. package/dist/oauth2/error.mjs.map +1 -1
  24. package/dist/oauth2/provider/apple.cjs +21 -5
  25. package/dist/oauth2/provider/apple.cjs.map +1 -1
  26. package/dist/oauth2/provider/apple.d.cts +12 -4
  27. package/dist/oauth2/provider/apple.d.ts +12 -4
  28. package/dist/oauth2/provider/apple.mjs +19 -4
  29. package/dist/oauth2/provider/apple.mjs.map +1 -1
  30. package/dist/oauth2/provider/common.cjs +5 -13
  31. package/dist/oauth2/provider/common.cjs.map +1 -1
  32. package/dist/oauth2/provider/common.d.cts +6 -11
  33. package/dist/oauth2/provider/common.d.ts +6 -11
  34. package/dist/oauth2/provider/common.mjs +5 -13
  35. package/dist/oauth2/provider/common.mjs.map +1 -1
  36. package/dist/oauth2/provider/facebook.cjs +19 -3
  37. package/dist/oauth2/provider/facebook.cjs.map +1 -1
  38. package/dist/oauth2/provider/facebook.d.cts +17 -3
  39. package/dist/oauth2/provider/facebook.d.ts +17 -3
  40. package/dist/oauth2/provider/facebook.mjs +18 -3
  41. package/dist/oauth2/provider/facebook.mjs.map +1 -1
  42. package/dist/oauth2/provider/github.cjs +6 -3
  43. package/dist/oauth2/provider/github.cjs.map +1 -1
  44. package/dist/oauth2/provider/github.d.cts +11 -3
  45. package/dist/oauth2/provider/github.d.ts +11 -3
  46. package/dist/oauth2/provider/github.mjs +5 -3
  47. package/dist/oauth2/provider/github.mjs.map +1 -1
  48. package/dist/oauth2/provider/google.cjs +17 -1
  49. package/dist/oauth2/provider/google.cjs.map +1 -1
  50. package/dist/oauth2/provider/google.d.cts +11 -4
  51. package/dist/oauth2/provider/google.d.ts +11 -4
  52. package/dist/oauth2/provider/google.mjs +16 -1
  53. package/dist/oauth2/provider/google.mjs.map +1 -1
  54. package/dist/oauth2/provider/x.cjs +16 -4
  55. package/dist/oauth2/provider/x.cjs.map +1 -1
  56. package/dist/oauth2/provider/x.d.cts +11 -3
  57. package/dist/oauth2/provider/x.d.ts +11 -3
  58. package/dist/oauth2/provider/x.mjs +15 -4
  59. package/dist/oauth2/provider/x.mjs.map +1 -1
  60. package/dist/oauth2/types.cjs +20 -0
  61. package/dist/oauth2/types.cjs.map +1 -1
  62. package/dist/oauth2/types.d.cts +73 -12
  63. package/dist/oauth2/types.d.ts +73 -12
  64. package/dist/oauth2/types.mjs +13 -0
  65. package/dist/oauth2/types.mjs.map +1 -1
  66. package/dist/session/redis-session.cjs +21 -0
  67. package/dist/session/redis-session.cjs.map +1 -1
  68. package/dist/session/redis-session.d.cts +4 -0
  69. package/dist/session/redis-session.d.ts +4 -0
  70. package/dist/session/redis-session.mjs +21 -0
  71. package/dist/session/redis-session.mjs.map +1 -1
  72. package/dist/session/types.cjs.map +1 -1
  73. package/dist/session/types.d.cts +3 -0
  74. package/dist/session/types.d.ts +3 -0
  75. package/package.json +1 -1
  76. package/dist/oidc/index.cjs +0 -39
  77. package/dist/oidc/index.cjs.map +0 -1
  78. package/dist/oidc/index.d.cts +0 -10
  79. package/dist/oidc/index.d.ts +0 -10
  80. package/dist/oidc/index.mjs +0 -14
  81. package/dist/oidc/index.mjs.map +0 -1
@@ -46,8 +46,10 @@ var Auth = class {
46
46
  PATH_CSRF = "/csrf";
47
47
  PATH_LOGOUT = "/logout";
48
48
  PATH_LOGGED = "/logged";
49
+ PATH_OAUTH2_STATE = "/oauth2/state/:registrationId";
49
50
  PATH_OAUTH2_AUTHORIZATION = "/oauth2/authorization/:registrationId";
50
- PATH_OAUTH2_REDIRECT = "/login/oauth2/code/:registrationId";
51
+ PATH_LOGIN_OAUTH2_CODE = "/login/oauth2/code/:registrationId";
52
+ PATH_LOGIN_OAUTH2_NATIVE = "/login/oauth2/native/:registrationId";
51
53
  ATTR_OAUTH2_AUTHORIZATION_REQUEST = "oauth2AuthorizationRequest";
52
54
  constructor({ repository, oauth2, cookie }) {
53
55
  this.repository = repository;
@@ -83,19 +85,42 @@ var Auth = class {
83
85
  (0, import_http.setCookie)(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
84
86
  return response;
85
87
  };
88
+ createPkceParameters() {
89
+ const code_verifier = (0, import_crypto.randomUUID)();
90
+ const code_challenge = (0, import_crypto.createHash)("sha256").update(code_verifier).digest().toString("base64url");
91
+ return { code_verifier, code_challenge, code_challenge_method: "S256" };
92
+ }
93
+ getOauth2StateKey(state) {
94
+ return `oauth2:state:${state}`;
95
+ }
96
+ oauth2State = async (request) => {
97
+ const { registrationId } = (0, import_http.param)(request, this.PATH_OAUTH2_STATE);
98
+ (0, import_tiny_invariant.default)(this.oauth2Client, "oauth2Client is not initialized");
99
+ const state = (0, import_crypto.randomUUID)();
100
+ const nonce = (0, import_crypto.randomUUID)();
101
+ const pkce = this.createPkceParameters();
102
+ const value = { state, nonce, registrationId, ...pkce };
103
+ await this.repository.setItem(this.getOauth2StateKey(state), value, 10 * 60);
104
+ return Response.json({
105
+ state,
106
+ nonce,
107
+ code_challenge: pkce.code_challenge,
108
+ code_challenge_method: pkce.code_challenge_method
109
+ });
110
+ };
86
111
  oauth2Authorization = async (request) => {
87
112
  (0, import_tiny_invariant.default)(this.oauth2Client, "oauth2Client is not initialized");
88
113
  const { registrationId } = (0, import_http.param)(request, this.PATH_OAUTH2_AUTHORIZATION);
89
114
  const state = (0, import_crypto.randomUUID)();
90
- const codeVerifier = (0, import_crypto.randomUUID)();
91
- const uri = this.oauth2Client.createAuthorizationUri(registrationId, state, codeVerifier);
115
+ const pkce = this.createPkceParameters();
116
+ const uri = this.oauth2Client.createAuthorizationUri({ registrationId, state, pkce });
92
117
  const sessionId = (0, import_http.getCookie)(request, this.cookieName);
93
118
  const session = sessionId ? await this.repository.findById(sessionId) ?? this.repository.createSession() : this.repository.createSession();
94
119
  const authorizationRequest = {
95
120
  state,
96
- codeVerifier,
97
121
  registrationId,
98
- authorizationRequestUri: uri.href
122
+ authorizationRequestUri: uri.href,
123
+ additionalParameters: pkce
99
124
  };
100
125
  const value = JSON.stringify(authorizationRequest);
101
126
  session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);
@@ -105,23 +130,26 @@ var Auth = class {
105
130
  (0, import_http.setCookie)(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
106
131
  return response;
107
132
  };
108
- redirect = (error) => {
133
+ redirect = (error, description) => {
109
134
  (0, import_tiny_invariant.default)(this.oauth2Client, "oauth2Client is not initialized");
110
- return Response.redirect(`${this.oauth2Client.errorUri}?error=${error}`, 302);
135
+ const uri = new URL(this.oauth2Client.errorUri);
136
+ uri.searchParams.set("error", error);
137
+ if (description) uri.searchParams.set("error_description", description);
138
+ return Response.redirect(uri.href, 302);
111
139
  };
112
- loginOAuth2 = async (request, onAuthorized) => {
140
+ loginOAuth2Code = async (request, onAuthorized) => {
113
141
  (0, import_tiny_invariant.default)(this.oauth2Client, "oauth2Client is not initialized");
114
142
  const sessionId = (0, import_http.getCookie)(request, this.cookieName);
115
143
  const session = sessionId ? await this.repository.findById(sessionId) : null;
116
144
  if (!session) {
117
- return this.redirect("OAUTH2_AUTHORIZATION_SESSION_NOT_FOUND");
145
+ return this.redirect("invalid_request", "session not found");
118
146
  }
119
147
  const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
120
148
  if (!json) {
121
- return this.redirect("OAUTH2_AUTHORIZATION_REQUEST_NOT_FOUND");
149
+ return this.redirect("invalid_request", "authorization request not found");
122
150
  }
123
151
  const cached = JSON.parse(json);
124
- const { registrationId } = (0, import_http.param)(request, this.PATH_OAUTH2_REDIRECT);
152
+ const { registrationId } = (0, import_http.param)(request, this.PATH_LOGIN_OAUTH2_CODE);
125
153
  let data;
126
154
  if (request.method === "POST" && request.headers.get("content-type")?.toLowerCase() === "application/x-www-form-urlencoded") {
127
155
  const params = new URLSearchParams(await request.text());
@@ -131,26 +159,24 @@ var Auth = class {
131
159
  }
132
160
  const parsed = import_client.oauth2RedirectQuerySchema.safeParse(data);
133
161
  if (!parsed.success) {
134
- return this.redirect("INVALID_OAUTH2_REDIRECT_QUERY");
162
+ return this.redirect("invalid_request", "invalid redirect query");
135
163
  }
136
164
  if (!parsed.data.code || !parsed.data.state) {
137
- console.error("oauth2 redirect error:", parsed.data);
138
- return this.redirect("OAUTH2_AUTHORIZATION_ERROR");
165
+ return this.redirect("invalid_request", "invalid redirect query");
139
166
  }
140
167
  if (parsed.data.state !== cached.state) {
141
- return this.redirect("INVALID_OAUTH2_REDIRECT_STATE");
168
+ return this.redirect("invalid_request", "redirect state mismatch");
142
169
  }
143
170
  if (registrationId !== cached.registrationId) {
144
- return this.redirect("INVALID_OAUTH2_REDIRECT_REGISTRATION_ID");
171
+ return this.redirect("invalid_request", "redirect registration mismatch");
145
172
  }
146
173
  const { code } = parsed.data;
147
- const { codeVerifier } = cached;
148
- const token = await this.oauth2Client.exchangeAuthorizationCode(
174
+ const token = await this.oauth2Client.exchangeAuthorizationCode({
149
175
  registrationId,
150
176
  code,
151
- codeVerifier
152
- );
153
- const userInfo = await this.oauth2Client.getUserInfo(registrationId, token);
177
+ pkce: cached.additionalParameters
178
+ });
179
+ const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });
154
180
  const principal = await onAuthorized(request, registrationId, userInfo, token);
155
181
  session.setLastAccessedTime(Date.now());
156
182
  session.removeAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
@@ -158,7 +184,43 @@ var Auth = class {
158
184
  await this.repository.save(session);
159
185
  const response = new Response(null, {
160
186
  status: 302,
161
- headers: { location: this.oauth2Client.baseUri }
187
+ headers: { location: this.oauth2Client.successUri }
188
+ });
189
+ const maxAge = session.getMaxInactiveInterval();
190
+ (0, import_http.setCookie)(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
191
+ return response;
192
+ };
193
+ loginOAuth2Native = async (request, onAuthorized) => {
194
+ (0, import_tiny_invariant.default)(this.oauth2Client, "oauth2Client is not initialized");
195
+ const { registrationId } = (0, import_http.param)(request, this.PATH_LOGIN_OAUTH2_NATIVE);
196
+ const credentials = await request.json();
197
+ const key = this.getOauth2StateKey(credentials.state);
198
+ const cached = await this.repository.getItem(key);
199
+ if (!cached) {
200
+ const json = { error: "invalid_request", error_description: "oauth2 state not found" };
201
+ return Response.json(json, { status: 400 });
202
+ }
203
+ if (cached.registrationId !== registrationId) {
204
+ const json = { error: "invalid_request", error_description: "registration mismatch" };
205
+ return Response.json(json, { status: 400 });
206
+ }
207
+ const pkce = {
208
+ code_verifier: cached.code_verifier,
209
+ code_challenge: cached.code_challenge,
210
+ code_challenge_method: cached.code_challenge_method
211
+ };
212
+ const { userInfo, token } = await this.oauth2Client.loginOAuth2Native({
213
+ registrationId,
214
+ credentials,
215
+ pkce
216
+ });
217
+ const principal = await onAuthorized(request, registrationId, userInfo, token);
218
+ const session = this.repository.createSession();
219
+ session.setAttribute(import_session.PRINCIPAL_NAME_INDEX_NAME, principal.name);
220
+ await this.repository.save(session);
221
+ const response = new Response(JSON.stringify(principal), {
222
+ status: 200,
223
+ headers: { "Content-Type": "application/json" }
162
224
  });
163
225
  const maxAge = session.getMaxInactiveInterval();
164
226
  (0, import_http.setCookie)(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
@@ -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';\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_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 }: 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 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 { 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\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\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: 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\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\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\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.successUri },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\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 { 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 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 const principal = await onAuthorized(request, registrationId, userInfo, token);\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 return response;\n };\n}\n"],"mappings":"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","names":["invariant"]}
@@ -13,15 +13,21 @@ declare class Auth implements AuthService {
13
13
  readonly PATH_CSRF: "/csrf";
14
14
  readonly PATH_LOGOUT: "/logout";
15
15
  readonly PATH_LOGGED: "/logged";
16
+ readonly PATH_OAUTH2_STATE: "/oauth2/state/:registrationId";
16
17
  readonly PATH_OAUTH2_AUTHORIZATION: "/oauth2/authorization/:registrationId";
17
- readonly PATH_OAUTH2_REDIRECT: "/login/oauth2/code/:registrationId";
18
+ readonly PATH_LOGIN_OAUTH2_CODE: "/login/oauth2/code/:registrationId";
19
+ readonly PATH_LOGIN_OAUTH2_NATIVE: "/login/oauth2/native/:registrationId";
18
20
  readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = "oauth2AuthorizationRequest";
19
21
  constructor({ repository, oauth2, cookie }: AuthConfig);
20
22
  logout: (request: Request) => Promise<Response>;
21
23
  logged: (request: Request, onLogged?: LoggedHandler) => Promise<Response>;
24
+ private createPkceParameters;
25
+ private getOauth2StateKey;
26
+ oauth2State: (request: Request) => Promise<Response>;
22
27
  oauth2Authorization: (request: Request) => Promise<Response>;
23
28
  private redirect;
24
- loginOAuth2: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
29
+ loginOAuth2Code: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
30
+ loginOAuth2Native: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
25
31
  }
26
32
 
27
33
  export { Auth };
@@ -13,15 +13,21 @@ declare class Auth implements AuthService {
13
13
  readonly PATH_CSRF: "/csrf";
14
14
  readonly PATH_LOGOUT: "/logout";
15
15
  readonly PATH_LOGGED: "/logged";
16
+ readonly PATH_OAUTH2_STATE: "/oauth2/state/:registrationId";
16
17
  readonly PATH_OAUTH2_AUTHORIZATION: "/oauth2/authorization/:registrationId";
17
- readonly PATH_OAUTH2_REDIRECT: "/login/oauth2/code/:registrationId";
18
+ readonly PATH_LOGIN_OAUTH2_CODE: "/login/oauth2/code/:registrationId";
19
+ readonly PATH_LOGIN_OAUTH2_NATIVE: "/login/oauth2/native/:registrationId";
18
20
  readonly ATTR_OAUTH2_AUTHORIZATION_REQUEST = "oauth2AuthorizationRequest";
19
21
  constructor({ repository, oauth2, cookie }: AuthConfig);
20
22
  logout: (request: Request) => Promise<Response>;
21
23
  logged: (request: Request, onLogged?: LoggedHandler) => Promise<Response>;
24
+ private createPkceParameters;
25
+ private getOauth2StateKey;
26
+ oauth2State: (request: Request) => Promise<Response>;
22
27
  oauth2Authorization: (request: Request) => Promise<Response>;
23
28
  private redirect;
24
- loginOAuth2: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
29
+ loginOAuth2Code: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
30
+ loginOAuth2Native: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
25
31
  }
26
32
 
27
33
  export { Auth };
@@ -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 {
@@ -18,8 +18,10 @@ var Auth = class {
18
18
  PATH_CSRF = "/csrf";
19
19
  PATH_LOGOUT = "/logout";
20
20
  PATH_LOGGED = "/logged";
21
+ PATH_OAUTH2_STATE = "/oauth2/state/:registrationId";
21
22
  PATH_OAUTH2_AUTHORIZATION = "/oauth2/authorization/:registrationId";
22
- PATH_OAUTH2_REDIRECT = "/login/oauth2/code/:registrationId";
23
+ PATH_LOGIN_OAUTH2_CODE = "/login/oauth2/code/:registrationId";
24
+ PATH_LOGIN_OAUTH2_NATIVE = "/login/oauth2/native/:registrationId";
23
25
  ATTR_OAUTH2_AUTHORIZATION_REQUEST = "oauth2AuthorizationRequest";
24
26
  constructor({ repository, oauth2, cookie }) {
25
27
  this.repository = repository;
@@ -55,19 +57,42 @@ var Auth = class {
55
57
  setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
56
58
  return response;
57
59
  };
60
+ createPkceParameters() {
61
+ const code_verifier = randomUUID();
62
+ const code_challenge = createHash("sha256").update(code_verifier).digest().toString("base64url");
63
+ return { code_verifier, code_challenge, code_challenge_method: "S256" };
64
+ }
65
+ getOauth2StateKey(state) {
66
+ return `oauth2:state:${state}`;
67
+ }
68
+ oauth2State = async (request) => {
69
+ const { registrationId } = param(request, this.PATH_OAUTH2_STATE);
70
+ invariant(this.oauth2Client, "oauth2Client is not initialized");
71
+ const state = randomUUID();
72
+ const nonce = randomUUID();
73
+ const pkce = this.createPkceParameters();
74
+ const value = { state, nonce, registrationId, ...pkce };
75
+ await this.repository.setItem(this.getOauth2StateKey(state), value, 10 * 60);
76
+ return Response.json({
77
+ state,
78
+ nonce,
79
+ code_challenge: pkce.code_challenge,
80
+ code_challenge_method: pkce.code_challenge_method
81
+ });
82
+ };
58
83
  oauth2Authorization = async (request) => {
59
84
  invariant(this.oauth2Client, "oauth2Client is not initialized");
60
85
  const { registrationId } = param(request, this.PATH_OAUTH2_AUTHORIZATION);
61
86
  const state = randomUUID();
62
- const codeVerifier = randomUUID();
63
- const uri = this.oauth2Client.createAuthorizationUri(registrationId, state, codeVerifier);
87
+ const pkce = this.createPkceParameters();
88
+ const uri = this.oauth2Client.createAuthorizationUri({ registrationId, state, pkce });
64
89
  const sessionId = getCookie(request, this.cookieName);
65
90
  const session = sessionId ? await this.repository.findById(sessionId) ?? this.repository.createSession() : this.repository.createSession();
66
91
  const authorizationRequest = {
67
92
  state,
68
- codeVerifier,
69
93
  registrationId,
70
- authorizationRequestUri: uri.href
94
+ authorizationRequestUri: uri.href,
95
+ additionalParameters: pkce
71
96
  };
72
97
  const value = JSON.stringify(authorizationRequest);
73
98
  session.setAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST, value);
@@ -77,23 +102,26 @@ var Auth = class {
77
102
  setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
78
103
  return response;
79
104
  };
80
- redirect = (error) => {
105
+ redirect = (error, description) => {
81
106
  invariant(this.oauth2Client, "oauth2Client is not initialized");
82
- return Response.redirect(`${this.oauth2Client.errorUri}?error=${error}`, 302);
107
+ const uri = new URL(this.oauth2Client.errorUri);
108
+ uri.searchParams.set("error", error);
109
+ if (description) uri.searchParams.set("error_description", description);
110
+ return Response.redirect(uri.href, 302);
83
111
  };
84
- loginOAuth2 = async (request, onAuthorized) => {
112
+ loginOAuth2Code = async (request, onAuthorized) => {
85
113
  invariant(this.oauth2Client, "oauth2Client is not initialized");
86
114
  const sessionId = getCookie(request, this.cookieName);
87
115
  const session = sessionId ? await this.repository.findById(sessionId) : null;
88
116
  if (!session) {
89
- return this.redirect("OAUTH2_AUTHORIZATION_SESSION_NOT_FOUND");
117
+ return this.redirect("invalid_request", "session not found");
90
118
  }
91
119
  const json = session.getAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
92
120
  if (!json) {
93
- return this.redirect("OAUTH2_AUTHORIZATION_REQUEST_NOT_FOUND");
121
+ return this.redirect("invalid_request", "authorization request not found");
94
122
  }
95
123
  const cached = JSON.parse(json);
96
- const { registrationId } = param(request, this.PATH_OAUTH2_REDIRECT);
124
+ const { registrationId } = param(request, this.PATH_LOGIN_OAUTH2_CODE);
97
125
  let data;
98
126
  if (request.method === "POST" && request.headers.get("content-type")?.toLowerCase() === "application/x-www-form-urlencoded") {
99
127
  const params = new URLSearchParams(await request.text());
@@ -103,26 +131,24 @@ var Auth = class {
103
131
  }
104
132
  const parsed = oauth2RedirectQuerySchema.safeParse(data);
105
133
  if (!parsed.success) {
106
- return this.redirect("INVALID_OAUTH2_REDIRECT_QUERY");
134
+ return this.redirect("invalid_request", "invalid redirect query");
107
135
  }
108
136
  if (!parsed.data.code || !parsed.data.state) {
109
- console.error("oauth2 redirect error:", parsed.data);
110
- return this.redirect("OAUTH2_AUTHORIZATION_ERROR");
137
+ return this.redirect("invalid_request", "invalid redirect query");
111
138
  }
112
139
  if (parsed.data.state !== cached.state) {
113
- return this.redirect("INVALID_OAUTH2_REDIRECT_STATE");
140
+ return this.redirect("invalid_request", "redirect state mismatch");
114
141
  }
115
142
  if (registrationId !== cached.registrationId) {
116
- return this.redirect("INVALID_OAUTH2_REDIRECT_REGISTRATION_ID");
143
+ return this.redirect("invalid_request", "redirect registration mismatch");
117
144
  }
118
145
  const { code } = parsed.data;
119
- const { codeVerifier } = cached;
120
- const token = await this.oauth2Client.exchangeAuthorizationCode(
146
+ const token = await this.oauth2Client.exchangeAuthorizationCode({
121
147
  registrationId,
122
148
  code,
123
- codeVerifier
124
- );
125
- const userInfo = await this.oauth2Client.getUserInfo(registrationId, token);
149
+ pkce: cached.additionalParameters
150
+ });
151
+ const userInfo = await this.oauth2Client.getUserInfo({ registrationId, token });
126
152
  const principal = await onAuthorized(request, registrationId, userInfo, token);
127
153
  session.setLastAccessedTime(Date.now());
128
154
  session.removeAttribute(this.ATTR_OAUTH2_AUTHORIZATION_REQUEST);
@@ -130,7 +156,43 @@ var Auth = class {
130
156
  await this.repository.save(session);
131
157
  const response = new Response(null, {
132
158
  status: 302,
133
- headers: { location: this.oauth2Client.baseUri }
159
+ headers: { location: this.oauth2Client.successUri }
160
+ });
161
+ const maxAge = session.getMaxInactiveInterval();
162
+ setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
163
+ return response;
164
+ };
165
+ loginOAuth2Native = async (request, onAuthorized) => {
166
+ invariant(this.oauth2Client, "oauth2Client is not initialized");
167
+ const { registrationId } = param(request, this.PATH_LOGIN_OAUTH2_NATIVE);
168
+ const credentials = await request.json();
169
+ const key = this.getOauth2StateKey(credentials.state);
170
+ const cached = await this.repository.getItem(key);
171
+ if (!cached) {
172
+ const json = { error: "invalid_request", error_description: "oauth2 state not found" };
173
+ return Response.json(json, { status: 400 });
174
+ }
175
+ if (cached.registrationId !== registrationId) {
176
+ const json = { error: "invalid_request", error_description: "registration mismatch" };
177
+ return Response.json(json, { status: 400 });
178
+ }
179
+ const pkce = {
180
+ code_verifier: cached.code_verifier,
181
+ code_challenge: cached.code_challenge,
182
+ code_challenge_method: cached.code_challenge_method
183
+ };
184
+ const { userInfo, token } = await this.oauth2Client.loginOAuth2Native({
185
+ registrationId,
186
+ credentials,
187
+ pkce
188
+ });
189
+ const principal = await onAuthorized(request, registrationId, userInfo, token);
190
+ const session = this.repository.createSession();
191
+ session.setAttribute(PRINCIPAL_NAME_INDEX_NAME, principal.name);
192
+ await this.repository.save(session);
193
+ const response = new Response(JSON.stringify(principal), {
194
+ status: 200,
195
+ headers: { "Content-Type": "application/json" }
134
196
  });
135
197
  const maxAge = session.getMaxInactiveInterval();
136
198
  setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });
@@ -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';\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_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 }: 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 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 { 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\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\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: 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\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\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\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.successUri },\n });\n const maxAge = session.getMaxInactiveInterval();\n setCookie(response, this.cookieName, session.getId(), { ...this.cookieOptions, maxAge });\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 { 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 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 const principal = await onAuthorized(request, registrationId, userInfo, token);\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 return response;\n };\n}\n"],"mappings":"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","names":[]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/auth/types.ts"],"sourcesContent":["import { Principal } from '../core';\nimport { OAuth2ClientConfig, UserInfo } from '../oauth2/types';\nimport { OAuth2Token } from '../oauth2/types';\nimport { SessionRepository } from '../session';\nimport { 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 AuthConfig {\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 oauth2Authorization: (request: Request) => Promise<Response>;\n loginOAuth2: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAAA;AAAA;","names":[]}
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 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":[]}
@@ -1,11 +1,16 @@
1
1
  import { Principal } from '../core/index.cjs';
2
- import { UserInfo, OAuth2Token, OAuth2ClientConfig } from '../oauth2/types.cjs';
2
+ import { UserInfo, OAuth2Token, OAuth2ClientConfig, PkceParameters } from '../oauth2/types.cjs';
3
3
  import { SessionRepository } from '../session/types.cjs';
4
4
  import { CookieOptions } from '../utils/http.cjs';
5
5
  import 'cookie';
6
6
 
7
7
  type LoggedHandler = (principal: Principal) => Promise<void>;
8
8
  type OAuth2AuthorizedHandler = (request: Request, registrationId: string, userInfo: UserInfo, token: OAuth2Token) => Promise<Principal>;
9
+ interface OAuth2State extends PkceParameters {
10
+ state: string;
11
+ nonce: string;
12
+ registrationId: string;
13
+ }
9
14
  interface AuthConfig {
10
15
  repository: SessionRepository;
11
16
  cookie?: CookieOptions & {
@@ -18,8 +23,10 @@ interface AuthConfig {
18
23
  interface AuthService {
19
24
  logout: (request: Request) => Promise<Response>;
20
25
  logged: (request: Request, onLogged?: LoggedHandler) => Promise<Response>;
26
+ oauth2State: (request: Request) => Promise<Response>;
21
27
  oauth2Authorization: (request: Request) => Promise<Response>;
22
- loginOAuth2: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
28
+ loginOAuth2Code: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
29
+ loginOAuth2Native: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
23
30
  }
24
31
 
25
- export type { AuthConfig, AuthService, LoggedHandler, OAuth2AuthorizedHandler };
32
+ export type { AuthConfig, AuthService, LoggedHandler, OAuth2AuthorizedHandler, OAuth2State };
@@ -1,11 +1,16 @@
1
1
  import { Principal } from '../core/index.js';
2
- import { UserInfo, OAuth2Token, OAuth2ClientConfig } from '../oauth2/types.js';
2
+ import { UserInfo, OAuth2Token, OAuth2ClientConfig, PkceParameters } from '../oauth2/types.js';
3
3
  import { SessionRepository } from '../session/types.js';
4
4
  import { CookieOptions } from '../utils/http.js';
5
5
  import 'cookie';
6
6
 
7
7
  type LoggedHandler = (principal: Principal) => Promise<void>;
8
8
  type OAuth2AuthorizedHandler = (request: Request, registrationId: string, userInfo: UserInfo, token: OAuth2Token) => Promise<Principal>;
9
+ interface OAuth2State extends PkceParameters {
10
+ state: string;
11
+ nonce: string;
12
+ registrationId: string;
13
+ }
9
14
  interface AuthConfig {
10
15
  repository: SessionRepository;
11
16
  cookie?: CookieOptions & {
@@ -18,8 +23,10 @@ interface AuthConfig {
18
23
  interface AuthService {
19
24
  logout: (request: Request) => Promise<Response>;
20
25
  logged: (request: Request, onLogged?: LoggedHandler) => Promise<Response>;
26
+ oauth2State: (request: Request) => Promise<Response>;
21
27
  oauth2Authorization: (request: Request) => Promise<Response>;
22
- loginOAuth2: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
28
+ loginOAuth2Code: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
29
+ loginOAuth2Native: (request: Request, onAuthorized: OAuth2AuthorizedHandler) => Promise<Response>;
23
30
  }
24
31
 
25
- export type { AuthConfig, AuthService, LoggedHandler, OAuth2AuthorizedHandler };
32
+ export type { AuthConfig, AuthService, LoggedHandler, OAuth2AuthorizedHandler, OAuth2State };
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { Auth } from './auth/index';\nexport { OAuth2Client, oauth2RedirectQuerySchema } from './oauth2/client';\nexport { RedisIndexedSessionRepository, PRINCIPAL_NAME_INDEX_NAME } from './session/redis-session';\n\nexport type { Namespace, SessionRepository } from './session/types';\nexport type { StandardClaims, OAuth2Token, UserInfo } from './oauth2/types';\nexport type { Principal } from './core/index';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAqB;AACrB,oBAAwD;AACxD,2BAAyE;","names":[]}
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export { Auth } from './auth/index';\nexport { OAuth2Client, oauth2RedirectQuerySchema } from './oauth2/client';\nexport { RedisIndexedSessionRepository, PRINCIPAL_NAME_INDEX_NAME } from './session/redis-session';\n\nexport type { Namespace, SessionRepository } from './session/types';\nexport type { StandardClaims, OAuth2Token, UserInfo, OidcToken, OidcScopes } from './oauth2/types';\nexport type { Principal } from './core/index';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,kBAAqB;AACrB,oBAAwD;AACxD,2BAAyE;","names":[]}
package/dist/index.d.cts CHANGED
@@ -2,7 +2,7 @@ export { Auth } from './auth/index.cjs';
2
2
  export { OAuth2Client, oauth2RedirectQuerySchema } from './oauth2/client.cjs';
3
3
  export { PRINCIPAL_NAME_INDEX_NAME, RedisIndexedSessionRepository } from './session/redis-session.cjs';
4
4
  export { Namespace, SessionRepository } from './session/types.cjs';
5
- export { OAuth2Token, StandardClaims, UserInfo } from './oauth2/types.cjs';
5
+ export { OAuth2Token, OidcScopes, OidcToken, StandardClaims, UserInfo } from './oauth2/types.cjs';
6
6
  export { Principal } from './core/index.cjs';
7
7
  import './auth/types.cjs';
8
8
  import './utils/http.cjs';