@nifrajs/authjs 0.0.0-bootstrap.0 → 4.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.
- package/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/client.d.ts +54 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +119 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +83 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +253 -0
- package/dist/index.js.map +1 -0
- package/package.json +64 -1
- package/src/client.ts +169 -0
- package/src/index.ts +328 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nifra Authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# `@nifrajs/authjs`
|
|
2
|
+
|
|
3
|
+
Official [Auth.js](https://authjs.dev) integration for nifra - backend mount, session helpers,
|
|
4
|
+
route guards, a framework-agnostic client, and React bindings.
|
|
5
|
+
|
|
6
|
+
```ts
|
|
7
|
+
import { authjs, getSession, requireAuthUser } from "@nifrajs/authjs"
|
|
8
|
+
import GitHub from "@auth/core/providers/github"
|
|
9
|
+
|
|
10
|
+
const authConfig = {
|
|
11
|
+
providers: [GitHub({ clientId: process.env.GITHUB_ID! })],
|
|
12
|
+
secret: process.env.AUTH_SECRET!,
|
|
13
|
+
trustHost: true,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const app = server()
|
|
17
|
+
.use(authjs(authConfig)) // serves /api/auth/* (sign-in, callbacks, session, sign-out)
|
|
18
|
+
.get("/me", async (c) => ({ user: (await getSession(c.req, authConfig))?.user ?? null }))
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
- `authjs(config, options?)` - identity plugin mounting Auth.js at `/api/auth/*` (GET + POST).
|
|
22
|
+
Secrets resolve per request (explicit → platform binding → `process.env`); missing fails loud.
|
|
23
|
+
- `getSession(req, config, options?)` - `Session | null`, usable in handlers and loaders. Pass the
|
|
24
|
+
mount's `authUrl`, `trustProxy` and `basePath` so the session is read where the mount serves it.
|
|
25
|
+
- `requireAuthUser(req, config, { redirectTo? })` - returns the user or throws a 401/redirect.
|
|
26
|
+
- `@nifrajs/authjs/client` - `createAuthClient()` (session, sign-in, sign-out) for any frontend.
|
|
27
|
+
- `@nifrajs/web-react/auth` - `<AuthSessionProvider>` + `useAuthSession()`.
|
|
28
|
+
|
|
29
|
+
Auth.js itself does the security-critical work (PKCE, state, token verification); this package
|
|
30
|
+
is the nifra-shaped surface over `@auth/core`. See `/docs/auth` (Auth.js section) for the full
|
|
31
|
+
guide, including proxies (`authUrl`) and edge secret bindings.
|
|
32
|
+
|
|
33
|
+
## For AI agents
|
|
34
|
+
|
|
35
|
+
Start with [`LLM.md`](./LLM.md) - this package's contract card. For the wider framework, see the
|
|
36
|
+
repository [`llms-full.txt`](../../llms-full.txt) corpus.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic Auth.js client - `fetch` against the mounted Auth.js endpoints, no DOM
|
|
3
|
+
* framework involved. Every UI adapter builds on this: pass it a base path (default
|
|
4
|
+
* `/api/auth`, matching the backend mount default) and an optional `fetch` override for tests
|
|
5
|
+
* or loaders that must forward headers.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { createAuthClient } from "@nifrajs/authjs/client"
|
|
9
|
+
*
|
|
10
|
+
* const auth = createAuthClient()
|
|
11
|
+
* await auth.signIn("github") // posts sign-in, then navigates to the provider (browser only)
|
|
12
|
+
* await auth.signOut() // posts signout, then navigates home
|
|
13
|
+
* const session = await auth.getSession() // works anywhere fetch does
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import type { Session } from "@auth/core/types";
|
|
17
|
+
export type { Session } from "@auth/core/types";
|
|
18
|
+
export interface AuthClientOptions {
|
|
19
|
+
/** Auth.js mount path. Default `"/api/auth"`. */
|
|
20
|
+
readonly basePath?: string;
|
|
21
|
+
/** `fetch` implementation. Default global `fetch` - override in tests or to forward headers. */
|
|
22
|
+
readonly fetch?: typeof fetch;
|
|
23
|
+
}
|
|
24
|
+
export interface SignInOptions {
|
|
25
|
+
/** Where the provider returns afterwards. Default the current page. */
|
|
26
|
+
readonly callbackUrl?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface SignOutOptions {
|
|
29
|
+
/** Where to land after sign-out. Default `"/"`. Set `redirect: false` to stay put. */
|
|
30
|
+
readonly callbackUrl?: string;
|
|
31
|
+
readonly redirect?: boolean;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Build `/api/auth/signin/:provider?callbackUrl=…` without touching `window` (testable).
|
|
35
|
+
*
|
|
36
|
+
* @deprecated Auth.js v5 accepts only a CSRF-carrying POST at this path and answers a GET - a link
|
|
37
|
+
* or a navigation here - with an error. Call `signIn()`, which submits that POST.
|
|
38
|
+
*/
|
|
39
|
+
export declare function signInUrl(providerId: string, options: {
|
|
40
|
+
readonly basePath?: string;
|
|
41
|
+
readonly callbackUrl: string;
|
|
42
|
+
}): string;
|
|
43
|
+
export interface AuthClient {
|
|
44
|
+
/** The Auth.js session, or `null` when unauthenticated. Usable anywhere `fetch` runs. */
|
|
45
|
+
getSession(init?: RequestInit): Promise<Session | null>;
|
|
46
|
+
/** Start an OAuth/OIDC sign-in (browser only): posts the CSRF-protected sign-in to Auth.js, then
|
|
47
|
+
* navigates to the provider page it answers with. */
|
|
48
|
+
signIn(providerId: string, options?: SignInOptions): Promise<void>;
|
|
49
|
+
/** Sign out via the CSRF-protected endpoint, then navigate (unless `redirect: false`). */
|
|
50
|
+
signOut(options?: SignOutOptions): Promise<void>;
|
|
51
|
+
}
|
|
52
|
+
/** Create the client. Stateless - every call hits the endpoints, so servers stay authoritative. */
|
|
53
|
+
export declare function createAuthClient(options?: AuthClientOptions): AuthClient;
|
|
54
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAE/C,YAAY,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAE/C,MAAM,WAAW,iBAAiB;IAChC,iDAAiD;IACjD,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,gGAAgG;IAChG,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,KAAK,CAAA;CAC9B;AAED,MAAM,WAAW,aAAa;IAC5B,uEAAuE;IACvE,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B;AAED,MAAM,WAAW,cAAc;IAC7B,sFAAsF;IACtF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;IAC7B,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;CAC5B;AAED;;;;;GAKG;AACH,wBAAgB,SAAS,CACvB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE,GACpE,MAAM,CAGR;AAwDD,MAAM,WAAW,UAAU;IACzB,yFAAyF;IACzF,UAAU,CAAC,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAAA;IACvD;yDACqD;IACrD,MAAM,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;IAClE,0FAA0F;IAC1F,OAAO,CAAC,OAAO,CAAC,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAAA;CACjD;AAED,mGAAmG;AACnG,wBAAgB,gBAAgB,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAoD5E"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build `/api/auth/signin/:provider?callbackUrl=…` without touching `window` (testable).
|
|
3
|
+
*
|
|
4
|
+
* @deprecated Auth.js v5 accepts only a CSRF-carrying POST at this path and answers a GET - a link
|
|
5
|
+
* or a navigation here - with an error. Call `signIn()`, which submits that POST.
|
|
6
|
+
*/
|
|
7
|
+
export function signInUrl(providerId, options) {
|
|
8
|
+
const base = options.basePath ?? "/api/auth";
|
|
9
|
+
return `${base}/signin/${encodeURIComponent(providerId)}?callbackUrl=${encodeURIComponent(options.callbackUrl)}`;
|
|
10
|
+
}
|
|
11
|
+
const currentUrl = () => {
|
|
12
|
+
if (typeof window === "undefined" || typeof window.location?.href !== "string") {
|
|
13
|
+
throw new Error("[nifra/authjs] signIn needs a browser page (no window.location.href)");
|
|
14
|
+
}
|
|
15
|
+
return window.location.href;
|
|
16
|
+
};
|
|
17
|
+
function isSafeRelativePath(value) {
|
|
18
|
+
if (!value.startsWith("/") || value.startsWith("//"))
|
|
19
|
+
return false;
|
|
20
|
+
for (let i = 1; i < value.length; i++) {
|
|
21
|
+
const code = value.charCodeAt(i);
|
|
22
|
+
if (code <= 0x1f || code === 0x7f || code === 0x5c)
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
/** Where Auth.js sent a sign-in: the provider's authorization page, usually on another origin. It
|
|
28
|
+
* came from the auth endpoint, not the caller, so only a non-web scheme (`javascript:`) is refused. */
|
|
29
|
+
function providerNavigationTarget(value) {
|
|
30
|
+
try {
|
|
31
|
+
const target = new URL(value, window.location.href);
|
|
32
|
+
if (target.protocol === "http:" || target.protocol === "https:")
|
|
33
|
+
return target.href;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// An unparseable answer falls back to the home path, as an unsafe callback does.
|
|
37
|
+
}
|
|
38
|
+
return "/";
|
|
39
|
+
}
|
|
40
|
+
/** Keep post-auth navigation on this origin; an unsafe callback is never an external redirect. */
|
|
41
|
+
function safeNavigationTarget(value) {
|
|
42
|
+
if (isSafeRelativePath(value))
|
|
43
|
+
return value;
|
|
44
|
+
if (typeof window !== "undefined" && typeof window.location?.href === "string") {
|
|
45
|
+
try {
|
|
46
|
+
const target = new URL(value, window.location.href);
|
|
47
|
+
if ((target.protocol === "http:" || target.protocol === "https:") &&
|
|
48
|
+
target.origin === window.location.origin) {
|
|
49
|
+
return target.href;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
// Invalid callback URLs fall back to the home path rather than becoming a navigation sink.
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return "/";
|
|
57
|
+
}
|
|
58
|
+
const navigate = (url, target = safeNavigationTarget) => {
|
|
59
|
+
if (typeof window === "undefined" || typeof window.location?.assign !== "function") {
|
|
60
|
+
throw new Error("[nifra/authjs] navigation needs a browser page");
|
|
61
|
+
}
|
|
62
|
+
window.location.assign(target(url));
|
|
63
|
+
};
|
|
64
|
+
/** Create the client. Stateless - every call hits the endpoints, so servers stay authoritative. */
|
|
65
|
+
export function createAuthClient(options = {}) {
|
|
66
|
+
const base = options.basePath ?? "/api/auth";
|
|
67
|
+
const impl = options.fetch ?? fetch;
|
|
68
|
+
const csrfToken = async (action) => {
|
|
69
|
+
const csrf = (await (await impl(`${base}/csrf`)).json());
|
|
70
|
+
if (typeof csrf.csrfToken !== "string" || csrf.csrfToken === "") {
|
|
71
|
+
throw new Error(`[nifra/authjs] ${action}: CSRF token endpoint did not answer`);
|
|
72
|
+
}
|
|
73
|
+
return csrf.csrfToken;
|
|
74
|
+
};
|
|
75
|
+
return {
|
|
76
|
+
async getSession(init) {
|
|
77
|
+
const response = await impl(`${base}/session`, init);
|
|
78
|
+
if (!response.ok)
|
|
79
|
+
return null;
|
|
80
|
+
const session = (await response.json());
|
|
81
|
+
return session?.user ? session : null;
|
|
82
|
+
},
|
|
83
|
+
async signIn(providerId, signInOptions = {}) {
|
|
84
|
+
const callbackUrl = signInOptions.callbackUrl ?? currentUrl();
|
|
85
|
+
const token = await csrfToken("signIn");
|
|
86
|
+
// `X-Auth-Return-Redirect` makes Auth.js answer `{ url }` instead of a 302 that `fetch` would
|
|
87
|
+
// follow on its own, so the browser navigates to the provider itself.
|
|
88
|
+
const response = await impl(`${base}/signin/${encodeURIComponent(providerId)}`, {
|
|
89
|
+
method: "POST",
|
|
90
|
+
headers: {
|
|
91
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
92
|
+
"x-auth-return-redirect": "1",
|
|
93
|
+
},
|
|
94
|
+
body: new URLSearchParams({ csrfToken: token, callbackUrl }),
|
|
95
|
+
});
|
|
96
|
+
const answer = response.ok ? await response.json().catch(() => null) : null;
|
|
97
|
+
const url = typeof answer === "object" && answer !== null ? Reflect.get(answer, "url") : undefined;
|
|
98
|
+
if (typeof url !== "string" || url === "") {
|
|
99
|
+
throw new Error(`[nifra/authjs] signIn failed (HTTP ${response.status})`);
|
|
100
|
+
}
|
|
101
|
+
navigate(url, providerNavigationTarget);
|
|
102
|
+
},
|
|
103
|
+
async signOut(signOutOptions = {}) {
|
|
104
|
+
const token = await csrfToken("signOut");
|
|
105
|
+
const callbackUrl = signOutOptions.callbackUrl ?? "/";
|
|
106
|
+
const response = await impl(`${base}/signout`, {
|
|
107
|
+
method: "POST",
|
|
108
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
109
|
+
body: new URLSearchParams({ csrfToken: token, callbackUrl }),
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
throw new Error(`[nifra/authjs] signOut failed (HTTP ${response.status})`);
|
|
113
|
+
}
|
|
114
|
+
if (signOutOptions.redirect !== false)
|
|
115
|
+
navigate(callbackUrl);
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official Auth.js integration for nifra - backend half. Mount the full Auth.js surface
|
|
3
|
+
* (OAuth/OIDC sign-in, callbacks, session, sign-out, CSRF) into a nifra app, read the session in
|
|
4
|
+
* handlers and loaders, and guard routes - with Auth.js, not a reimplementation, doing the
|
|
5
|
+
* security-critical work (PKCE, state, token verification, session cookies).
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { authjs, requireAuthUser } from "@nifrajs/authjs"
|
|
9
|
+
* import GitHub from "@auth/core/providers/github"
|
|
10
|
+
*
|
|
11
|
+
* const auth = {
|
|
12
|
+
* providers: [GitHub({ clientId: process.env.GITHUB_ID! })],
|
|
13
|
+
* secret: process.env.AUTH_SECRET,
|
|
14
|
+
* trustHost: true, // or a canonical origin: AuthJSOptions.authUrl / AUTH_URL
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* export const app = server()
|
|
18
|
+
* .use(authjs(auth))
|
|
19
|
+
* .get("/me", async (c) => ({ user: await requireAuthUser(c.req, auth) }))
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Frontend half: `createAuthClient` in `@nifrajs/authjs/client`, plus `<AuthSessionProvider>` /
|
|
23
|
+
* `useAuthSession()` in `@nifrajs/web-react/auth` (other adapters wrap the agnostic client the
|
|
24
|
+
* same way `@nifrajs/web-react/i18n` wraps `@nifrajs/i18n`).
|
|
25
|
+
*/
|
|
26
|
+
import type { AuthConfig } from "@auth/core";
|
|
27
|
+
import type { Session } from "@auth/core/types";
|
|
28
|
+
export type { Session } from "@auth/core/types";
|
|
29
|
+
/** The Auth.js config this integration drives - everything `@auth/core` accepts except `raw`. */
|
|
30
|
+
export type AuthJSConfig = Omit<AuthConfig, "raw">;
|
|
31
|
+
export interface AuthJSOptions {
|
|
32
|
+
/** Mount path for Auth.js routes. Defaults to `config.basePath`, then `"/api/auth"`. */
|
|
33
|
+
readonly basePath?: string;
|
|
34
|
+
/** Public origin (e.g. `"https://app.example.com"`); falls back to the `AUTH_URL` variable.
|
|
35
|
+
* Every request is rewritten onto it, so Auth.js trusts it and neither the Host header nor
|
|
36
|
+
* forwarded headers can replace it. Without one, Auth.js trusts the Host header only when
|
|
37
|
+
* `trustHost` (or `AUTH_TRUST_HOST`) says so, or outside production. */
|
|
38
|
+
readonly authUrl?: string;
|
|
39
|
+
/** Trust proxy-provided `x-forwarded-*` headers when `authUrl` is absent. Default `false`.
|
|
40
|
+
* Enable only when the deployment strips and replaces those headers at a trusted proxy. */
|
|
41
|
+
readonly trustProxy?: boolean;
|
|
42
|
+
/** Session secret. Falls back to the `secretEnv` platform binding, then `process.env`. Missing
|
|
43
|
+
* everywhere fails loud (500) on first use - Auth.js cannot sign cookies without it. */
|
|
44
|
+
readonly secret?: string;
|
|
45
|
+
/** Platform-binding name carrying the secret on edge runtimes (no `process.env` there).
|
|
46
|
+
* Default `"AUTH_SECRET"`. */
|
|
47
|
+
readonly secretEnv?: string;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Mount an Auth.js instance into a nifra app: serves `${basePath}/*` (default `/api/auth/*`) for
|
|
51
|
+
* `GET` + `POST`, so sign-in, OAuth callbacks, session, sign-out, and CSRF all run on your server
|
|
52
|
+
* through `@auth/core` itself. A type-IDENTITY plugin (see `defineIdentityPlugin`): mounting never
|
|
53
|
+
* changes the route registry's type, so the typed client keeps working below the mount.
|
|
54
|
+
*
|
|
55
|
+
* Idempotent (named `"authjs"` - applying twice mounts once).
|
|
56
|
+
*/
|
|
57
|
+
export declare function authjs(config: AuthJSConfig, options?: AuthJSOptions): import("@nifrajs/core").IdentityPlugin<never>;
|
|
58
|
+
/** What a guard does when the check fails: 302 to `redirectTo` (same-origin path), else 401 JSON.
|
|
59
|
+
* Pass the `basePath`, `authUrl` and `trustProxy` the mount was given, so the session is read on
|
|
60
|
+
* the origin and path the mount serves it from. */
|
|
61
|
+
export interface AuthGuardOptions extends Pick<AuthJSOptions, "basePath" | "authUrl" | "trustProxy"> {
|
|
62
|
+
readonly redirectTo?: string;
|
|
63
|
+
/** Secret override for this call. */
|
|
64
|
+
readonly secret?: string | string[];
|
|
65
|
+
/** Platform binding object used when `config.secret` is not present. */
|
|
66
|
+
readonly env?: unknown;
|
|
67
|
+
/** Secret key in `env`; defaults to `AUTH_SECRET`. */
|
|
68
|
+
readonly secretEnv?: string;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the Auth.js session for a request - `null` when unauthenticated. Takes the raw `Request`
|
|
72
|
+
* so it works in handlers (`c.req`), loaders/actions (`request`), and middleware. Mirrors the
|
|
73
|
+
* session-callback interception `@hono/auth-js` uses: the full `{ session, user }` flows through
|
|
74
|
+
* your configured `callbacks.session` first.
|
|
75
|
+
*/
|
|
76
|
+
export declare function getSession(req: Request, config: AuthJSConfig, options?: AuthGuardOptions): Promise<Session | null>;
|
|
77
|
+
/**
|
|
78
|
+
* Require an authenticated user. Returns `session.user`; otherwise throws a `status(...)` render
|
|
79
|
+
* (302/401) - control flow, caught by nifra's plain-data lane like `@nifrajs/auth` guards. Call at
|
|
80
|
+
* the top of a protected handler/loader/action.
|
|
81
|
+
*/
|
|
82
|
+
export declare function requireAuthUser(req: Request, config: AuthJSConfig, options?: AuthGuardOptions): Promise<NonNullable<Session["user"]>>;
|
|
83
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,UAAU,EAAkB,MAAM,YAAY,CAAA;AAC5D,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAI/C,YAAY,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAA;AAE/C,iGAAiG;AACjG,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAA;AAElD,MAAM,WAAW,aAAa;IAC5B,wFAAwF;IACxF,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B;;;4EAGwE;IACxE,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAA;IACzB;+FAC2F;IAC3F,QAAQ,CAAC,UAAU,CAAC,EAAE,OAAO,CAAA;IAC7B;4FACwF;IACxF,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,CAAA;IACxB;kCAC8B;IAC9B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AA6JD;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,MAAM,EAAE,YAAY,EAAE,OAAO,GAAE,aAAkB,iDAqCvE;AAED;;mDAEmD;AACnD,MAAM,WAAW,gBACf,SAAQ,IAAI,CAAC,aAAa,EAAE,UAAU,GAAG,SAAS,GAAG,YAAY,CAAC;IAClE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;IAC5B,qCAAqC;IACrC,QAAQ,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAA;IACnC,wEAAwE;IACxE,QAAQ,CAAC,GAAG,CAAC,EAAE,OAAO,CAAA;IACtB,sDAAsD;IACtD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAC5B;AAaD;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,OAAO,GAAG,IAAI,CAAC,CAoBzB;AAED;;;;GAIG;AACH,wBAAsB,eAAe,CACnC,GAAG,EAAE,OAAO,EACZ,MAAM,EAAE,YAAY,EACpB,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,CAIvC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official Auth.js integration for nifra - backend half. Mount the full Auth.js surface
|
|
3
|
+
* (OAuth/OIDC sign-in, callbacks, session, sign-out, CSRF) into a nifra app, read the session in
|
|
4
|
+
* handlers and loaders, and guard routes - with Auth.js, not a reimplementation, doing the
|
|
5
|
+
* security-critical work (PKCE, state, token verification, session cookies).
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { authjs, requireAuthUser } from "@nifrajs/authjs"
|
|
9
|
+
* import GitHub from "@auth/core/providers/github"
|
|
10
|
+
*
|
|
11
|
+
* const auth = {
|
|
12
|
+
* providers: [GitHub({ clientId: process.env.GITHUB_ID! })],
|
|
13
|
+
* secret: process.env.AUTH_SECRET,
|
|
14
|
+
* trustHost: true, // or a canonical origin: AuthJSOptions.authUrl / AUTH_URL
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* export const app = server()
|
|
18
|
+
* .use(authjs(auth))
|
|
19
|
+
* .get("/me", async (c) => ({ user: await requireAuthUser(c.req, auth) }))
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Frontend half: `createAuthClient` in `@nifrajs/authjs/client`, plus `<AuthSessionProvider>` /
|
|
23
|
+
* `useAuthSession()` in `@nifrajs/web-react/auth` (other adapters wrap the agnostic client the
|
|
24
|
+
* same way `@nifrajs/web-react/i18n` wraps `@nifrajs/i18n`).
|
|
25
|
+
*/
|
|
26
|
+
import { defineIdentityPlugin, isSameOriginPath, status } from "@nifrajs/core/server";
|
|
27
|
+
const HANDLED_METHODS = ["GET", "POST"];
|
|
28
|
+
/** Read a secret from the process environment where one exists (never on edge workers). */
|
|
29
|
+
function readEnvSecret(name) {
|
|
30
|
+
if (typeof process === "undefined")
|
|
31
|
+
return undefined;
|
|
32
|
+
const value = process.env?.[name];
|
|
33
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
34
|
+
}
|
|
35
|
+
/** Resolve the Auth.js secret: explicit config (a rotation list passes through untouched -
|
|
36
|
+
* Auth.js itself tries each entry), platform binding, then the process environment. */
|
|
37
|
+
function resolveSecret(env, secretEnv, configured) {
|
|
38
|
+
if (configured !== undefined)
|
|
39
|
+
return typeof configured === "string" ? configured : [...configured];
|
|
40
|
+
const name = secretEnv ?? "AUTH_SECRET";
|
|
41
|
+
const fromEnv = typeof env === "object" && env !== null ? env[name] : undefined;
|
|
42
|
+
if (typeof fromEnv === "string" && fromEnv !== "")
|
|
43
|
+
return fromEnv;
|
|
44
|
+
return readEnvSecret(name);
|
|
45
|
+
}
|
|
46
|
+
/** Validate the configured public origin once; request headers must never redefine it. */
|
|
47
|
+
function canonicalPublicOrigin(value) {
|
|
48
|
+
let url;
|
|
49
|
+
try {
|
|
50
|
+
url = new URL(value);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
throw new Error("[nifra/authjs] authUrl must be an absolute http(s) origin");
|
|
54
|
+
}
|
|
55
|
+
if ((url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
56
|
+
url.username !== "" ||
|
|
57
|
+
url.password !== "" ||
|
|
58
|
+
url.pathname !== "/" ||
|
|
59
|
+
url.search !== "" ||
|
|
60
|
+
url.hash !== "") {
|
|
61
|
+
throw new Error("[nifra/authjs] authUrl must be an absolute http(s) origin");
|
|
62
|
+
}
|
|
63
|
+
return url.origin;
|
|
64
|
+
}
|
|
65
|
+
/** What Auth.js reads its documented variables from (`AUTH_URL`, `AUTH_TRUST_HOST`, `NODE_ENV`,
|
|
66
|
+
* `AUTH_<PROVIDER>_ID`, ...): the platform bindings, then the process environment. */
|
|
67
|
+
function authEnv(bindings) {
|
|
68
|
+
const bound = typeof bindings === "object" && bindings !== null ? bindings : undefined;
|
|
69
|
+
const processEnv = typeof process === "undefined" ? undefined : process.env;
|
|
70
|
+
return new Proxy({}, {
|
|
71
|
+
get: (_target, key) => {
|
|
72
|
+
if (typeof key !== "string")
|
|
73
|
+
return undefined;
|
|
74
|
+
if (bound !== undefined && Object.hasOwn(bound, key))
|
|
75
|
+
return Reflect.get(bound, key);
|
|
76
|
+
return processEnv?.[key];
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
/** The origin of the documented `AUTH_URL` (or `NEXTAUTH_URL`), which may carry a path. */
|
|
81
|
+
function envOrigin(env) {
|
|
82
|
+
const value = env.AUTH_URL ?? env.NEXTAUTH_URL;
|
|
83
|
+
if (value === undefined || value === "")
|
|
84
|
+
return undefined;
|
|
85
|
+
let url;
|
|
86
|
+
try {
|
|
87
|
+
url = new URL(String(value));
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
throw new Error("[nifra/authjs] AUTH_URL must be an absolute http(s) URL");
|
|
91
|
+
}
|
|
92
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
93
|
+
throw new Error("[nifra/authjs] AUTH_URL must be an absolute http(s) URL");
|
|
94
|
+
}
|
|
95
|
+
return url.origin;
|
|
96
|
+
}
|
|
97
|
+
function normalizeBasePath(value) {
|
|
98
|
+
let base = value ?? "/api/auth";
|
|
99
|
+
while (base.endsWith("/"))
|
|
100
|
+
base = base.slice(0, -1);
|
|
101
|
+
return base === "" ? "/api/auth" : base;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The config Auth.js runs with - never the caller's object, which `setEnvDefaults` would mutate.
|
|
105
|
+
* `basePath` is always explicit: core applies no default of its own here, and an unset basePath
|
|
106
|
+
* answers every action (even `/csrf`) with a flat 400.
|
|
107
|
+
*/
|
|
108
|
+
function activeConfig(config, basePath, secret, origin, env, applyEnvDefaults) {
|
|
109
|
+
const active = {
|
|
110
|
+
...config,
|
|
111
|
+
basePath,
|
|
112
|
+
secret: typeof secret === "string" ? secret : [...secret],
|
|
113
|
+
};
|
|
114
|
+
// A request is rewritten onto a configured origin, so its Host header names nothing. Without
|
|
115
|
+
// one, trusting the Host header is Auth.js's own rule: `trustHost`, `AUTH_TRUST_HOST`, or a
|
|
116
|
+
// `NODE_ENV` other than production.
|
|
117
|
+
if (origin !== undefined)
|
|
118
|
+
active.trustHost ??= true;
|
|
119
|
+
applyEnvDefaults(env, active, true);
|
|
120
|
+
return active;
|
|
121
|
+
}
|
|
122
|
+
/** Remove proxy routing metadata so Auth.js cannot derive a URL from attacker-controlled headers. */
|
|
123
|
+
function sanitizePublicRequest(req, url, host) {
|
|
124
|
+
const rewritten = new Request(url, req);
|
|
125
|
+
rewritten.headers.delete("forwarded");
|
|
126
|
+
rewritten.headers.delete("x-forwarded-host");
|
|
127
|
+
rewritten.headers.delete("x-forwarded-proto");
|
|
128
|
+
rewritten.headers.delete("host");
|
|
129
|
+
rewritten.headers.set("host", host);
|
|
130
|
+
return rewritten;
|
|
131
|
+
}
|
|
132
|
+
/** Derive the public request URL: the configured origin, else the request's own, with forwarded
|
|
133
|
+
* headers trusted only on an explicit opt-in. */
|
|
134
|
+
function publicUrl(req, origin, trustProxy) {
|
|
135
|
+
const url = new URL(req.url);
|
|
136
|
+
if (origin !== undefined) {
|
|
137
|
+
const target = new URL(origin);
|
|
138
|
+
url.protocol = target.protocol;
|
|
139
|
+
url.host = target.host;
|
|
140
|
+
return url;
|
|
141
|
+
}
|
|
142
|
+
if (!trustProxy)
|
|
143
|
+
return url;
|
|
144
|
+
const proto = req.headers.get("x-forwarded-proto");
|
|
145
|
+
const host = req.headers.get("x-forwarded-host") ?? req.headers.get("host");
|
|
146
|
+
if (proto === null && host === null)
|
|
147
|
+
return url;
|
|
148
|
+
const forwarded = new URL(req.url);
|
|
149
|
+
try {
|
|
150
|
+
if (proto !== null) {
|
|
151
|
+
const protocol = proto.endsWith(":") ? proto : `${proto}:`;
|
|
152
|
+
if (protocol !== "http:" && protocol !== "https:")
|
|
153
|
+
throw new Error("invalid proxy protocol");
|
|
154
|
+
forwarded.protocol = protocol;
|
|
155
|
+
}
|
|
156
|
+
if (host !== null)
|
|
157
|
+
forwarded.host = host;
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
return url;
|
|
161
|
+
}
|
|
162
|
+
return forwarded;
|
|
163
|
+
}
|
|
164
|
+
/** Rewrite a request onto its public URL, preserving method/headers/body. */
|
|
165
|
+
function publicRequest(req, origin, trustProxy) {
|
|
166
|
+
const url = publicUrl(req, origin, trustProxy);
|
|
167
|
+
return sanitizePublicRequest(req, url.href, url.host);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Mount an Auth.js instance into a nifra app: serves `${basePath}/*` (default `/api/auth/*`) for
|
|
171
|
+
* `GET` + `POST`, so sign-in, OAuth callbacks, session, sign-out, and CSRF all run on your server
|
|
172
|
+
* through `@auth/core` itself. A type-IDENTITY plugin (see `defineIdentityPlugin`): mounting never
|
|
173
|
+
* changes the route registry's type, so the typed client keeps working below the mount.
|
|
174
|
+
*
|
|
175
|
+
* Idempotent (named `"authjs"` - applying twice mounts once).
|
|
176
|
+
*/
|
|
177
|
+
export function authjs(config, options = {}) {
|
|
178
|
+
const base = normalizeBasePath(options.basePath ?? config.basePath);
|
|
179
|
+
const pattern = `${base}/*`;
|
|
180
|
+
const publicOrigin = options.authUrl === undefined ? undefined : canonicalPublicOrigin(options.authUrl);
|
|
181
|
+
return defineIdentityPlugin("authjs", (app) => {
|
|
182
|
+
for (const method of HANDLED_METHODS) {
|
|
183
|
+
// `register`'s handler is typed `(context: never) => unknown`; the framework invokes it with
|
|
184
|
+
// the real Context, so reading `c.req`/`c.env` is sound. Auth.js's Response passes through.
|
|
185
|
+
app.register(method, pattern, undefined, async (c) => {
|
|
186
|
+
const secret = resolveSecret(c.env, options.secretEnv, options.secret ?? config.secret);
|
|
187
|
+
if (secret === undefined) {
|
|
188
|
+
return status(500, { ok: false, error: "auth_misconfigured" });
|
|
189
|
+
}
|
|
190
|
+
const env = authEnv(c.env);
|
|
191
|
+
let origin;
|
|
192
|
+
try {
|
|
193
|
+
origin = publicOrigin ?? envOrigin(env);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
return status(500, { ok: false, error: "auth_misconfigured" });
|
|
197
|
+
}
|
|
198
|
+
const { Auth, setEnvDefaults } = await import("@auth/core");
|
|
199
|
+
const active = activeConfig(config, base, secret, origin, env, setEnvDefaults);
|
|
200
|
+
return Auth(publicRequest(c.req, origin, options.trustProxy === true), active);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
return app;
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
const guardRejection = (options) => {
|
|
207
|
+
const to = options.redirectTo;
|
|
208
|
+
if (to === undefined)
|
|
209
|
+
return status(401, { ok: false, error: "unauthorized" });
|
|
210
|
+
if (!isSameOriginPath(to)) {
|
|
211
|
+
throw new Error(`[nifra/authjs] guard redirectTo must be a same-origin path beginning with "/" (got ${JSON.stringify(to)})`);
|
|
212
|
+
}
|
|
213
|
+
return status(302, undefined, { headers: { location: to } });
|
|
214
|
+
};
|
|
215
|
+
/**
|
|
216
|
+
* Resolve the Auth.js session for a request - `null` when unauthenticated. Takes the raw `Request`
|
|
217
|
+
* so it works in handlers (`c.req`), loaders/actions (`request`), and middleware. Mirrors the
|
|
218
|
+
* session-callback interception `@hono/auth-js` uses: the full `{ session, user }` flows through
|
|
219
|
+
* your configured `callbacks.session` first.
|
|
220
|
+
*/
|
|
221
|
+
export async function getSession(req, config, options = {}) {
|
|
222
|
+
const secret = resolveSecret(options.env, options.secretEnv, options.secret ?? config.secret);
|
|
223
|
+
if (secret === undefined) {
|
|
224
|
+
throw new Error("[nifra/authjs] getSession needs config.secret, options.secret, or AUTH_SECRET");
|
|
225
|
+
}
|
|
226
|
+
const env = authEnv(options.env);
|
|
227
|
+
const origin = options.authUrl === undefined ? envOrigin(env) : canonicalPublicOrigin(options.authUrl);
|
|
228
|
+
const base = normalizeBasePath(options.basePath ?? config.basePath);
|
|
229
|
+
const { Auth, setEnvDefaults } = await import("@auth/core");
|
|
230
|
+
const active = activeConfig(config, base, secret, origin, env, setEnvDefaults);
|
|
231
|
+
// Read where the mount serves it: the public origin's scheme picks the `__Secure-` cookie names.
|
|
232
|
+
const url = publicUrl(req, origin, options.trustProxy === true);
|
|
233
|
+
const sessionReq = new Request(`${url.origin}${base}/session`, {
|
|
234
|
+
headers: { cookie: req.headers.get("cookie") ?? "" },
|
|
235
|
+
});
|
|
236
|
+
const response = await Auth(sessionReq, active);
|
|
237
|
+
if (!response.ok)
|
|
238
|
+
return null;
|
|
239
|
+
const session = (await response.json());
|
|
240
|
+
return session?.user ? session : null;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Require an authenticated user. Returns `session.user`; otherwise throws a `status(...)` render
|
|
244
|
+
* (302/401) - control flow, caught by nifra's plain-data lane like `@nifrajs/auth` guards. Call at
|
|
245
|
+
* the top of a protected handler/loader/action.
|
|
246
|
+
*/
|
|
247
|
+
export async function requireAuthUser(req, config, options = {}) {
|
|
248
|
+
const session = await getSession(req, config, options);
|
|
249
|
+
if (session?.user)
|
|
250
|
+
return session.user;
|
|
251
|
+
throw guardRejection(options);
|
|
252
|
+
}
|
|
253
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1 +1,64 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"name": "@nifrajs/authjs",
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Official Auth.js integration for nifra - mount Auth.js (OAuth, OIDC, credentials, WebAuthn-ready) into a nifra app, read sessions in handlers and loaders, guard routes, and drive it all from any frontend.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bun",
|
|
7
|
+
"typescript",
|
|
8
|
+
"nifra",
|
|
9
|
+
"auth",
|
|
10
|
+
"authjs",
|
|
11
|
+
"oauth",
|
|
12
|
+
"oidc",
|
|
13
|
+
"session"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"nifra": {
|
|
17
|
+
"environment": "library"
|
|
18
|
+
},
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"module": "./dist/index.js",
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"exports": {
|
|
23
|
+
".": {
|
|
24
|
+
"bun": "./src/index.ts",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"./client": {
|
|
29
|
+
"bun": "./src/client.ts",
|
|
30
|
+
"types": "./dist/client.d.ts",
|
|
31
|
+
"default": "./dist/client.js"
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"src"
|
|
37
|
+
],
|
|
38
|
+
"sideEffects": false,
|
|
39
|
+
"scripts": {
|
|
40
|
+
"build": "tsc -p tsconfig.build.json && bun run ../../scripts/fix-dts.ts dist"
|
|
41
|
+
},
|
|
42
|
+
"license": "MIT",
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@auth/core": "^0.41.3"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"@nifrajs/core": "^4.0.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@nifrajs/core": "workspace:*"
|
|
54
|
+
},
|
|
55
|
+
"repository": {
|
|
56
|
+
"type": "git",
|
|
57
|
+
"url": "git+https://github.com/nifrajs/nifra.git",
|
|
58
|
+
"directory": "packages/authjs"
|
|
59
|
+
},
|
|
60
|
+
"bugs": {
|
|
61
|
+
"url": "https://github.com/nifrajs/nifra/issues"
|
|
62
|
+
},
|
|
63
|
+
"homepage": "https://github.com/nifrajs/nifra#readme"
|
|
64
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-agnostic Auth.js client - `fetch` against the mounted Auth.js endpoints, no DOM
|
|
3
|
+
* framework involved. Every UI adapter builds on this: pass it a base path (default
|
|
4
|
+
* `/api/auth`, matching the backend mount default) and an optional `fetch` override for tests
|
|
5
|
+
* or loaders that must forward headers.
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { createAuthClient } from "@nifrajs/authjs/client"
|
|
9
|
+
*
|
|
10
|
+
* const auth = createAuthClient()
|
|
11
|
+
* await auth.signIn("github") // posts sign-in, then navigates to the provider (browser only)
|
|
12
|
+
* await auth.signOut() // posts signout, then navigates home
|
|
13
|
+
* const session = await auth.getSession() // works anywhere fetch does
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import type { Session } from "@auth/core/types"
|
|
17
|
+
|
|
18
|
+
export type { Session } from "@auth/core/types"
|
|
19
|
+
|
|
20
|
+
export interface AuthClientOptions {
|
|
21
|
+
/** Auth.js mount path. Default `"/api/auth"`. */
|
|
22
|
+
readonly basePath?: string
|
|
23
|
+
/** `fetch` implementation. Default global `fetch` - override in tests or to forward headers. */
|
|
24
|
+
readonly fetch?: typeof fetch
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface SignInOptions {
|
|
28
|
+
/** Where the provider returns afterwards. Default the current page. */
|
|
29
|
+
readonly callbackUrl?: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface SignOutOptions {
|
|
33
|
+
/** Where to land after sign-out. Default `"/"`. Set `redirect: false` to stay put. */
|
|
34
|
+
readonly callbackUrl?: string
|
|
35
|
+
readonly redirect?: boolean
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Build `/api/auth/signin/:provider?callbackUrl=…` without touching `window` (testable).
|
|
40
|
+
*
|
|
41
|
+
* @deprecated Auth.js v5 accepts only a CSRF-carrying POST at this path and answers a GET - a link
|
|
42
|
+
* or a navigation here - with an error. Call `signIn()`, which submits that POST.
|
|
43
|
+
*/
|
|
44
|
+
export function signInUrl(
|
|
45
|
+
providerId: string,
|
|
46
|
+
options: { readonly basePath?: string; readonly callbackUrl: string },
|
|
47
|
+
): string {
|
|
48
|
+
const base = options.basePath ?? "/api/auth"
|
|
49
|
+
return `${base}/signin/${encodeURIComponent(providerId)}?callbackUrl=${encodeURIComponent(options.callbackUrl)}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const currentUrl = (): string => {
|
|
53
|
+
if (typeof window === "undefined" || typeof window.location?.href !== "string") {
|
|
54
|
+
throw new Error("[nifra/authjs] signIn needs a browser page (no window.location.href)")
|
|
55
|
+
}
|
|
56
|
+
return window.location.href
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isSafeRelativePath(value: string): boolean {
|
|
60
|
+
if (!value.startsWith("/") || value.startsWith("//")) return false
|
|
61
|
+
for (let i = 1; i < value.length; i++) {
|
|
62
|
+
const code = value.charCodeAt(i)
|
|
63
|
+
if (code <= 0x1f || code === 0x7f || code === 0x5c) return false
|
|
64
|
+
}
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Where Auth.js sent a sign-in: the provider's authorization page, usually on another origin. It
|
|
69
|
+
* came from the auth endpoint, not the caller, so only a non-web scheme (`javascript:`) is refused. */
|
|
70
|
+
function providerNavigationTarget(value: string): string {
|
|
71
|
+
try {
|
|
72
|
+
const target = new URL(value, window.location.href)
|
|
73
|
+
if (target.protocol === "http:" || target.protocol === "https:") return target.href
|
|
74
|
+
} catch {
|
|
75
|
+
// An unparseable answer falls back to the home path, as an unsafe callback does.
|
|
76
|
+
}
|
|
77
|
+
return "/"
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Keep post-auth navigation on this origin; an unsafe callback is never an external redirect. */
|
|
81
|
+
function safeNavigationTarget(value: string): string {
|
|
82
|
+
if (isSafeRelativePath(value)) return value
|
|
83
|
+
if (typeof window !== "undefined" && typeof window.location?.href === "string") {
|
|
84
|
+
try {
|
|
85
|
+
const target = new URL(value, window.location.href)
|
|
86
|
+
if (
|
|
87
|
+
(target.protocol === "http:" || target.protocol === "https:") &&
|
|
88
|
+
target.origin === window.location.origin
|
|
89
|
+
) {
|
|
90
|
+
return target.href
|
|
91
|
+
}
|
|
92
|
+
} catch {
|
|
93
|
+
// Invalid callback URLs fall back to the home path rather than becoming a navigation sink.
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return "/"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const navigate = (url: string, target: (url: string) => string = safeNavigationTarget): void => {
|
|
100
|
+
if (typeof window === "undefined" || typeof window.location?.assign !== "function") {
|
|
101
|
+
throw new Error("[nifra/authjs] navigation needs a browser page")
|
|
102
|
+
}
|
|
103
|
+
window.location.assign(target(url))
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface AuthClient {
|
|
107
|
+
/** The Auth.js session, or `null` when unauthenticated. Usable anywhere `fetch` runs. */
|
|
108
|
+
getSession(init?: RequestInit): Promise<Session | null>
|
|
109
|
+
/** Start an OAuth/OIDC sign-in (browser only): posts the CSRF-protected sign-in to Auth.js, then
|
|
110
|
+
* navigates to the provider page it answers with. */
|
|
111
|
+
signIn(providerId: string, options?: SignInOptions): Promise<void>
|
|
112
|
+
/** Sign out via the CSRF-protected endpoint, then navigate (unless `redirect: false`). */
|
|
113
|
+
signOut(options?: SignOutOptions): Promise<void>
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Create the client. Stateless - every call hits the endpoints, so servers stay authoritative. */
|
|
117
|
+
export function createAuthClient(options: AuthClientOptions = {}): AuthClient {
|
|
118
|
+
const base = options.basePath ?? "/api/auth"
|
|
119
|
+
const impl = options.fetch ?? fetch
|
|
120
|
+
const csrfToken = async (action: string): Promise<string> => {
|
|
121
|
+
const csrf = (await (await impl(`${base}/csrf`)).json()) as { csrfToken?: unknown }
|
|
122
|
+
if (typeof csrf.csrfToken !== "string" || csrf.csrfToken === "") {
|
|
123
|
+
throw new Error(`[nifra/authjs] ${action}: CSRF token endpoint did not answer`)
|
|
124
|
+
}
|
|
125
|
+
return csrf.csrfToken
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
async getSession(init?: RequestInit): Promise<Session | null> {
|
|
129
|
+
const response = await impl(`${base}/session`, init)
|
|
130
|
+
if (!response.ok) return null
|
|
131
|
+
const session = (await response.json()) as Session | null
|
|
132
|
+
return session?.user ? session : null
|
|
133
|
+
},
|
|
134
|
+
async signIn(providerId: string, signInOptions: SignInOptions = {}): Promise<void> {
|
|
135
|
+
const callbackUrl = signInOptions.callbackUrl ?? currentUrl()
|
|
136
|
+
const token = await csrfToken("signIn")
|
|
137
|
+
// `X-Auth-Return-Redirect` makes Auth.js answer `{ url }` instead of a 302 that `fetch` would
|
|
138
|
+
// follow on its own, so the browser navigates to the provider itself.
|
|
139
|
+
const response = await impl(`${base}/signin/${encodeURIComponent(providerId)}`, {
|
|
140
|
+
method: "POST",
|
|
141
|
+
headers: {
|
|
142
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
143
|
+
"x-auth-return-redirect": "1",
|
|
144
|
+
},
|
|
145
|
+
body: new URLSearchParams({ csrfToken: token, callbackUrl }),
|
|
146
|
+
})
|
|
147
|
+
const answer: unknown = response.ok ? await response.json().catch(() => null) : null
|
|
148
|
+
const url =
|
|
149
|
+
typeof answer === "object" && answer !== null ? Reflect.get(answer, "url") : undefined
|
|
150
|
+
if (typeof url !== "string" || url === "") {
|
|
151
|
+
throw new Error(`[nifra/authjs] signIn failed (HTTP ${response.status})`)
|
|
152
|
+
}
|
|
153
|
+
navigate(url, providerNavigationTarget)
|
|
154
|
+
},
|
|
155
|
+
async signOut(signOutOptions: SignOutOptions = {}): Promise<void> {
|
|
156
|
+
const token = await csrfToken("signOut")
|
|
157
|
+
const callbackUrl = signOutOptions.callbackUrl ?? "/"
|
|
158
|
+
const response = await impl(`${base}/signout`, {
|
|
159
|
+
method: "POST",
|
|
160
|
+
headers: { "content-type": "application/x-www-form-urlencoded" },
|
|
161
|
+
body: new URLSearchParams({ csrfToken: token, callbackUrl }),
|
|
162
|
+
})
|
|
163
|
+
if (!response.ok) {
|
|
164
|
+
throw new Error(`[nifra/authjs] signOut failed (HTTP ${response.status})`)
|
|
165
|
+
}
|
|
166
|
+
if (signOutOptions.redirect !== false) navigate(callbackUrl)
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Official Auth.js integration for nifra - backend half. Mount the full Auth.js surface
|
|
3
|
+
* (OAuth/OIDC sign-in, callbacks, session, sign-out, CSRF) into a nifra app, read the session in
|
|
4
|
+
* handlers and loaders, and guard routes - with Auth.js, not a reimplementation, doing the
|
|
5
|
+
* security-critical work (PKCE, state, token verification, session cookies).
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* import { authjs, requireAuthUser } from "@nifrajs/authjs"
|
|
9
|
+
* import GitHub from "@auth/core/providers/github"
|
|
10
|
+
*
|
|
11
|
+
* const auth = {
|
|
12
|
+
* providers: [GitHub({ clientId: process.env.GITHUB_ID! })],
|
|
13
|
+
* secret: process.env.AUTH_SECRET,
|
|
14
|
+
* trustHost: true, // or a canonical origin: AuthJSOptions.authUrl / AUTH_URL
|
|
15
|
+
* }
|
|
16
|
+
*
|
|
17
|
+
* export const app = server()
|
|
18
|
+
* .use(authjs(auth))
|
|
19
|
+
* .get("/me", async (c) => ({ user: await requireAuthUser(c.req, auth) }))
|
|
20
|
+
* ```
|
|
21
|
+
*
|
|
22
|
+
* Frontend half: `createAuthClient` in `@nifrajs/authjs/client`, plus `<AuthSessionProvider>` /
|
|
23
|
+
* `useAuthSession()` in `@nifrajs/web-react/auth` (other adapters wrap the agnostic client the
|
|
24
|
+
* same way `@nifrajs/web-react/i18n` wraps `@nifrajs/i18n`).
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import type { AuthConfig, setEnvDefaults } from "@auth/core"
|
|
28
|
+
import type { Session } from "@auth/core/types"
|
|
29
|
+
import type { AnyServer, Platform, ResponseResult } from "@nifrajs/core/server"
|
|
30
|
+
import { defineIdentityPlugin, isSameOriginPath, status } from "@nifrajs/core/server"
|
|
31
|
+
|
|
32
|
+
export type { Session } from "@auth/core/types"
|
|
33
|
+
|
|
34
|
+
/** The Auth.js config this integration drives - everything `@auth/core` accepts except `raw`. */
|
|
35
|
+
export type AuthJSConfig = Omit<AuthConfig, "raw">
|
|
36
|
+
|
|
37
|
+
export interface AuthJSOptions {
|
|
38
|
+
/** Mount path for Auth.js routes. Defaults to `config.basePath`, then `"/api/auth"`. */
|
|
39
|
+
readonly basePath?: string
|
|
40
|
+
/** Public origin (e.g. `"https://app.example.com"`); falls back to the `AUTH_URL` variable.
|
|
41
|
+
* Every request is rewritten onto it, so Auth.js trusts it and neither the Host header nor
|
|
42
|
+
* forwarded headers can replace it. Without one, Auth.js trusts the Host header only when
|
|
43
|
+
* `trustHost` (or `AUTH_TRUST_HOST`) says so, or outside production. */
|
|
44
|
+
readonly authUrl?: string
|
|
45
|
+
/** Trust proxy-provided `x-forwarded-*` headers when `authUrl` is absent. Default `false`.
|
|
46
|
+
* Enable only when the deployment strips and replaces those headers at a trusted proxy. */
|
|
47
|
+
readonly trustProxy?: boolean
|
|
48
|
+
/** Session secret. Falls back to the `secretEnv` platform binding, then `process.env`. Missing
|
|
49
|
+
* everywhere fails loud (500) on first use - Auth.js cannot sign cookies without it. */
|
|
50
|
+
readonly secret?: string
|
|
51
|
+
/** Platform-binding name carrying the secret on edge runtimes (no `process.env` there).
|
|
52
|
+
* Default `"AUTH_SECRET"`. */
|
|
53
|
+
readonly secretEnv?: string
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const HANDLED_METHODS = ["GET", "POST"] as const
|
|
57
|
+
|
|
58
|
+
/** Read a secret from the process environment where one exists (never on edge workers). */
|
|
59
|
+
function readEnvSecret(name: string): string | undefined {
|
|
60
|
+
if (typeof process === "undefined") return undefined
|
|
61
|
+
const value = process.env?.[name]
|
|
62
|
+
return typeof value === "string" && value !== "" ? value : undefined
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Resolve the Auth.js secret: explicit config (a rotation list passes through untouched -
|
|
66
|
+
* Auth.js itself tries each entry), platform binding, then the process environment. */
|
|
67
|
+
function resolveSecret(
|
|
68
|
+
env: unknown,
|
|
69
|
+
secretEnv: string | undefined,
|
|
70
|
+
configured: string | readonly string[] | undefined,
|
|
71
|
+
): string | string[] | undefined {
|
|
72
|
+
if (configured !== undefined) return typeof configured === "string" ? configured : [...configured]
|
|
73
|
+
const name = secretEnv ?? "AUTH_SECRET"
|
|
74
|
+
const fromEnv =
|
|
75
|
+
typeof env === "object" && env !== null ? (env as Record<string, unknown>)[name] : undefined
|
|
76
|
+
if (typeof fromEnv === "string" && fromEnv !== "") return fromEnv
|
|
77
|
+
return readEnvSecret(name)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Validate the configured public origin once; request headers must never redefine it. */
|
|
81
|
+
function canonicalPublicOrigin(value: string): string {
|
|
82
|
+
let url: URL
|
|
83
|
+
try {
|
|
84
|
+
url = new URL(value)
|
|
85
|
+
} catch {
|
|
86
|
+
throw new Error("[nifra/authjs] authUrl must be an absolute http(s) origin")
|
|
87
|
+
}
|
|
88
|
+
if (
|
|
89
|
+
(url.protocol !== "http:" && url.protocol !== "https:") ||
|
|
90
|
+
url.username !== "" ||
|
|
91
|
+
url.password !== "" ||
|
|
92
|
+
url.pathname !== "/" ||
|
|
93
|
+
url.search !== "" ||
|
|
94
|
+
url.hash !== ""
|
|
95
|
+
) {
|
|
96
|
+
throw new Error("[nifra/authjs] authUrl must be an absolute http(s) origin")
|
|
97
|
+
}
|
|
98
|
+
return url.origin
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** What Auth.js reads its documented variables from (`AUTH_URL`, `AUTH_TRUST_HOST`, `NODE_ENV`,
|
|
102
|
+
* `AUTH_<PROVIDER>_ID`, ...): the platform bindings, then the process environment. */
|
|
103
|
+
function authEnv(bindings: unknown): Record<string, unknown> {
|
|
104
|
+
const bound = typeof bindings === "object" && bindings !== null ? bindings : undefined
|
|
105
|
+
const processEnv = typeof process === "undefined" ? undefined : process.env
|
|
106
|
+
return new Proxy(
|
|
107
|
+
{},
|
|
108
|
+
{
|
|
109
|
+
get: (_target, key) => {
|
|
110
|
+
if (typeof key !== "string") return undefined
|
|
111
|
+
if (bound !== undefined && Object.hasOwn(bound, key)) return Reflect.get(bound, key)
|
|
112
|
+
return processEnv?.[key]
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The origin of the documented `AUTH_URL` (or `NEXTAUTH_URL`), which may carry a path. */
|
|
119
|
+
function envOrigin(env: Record<string, unknown>): string | undefined {
|
|
120
|
+
const value = env.AUTH_URL ?? env.NEXTAUTH_URL
|
|
121
|
+
if (value === undefined || value === "") return undefined
|
|
122
|
+
let url: URL
|
|
123
|
+
try {
|
|
124
|
+
url = new URL(String(value))
|
|
125
|
+
} catch {
|
|
126
|
+
throw new Error("[nifra/authjs] AUTH_URL must be an absolute http(s) URL")
|
|
127
|
+
}
|
|
128
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
129
|
+
throw new Error("[nifra/authjs] AUTH_URL must be an absolute http(s) URL")
|
|
130
|
+
}
|
|
131
|
+
return url.origin
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function normalizeBasePath(value: string | undefined): string {
|
|
135
|
+
let base = value ?? "/api/auth"
|
|
136
|
+
while (base.endsWith("/")) base = base.slice(0, -1)
|
|
137
|
+
return base === "" ? "/api/auth" : base
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* The config Auth.js runs with - never the caller's object, which `setEnvDefaults` would mutate.
|
|
142
|
+
* `basePath` is always explicit: core applies no default of its own here, and an unset basePath
|
|
143
|
+
* answers every action (even `/csrf`) with a flat 400.
|
|
144
|
+
*/
|
|
145
|
+
function activeConfig(
|
|
146
|
+
config: AuthJSConfig,
|
|
147
|
+
basePath: string,
|
|
148
|
+
secret: string | string[],
|
|
149
|
+
origin: string | undefined,
|
|
150
|
+
env: Record<string, unknown>,
|
|
151
|
+
applyEnvDefaults: typeof setEnvDefaults,
|
|
152
|
+
): AuthConfig {
|
|
153
|
+
const active: AuthConfig = {
|
|
154
|
+
...config,
|
|
155
|
+
basePath,
|
|
156
|
+
secret: typeof secret === "string" ? secret : [...secret],
|
|
157
|
+
}
|
|
158
|
+
// A request is rewritten onto a configured origin, so its Host header names nothing. Without
|
|
159
|
+
// one, trusting the Host header is Auth.js's own rule: `trustHost`, `AUTH_TRUST_HOST`, or a
|
|
160
|
+
// `NODE_ENV` other than production.
|
|
161
|
+
if (origin !== undefined) active.trustHost ??= true
|
|
162
|
+
applyEnvDefaults(env, active, true)
|
|
163
|
+
return active
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Remove proxy routing metadata so Auth.js cannot derive a URL from attacker-controlled headers. */
|
|
167
|
+
function sanitizePublicRequest(req: Request, url: string, host: string): Request {
|
|
168
|
+
const rewritten = new Request(url, req)
|
|
169
|
+
rewritten.headers.delete("forwarded")
|
|
170
|
+
rewritten.headers.delete("x-forwarded-host")
|
|
171
|
+
rewritten.headers.delete("x-forwarded-proto")
|
|
172
|
+
rewritten.headers.delete("host")
|
|
173
|
+
rewritten.headers.set("host", host)
|
|
174
|
+
return rewritten
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Derive the public request URL: the configured origin, else the request's own, with forwarded
|
|
178
|
+
* headers trusted only on an explicit opt-in. */
|
|
179
|
+
function publicUrl(req: Request, origin: string | undefined, trustProxy: boolean): URL {
|
|
180
|
+
const url = new URL(req.url)
|
|
181
|
+
if (origin !== undefined) {
|
|
182
|
+
const target = new URL(origin)
|
|
183
|
+
url.protocol = target.protocol
|
|
184
|
+
url.host = target.host
|
|
185
|
+
return url
|
|
186
|
+
}
|
|
187
|
+
if (!trustProxy) return url
|
|
188
|
+
const proto = req.headers.get("x-forwarded-proto")
|
|
189
|
+
const host = req.headers.get("x-forwarded-host") ?? req.headers.get("host")
|
|
190
|
+
if (proto === null && host === null) return url
|
|
191
|
+
const forwarded = new URL(req.url)
|
|
192
|
+
try {
|
|
193
|
+
if (proto !== null) {
|
|
194
|
+
const protocol = proto.endsWith(":") ? proto : `${proto}:`
|
|
195
|
+
if (protocol !== "http:" && protocol !== "https:") throw new Error("invalid proxy protocol")
|
|
196
|
+
forwarded.protocol = protocol
|
|
197
|
+
}
|
|
198
|
+
if (host !== null) forwarded.host = host
|
|
199
|
+
} catch {
|
|
200
|
+
return url
|
|
201
|
+
}
|
|
202
|
+
return forwarded
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/** Rewrite a request onto its public URL, preserving method/headers/body. */
|
|
206
|
+
function publicRequest(req: Request, origin: string | undefined, trustProxy: boolean): Request {
|
|
207
|
+
const url = publicUrl(req, origin, trustProxy)
|
|
208
|
+
return sanitizePublicRequest(req, url.href, url.host)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Mount an Auth.js instance into a nifra app: serves `${basePath}/*` (default `/api/auth/*`) for
|
|
213
|
+
* `GET` + `POST`, so sign-in, OAuth callbacks, session, sign-out, and CSRF all run on your server
|
|
214
|
+
* through `@auth/core` itself. A type-IDENTITY plugin (see `defineIdentityPlugin`): mounting never
|
|
215
|
+
* changes the route registry's type, so the typed client keeps working below the mount.
|
|
216
|
+
*
|
|
217
|
+
* Idempotent (named `"authjs"` - applying twice mounts once).
|
|
218
|
+
*/
|
|
219
|
+
export function authjs(config: AuthJSConfig, options: AuthJSOptions = {}) {
|
|
220
|
+
const base = normalizeBasePath(options.basePath ?? config.basePath)
|
|
221
|
+
const pattern = `${base}/*`
|
|
222
|
+
const publicOrigin =
|
|
223
|
+
options.authUrl === undefined ? undefined : canonicalPublicOrigin(options.authUrl)
|
|
224
|
+
return defineIdentityPlugin("authjs", <S extends AnyServer>(app: S): S => {
|
|
225
|
+
for (const method of HANDLED_METHODS) {
|
|
226
|
+
// `register`'s handler is typed `(context: never) => unknown`; the framework invokes it with
|
|
227
|
+
// the real Context, so reading `c.req`/`c.env` is sound. Auth.js's Response passes through.
|
|
228
|
+
app.register(
|
|
229
|
+
method,
|
|
230
|
+
pattern,
|
|
231
|
+
undefined,
|
|
232
|
+
async (c: {
|
|
233
|
+
readonly req: Request
|
|
234
|
+
readonly env: unknown
|
|
235
|
+
readonly platform?: Platform
|
|
236
|
+
}) => {
|
|
237
|
+
const secret = resolveSecret(c.env, options.secretEnv, options.secret ?? config.secret)
|
|
238
|
+
if (secret === undefined) {
|
|
239
|
+
return status(500, { ok: false, error: "auth_misconfigured" })
|
|
240
|
+
}
|
|
241
|
+
const env = authEnv(c.env)
|
|
242
|
+
let origin: string | undefined
|
|
243
|
+
try {
|
|
244
|
+
origin = publicOrigin ?? envOrigin(env)
|
|
245
|
+
} catch {
|
|
246
|
+
return status(500, { ok: false, error: "auth_misconfigured" })
|
|
247
|
+
}
|
|
248
|
+
const { Auth, setEnvDefaults } = await import("@auth/core")
|
|
249
|
+
const active = activeConfig(config, base, secret, origin, env, setEnvDefaults)
|
|
250
|
+
return Auth(publicRequest(c.req, origin, options.trustProxy === true), active)
|
|
251
|
+
},
|
|
252
|
+
)
|
|
253
|
+
}
|
|
254
|
+
return app
|
|
255
|
+
})
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** What a guard does when the check fails: 302 to `redirectTo` (same-origin path), else 401 JSON.
|
|
259
|
+
* Pass the `basePath`, `authUrl` and `trustProxy` the mount was given, so the session is read on
|
|
260
|
+
* the origin and path the mount serves it from. */
|
|
261
|
+
export interface AuthGuardOptions
|
|
262
|
+
extends Pick<AuthJSOptions, "basePath" | "authUrl" | "trustProxy"> {
|
|
263
|
+
readonly redirectTo?: string
|
|
264
|
+
/** Secret override for this call. */
|
|
265
|
+
readonly secret?: string | string[]
|
|
266
|
+
/** Platform binding object used when `config.secret` is not present. */
|
|
267
|
+
readonly env?: unknown
|
|
268
|
+
/** Secret key in `env`; defaults to `AUTH_SECRET`. */
|
|
269
|
+
readonly secretEnv?: string
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const guardRejection = (options: AuthGuardOptions): ResponseResult => {
|
|
273
|
+
const to = options.redirectTo
|
|
274
|
+
if (to === undefined) return status(401, { ok: false, error: "unauthorized" })
|
|
275
|
+
if (!isSameOriginPath(to)) {
|
|
276
|
+
throw new Error(
|
|
277
|
+
`[nifra/authjs] guard redirectTo must be a same-origin path beginning with "/" (got ${JSON.stringify(to)})`,
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
return status(302, undefined, { headers: { location: to } })
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Resolve the Auth.js session for a request - `null` when unauthenticated. Takes the raw `Request`
|
|
285
|
+
* so it works in handlers (`c.req`), loaders/actions (`request`), and middleware. Mirrors the
|
|
286
|
+
* session-callback interception `@hono/auth-js` uses: the full `{ session, user }` flows through
|
|
287
|
+
* your configured `callbacks.session` first.
|
|
288
|
+
*/
|
|
289
|
+
export async function getSession(
|
|
290
|
+
req: Request,
|
|
291
|
+
config: AuthJSConfig,
|
|
292
|
+
options: AuthGuardOptions = {},
|
|
293
|
+
): Promise<Session | null> {
|
|
294
|
+
const secret = resolveSecret(options.env, options.secretEnv, options.secret ?? config.secret)
|
|
295
|
+
if (secret === undefined) {
|
|
296
|
+
throw new Error("[nifra/authjs] getSession needs config.secret, options.secret, or AUTH_SECRET")
|
|
297
|
+
}
|
|
298
|
+
const env = authEnv(options.env)
|
|
299
|
+
const origin =
|
|
300
|
+
options.authUrl === undefined ? envOrigin(env) : canonicalPublicOrigin(options.authUrl)
|
|
301
|
+
const base = normalizeBasePath(options.basePath ?? config.basePath)
|
|
302
|
+
const { Auth, setEnvDefaults } = await import("@auth/core")
|
|
303
|
+
const active = activeConfig(config, base, secret, origin, env, setEnvDefaults)
|
|
304
|
+
// Read where the mount serves it: the public origin's scheme picks the `__Secure-` cookie names.
|
|
305
|
+
const url = publicUrl(req, origin, options.trustProxy === true)
|
|
306
|
+
const sessionReq = new Request(`${url.origin}${base}/session`, {
|
|
307
|
+
headers: { cookie: req.headers.get("cookie") ?? "" },
|
|
308
|
+
})
|
|
309
|
+
const response = await Auth(sessionReq, active)
|
|
310
|
+
if (!response.ok) return null
|
|
311
|
+
const session = (await response.json()) as Session | null
|
|
312
|
+
return session?.user ? session : null
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* Require an authenticated user. Returns `session.user`; otherwise throws a `status(...)` render
|
|
317
|
+
* (302/401) - control flow, caught by nifra's plain-data lane like `@nifrajs/auth` guards. Call at
|
|
318
|
+
* the top of a protected handler/loader/action.
|
|
319
|
+
*/
|
|
320
|
+
export async function requireAuthUser(
|
|
321
|
+
req: Request,
|
|
322
|
+
config: AuthJSConfig,
|
|
323
|
+
options: AuthGuardOptions = {},
|
|
324
|
+
): Promise<NonNullable<Session["user"]>> {
|
|
325
|
+
const session = await getSession(req, config, options)
|
|
326
|
+
if (session?.user) return session.user
|
|
327
|
+
throw guardRejection(options)
|
|
328
|
+
}
|