@lpshanley/oidc-start-react 0.0.0-stage → 0.1.0-next.1
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 +111 -2
- package/dist/context.d.ts +17 -0
- package/dist/context.js +16 -0
- package/dist/functions.d.ts +17 -0
- package/dist/functions.js +21 -0
- package/dist/functions.server.d.ts +4 -0
- package/dist/functions.server.js +21 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +2 -0
- package/dist/middleware.d.ts +19 -0
- package/dist/middleware.js +50 -0
- package/dist/routes.d.ts +13 -0
- package/dist/routes.js +30 -0
- package/dist/routes.server.d.ts +10 -0
- package/dist/routes.server.js +73 -0
- package/dist/server.d.ts +1 -0
- package/dist/server.js +1 -0
- package/dist/sign-out.server.d.ts +1 -0
- package/dist/sign-out.server.js +18 -0
- package/package.json +60 -3
package/README.md
CHANGED
|
@@ -1,3 +1,112 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lpshanley/oidc-start-react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Add OpenID Connect sign-in to a TanStack Start app. Loaders can read the current user, components can start sign-in or sign-out, and Start’s middleware handles the session cookies. The code exchange and provider tokens stay on the server.
|
|
4
|
+
|
|
5
|
+
This guide adds two routes to an existing app. The home page at `/` shows who is signed in. The callback at `/callback` receives the browser when the provider sends it back. [Complete example app](https://github.com/lpshanley/oidc-start/tree/main/examples/tanstack-start)
|
|
6
|
+
|
|
7
|
+
## 1. Install and configure the server
|
|
8
|
+
|
|
9
|
+
Use Node.js 22.12 or newer. Your app supplies `@tanstack/react-start@^1.169.2` and `@tanstack/react-router@^1.170.44`. Install both OIDC packages because your app imports the core directly.
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install @lpshanley/oidc-start-react@next @lpshanley/oidc-start-core@next
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Register a confidential web client with your OIDC provider. If your app runs at `http://localhost:5173`, register these exact URLs. Use your public HTTPS origin in production.
|
|
16
|
+
|
|
17
|
+
| Provider setting | Local value |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| Sign-in redirect URI | `http://localhost:5173/callback` |
|
|
20
|
+
| Post-logout redirect URI | `http://localhost:5173/` |
|
|
21
|
+
|
|
22
|
+
Set these values in your **server environment**. Generate `SESSION_SECRET` once with `openssl rand -hex 32` and reuse it across server instances. Your provider must supply the account’s email in the ID token or through UserInfo.
|
|
23
|
+
|
|
24
|
+
```dotenv
|
|
25
|
+
APP_ORIGIN=http://localhost:5173
|
|
26
|
+
OIDC_ISSUER=https://id.example.com
|
|
27
|
+
OIDC_CLIENT_ID=your-client-id
|
|
28
|
+
OIDC_CLIENT_SECRET=your-client-secret
|
|
29
|
+
# Paste the generated secret here before starting the app.
|
|
30
|
+
SESSION_SECRET=
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
For local development, copy the example’s `auth.server.ts` into `src/auth.server.ts`. It exports an `auth` instance configured with `callbackPath: '/callback'` and `postLogoutPath: '/'`. The example admits any authenticated user and keeps revocations in memory. For deployment, set `allowSession` to your access policy and use a durable revocation store shared by all instances. [Copy auth.server.ts](https://github.com/lpshanley/oidc-start/blob/main/examples/tanstack-start/src/auth.server.ts) · [Core configuration and storage](https://www.npmjs.com/package/@lpshanley/oidc-start-core)
|
|
34
|
+
|
|
35
|
+
## 2. Register the request middleware
|
|
36
|
+
|
|
37
|
+
In `src/start.ts`, add Start’s CSRF middleware before the OIDC middleware. The first checks the origin of browser calls to server functions, including sign-out. The second resolves the session for the request and writes any refreshed cookies. The `createServerOnlyFn` wrapper keeps your `auth` instance out of the client bundle.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { oidcMiddleware } from '@lpshanley/oidc-start-react'
|
|
41
|
+
import { createCsrfMiddleware, createServerOnlyFn, createStart } from '@tanstack/react-start'
|
|
42
|
+
import { auth } from './auth.server'
|
|
43
|
+
|
|
44
|
+
const csrf = createCsrfMiddleware({ filter: ctx => ctx.handlerType === 'serverFn' })
|
|
45
|
+
const oidc = oidcMiddleware({ core: createServerOnlyFn(() => auth) })
|
|
46
|
+
|
|
47
|
+
export const startInstance = createStart(() => ({
|
|
48
|
+
requestMiddleware: [csrf, oidc],
|
|
49
|
+
}))
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## 3. Add the callback route
|
|
53
|
+
|
|
54
|
+
Create `src/routes/callback.ts`. The handler checks the provider response, establishes the session, and redirects to the local path chosen at sign-in.
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { handleCallbackRoute } from '@lpshanley/oidc-start-react'
|
|
58
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
59
|
+
|
|
60
|
+
export const Route = createFileRoute('/callback')({
|
|
61
|
+
server: { handlers: { GET: handleCallbackRoute() } },
|
|
62
|
+
})
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 4. Connect the buttons
|
|
66
|
+
|
|
67
|
+
In `src/routes/index.tsx`, call `getAuth()` from the loader. It returns `{ user }`, with `user: null` for a signed-out visitor. Bind `signOut` with `useServerFn` so TanStack follows the redirect to the provider when the user clicks the button.
|
|
68
|
+
|
|
69
|
+
```tsx
|
|
70
|
+
import { displayName } from '@lpshanley/oidc-start-core/models'
|
|
71
|
+
import { getAuth, getSignInUrl, signOut } from '@lpshanley/oidc-start-react/functions'
|
|
72
|
+
import { createFileRoute } from '@tanstack/react-router'
|
|
73
|
+
import { useServerFn } from '@tanstack/react-start'
|
|
74
|
+
|
|
75
|
+
export const Route = createFileRoute('/')({
|
|
76
|
+
loader: () => getAuth(),
|
|
77
|
+
component: Home,
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
function Home() {
|
|
81
|
+
const { user } = Route.useLoaderData()
|
|
82
|
+
const signOutAndRedirect = useServerFn(signOut)
|
|
83
|
+
|
|
84
|
+
if (!user) {
|
|
85
|
+
return <button onClick={async () => location.assign(await getSignInUrl())}>Sign in</button>
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return (
|
|
89
|
+
<main>
|
|
90
|
+
<p>Signed in as {displayName(user)}.</p>
|
|
91
|
+
<button onClick={() => signOutAndRedirect()}>Sign out</button>
|
|
92
|
+
</main>
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Try it.** Open `/` in a fresh browser session and choose **Sign in**. After the provider redirects you back, your name should appear and survive a reload. Choose **Sign out**, finish logout at the provider if prompted, and reload. You should see **Sign in** again.
|
|
98
|
+
|
|
99
|
+
To return someone to a specific page, pass a local path when starting sign-in, for example `getSignInUrl({ data: { returnPathname: '/account' } })`.
|
|
100
|
+
|
|
101
|
+
## HTTP routes and server access
|
|
102
|
+
|
|
103
|
+
The buttons above use server functions. If you prefer ordinary links and forms, mount these handlers from `@lpshanley/oidc-start-react` behind the same OIDC middleware. Choose the paths in your app.
|
|
104
|
+
|
|
105
|
+
| Method | Handler | When to use it |
|
|
106
|
+
| --- | --- | --- |
|
|
107
|
+
| GET | `handleSignInRoute()` | Link to sign-in. The optional `returnPathname` query parameter selects a local return path. |
|
|
108
|
+
| GET | `handleCallbackRoute()` | Mount at the exact callback path registered with your provider. |
|
|
109
|
+
| POST | `handleSignOutRoute()` | Submit a same-origin form. Add `server.middleware: [createCsrfMiddleware()]` to this route because the global filter above only covers server functions. |
|
|
110
|
+
| POST | `handleBackchannelLogoutRoute()` | Register this URL with your provider for back-channel logout. The provider sends a signed logout token without a browser Origin header. Keep the browser CSRF check scoped to the browser routes. |
|
|
111
|
+
|
|
112
|
+
In a server handler, import `getOidcContext` from `@lpshanley/oidc-start-react/server` to access the resolved session and core for the current request. Use `getAuth()` from `/functions` when a loader or component only needs the user.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OidcCallContext } from "@lpshanley/oidc-start-core/config";
|
|
2
|
+
import type { OidcAuth, OidcCore } from "@lpshanley/oidc-start-core";
|
|
3
|
+
import type { Session } from "@lpshanley/oidc-start-core";
|
|
4
|
+
/** Request-scoped auth state that oidcMiddleware places in Start's context. */
|
|
5
|
+
export type OidcContext = {
|
|
6
|
+
core: OidcCore;
|
|
7
|
+
request: Request;
|
|
8
|
+
cookies: Map<string, string>;
|
|
9
|
+
session: Session | null;
|
|
10
|
+
/** The caller's request context, when one was provided upstream. */
|
|
11
|
+
call: OidcCallContext | undefined;
|
|
12
|
+
auth: () => OidcAuth;
|
|
13
|
+
/** Queue a Set-Cookie line for the final response. */
|
|
14
|
+
appendSetCookie: (line: string) => void;
|
|
15
|
+
};
|
|
16
|
+
/** The current request's OIDC context; throws outside the middleware. */
|
|
17
|
+
export declare function getOidcContext(): OidcContext;
|
package/dist/context.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { getGlobalStartContext } from "@tanstack/react-start";
|
|
2
|
+
const NOT_CONFIGURED = "OIDC middleware is not configured. Add oidcMiddleware() to requestMiddleware in src/start.ts.";
|
|
3
|
+
function isOidcContext(value) {
|
|
4
|
+
return (typeof value === "object" &&
|
|
5
|
+
value !== null &&
|
|
6
|
+
"core" in value &&
|
|
7
|
+
"auth" in value &&
|
|
8
|
+
"appendSetCookie" in value);
|
|
9
|
+
}
|
|
10
|
+
/** The current request's OIDC context; throws outside the middleware. */
|
|
11
|
+
export function getOidcContext() {
|
|
12
|
+
const context = getGlobalStartContext();
|
|
13
|
+
if (!isOidcContext(context?.oidc))
|
|
14
|
+
throw new Error(NOT_CONFIGURED);
|
|
15
|
+
return context.oidc;
|
|
16
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** The signed-in user, or null. Never returns tokens. */
|
|
3
|
+
export declare const getAuth: import("@tanstack/react-start").OptionalFetcher<undefined, undefined, Promise<{
|
|
4
|
+
user: {
|
|
5
|
+
sub: string;
|
|
6
|
+
email: string;
|
|
7
|
+
name: string | null;
|
|
8
|
+
picture?: string | null | undefined;
|
|
9
|
+
roles?: string[] | undefined;
|
|
10
|
+
} | null;
|
|
11
|
+
}>>;
|
|
12
|
+
/** An authorization URL; also queues the flow's verifier cookie. */
|
|
13
|
+
export declare const getSignInUrl: import("@tanstack/react-start").OptionalFetcher<undefined, z.ZodOptional<z.ZodObject<{
|
|
14
|
+
returnPathname: z.ZodOptional<z.ZodString>;
|
|
15
|
+
}, z.core.$strip>>, Promise<string>>;
|
|
16
|
+
/** Clears the session and redirects to provider logout. In a component, call via useServerFn. */
|
|
17
|
+
export declare const signOut: import("@tanstack/react-start").OptionalFetcher<undefined, undefined, Promise<never>>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { createServerFn } from "@tanstack/react-start";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
// Server functions after WorkOS's server-functions.ts; bodies load lazily so
|
|
4
|
+
// server code stays out of the client bundle.
|
|
5
|
+
/** The signed-in user, or null. Never returns tokens. */
|
|
6
|
+
export const getAuth = createServerFn({ method: "GET" }).handler(async () => {
|
|
7
|
+
const { getAuthBody } = await import("./functions.server.js");
|
|
8
|
+
return getAuthBody();
|
|
9
|
+
});
|
|
10
|
+
/** An authorization URL; also queues the flow's verifier cookie. */
|
|
11
|
+
export const getSignInUrl = createServerFn({ method: "GET" })
|
|
12
|
+
.validator(z.object({ returnPathname: z.string().optional() }).optional())
|
|
13
|
+
.handler(async ({ data }) => {
|
|
14
|
+
const { getSignInUrlBody } = await import("./functions.server.js");
|
|
15
|
+
return getSignInUrlBody(data?.returnPathname);
|
|
16
|
+
});
|
|
17
|
+
/** Clears the session and redirects to provider logout. In a component, call via useServerFn. */
|
|
18
|
+
export const signOut = createServerFn({ method: "POST" }).handler(async () => {
|
|
19
|
+
const { signOutBody } = await import("./functions.server.js");
|
|
20
|
+
return signOutBody();
|
|
21
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { redirect } from "@tanstack/react-router";
|
|
2
|
+
import { setResponseHeader } from "@tanstack/react-start/server";
|
|
3
|
+
import { endSession } from "./sign-out.server.js";
|
|
4
|
+
import { getOidcContext } from "./context.js";
|
|
5
|
+
export function getAuthBody() {
|
|
6
|
+
setResponseHeader("Cache-Control", "no-store");
|
|
7
|
+
return { user: getOidcContext().auth().user };
|
|
8
|
+
}
|
|
9
|
+
export async function getSignInUrlBody(returnPathname) {
|
|
10
|
+
const context = getOidcContext();
|
|
11
|
+
const { url, setCookies } = await context.core.signIn(context.request, context.cookies, returnPathname, context.call);
|
|
12
|
+
for (const line of setCookies)
|
|
13
|
+
context.appendSetCookie(line);
|
|
14
|
+
return url;
|
|
15
|
+
}
|
|
16
|
+
export async function signOutBody() {
|
|
17
|
+
const result = await endSession();
|
|
18
|
+
if (result instanceof Response)
|
|
19
|
+
throw result;
|
|
20
|
+
throw redirect({ href: result, reloadDocument: true });
|
|
21
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export type { OidcCallContext, OidcErrorEvent, OidcMiddlewareOptions, } from "@lpshanley/oidc-start-core/config";
|
|
2
|
+
export { type OidcMiddlewareSetup, oidcMiddleware } from "./middleware.js";
|
|
3
|
+
export { type CallbackRouteOptions, handleBackchannelLogoutRoute, handleCallbackRoute, handleSignInRoute, handleSignOutRoute, } from "./routes.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { OidcMiddlewareOptions } from "@lpshanley/oidc-start-core/config";
|
|
2
|
+
import type { OidcContext } from "./context.js";
|
|
3
|
+
import type { OidcCore } from "@lpshanley/oidc-start-core";
|
|
4
|
+
export type OidcMiddlewareSetup = OidcMiddlewareOptions | {
|
|
5
|
+
/**
|
|
6
|
+
* A core the app composed itself, resolved on first request from
|
|
7
|
+
* the context earlier request middleware provided.
|
|
8
|
+
*/
|
|
9
|
+
core: (context: unknown) => OidcCore;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Request middleware after WorkOS's authkitMiddleware: resolves the session
|
|
13
|
+
* (refreshing or clearing it as needed), exposes it as `context.oidc`, and
|
|
14
|
+
* appends every queued Set-Cookie line to the final response. Create it once
|
|
15
|
+
* outside createStart so discovery and refresh state persist across requests.
|
|
16
|
+
*/
|
|
17
|
+
export declare function oidcMiddleware(setup: OidcMiddlewareSetup): import("@tanstack/react-start").RequestMiddlewareAfterServer<{}, undefined, {
|
|
18
|
+
oidc: OidcContext;
|
|
19
|
+
}>;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createMiddleware } from "@tanstack/react-start";
|
|
2
|
+
/** `context.requestContext` from an earlier middleware, when it has the shape. */
|
|
3
|
+
function callContext(context) {
|
|
4
|
+
const value = context
|
|
5
|
+
?.requestContext;
|
|
6
|
+
if (typeof value !== "object" || value === null)
|
|
7
|
+
return undefined;
|
|
8
|
+
const { correlationId, deadline, signal } = value;
|
|
9
|
+
return {
|
|
10
|
+
...(signal instanceof AbortSignal ? { signal } : {}),
|
|
11
|
+
correlationId: typeof correlationId === "string" ? correlationId : undefined,
|
|
12
|
+
deadline: typeof deadline === "number" ? deadline : undefined,
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Request middleware after WorkOS's authkitMiddleware: resolves the session
|
|
17
|
+
* (refreshing or clearing it as needed), exposes it as `context.oidc`, and
|
|
18
|
+
* appends every queued Set-Cookie line to the final response. Create it once
|
|
19
|
+
* outside createStart so discovery and refresh state persist across requests.
|
|
20
|
+
*/
|
|
21
|
+
export function oidcMiddleware(setup) {
|
|
22
|
+
let core;
|
|
23
|
+
return createMiddleware({ type: "request" }).server(async ({ request, next, context }) => {
|
|
24
|
+
if ("core" in setup)
|
|
25
|
+
core ??= setup.core(context);
|
|
26
|
+
else {
|
|
27
|
+
const { createOidcCore } = await import("@lpshanley/oidc-start-core");
|
|
28
|
+
core ??= createOidcCore(setup);
|
|
29
|
+
}
|
|
30
|
+
const call = callContext(context);
|
|
31
|
+
const { cookies, session, setCookies } = await core.resolve(request, call);
|
|
32
|
+
const pending = [...setCookies];
|
|
33
|
+
const oidc = {
|
|
34
|
+
core,
|
|
35
|
+
request,
|
|
36
|
+
cookies,
|
|
37
|
+
session,
|
|
38
|
+
call,
|
|
39
|
+
auth: () => ({ user: session?.user ?? null }),
|
|
40
|
+
appendSetCookie: (line) => pending.push(line),
|
|
41
|
+
};
|
|
42
|
+
const result = await next({ context: { oidc } });
|
|
43
|
+
if (pending.length) {
|
|
44
|
+
// Start must receive its original response to serialize RPC redirects.
|
|
45
|
+
const { appendResponseHeader } = await import("@tanstack/react-start/server");
|
|
46
|
+
appendResponseHeader("Set-Cookie", pending);
|
|
47
|
+
}
|
|
48
|
+
return result;
|
|
49
|
+
});
|
|
50
|
+
}
|
package/dist/routes.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { CallbackRouteOptions } from "./routes.server.js";
|
|
2
|
+
export type { CallbackRouteOptions };
|
|
3
|
+
type RouteHandler = (args: {
|
|
4
|
+
request: Request;
|
|
5
|
+
}) => Promise<Response>;
|
|
6
|
+
/** GET: redirects to the provider; `?returnPathname=` sets the destination. */
|
|
7
|
+
export declare function handleSignInRoute(options?: CallbackRouteOptions): RouteHandler;
|
|
8
|
+
/** GET: completes sign-in and redirects to the sanitized return path. */
|
|
9
|
+
export declare function handleCallbackRoute(options?: CallbackRouteOptions): RouteHandler;
|
|
10
|
+
/** POST: the provider's back-channel logout; ends the sessions its token names. */
|
|
11
|
+
export declare function handleBackchannelLogoutRoute(): RouteHandler;
|
|
12
|
+
/** POST: clears the session and redirects to the provider logout. */
|
|
13
|
+
export declare function handleSignOutRoute(): RouteHandler;
|
package/dist/routes.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Server route handler factories, after WorkOS's handleCallbackRoute. They run
|
|
2
|
+
// behind oidcMiddleware, which applies the cookies they queue.
|
|
3
|
+
/** GET: redirects to the provider; `?returnPathname=` sets the destination. */
|
|
4
|
+
export function handleSignInRoute(options = {}) {
|
|
5
|
+
return async ({ request }) => {
|
|
6
|
+
const { signInRoute } = await import("./routes.server.js");
|
|
7
|
+
return signInRoute(request, options);
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
/** GET: completes sign-in and redirects to the sanitized return path. */
|
|
11
|
+
export function handleCallbackRoute(options = {}) {
|
|
12
|
+
return async ({ request }) => {
|
|
13
|
+
const { callbackRoute } = await import("./routes.server.js");
|
|
14
|
+
return callbackRoute(request, options);
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** POST: the provider's back-channel logout; ends the sessions its token names. */
|
|
18
|
+
export function handleBackchannelLogoutRoute() {
|
|
19
|
+
return async ({ request }) => {
|
|
20
|
+
const { backchannelLogoutRoute } = await import("./routes.server.js");
|
|
21
|
+
return backchannelLogoutRoute(request);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/** POST: clears the session and redirects to the provider logout. */
|
|
25
|
+
export function handleSignOutRoute() {
|
|
26
|
+
return async ({ request }) => {
|
|
27
|
+
const { signOutRoute } = await import("./routes.server.js");
|
|
28
|
+
return signOutRoute(request);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type CallbackRouteOptions = {
|
|
2
|
+
/** Where to send the browser when sign-in fails. Defaults to a 400. */
|
|
3
|
+
errorRedirectUrl?: string;
|
|
4
|
+
/** Shown when the host denies a session to an authenticated user. */
|
|
5
|
+
accessDeniedMessage?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function signInRoute(request: Request, options: CallbackRouteOptions): Promise<Response>;
|
|
8
|
+
export declare function callbackRoute(request: Request, options: CallbackRouteOptions): Promise<Response>;
|
|
9
|
+
export declare function signOutRoute(request: Request): Promise<Response>;
|
|
10
|
+
export declare function backchannelLogoutRoute(request: Request): Promise<Response>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { endSession } from "./sign-out.server.js";
|
|
2
|
+
import { getOidcContext } from "./context.js";
|
|
3
|
+
import { SessionAccessDeniedError } from "@lpshanley/oidc-start-core";
|
|
4
|
+
function redirectTo(location, status) {
|
|
5
|
+
return new Response(null, {
|
|
6
|
+
status,
|
|
7
|
+
headers: { Location: location, "Cache-Control": "no-store" },
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function failure(request, options, operation, error) {
|
|
11
|
+
// Provider error details go to onError, never into the response.
|
|
12
|
+
const context = getOidcContext();
|
|
13
|
+
context.core.report({ operation, error, context: context.call });
|
|
14
|
+
if (options.errorRedirectUrl) {
|
|
15
|
+
return redirectTo(new URL(options.errorRedirectUrl, request.url).toString(), 303);
|
|
16
|
+
}
|
|
17
|
+
return new Response("Authentication failed. Restart sign-in.", {
|
|
18
|
+
status: 400,
|
|
19
|
+
headers: { "Content-Type": "text/plain", "Cache-Control": "no-store" },
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
export async function signInRoute(request, options) {
|
|
23
|
+
const context = getOidcContext();
|
|
24
|
+
try {
|
|
25
|
+
const { url, setCookies } = await context.core.signIn(request, context.cookies, new URL(request.url).searchParams.get("returnPathname"), context.call);
|
|
26
|
+
for (const line of setCookies)
|
|
27
|
+
context.appendSetCookie(line);
|
|
28
|
+
return redirectTo(url, 307);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return failure(request, options, "sign-in", error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
export async function callbackRoute(request, options) {
|
|
35
|
+
const context = getOidcContext();
|
|
36
|
+
const result = await context.core.callback(request, context.cookies, context.call);
|
|
37
|
+
for (const line of result.setCookies)
|
|
38
|
+
context.appendSetCookie(line);
|
|
39
|
+
if (result.ok)
|
|
40
|
+
return redirectTo(result.location, 307);
|
|
41
|
+
if (result.error instanceof SessionAccessDeniedError) {
|
|
42
|
+
return new Response(options.accessDeniedMessage ?? "Your account does not have access.", {
|
|
43
|
+
status: 403,
|
|
44
|
+
headers: { "Content-Type": "text/plain", "Cache-Control": "no-store" },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return failure(request, options, "callback", result.error);
|
|
48
|
+
}
|
|
49
|
+
export async function signOutRoute(request) {
|
|
50
|
+
if (request.method !== "POST") {
|
|
51
|
+
return new Response("Method Not Allowed", {
|
|
52
|
+
status: 405,
|
|
53
|
+
headers: { Allow: "POST" },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
const form = await request.formData().catch(() => undefined);
|
|
57
|
+
const result = await endSession(form?.get("everywhere") === "1");
|
|
58
|
+
return result instanceof Response ? result : redirectTo(result, 303);
|
|
59
|
+
}
|
|
60
|
+
export async function backchannelLogoutRoute(request) {
|
|
61
|
+
if (request.method !== "POST") {
|
|
62
|
+
return new Response("Method Not Allowed", {
|
|
63
|
+
status: 405,
|
|
64
|
+
headers: { Allow: "POST" },
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
const context = getOidcContext();
|
|
68
|
+
const { status } = await context.core.backchannelLogout(request, context.call);
|
|
69
|
+
return new Response(null, {
|
|
70
|
+
status,
|
|
71
|
+
headers: { "Cache-Control": "no-store" },
|
|
72
|
+
});
|
|
73
|
+
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getOidcContext, type OidcContext } from "./context.js";
|
package/dist/server.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { getOidcContext } from "./context.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function endSession(everywhere?: boolean): Promise<string | Response>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { getOidcContext } from "./context.js";
|
|
2
|
+
export async function endSession(everywhere = false) {
|
|
3
|
+
const context = getOidcContext();
|
|
4
|
+
let result;
|
|
5
|
+
try {
|
|
6
|
+
result = await context.core.signOut(context.request, context.cookies, context.session, context.call, everywhere);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
context.core.report({ operation: "end-session", error, context: context.call });
|
|
10
|
+
return new Response("Sign-out could not be recorded. Try again.", {
|
|
11
|
+
status: 503,
|
|
12
|
+
headers: { "Content-Type": "text/plain", "Cache-Control": "no-store" },
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
for (const line of result.setCookies)
|
|
16
|
+
context.appendSetCookie(line);
|
|
17
|
+
return result.location;
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lpshanley/oidc-start-react",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
|
|
3
|
+
"version": "0.1.0-next.1",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/lpshanley/oidc-start.git",
|
|
7
|
+
"directory": "packages/react"
|
|
8
|
+
},
|
|
9
|
+
"description": "TanStack Start for React bindings for OpenID Connect",
|
|
10
|
+
"type": "module",
|
|
11
|
+
"sideEffects": false,
|
|
12
|
+
"types": "./dist/index.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js"
|
|
17
|
+
},
|
|
18
|
+
"./functions": {
|
|
19
|
+
"types": "./dist/functions.d.ts",
|
|
20
|
+
"import": "./dist/functions.js"
|
|
21
|
+
},
|
|
22
|
+
"./server": {
|
|
23
|
+
"types": "./dist/server.d.ts",
|
|
24
|
+
"import": "./dist/server.js"
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"dist",
|
|
29
|
+
"README.md"
|
|
30
|
+
],
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22.12.0"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@lpshanley/oidc-start-core": "0.1.0-next.1",
|
|
39
|
+
"zod": "4.6.7"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"typescript": "6.0.3",
|
|
43
|
+
"vitest": "5.0.5",
|
|
44
|
+
"publint": "^0.3.0",
|
|
45
|
+
"@types/node": "22.20.2",
|
|
46
|
+
"@tanstack/react-router": "1.170.46",
|
|
47
|
+
"@tanstack/react-start": "1.169.4",
|
|
48
|
+
"react": "19.3.1",
|
|
49
|
+
"react-dom": "19.3.1",
|
|
50
|
+
"@types/react": "^19.2.0",
|
|
51
|
+
"@types/react-dom": "^19.2.0"
|
|
52
|
+
},
|
|
53
|
+
"peerDependencies": {
|
|
54
|
+
"@tanstack/react-router": "^1.170.44",
|
|
55
|
+
"@tanstack/react-start": "^1.169.2"
|
|
56
|
+
},
|
|
57
|
+
"scripts": {
|
|
58
|
+
"build": "tsc -p tsconfig.build.json",
|
|
59
|
+
"typecheck": "tsc --noEmit",
|
|
60
|
+
"test": "vitest run",
|
|
61
|
+
"check-package": "publint --strict"
|
|
62
|
+
}
|
|
6
63
|
}
|