@revojs/auth 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type Route } from "@revojs/server";
2
2
  import { type Scope } from "revojs";
3
- import type { Awaitable } from "revojs/shared";
3
+ import { type Awaitable } from "revojs/shared";
4
4
  import { type InferInput } from "valibot";
5
5
  export type Principal = InferInput<typeof principalSchema>;
6
6
  export type PasswordMethod = InferInput<typeof passwordMethodSchema>;
@@ -39,6 +39,7 @@ export interface ProviderUserInfo {
39
39
  export interface AuthProviderOptions {
40
40
  issuer: string;
41
41
  scope: string;
42
+ pkce: boolean;
42
43
  clientKey: string;
43
44
  clientSecret: string;
44
45
  authorize: ProviderEndpoint;
@@ -60,16 +61,18 @@ export declare function getSession<T>(scope: Scope): Promise<T | undefined>;
60
61
  export declare function setSession<T>(scope: Scope, session: T): Promise<void>;
61
62
  export declare function clearSession(scope: Scope): void;
62
63
  export declare function defineAuthProvider(provider: AuthProvider): Route;
64
+ export declare const AUTHORIZE_HOOK: import("revojs").Descriptor<(method: Method) => Awaitable<boolean>>;
63
65
  export declare const GET_PRINCIPAL_HOOK: import("revojs").Descriptor<(method: Method) => Awaitable<Principal | undefined>>;
64
66
  export declare const CREATE_PRINCIPAL_HOOK: import("revojs").Descriptor<(method: Method) => Awaitable<Principal | undefined>>;
65
67
  export declare const CREATE_SESSION_HOOK: import("revojs").Descriptor<(principal: Principal) => Awaitable<unknown>>;
66
68
  export declare const DELETE_SESSION_HOOK: import("revojs").Descriptor<(session: any) => Awaitable<unknown>>;
67
- export declare const registerAuthHooks: (scope: Scope, hooks: import("revojs").Hooks<{
69
+ export declare const registerAuthHooks: (scope: Scope, hooks: Partial<import("revojs").Hooks<{
70
+ authorize: import("revojs").Descriptor<(method: Method) => Awaitable<boolean>>;
68
71
  getPrincipal: import("revojs").Descriptor<(method: Method) => Awaitable<Principal | undefined>>;
69
72
  createPrincipal: import("revojs").Descriptor<(method: Method) => Awaitable<Principal | undefined>>;
70
73
  createSession: import("revojs").Descriptor<(principal: Principal) => Awaitable<unknown>>;
71
74
  deleteSession: import("revojs").Descriptor<(session: any) => Awaitable<unknown>>;
72
- }>) => void;
75
+ }>>) => void;
73
76
  export declare const principalSchema: import("valibot").ObjectSchema<{
74
77
  readonly sub: import("valibot").StringSchema<undefined>;
75
78
  readonly email: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
@@ -91,6 +94,7 @@ export declare const passkeyMethodSchema: import("valibot").ObjectSchema<{
91
94
  export declare const providerMethodSchema: import("valibot").ObjectSchema<{
92
95
  readonly type: import("valibot").LiteralSchema<"PROVIDER", undefined>;
93
96
  readonly issuer: import("valibot").StringSchema<undefined>;
97
+ readonly accessToken: import("valibot").StringSchema<undefined>;
94
98
  readonly principal: import("valibot").ObjectSchema<{
95
99
  readonly sub: import("valibot").StringSchema<undefined>;
96
100
  readonly email: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
@@ -111,6 +115,7 @@ export declare const methodSchema: import("valibot").UnionSchema<[import("valibo
111
115
  }, undefined>, import("valibot").ObjectSchema<{
112
116
  readonly type: import("valibot").LiteralSchema<"PROVIDER", undefined>;
113
117
  readonly issuer: import("valibot").StringSchema<undefined>;
118
+ readonly accessToken: import("valibot").StringSchema<undefined>;
114
119
  readonly principal: import("valibot").ObjectSchema<{
115
120
  readonly sub: import("valibot").StringSchema<undefined>;
116
121
  readonly email: import("valibot").OptionalSchema<import("valibot").StringSchema<undefined>, undefined>;
package/dist/index.js CHANGED
@@ -1,7 +1,8 @@
1
- import options from "#config/auth";
1
+ import config from "#config/auth";
2
2
  import { isClient, isServer } from "#variables";
3
- import { deleteCookie, fetch, sendRedirect, setCookie, useCookies, useHeaders, useQuery, useUrl, withQuery } from "@revojs/server";
4
- import { createRegisterHooks, defineHook, dispatchHookSingle, useVariables } from "revojs";
3
+ import { deleteCookie, fetch, sendForbidden, sendRedirect, setCookie, useCookies, useHeaders, useQuery, useUrl, withQuery } from "@revojs/server";
4
+ import { createRegisterHooks, defineHook, dispatchHook, dispatchHookSingle, useVariables } from "revojs";
5
+ import { mergeObjects } from "revojs/shared";
5
6
  import { boolean, literal, nullish, number, object, optional, parse, pipe, string, transform, union } from "valibot";
6
7
  //#region src/index.ts
7
8
  function useAuth(scope) {
@@ -48,7 +49,7 @@ async function getCryptoKey(scope) {
48
49
  }, false, ["sign", "verify"]);
49
50
  }
50
51
  async function getSession(scope) {
51
- const session = useCookies(scope)[options.session.name];
52
+ const session = useCookies(scope)[config.session.name];
52
53
  if (session) {
53
54
  const index = session.lastIndexOf(".");
54
55
  if (index > 0) try {
@@ -60,67 +61,107 @@ async function getSession(scope) {
60
61
  async function setSession(scope, session) {
61
62
  const value = encoder.encode(JSON.stringify(session));
62
63
  const cookie = value.toBase64(BASE64_SIGN) + "." + new Uint8Array(await crypto.subtle.sign("HMAC", await getCryptoKey(scope), value)).toBase64(BASE64_SIGN);
63
- setCookie(scope, options.session.name, cookie, options.session.cookie);
64
+ setCookie(scope, config.session.name, cookie, config.session.cookie);
64
65
  }
65
66
  function clearSession(scope) {
66
- deleteCookie(scope, options.session.name, options.session.cookie);
67
+ deleteCookie(scope, config.session.name, config.session.cookie);
67
68
  }
68
69
  function defineAuthProvider(provider) {
69
70
  return { async fetch(scope) {
70
- const options = provider.setup(scope);
71
- const { origin } = useUrl(scope);
72
- const { code, state, redirect } = useQuery(scope);
73
- const target = state ?? redirect ?? "/";
71
+ const defaults = provider.setup(scope);
72
+ const options = mergeObjects(config.providers?.[defaults.issuer] ?? {}, defaults);
73
+ const { origin, protocol } = useUrl(scope);
74
+ const { code, state, redirect, error } = useQuery(scope);
75
+ const secure = protocol === "https:";
76
+ const cookieName = `${secure ? "__Host-revojs-oauth" : "revojs-oauth"}-${options.issuer}`;
74
77
  const redirectUri = `${origin}/auth/provider/${options.issuer}`;
75
- const successPath = target.startsWith("/") && !target.startsWith("//") ? target : "/";
76
- if (code) {
78
+ if (code || state || error) {
79
+ const cookie = useCookies(scope)[cookieName];
80
+ deleteCookie(scope, cookieName, {
81
+ path: "/",
82
+ secure
83
+ });
84
+ let flow;
85
+ try {
86
+ flow = parse(oauthFlowSchema, JSON.parse(cookie));
87
+ } catch {
88
+ return sendForbidden(scope);
89
+ }
90
+ if (error || !code || !state || state !== flow.state || !flow.redirect.startsWith("/") || !URL.canParse(flow.redirect, origin) || new URL(flow.redirect, origin).origin !== origin || options.pkce && !flow.verifier) return sendForbidden(scope);
91
+ const body = new URLSearchParams({
92
+ grant_type: "authorization_code",
93
+ client_id: options.clientKey,
94
+ client_secret: options.clientSecret,
95
+ redirect_uri: redirectUri,
96
+ code,
97
+ ...options.token.parameters
98
+ });
99
+ if (options.pkce) body.set("code_verifier", flow.verifier);
77
100
  const token = await fetch(scope, options.token.url, {
78
101
  method: "POST",
79
102
  headers: {
80
103
  "Content-Type": "application/x-www-form-urlencoded",
81
104
  ...options.token.headers
82
105
  },
83
- body: new URLSearchParams({
84
- grant_type: "authorization_code",
85
- client_id: options.clientKey,
86
- client_secret: options.clientSecret,
87
- redirect_uri: redirectUri,
88
- code,
89
- ...options.token.parameters
90
- })
106
+ body
91
107
  });
92
108
  const method = {
93
109
  type: "PROVIDER",
94
110
  issuer: options.issuer,
111
+ accessToken: token.access_token,
95
112
  principal: (options.toPrincipal ?? ((user) => parse(userInfoSchema, user)))(await fetch(scope, options.userInfo.url, { headers: {
96
113
  Authorization: `Bearer ${token.access_token}`,
97
114
  ...options.userInfo.headers
98
115
  } }))
99
116
  };
117
+ if ((await dispatchHook(scope, AUTHORIZE_HOOK, method)).includes(false)) return sendForbidden(scope);
100
118
  const principal = await dispatchHookSingle(scope, GET_PRINCIPAL_HOOK, method) ?? await dispatchHookSingle(scope, CREATE_PRINCIPAL_HOOK, method);
101
119
  if (principal) {
102
120
  const session = await dispatchHookSingle(scope, CREATE_SESSION_HOOK, principal);
103
121
  if (session) {
104
122
  await setSession(scope, session);
105
- return sendRedirect(scope, successPath);
123
+ return sendRedirect(scope, flow.redirect);
106
124
  }
107
125
  }
126
+ return sendForbidden(scope);
108
127
  }
109
- return sendRedirect(scope, withQuery(scope, options.authorize.url, {
128
+ const verifier = options.pkce ? crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(32)).toBase64(BASE64_SIGN) : "";
129
+ const redirectUrl = redirect && URL.canParse(redirect, origin) ? new URL(redirect, origin) : null;
130
+ const flow = {
131
+ state: crypto.randomUUID(),
132
+ redirect: redirect?.startsWith("/") && redirectUrl?.origin === origin ? redirect : "/",
133
+ verifier
134
+ };
135
+ const parameters = new URLSearchParams({
110
136
  response_type: "code",
111
137
  client_id: options.clientKey,
112
138
  redirect_uri: redirectUri,
113
139
  scope: options.scope,
114
- state: successPath,
115
- ...options.authorize.parameters
116
- }));
140
+ ...options.authorize.parameters,
141
+ state: flow.state
142
+ });
143
+ if (verifier) {
144
+ const digest = await crypto.subtle.digest("SHA-256", encoder.encode(verifier));
145
+ parameters.set("code_challenge", new Uint8Array(digest).toBase64(BASE64_SIGN));
146
+ parameters.set("code_challenge_method", "S256");
147
+ }
148
+ setCookie(scope, cookieName, JSON.stringify(flow), {
149
+ path: "/",
150
+ secure,
151
+ httpOnly: true,
152
+ sameSite: "Lax",
153
+ maxAge: 600
154
+ });
155
+ return sendRedirect(scope, withQuery(scope, options.authorize.url, parameters));
117
156
  } };
118
157
  }
158
+ var AUTHORIZE_HOOK = defineHook("AUTHORIZE_HOOK");
119
159
  var GET_PRINCIPAL_HOOK = defineHook("GET_PRINCIPAL_HOOK");
120
160
  var CREATE_PRINCIPAL_HOOK = defineHook("CREATE_PRINCIPAL_HOOK");
121
161
  var CREATE_SESSION_HOOK = defineHook("CREATE_SESSION_HOOK");
122
162
  var DELETE_SESSION_HOOK = defineHook("DELETE_SESSION_HOOK");
123
163
  var registerAuthHooks = createRegisterHooks({
164
+ authorize: AUTHORIZE_HOOK,
124
165
  getPrincipal: GET_PRINCIPAL_HOOK,
125
166
  createPrincipal: CREATE_PRINCIPAL_HOOK,
126
167
  createSession: CREATE_SESSION_HOOK,
@@ -145,6 +186,7 @@ var passkeyMethodSchema = object({ type: literal("PASSKEY") });
145
186
  var providerMethodSchema = object({
146
187
  type: literal("PROVIDER"),
147
188
  issuer: string(),
189
+ accessToken: string(),
148
190
  principal: principalSchema
149
191
  });
150
192
  var methodSchema = union([
@@ -155,6 +197,11 @@ var methodSchema = union([
155
197
  var nullableStringSchema = nullish(string());
156
198
  var nullableIdentifierSchema = nullish(union([string(), number()]));
157
199
  var nullableBooleanSchema = nullish(boolean());
200
+ var oauthFlowSchema = object({
201
+ state: string(),
202
+ redirect: string(),
203
+ verifier: string()
204
+ });
158
205
  var userInfoSchema = pipe(object({
159
206
  sub: nullableIdentifierSchema,
160
207
  id: nullableIdentifierSchema,
@@ -187,4 +234,4 @@ var BASE64_VERIFY = { alphabet: "base64url" };
187
234
  var encoder = new TextEncoder();
188
235
  var decoder = new TextDecoder();
189
236
  //#endregion
190
- export { CREATE_PRINCIPAL_HOOK, CREATE_SESSION_HOOK, DELETE_SESSION_HOOK, GET_PRINCIPAL_HOOK, clearSession, defineAuthProvider, getCryptoKey, getSession, methodSchema, passkeyMethodSchema, passwordMethodSchema, principalSchema, providerMethodSchema, registerAuthHooks, requireSession, setSession, useAuth };
237
+ export { AUTHORIZE_HOOK, CREATE_PRINCIPAL_HOOK, CREATE_SESSION_HOOK, DELETE_SESSION_HOOK, GET_PRINCIPAL_HOOK, clearSession, defineAuthProvider, getCryptoKey, getSession, methodSchema, passkeyMethodSchema, passwordMethodSchema, principalSchema, providerMethodSchema, registerAuthHooks, requireSession, setSession, useAuth };
@@ -1,10 +1,13 @@
1
1
  import type { CookieOptions } from "@revojs/server";
2
+ import type { AuthProviderOptions, Providers } from "@revojs/auth";
3
+ import type { Mergeable } from "revojs/shared";
2
4
  export interface SessionOptions {
3
5
  name: string;
4
6
  cookie: CookieOptions;
5
7
  }
6
8
  export interface AuthConfig {
7
9
  session: SessionOptions;
10
+ providers?: Partial<Record<Providers | (string & {}), Mergeable<Pick<AuthProviderOptions, "scope" | "pkce" | "authorize" | "token" | "userInfo">>>>;
8
11
  }
9
12
  declare const _default: import("revojs/module").Module<"auth", AuthConfig, []>;
10
13
  export default _default;
@@ -8,6 +8,7 @@ var get_default = defineAuthProvider({ setup(scope) {
8
8
  return {
9
9
  issuer: "apple",
10
10
  scope: "name email",
11
+ pkce: false,
11
12
  clientKey: appleClientKey,
12
13
  clientSecret: appleClientSecret,
13
14
  authorize: {
@@ -8,6 +8,7 @@ var get_default = defineAuthProvider({ setup(scope) {
8
8
  return {
9
9
  issuer: "github",
10
10
  scope: "read:user user:email",
11
+ pkce: true,
11
12
  clientKey: githubClientKey,
12
13
  clientSecret: githubClientSecret,
13
14
  authorize: { url: "https://github.com/login/oauth/authorize" },
@@ -8,6 +8,7 @@ var get_default = defineAuthProvider({ setup(scope) {
8
8
  return {
9
9
  issuer: "google",
10
10
  scope: "email profile",
11
+ pkce: true,
11
12
  clientKey: googleClientKey,
12
13
  clientSecret: googleClientSecret,
13
14
  authorize: { url: "https://accounts.google.com/o/oauth2/v2/auth" },
@@ -8,6 +8,7 @@ var get_default = defineAuthProvider({ setup(scope) {
8
8
  return {
9
9
  issuer: "microsoft",
10
10
  scope: "openid email profile",
11
+ pkce: true,
11
12
  clientKey: microsoftClientKey,
12
13
  clientSecret: microsoftClientSecret,
13
14
  authorize: { url: `https://login.microsoftonline.com/${microsoftTenant ?? "common"}/oauth2/v2.0/authorize` },
@@ -1,10 +1,11 @@
1
- import { CREATE_PRINCIPAL_HOOK, CREATE_SESSION_HOOK, GET_PRINCIPAL_HOOK, passkeyMethodSchema, passwordMethodSchema, setSession } from "../../../index.js";
2
- import { defineRoute, sendNotFound, sendOk, useBody } from "@revojs/server";
3
- import { dispatchHookSingle } from "revojs";
1
+ import { AUTHORIZE_HOOK, CREATE_PRINCIPAL_HOOK, CREATE_SESSION_HOOK, GET_PRINCIPAL_HOOK, passkeyMethodSchema, passwordMethodSchema, setSession } from "../../../index.js";
2
+ import { defineRoute, sendForbidden, sendNotFound, sendOk, useBody } from "@revojs/server";
3
+ import { dispatchHook, dispatchHookSingle } from "revojs";
4
4
  import { union } from "valibot";
5
5
  //#region src/routes/auth/sessions/post.ts
6
6
  var post_default = defineRoute({ async fetch(scope) {
7
7
  const method = await useBody(scope, union([passwordMethodSchema, passkeyMethodSchema]));
8
+ if ((await dispatchHook(scope, AUTHORIZE_HOOK, method)).includes(false)) return sendForbidden(scope);
8
9
  const principal = await dispatchHookSingle(scope, GET_PRINCIPAL_HOOK, method) ?? (method.type !== "PASSWORD" || method.register ? await dispatchHookSingle(scope, CREATE_PRINCIPAL_HOOK, method) : void 0);
9
10
  if (principal) {
10
11
  const session = await dispatchHookSingle(scope, CREATE_SESSION_HOOK, principal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@revojs/auth",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",