authfyio-nextjs 0.2.3

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/README.md ADDED
@@ -0,0 +1,90 @@
1
+ # authfyio-nextjs
2
+
3
+ Next.js SDK for Authfyio. Ships middleware for the App Router edge runtime, a server-side session reader for Route Handlers and Server Components, and typed helpers that sit directly on top of `authfyio-backend`.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install authfyio-nextjs
9
+ ```
10
+
11
+ Next.js 14+ (App Router) and Node 20+.
12
+
13
+ ## Middleware (edge)
14
+
15
+ Protect routes globally with one import. The middleware verifies the `__session` JWT at the edge — no database or origin round-trip.
16
+
17
+ ```ts
18
+ // middleware.ts
19
+ import type { NextRequest } from 'next/server';
20
+ import { requireSessionMiddleware } from 'authfyio-nextjs';
21
+
22
+ export async function middleware(req: NextRequest) {
23
+ return await requireSessionMiddleware(req, {
24
+ baseUrl: process.env.AF_API_BASE_URL!,
25
+ issuer: process.env.AF_JWT_ISSUER,
26
+ redirectTo: '/sign-in',
27
+ });
28
+ }
29
+
30
+ export const config = {
31
+ matcher: ['/app/:path*', '/api/private/:path*'],
32
+ };
33
+ ```
34
+
35
+ Unauthenticated requests to matched routes are redirected to `redirectTo` (or return a `401` for `/api/*` paths). The signed-in user's claims are forwarded as `x-af-user-id` and `x-af-env-id` headers so downstream handlers can read them without re-verifying.
36
+
37
+ ## Server Components & Route Handlers
38
+
39
+ ```ts
40
+ // app/api/me/route.ts
41
+ import { NextResponse } from 'next/server';
42
+ import { getSessionFromNextRequest } from 'authfyio-nextjs';
43
+
44
+ export async function GET(req: Request) {
45
+ const session = await getSessionFromNextRequest(req, {
46
+ baseUrl: process.env.AF_API_BASE_URL!,
47
+ issuer: process.env.AF_JWT_ISSUER,
48
+ });
49
+ if (!session) return NextResponse.json({ error: 'unauthenticated' }, { status: 401 });
50
+
51
+ return NextResponse.json({ userId: session.sub });
52
+ }
53
+ ```
54
+
55
+ ## API
56
+
57
+ ### `requireSessionMiddleware(req, options)`
58
+
59
+ | Option | Type | Required | Description |
60
+ | ------------ | -------- | -------- | ------------------------------------------------------------------------------------------------------ |
61
+ | `baseUrl` | `string` | yes | Instance API base URL. |
62
+ | `issuer` | `string` | no | Expected `iss`. Strongly recommended in production. |
63
+ | `audience` | `string` | no | Expected `aud`, if set. |
64
+ | `redirectTo` | `string` | no | Where to send unauthenticated requests. When omitted, returns `401` with a JSON body. |
65
+ | `publicPaths` | `string[]` | no | Skip session enforcement for these paths (e.g. `['/health']`). |
66
+
67
+ ### `getSessionFromNextRequest(req, options)`
68
+
69
+ Pulls the `__session` cookie out of the request, verifies it, and returns the claims. Returns `null` when no cookie is present. Throws on an invalid token.
70
+
71
+ ## Environment variables
72
+
73
+ Minimum set to wire up:
74
+
75
+ ```bash
76
+ AF_API_BASE_URL=https://auth.example.com
77
+ AF_JWT_ISSUER=https://auth.example.com/
78
+ ```
79
+
80
+ ## How it integrates with `authfyio-react`
81
+
82
+ Browser-side: `authfyio-react` reads `__session` from cookies, exposes it via hooks, and refreshes it automatically.
83
+
84
+ Edge/server-side: `authfyio-nextjs` re-verifies the same cookie in middleware and Server Components for defence-in-depth — a compromised browser cookie is rejected at the edge because JWKS verification still runs.
85
+
86
+ You will almost always install both in a Next.js app.
87
+
88
+ ## License
89
+
90
+ MIT
package/dist/auth.d.ts ADDED
@@ -0,0 +1,43 @@
1
+ import { type SessionClaims } from './session.js';
2
+ export type AuthOptions = {
3
+ /**
4
+ * Base URL of the instance API (JWKS source). Defaults to
5
+ * https://api.authfyio.com or `process.env.AF_API_BASE_URL` if set.
6
+ */
7
+ baseUrl?: string;
8
+ /** Optional expected issuer. Matches `AF_JWT_ISSUER` on the API side. */
9
+ issuer?: string;
10
+ /** Optional expected audience. */
11
+ audience?: string;
12
+ };
13
+ export type AuthObject = {
14
+ userId: string | null;
15
+ sessionId: string | null;
16
+ orgId: string | null;
17
+ orgRole: string | null;
18
+ environmentId: string | null;
19
+ isSignedIn: boolean;
20
+ getToken: () => string | null;
21
+ claims: SessionClaims | null;
22
+ };
23
+ /**
24
+ * Server-side auth helper for Next.js App Router.
25
+ * Reads `__session` from the incoming cookie, verifies it via JWKS, and
26
+ * returns a flat object your server components / server actions / route
27
+ * handlers can use.
28
+ *
29
+ * When the middleware has already verified the request (see
30
+ * `authfyioMiddleware`), we prefer the decoded claims it forwarded via
31
+ * headers (`x-af-user-id`, `x-af-session-id`, `x-af-environment-id`). That
32
+ * avoids a second JWKS verification per request.
33
+ *
34
+ * Usage:
35
+ * import { auth } from 'authfyio-nextjs';
36
+ * export default async function Page() {
37
+ * const { userId, isSignedIn } = await auth({ baseUrl: process.env.AF_API_BASE_URL! });
38
+ * if (!isSignedIn) redirect('/sign-in');
39
+ * return <p>Hi {userId}</p>;
40
+ * }
41
+ */
42
+ export declare function auth(opts?: AuthOptions): Promise<AuthObject>;
43
+ //# sourceMappingURL=auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.d.ts","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAEpE,MAAM,MAAM,WAAW,GAAG;IACxB;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,UAAU,GAAG;IACvB,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;IAC9B,MAAM,EAAE,aAAa,GAAG,IAAI,CAAC;CAC9B,CAAC;AAEF;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAsB,IAAI,CAAC,IAAI,GAAE,WAAgB,GAAG,OAAO,CAAC,UAAU,CAAC,CA2CtE"}
package/dist/auth.js ADDED
@@ -0,0 +1,77 @@
1
+ import { cookies, headers } from 'next/headers';
2
+ import { resolveBaseUrl } from './next.js';
3
+ import { verifySessionJwt } from './session.js';
4
+ /**
5
+ * Server-side auth helper for Next.js App Router.
6
+ * Reads `__session` from the incoming cookie, verifies it via JWKS, and
7
+ * returns a flat object your server components / server actions / route
8
+ * handlers can use.
9
+ *
10
+ * When the middleware has already verified the request (see
11
+ * `authfyioMiddleware`), we prefer the decoded claims it forwarded via
12
+ * headers (`x-af-user-id`, `x-af-session-id`, `x-af-environment-id`). That
13
+ * avoids a second JWKS verification per request.
14
+ *
15
+ * Usage:
16
+ * import { auth } from 'authfyio-nextjs';
17
+ * export default async function Page() {
18
+ * const { userId, isSignedIn } = await auth({ baseUrl: process.env.AF_API_BASE_URL! });
19
+ * if (!isSignedIn) redirect('/sign-in');
20
+ * return <p>Hi {userId}</p>;
21
+ * }
22
+ */
23
+ export async function auth(opts = {}) {
24
+ const hdr = await headers();
25
+ const forwardedUserId = hdr.get('x-af-user-id');
26
+ const forwardedSessionId = hdr.get('x-af-session-id');
27
+ const forwardedEnvId = hdr.get('x-af-environment-id');
28
+ const cookieJar = await cookies();
29
+ const token = cookieJar.get('__session')?.value ?? null;
30
+ // Middleware has already verified — trust headers, skip JWKS round-trip.
31
+ if (forwardedUserId && forwardedSessionId && forwardedEnvId && token) {
32
+ return {
33
+ userId: forwardedUserId,
34
+ sessionId: forwardedSessionId,
35
+ orgId: hdr.get('x-af-org-id'),
36
+ orgRole: hdr.get('x-af-org-role'),
37
+ environmentId: forwardedEnvId,
38
+ isSignedIn: true,
39
+ getToken: () => token,
40
+ claims: null,
41
+ };
42
+ }
43
+ if (!token)
44
+ return emptyAuth();
45
+ try {
46
+ const claims = await verifySessionJwt(token, {
47
+ jwksUrl: `${resolveBaseUrl(opts)}/.well-known/jwks.json`,
48
+ issuer: opts.issuer,
49
+ audience: opts.audience,
50
+ });
51
+ return {
52
+ userId: claims.sub,
53
+ sessionId: claims.sid,
54
+ orgId: claims.org ?? null,
55
+ orgRole: claims.org_role ?? null,
56
+ environmentId: claims.env,
57
+ isSignedIn: true,
58
+ getToken: () => token,
59
+ claims,
60
+ };
61
+ }
62
+ catch {
63
+ return emptyAuth();
64
+ }
65
+ }
66
+ function emptyAuth() {
67
+ return {
68
+ userId: null,
69
+ sessionId: null,
70
+ orgId: null,
71
+ orgRole: null,
72
+ environmentId: null,
73
+ isSignedIn: false,
74
+ getToken: () => null,
75
+ claims: null,
76
+ };
77
+ }
@@ -0,0 +1,8 @@
1
+ export * from './session.js';
2
+ export * from './next.js';
3
+ export * from './middleware.js';
4
+ export * from './route-matcher.js';
5
+ export * from './next-middleware.js';
6
+ export * from './auth.js';
7
+ export { createAuthfyioProxy, type AuthfyioProxyOptions } from './proxy.js';
8
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,WAAW,CAAC;AAC1B,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,sBAAsB,CAAC;AACrC,cAAc,WAAW,CAAC;AAC1B,OAAO,EAAE,mBAAmB,EAAE,KAAK,oBAAoB,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export * from './session.js';
2
+ export * from './next.js';
3
+ export * from './middleware.js';
4
+ export * from './route-matcher.js';
5
+ export * from './next-middleware.js';
6
+ export * from './auth.js';
7
+ export { createAuthfyioProxy } from './proxy.js';
@@ -0,0 +1,12 @@
1
+ import type { NextRequest } from 'next/server';
2
+ import { NextResponse } from 'next/server';
3
+ import { type AuthfyioNextOptions } from './next.js';
4
+ export type RequireSessionMiddlewareOptions = AuthfyioNextOptions & {
5
+ /**
6
+ * If unauthorized, redirect here (e.g. "/sign-in").
7
+ * If omitted, returns 401.
8
+ */
9
+ redirectTo?: string;
10
+ };
11
+ export declare function requireSessionMiddleware(req: NextRequest, opts: RequireSessionMiddlewareOptions): Promise<NextResponse<unknown>>;
12
+ //# sourceMappingURL=middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAEhF,MAAM,MAAM,+BAA+B,GAAG,mBAAmB,GAAG;IAClE;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,wBAAsB,wBAAwB,CAAC,GAAG,EAAE,WAAW,EAAE,IAAI,EAAE,+BAA+B,kCAwBrG"}
@@ -0,0 +1,27 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getSessionFromNextRequest } from './next.js';
3
+ export async function requireSessionMiddleware(req, opts) {
4
+ try {
5
+ const session = await getSessionFromNextRequest(req, opts);
6
+ if (session) {
7
+ const res = NextResponse.next();
8
+ res.headers.set('x-af-user-id', session.sub);
9
+ res.headers.set('x-af-session-id', session.sid);
10
+ res.headers.set('x-af-environment-id', session.env);
11
+ return res;
12
+ }
13
+ }
14
+ catch {
15
+ // treat as unauthorized
16
+ }
17
+ if (opts.redirectTo) {
18
+ const url = req.nextUrl.clone();
19
+ url.pathname = opts.redirectTo;
20
+ url.searchParams.set('next', req.nextUrl.pathname);
21
+ return NextResponse.redirect(url);
22
+ }
23
+ return new NextResponse(JSON.stringify({ error: 'unauthorized' }), {
24
+ status: 401,
25
+ headers: { 'content-type': 'application/json' },
26
+ });
27
+ }
@@ -0,0 +1,51 @@
1
+ import type { NextRequest } from 'next/server';
2
+ import { NextResponse } from 'next/server';
3
+ import { type AuthfyioNextOptions } from './next.js';
4
+ import type { RouteMatcher } from './route-matcher.js';
5
+ export type MiddlewareAuthContext = {
6
+ userId: string | null;
7
+ sessionId: string | null;
8
+ orgId: string | null;
9
+ orgRole: string | null;
10
+ environmentId: string | null;
11
+ isSignedIn: boolean;
12
+ /** Return a NextResponse.redirect to your sign-in page, preserving the caller path. */
13
+ redirectToSignIn: (signInUrl?: string) => NextResponse;
14
+ /** Return a 401 JSON response for API routes. */
15
+ unauthorized: () => NextResponse;
16
+ };
17
+ export type AuthfyioMiddlewareOptions = AuthfyioNextOptions & {
18
+ /** Default redirect target when protected route hits + user is signed out. */
19
+ signInUrl?: string;
20
+ /**
21
+ * Route matcher declaring which paths REQUIRE authentication. If omitted,
22
+ * the middleware verifies opportunistically but never blocks — protect
23
+ * via `auth.protect()` inside your handler instead.
24
+ */
25
+ isProtectedRoute?: RouteMatcher;
26
+ /**
27
+ * Optional custom handler — called with the verified auth context plus the
28
+ * request. Return `undefined` to continue with the default behavior, or any
29
+ * NextResponse to short-circuit.
30
+ */
31
+ handler?: (auth: MiddlewareAuthContext, req: NextRequest) => NextResponse | Promise<NextResponse | undefined> | undefined;
32
+ };
33
+ /**
34
+ * Next.js middleware that verifies `__session` on every request, forwards
35
+ * decoded claims as headers so `auth()` in server components can skip
36
+ * re-verification, and optionally guards routes selected by an
37
+ * `isProtectedRoute` matcher.
38
+ *
39
+ * Usage:
40
+ * // middleware.ts
41
+ * import { authfyioMiddleware, createRouteMatcher } from 'authfyio-nextjs';
42
+ * const isProtected = createRouteMatcher(['/dashboard(.*)', '/api/private(.*)']);
43
+ * export default authfyioMiddleware({
44
+ * baseUrl: process.env.AF_API_BASE_URL!,
45
+ * isProtectedRoute: isProtected,
46
+ * signInUrl: '/sign-in',
47
+ * });
48
+ * export const config = { matcher: ['/((?!_next|.*\\..*).*)'] };
49
+ */
50
+ export declare function authfyioMiddleware(opts: AuthfyioMiddlewareOptions): (req: NextRequest) => Promise<NextResponse>;
51
+ //# sourceMappingURL=next-middleware.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next-middleware.d.ts","sourceRoot":"","sources":["../src/next-middleware.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAA6B,KAAK,mBAAmB,EAAE,MAAM,WAAW,CAAC;AAChF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,MAAM,MAAM,qBAAqB,GAAG;IAClC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,UAAU,EAAE,OAAO,CAAC;IACpB,uFAAuF;IACvF,gBAAgB,EAAE,CAAC,SAAS,CAAC,EAAE,MAAM,KAAK,YAAY,CAAC;IACvD,iDAAiD;IACjD,YAAY,EAAE,MAAM,YAAY,CAAC;CAClC,CAAC;AAEF,MAAM,MAAM,yBAAyB,GAAG,mBAAmB,GAAG;IAC5D,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,YAAY,CAAC;IAChC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,IAAI,EAAE,qBAAqB,EAAE,GAAG,EAAE,WAAW,KAAK,YAAY,GAAG,OAAO,CAAC,YAAY,GAAG,SAAS,CAAC,GAAG,SAAS,CAAC;CAC3H,CAAC;AAEF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,yBAAyB,IAClD,KAAK,WAAW,KAAG,OAAO,CAAC,YAAY,CAAC,CA4CvD"}
@@ -0,0 +1,65 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { getSessionFromNextRequest } from './next.js';
3
+ /**
4
+ * Next.js middleware that verifies `__session` on every request, forwards
5
+ * decoded claims as headers so `auth()` in server components can skip
6
+ * re-verification, and optionally guards routes selected by an
7
+ * `isProtectedRoute` matcher.
8
+ *
9
+ * Usage:
10
+ * // middleware.ts
11
+ * import { authfyioMiddleware, createRouteMatcher } from 'authfyio-nextjs';
12
+ * const isProtected = createRouteMatcher(['/dashboard(.*)', '/api/private(.*)']);
13
+ * export default authfyioMiddleware({
14
+ * baseUrl: process.env.AF_API_BASE_URL!,
15
+ * isProtectedRoute: isProtected,
16
+ * signInUrl: '/sign-in',
17
+ * });
18
+ * export const config = { matcher: ['/((?!_next|.*\\..*).*)'] };
19
+ */
20
+ export function authfyioMiddleware(opts) {
21
+ return async (req) => {
22
+ const claims = await getSessionFromNextRequest(req, opts).catch(() => null);
23
+ const ctx = {
24
+ userId: claims?.sub ?? null,
25
+ sessionId: claims?.sid ?? null,
26
+ orgId: claims?.org ?? null,
27
+ orgRole: claims?.org_role ?? null,
28
+ environmentId: claims?.env ?? null,
29
+ isSignedIn: !!claims,
30
+ redirectToSignIn: (signInUrl) => {
31
+ const url = req.nextUrl.clone();
32
+ url.pathname = signInUrl ?? opts.signInUrl ?? '/sign-in';
33
+ url.searchParams.set('redirect_url', req.nextUrl.pathname + req.nextUrl.search);
34
+ return NextResponse.redirect(url);
35
+ },
36
+ unauthorized: () => new NextResponse(JSON.stringify({ error: 'unauthorized' }), {
37
+ status: 401,
38
+ headers: { 'content-type': 'application/json' },
39
+ }),
40
+ };
41
+ if (opts.handler) {
42
+ const custom = await opts.handler(ctx, req);
43
+ if (custom)
44
+ return custom;
45
+ }
46
+ if (opts.isProtectedRoute && opts.isProtectedRoute(req) && !ctx.isSignedIn) {
47
+ // API routes get JSON 401; pages get a redirect.
48
+ if (req.nextUrl.pathname.startsWith('/api/'))
49
+ return ctx.unauthorized();
50
+ return ctx.redirectToSignIn();
51
+ }
52
+ // Forward claim headers so the server-side `auth()` helper can skip JWKS.
53
+ const res = NextResponse.next();
54
+ if (claims) {
55
+ res.headers.set('x-af-user-id', claims.sub);
56
+ res.headers.set('x-af-session-id', claims.sid);
57
+ res.headers.set('x-af-environment-id', claims.env);
58
+ if (claims.org)
59
+ res.headers.set('x-af-org-id', claims.org);
60
+ if (claims.org_role)
61
+ res.headers.set('x-af-org-role', claims.org_role);
62
+ }
63
+ return res;
64
+ };
65
+ }
package/dist/next.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ import type { NextRequest } from 'next/server';
2
+ import { type SessionClaims } from './session.js';
3
+ /**
4
+ * Canonical Authfyio API URL. Customers using the hosted SaaS shouldn't
5
+ * have to set `AF_API_BASE_URL` — the default points at production. Override
6
+ * this only when self-hosting or pointing at a staging instance.
7
+ */
8
+ export declare const DEFAULT_API_BASE_URL = "https://api.authfyio.com";
9
+ export type AuthfyioNextOptions = {
10
+ /**
11
+ * Base URL of the instance API. Defaults to https://api.authfyio.com
12
+ * (the hosted SaaS). You only need to set this for self-hosted instances
13
+ * or staging.
14
+ */
15
+ baseUrl?: string;
16
+ issuer?: string;
17
+ audience?: string;
18
+ };
19
+ export declare function resolveBaseUrl(opts: AuthfyioNextOptions): string;
20
+ export declare function getJwksUrl(opts: AuthfyioNextOptions): string;
21
+ export declare function getSessionJwtFromNextRequest(req: NextRequest): string | null;
22
+ export declare function getSessionFromNextRequest(req: NextRequest, opts: AuthfyioNextOptions): Promise<SessionClaims | null>;
23
+ //# sourceMappingURL=next.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"next.d.ts","sourceRoot":"","sources":["../src/next.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,OAAO,EAAoB,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAEpE;;;;GAIG;AACH,eAAO,MAAM,oBAAoB,6BAA6B,CAAC;AAE/D,MAAM,MAAM,mBAAmB,GAAG;IAChC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,wBAAgB,cAAc,CAAC,IAAI,EAAE,mBAAmB,GAAG,MAAM,CAEhE;AAED,wBAAgB,UAAU,CAAC,IAAI,EAAE,mBAAmB,UAEnD;AAED,wBAAgB,4BAA4B,CAAC,GAAG,EAAE,WAAW,GAAG,MAAM,GAAG,IAAI,CAE5E;AAED,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,WAAW,EAChB,IAAI,EAAE,mBAAmB,GACxB,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAQ/B"}
package/dist/next.js ADDED
@@ -0,0 +1,26 @@
1
+ import { verifySessionJwt } from './session.js';
2
+ /**
3
+ * Canonical Authfyio API URL. Customers using the hosted SaaS shouldn't
4
+ * have to set `AF_API_BASE_URL` — the default points at production. Override
5
+ * this only when self-hosting or pointing at a staging instance.
6
+ */
7
+ export const DEFAULT_API_BASE_URL = 'https://api.authfyio.com';
8
+ export function resolveBaseUrl(opts) {
9
+ return (opts.baseUrl ?? process.env.AF_API_BASE_URL ?? DEFAULT_API_BASE_URL).replace(/\/+$/, '');
10
+ }
11
+ export function getJwksUrl(opts) {
12
+ return `${resolveBaseUrl(opts)}/.well-known/jwks.json`;
13
+ }
14
+ export function getSessionJwtFromNextRequest(req) {
15
+ return req.cookies.get('__session')?.value ?? null;
16
+ }
17
+ export async function getSessionFromNextRequest(req, opts) {
18
+ const token = getSessionJwtFromNextRequest(req);
19
+ if (!token)
20
+ return null;
21
+ return await verifySessionJwt(token, {
22
+ jwksUrl: getJwksUrl(opts),
23
+ issuer: opts.issuer,
24
+ audience: opts.audience,
25
+ });
26
+ }
@@ -0,0 +1,34 @@
1
+ import { NextResponse } from 'next/server';
2
+ export type AuthfyioProxyOptions = {
3
+ /**
4
+ * Upstream base URL. Defaults to `process.env.AF_API_BASE_URL` or
5
+ * `https://api.authfyio.com`.
6
+ */
7
+ baseUrl?: string;
8
+ };
9
+ type RouteContext = {
10
+ params: Promise<{
11
+ path: string[];
12
+ }>;
13
+ };
14
+ /**
15
+ * Builds a set of route handlers bound to a specific upstream URL. Use this
16
+ * when you self-host the API or run multiple environments — pass the
17
+ * appropriate `baseUrl` per environment.
18
+ */
19
+ export declare function createAuthfyioProxy(opts?: AuthfyioProxyOptions): {
20
+ GET: (req: Request, ctx: RouteContext) => Promise<NextResponse<unknown>>;
21
+ POST: (req: Request, ctx: RouteContext) => Promise<NextResponse<unknown>>;
22
+ PUT: (req: Request, ctx: RouteContext) => Promise<NextResponse<unknown>>;
23
+ PATCH: (req: Request, ctx: RouteContext) => Promise<NextResponse<unknown>>;
24
+ DELETE: (req: Request, ctx: RouteContext) => Promise<NextResponse<unknown>>;
25
+ OPTIONS: (req: Request, ctx: RouteContext) => Promise<NextResponse<unknown>>;
26
+ };
27
+ export declare const GET: (req: Request, ctx: RouteContext) => Promise<NextResponse<unknown>>;
28
+ export declare const POST: (req: Request, ctx: RouteContext) => Promise<NextResponse<unknown>>;
29
+ export declare const PUT: (req: Request, ctx: RouteContext) => Promise<NextResponse<unknown>>;
30
+ export declare const PATCH: (req: Request, ctx: RouteContext) => Promise<NextResponse<unknown>>;
31
+ export declare const DELETE: (req: Request, ctx: RouteContext) => Promise<NextResponse<unknown>>;
32
+ export declare const OPTIONS: (req: Request, ctx: RouteContext) => Promise<NextResponse<unknown>>;
33
+ export {};
34
+ //# sourceMappingURL=proxy.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"proxy.d.ts","sourceRoot":"","sources":["../src/proxy.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AA8C3C,MAAM,MAAM,oBAAoB,GAAG;IACjC;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,KAAK,YAAY,GAAG;IAAE,MAAM,EAAE,OAAO,CAAC;QAAE,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAA;CAAE,CAAC;AAiG5D;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,oBAAyB;eAC3C,OAAO,OAAO,YAAY;gBAA1B,OAAO,OAAO,YAAY;eAA1B,OAAO,OAAO,YAAY;iBAA1B,OAAO,OAAO,YAAY;kBAA1B,OAAO,OAAO,YAAY;mBAA1B,OAAO,OAAO,YAAY;EASjD;AAID,eAAO,MAAM,GAAG,QAbQ,OAAO,OAAO,YAAY,mCAaf,CAAC;AACpC,eAAO,MAAM,IAAI,QAdO,OAAO,OAAO,YAAY,mCAcb,CAAC;AACtC,eAAO,MAAM,GAAG,QAfQ,OAAO,OAAO,YAAY,mCAef,CAAC;AACpC,eAAO,MAAM,KAAK,QAhBM,OAAO,OAAO,YAAY,mCAgBX,CAAC;AACxC,eAAO,MAAM,MAAM,QAjBK,OAAO,OAAO,YAAY,mCAiBT,CAAC;AAC1C,eAAO,MAAM,OAAO,QAlBI,OAAO,OAAO,YAAY,mCAkBP,CAAC"}
package/dist/proxy.js ADDED
@@ -0,0 +1,149 @@
1
+ import { NextResponse } from 'next/server';
2
+ import { DEFAULT_API_BASE_URL } from './next.js';
3
+ /**
4
+ * Same-origin reverse proxy for the Authfyio instance API. Customers mount
5
+ * this at `/api/af/[...path]/route.ts` so the React SDK's browser fetches
6
+ * stay on the customer's own origin — that's what makes the `__session` and
7
+ * `__client` cookies set by `api.authfyio.com` actually arrive at the
8
+ * customer's app.
9
+ *
10
+ * Without this proxy, cookies set on `api.authfyio.com` are scoped to that
11
+ * eTLD+1 and the browser refuses to send them to `myapp.com` (or
12
+ * `localhost:3000`). With this proxy, every Set-Cookie from upstream is
13
+ * re-emitted on the customer's origin with the `Domain=` attribute stripped,
14
+ * so the cookies land where the React SDK can use them.
15
+ *
16
+ * Usage:
17
+ *
18
+ * // app/api/af/[...path]/route.ts
19
+ * export {
20
+ * GET, POST, PUT, PATCH, DELETE, OPTIONS,
21
+ * } from 'authfyio-nextjs/proxy';
22
+ *
23
+ * The proxy resolves the upstream URL from `process.env.AF_API_BASE_URL`,
24
+ * falling back to `https://api.authfyio.com`. Override at call site by
25
+ * importing `createAuthfyioProxy({ baseUrl })` instead.
26
+ */
27
+ const FORWARD_REQUEST_HEADERS = new Set([
28
+ 'accept',
29
+ 'accept-language',
30
+ 'authorization',
31
+ 'content-type',
32
+ 'cookie',
33
+ 'user-agent',
34
+ 'x-authfyio-publishable-key',
35
+ ]);
36
+ const STRIP_RESPONSE_HEADERS = new Set([
37
+ 'transfer-encoding',
38
+ 'content-encoding',
39
+ 'content-length',
40
+ 'connection',
41
+ ]);
42
+ function resolveUpstream(opts) {
43
+ return (opts.baseUrl ?? process.env.AF_API_BASE_URL ?? DEFAULT_API_BASE_URL).replace(/\/+$/, '');
44
+ }
45
+ async function forward(req, ctx, opts) {
46
+ const upstreamBase = resolveUpstream(opts);
47
+ const { path } = await ctx.params;
48
+ const search = new URL(req.url).search;
49
+ // Special-case: OAuth bridge. The Authfyio API's OAuth callback
50
+ // 302s the browser to `${customer_origin}/api/af/oauth-finish?ott=…&to=…`.
51
+ // We exchange the one-time token for cookies (server-to-server POST
52
+ // with cookie: omitted), re-emit the Set-Cookie headers on our origin,
53
+ // and 302 the user to the `to` path so the browser lands at the app's
54
+ // intended destination already authenticated.
55
+ if (path[0] === 'oauth-finish') {
56
+ return await oauthFinish(req, upstreamBase);
57
+ }
58
+ const target = `${upstreamBase}/${path.map(encodeURIComponent).join('/')}${search}`;
59
+ const headers = {};
60
+ req.headers.forEach((value, key) => {
61
+ if (FORWARD_REQUEST_HEADERS.has(key.toLowerCase()))
62
+ headers[key] = value;
63
+ });
64
+ // Backstop the publishable-key header from the customer's server env.
65
+ // Browser fetch() requests with `credentials: 'include'` carry the
66
+ // header set by the React SDK, but top-level navigations (anchor
67
+ // clicks for OAuth /authorize, magic-link /verify GETs) don't. Without
68
+ // this, those requests would lose tenant identity on every page-level
69
+ // jump and the API would fall back to the env-pinned default.
70
+ if (!headers['x-authfyio-publishable-key']) {
71
+ const envKey = process.env.NEXT_PUBLIC_AUTHFYIO_PUBLISHABLE_KEY ?? process.env.AUTHFYIO_PUBLISHABLE_KEY;
72
+ if (envKey)
73
+ headers['x-authfyio-publishable-key'] = envKey;
74
+ }
75
+ const init = { method: req.method, headers, redirect: 'manual' };
76
+ if (req.method !== 'GET' && req.method !== 'HEAD') {
77
+ init.body = await req.arrayBuffer();
78
+ }
79
+ const upstream = await fetch(target, init);
80
+ const body = await upstream.arrayBuffer();
81
+ const res = new NextResponse(body, { status: upstream.status });
82
+ upstream.headers.forEach((value, key) => {
83
+ const k = key.toLowerCase();
84
+ if (STRIP_RESPONSE_HEADERS.has(k))
85
+ return;
86
+ if (k === 'set-cookie')
87
+ return;
88
+ res.headers.set(key, value);
89
+ });
90
+ // Re-emit Set-Cookie with Domain= stripped so the browser scopes the
91
+ // cookies to the customer's origin instead of api.authfyio.com.
92
+ const setCookies = upstream.headers.getSetCookie?.() ??
93
+ upstream.headers.get('set-cookie')?.split(/,(?=\s*\w+=)/) ??
94
+ [];
95
+ for (const raw of setCookies) {
96
+ res.headers.append('set-cookie', raw.replace(/;\s*Domain=[^;]+/i, ''));
97
+ }
98
+ return res;
99
+ }
100
+ async function oauthFinish(req, upstreamBase) {
101
+ const url = new URL(req.url);
102
+ const ott = url.searchParams.get('ott');
103
+ const to = url.searchParams.get('to') || '/';
104
+ if (!ott) {
105
+ return new NextResponse('missing_ott', { status: 400 });
106
+ }
107
+ const upstream = await fetch(`${upstreamBase}/v1/auth/oauth/redeem`, {
108
+ method: 'POST',
109
+ headers: { 'content-type': 'application/json' },
110
+ body: JSON.stringify({ ott }),
111
+ redirect: 'manual',
112
+ });
113
+ if (!upstream.ok) {
114
+ return new NextResponse(`oauth_redeem_failed_${upstream.status}`, { status: upstream.status });
115
+ }
116
+ // Build the 302 response to the customer's `to` URL with the
117
+ // upstream cookies re-emitted (Domain stripped) on our origin.
118
+ const res = NextResponse.redirect(new URL(to, req.url), 302);
119
+ const setCookies = upstream.headers.getSetCookie?.() ??
120
+ upstream.headers.get('set-cookie')?.split(/,(?=\s*\w+=)/) ??
121
+ [];
122
+ for (const raw of setCookies) {
123
+ res.headers.append('set-cookie', raw.replace(/;\s*Domain=[^;]+/i, ''));
124
+ }
125
+ return res;
126
+ }
127
+ /**
128
+ * Builds a set of route handlers bound to a specific upstream URL. Use this
129
+ * when you self-host the API or run multiple environments — pass the
130
+ * appropriate `baseUrl` per environment.
131
+ */
132
+ export function createAuthfyioProxy(opts = {}) {
133
+ const handler = (req, ctx) => forward(req, ctx, opts);
134
+ return {
135
+ GET: handler,
136
+ POST: handler,
137
+ PUT: handler,
138
+ PATCH: handler,
139
+ DELETE: handler,
140
+ OPTIONS: handler,
141
+ };
142
+ }
143
+ const defaultProxy = createAuthfyioProxy();
144
+ export const GET = defaultProxy.GET;
145
+ export const POST = defaultProxy.POST;
146
+ export const PUT = defaultProxy.PUT;
147
+ export const PATCH = defaultProxy.PATCH;
148
+ export const DELETE = defaultProxy.DELETE;
149
+ export const OPTIONS = defaultProxy.OPTIONS;
@@ -0,0 +1,17 @@
1
+ import type { NextRequest } from 'next/server';
2
+ export type RouteMatcher = (req: NextRequest) => boolean;
3
+ /**
4
+ * Returns a function that matches incoming Next.js requests against a list
5
+ * of route patterns.
6
+ *
7
+ * Patterns can be:
8
+ * * Exact paths: '/about'
9
+ * * Wildcards: '/admin(.*)' → matches '/admin', '/admin/users', etc.
10
+ * * Parameter segments: '/users/:id' → '/users/42'
11
+ * * RegExp objects: /^\/api\/.+/
12
+ *
13
+ * String patterns are compiled once into a RegExp that anchors at start AND
14
+ * end to prevent accidental prefix matches.
15
+ */
16
+ export declare function createRouteMatcher(patterns: Array<string | RegExp>): RouteMatcher;
17
+ //# sourceMappingURL=route-matcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-matcher.d.ts","sourceRoot":"","sources":["../src/route-matcher.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,MAAM,YAAY,GAAG,CAAC,GAAG,EAAE,WAAW,KAAK,OAAO,CAAC;AAEzD;;;;;;;;;;;;GAYG;AACH,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,YAAY,CASjF"}
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Returns a function that matches incoming Next.js requests against a list
3
+ * of route patterns.
4
+ *
5
+ * Patterns can be:
6
+ * * Exact paths: '/about'
7
+ * * Wildcards: '/admin(.*)' → matches '/admin', '/admin/users', etc.
8
+ * * Parameter segments: '/users/:id' → '/users/42'
9
+ * * RegExp objects: /^\/api\/.+/
10
+ *
11
+ * String patterns are compiled once into a RegExp that anchors at start AND
12
+ * end to prevent accidental prefix matches.
13
+ */
14
+ export function createRouteMatcher(patterns) {
15
+ const compiled = patterns.map((p) => (p instanceof RegExp ? p : compileStringPattern(p)));
16
+ return (req) => {
17
+ const pathname = req.nextUrl.pathname;
18
+ for (const re of compiled) {
19
+ if (re.test(pathname))
20
+ return true;
21
+ }
22
+ return false;
23
+ };
24
+ }
25
+ function compileStringPattern(pattern) {
26
+ // Escape regex metacharacters, then translate our allowed wildcards:
27
+ // :param → [^/]+
28
+ // (.*) → left as-is (greedy wildcard, already regex)
29
+ // * → [^/]*
30
+ let src = pattern;
31
+ // Preserve explicit "(.*)" — user already opted into a full regex group.
32
+ // Split on "(.*)" so the escaper doesn't mangle the parens.
33
+ const parts = src.split(/(\(\.\*\))/g);
34
+ src = parts
35
+ .map((seg) => {
36
+ if (seg === '(.*)')
37
+ return '.*';
38
+ return seg
39
+ .replace(/[\\^$+?.()|[\]{}]/g, '\\$&')
40
+ .replace(/\*/g, '[^/]*')
41
+ .replace(/:([A-Za-z0-9_]+)/g, '[^/]+');
42
+ })
43
+ .join('');
44
+ return new RegExp(`^${src}\\/?$`);
45
+ }
@@ -0,0 +1,19 @@
1
+ export type SessionClaims = {
2
+ sid: string;
3
+ sub: string;
4
+ env: string;
5
+ org?: string;
6
+ org_role?: string;
7
+ iat?: number;
8
+ exp?: number;
9
+ iss?: string;
10
+ aud?: string | string[];
11
+ };
12
+ export type VerifySessionJwtOptions = {
13
+ jwksUrl: string;
14
+ issuer?: string;
15
+ audience?: string;
16
+ clockToleranceSeconds?: number;
17
+ };
18
+ export declare function verifySessionJwt(token: string, opts: VerifySessionJwtOptions): Promise<SessionClaims>;
19
+ //# sourceMappingURL=session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,aAAa,GAAG;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,wBAAsB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,uBAAuB,GAAG,OAAO,CAAC,aAAa,CAAC,CAQ3G"}
@@ -0,0 +1,10 @@
1
+ import { createRemoteJWKSet, jwtVerify } from 'jose';
2
+ export async function verifySessionJwt(token, opts) {
3
+ const jwks = createRemoteJWKSet(new URL(opts.jwksUrl));
4
+ const { payload } = await jwtVerify(token, jwks, {
5
+ issuer: opts.issuer,
6
+ audience: opts.audience,
7
+ clockTolerance: opts.clockToleranceSeconds,
8
+ });
9
+ return payload;
10
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "authfyio-nextjs",
3
+ "version": "0.2.3",
4
+ "description": "Next.js helpers for Authfyio — middleware, route matchers, server-side auth(), built-in cookie-bridge proxy.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./proxy": {
15
+ "types": "./dist/proxy.d.ts",
16
+ "default": "./dist/proxy.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsc -p tsconfig.build.json",
25
+ "typecheck": "tsc -p tsconfig.build.json --noEmit",
26
+ "prepublishOnly": "npm run build"
27
+ },
28
+ "peerDependencies": {
29
+ "next": ">=14"
30
+ },
31
+ "dependencies": {
32
+ "jose": "^5.9.6"
33
+ },
34
+ "keywords": [
35
+ "authfyio",
36
+ "auth",
37
+ "authentication",
38
+ "nextjs",
39
+ "middleware",
40
+ "jwt"
41
+ ],
42
+ "publishConfig": {
43
+ "access": "public"
44
+ },
45
+ "homepage": "https://authfyio.com/docs",
46
+ "repository": {
47
+ "type": "git",
48
+ "url": "git+https://github.com/authfyio/authfyio.git"
49
+ }
50
+ }