Implement end_session_endpoint for federated (RP-initiated) logouts #1162

Closed
opened 2026-03-13 08:25:43 -05:00 by GiteaMirror · 9 comments
Owner

Originally created by @coopbri on GitHub (May 5, 2025).

Is this suited for github?

  • Yes, this is suited for github

Currently, signing out from an OIDC RP clears the local session, but not the remote IDP (OIDC OP) session, resulting in session desync.

Originally asked on Discord: https://discord.com/channels/1288403910284935179/1362905067916759351/1362905067916759351

Federated (RP-initiated) logouts are part of the OIDC spec described in Section 2 "RP-Initiated Logout" here: https://discord.com/channels/1288403910284935179/1362905067916759351/1362905067916759351

Describe the solution you'd like

I would love if BA implemented federated logouts for OIDC RPs via the standard end_session_endpoint config.

Describe alternatives you've considered

Manually send a request to the BA IDP to manually "sync" logout intents

Additional context

No response

Originally created by @coopbri on GitHub (May 5, 2025). ### Is this suited for github? - [x] Yes, this is suited for github ### Is your feature request related to a problem? Please describe. Currently, signing out from an OIDC RP clears the local session, but not the remote IDP (OIDC OP) session, resulting in session desync. Originally asked on Discord: https://discord.com/channels/1288403910284935179/1362905067916759351/1362905067916759351 Federated (RP-initiated) logouts are part of the OIDC spec described in Section 2 "RP-Initiated Logout" here: https://discord.com/channels/1288403910284935179/1362905067916759351/1362905067916759351 ### Describe the solution you'd like I would love if BA implemented federated logouts for OIDC RPs via the standard `end_session_endpoint` config. ### Describe alternatives you've considered Manually send a request to the BA IDP to manually "sync" logout intents ### Additional context _No response_
Author
Owner

@Makisuo commented on GitHub (May 19, 2025):

Would be super helpful to have!

@Makisuo commented on GitHub (May 19, 2025): Would be super helpful to have!
Author
Owner

@guptaashwanee commented on GitHub (May 30, 2025):

Is there any update?

@guptaashwanee commented on GitHub (May 30, 2025): Is there any update?
Author
Owner

@bestickley commented on GitHub (Aug 18, 2025):

Related how-to article: https://kinderas.com/technology/implementing-federated-sign-out-when-using-next-auth-in-you-next-js-app. Base on that article, we'd need idToken which is stored in DB. We should be able to use hooks like:

hooks: {
    async before(ctx) {
      if (ctx.request?.url.endsWith("/api/auth/signout")) {
        // 1. get id token
        // 2. signout from provider
      }
    },
  },

or maybe we need to redirect to provider's logout url and then redirect that back to /api/auth/signout? That's what article does.

@bestickley commented on GitHub (Aug 18, 2025): Related how-to article: https://kinderas.com/technology/implementing-federated-sign-out-when-using-next-auth-in-you-next-js-app. Base on that article, we'd need `idToken` which is stored in DB. We should be able to use hooks like: ```ts hooks: { async before(ctx) { if (ctx.request?.url.endsWith("/api/auth/signout")) { // 1. get id token // 2. signout from provider } }, }, ``` or maybe we need to redirect to provider's logout url and then redirect that back to /api/auth/signout? That's what article does.
Author
Owner

@MartinBspheroid commented on GitHub (Sep 3, 2025):

Yes, we are currently working on similar setup with custom server function in tanstack start, but it's really cumbersome. As far as my GitHub research goes, most of the people are not using external provide this way, but we would really like to see this implemented in library, rather than hacking our way through

@MartinBspheroid commented on GitHub (Sep 3, 2025): Yes, we are currently working on similar setup with custom server function in tanstack start, but it's really cumbersome. As far as my GitHub research goes, most of the people are not using external provide this way, but we would really like to see this implemented in library, rather than hacking our way through
Author
Owner

@MartinBspheroid commented on GitHub (Sep 4, 2025):

Hm, yesterday we came up with solution to (using tanstack start) use server function to:
grab id token
signout from better-auth (which will effectively cancel the current session, but only for better-auth)
redirect to the provider end_session_endpoint endpoint with redirect url.

at this point it's working, but we would really like to see something like this in the library.

@MartinBspheroid commented on GitHub (Sep 4, 2025): Hm, yesterday we came up with solution to (using tanstack start) use server function to: grab id token signout from better-auth (which will effectively cancel the current session, but only for better-auth) redirect to the provider `end_session_endpoint` endpoint with redirect url. at this point it's working, but we would really like to see something like this in the library.
Author
Owner

@humemm commented on GitHub (Sep 11, 2025):

Hi 👋 — I got RP-initiated logout working with a small workaround and wanted to share details + a types fix request.

What I did

  1. Added end_session_endpoint to the OIDC provider metadata:
metadata: {
  end_session_endpoint: 'https://myhost.com/api/auth/oauth2/logout'
},
  1. Implemented that endpoint to clear the Better Auth session and then redirect:
export const ServerRoute = createServerFileRoute('/api/auth/oauth2/logout').methods({
  GET: async ({ request }) => {
    const signoutRes = await auth.api.signOut({ headers: request.headers, asResponse: true });
    const headers = new Headers(signoutRes.headers);

    // Always go to homepage (ignore post_logout_redirect_uri)
    headers.set('Location', FINAL_REDIRECT);

    return new Response(null, { status: 302, headers });
  }
});

With this in place, the endpoint shows up in the discovery document and RPs can discover and call it. Practically, this behaves like the OIDC end_session_endpoint for RP-initiated logout.

Type bug

The current Partial<OIDCMetadata> type doesn’t include end_session_endpoint, which causes an LSP error:

Object literal may only specify known properties, and 'end_session_endpoint' does not exist in type 'Partial<OIDCMetadata>'. [2353]

Despite the error, the field is emitted into the discovery config and works at runtime.

Request: add end_session_endpoint to metadata types

Please add end_session_endpoint?: string to the OIDC metadata types (and docs).

Request: implement a built-in RP-initiated logout route

It would be great if the OIDC plugin exposed a first-class end_session_endpoint that:

  • Accepts GET (and optionally POST) with standard params:
    • id_token_hint (optional)
    • post_logout_redirect_uri (optional)
    • state (optional)
  • Clears the Better Auth session (server + client), then redirects.
  • Validates post_logout_redirect_uri against a configured allow-list (or registered RP metadata) to prevent open-redirects; falls back to a default redirect when absent/invalid.
  • Optionally validates id_token_hint (aud/iss/exp) when present.
  • Publishes the endpoint in the discovery document as end_session_endpoint.

Example config sketch:

oidc: {
  // ...
  logout: {
    enabled: true,
    defaultRedirectUri: 'https://myhost.com/',
    allowedPostLogoutRedirectUris: ['https://app.example.com/logout/callback'],
    requireIdTokenHint: false
  }
}

Discovery example:

{
  "end_session_endpoint": "https://myhost.com/api/auth/oauth2/logout"
}

This would make RP-initiated logout work out of the box and align the plugin with common OIDC client expectations.

@humemm commented on GitHub (Sep 11, 2025): Hi 👋 — I got RP-initiated logout working with a small workaround and wanted to share details + a types fix request. ## What I did 1) Added `end_session_endpoint` to the OIDC provider metadata: ```ts metadata: { end_session_endpoint: 'https://myhost.com/api/auth/oauth2/logout' }, ``` 2) Implemented that endpoint to clear the Better Auth session and then redirect: ```ts export const ServerRoute = createServerFileRoute('/api/auth/oauth2/logout').methods({ GET: async ({ request }) => { const signoutRes = await auth.api.signOut({ headers: request.headers, asResponse: true }); const headers = new Headers(signoutRes.headers); // Always go to homepage (ignore post_logout_redirect_uri) headers.set('Location', FINAL_REDIRECT); return new Response(null, { status: 302, headers }); } }); ``` With this in place, the endpoint shows up in the discovery document and RPs can discover and call it. Practically, this behaves like the OIDC **`end_session_endpoint`** for RP-initiated logout. ## Type bug The current `Partial<OIDCMetadata>` type doesn’t include `end_session_endpoint`, which causes an LSP error: ``` Object literal may only specify known properties, and 'end_session_endpoint' does not exist in type 'Partial<OIDCMetadata>'. [2353] ``` Despite the error, the field is emitted into the discovery config and works at runtime. ## Request: add `end_session_endpoint` to metadata types Please add `end_session_endpoint?: string` to the OIDC metadata types (and docs). ## Request: implement a built-in RP-initiated logout route It would be great if the OIDC plugin exposed a first-class `end_session_endpoint` that: - Accepts `GET` (and optionally `POST`) with standard params: - `id_token_hint` (optional) - `post_logout_redirect_uri` (optional) - `state` (optional) - Clears the Better Auth session (server + client), then redirects. - Validates `post_logout_redirect_uri` against a configured allow-list (or registered RP metadata) to prevent open-redirects; falls back to a default redirect when absent/invalid. - Optionally validates `id_token_hint` (aud/iss/exp) when present. - Publishes the endpoint in the discovery document as `end_session_endpoint`. **Example config sketch:** ```ts oidc: { // ... logout: { enabled: true, defaultRedirectUri: 'https://myhost.com/', allowedPostLogoutRedirectUris: ['https://app.example.com/logout/callback'], requireIdTokenHint: false } } ``` **Discovery example:** ```json { "end_session_endpoint": "https://myhost.com/api/auth/oauth2/logout" } ``` This would make RP-initiated logout work out of the box and align the plugin with common OIDC client expectations.
Author
Owner

@tnkuehne commented on GitHub (Oct 13, 2025):

This is our current workaround in SvelteKit:

import { auth } from "$lib/server/auth";
import { db } from "$lib/server/db";
import { account, ssoProvider } from "$lib/server/db/auth.schema";
import { redirect } from "@sveltejs/kit";
import { and, desc, eq } from "drizzle-orm";

import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ request, locals, url }) => {
    const loginURL = new URL("/login", url.origin).toString();
    let federatedLogoutURL: string | null = null;

    const orgId = locals.session?.activeOrganizationId;
    if (orgId) {
        const providers = await db
            .select({
                oidcConfig: ssoProvider.oidcConfig,
                providerId: ssoProvider.providerId,
            })
            .from(ssoProvider)
            .where(eq(ssoProvider.organizationId, orgId))
            .limit(1);

        const provider = providers[0];
        if (provider?.oidcConfig) {
            try {
                const cfg = JSON.parse(provider.oidcConfig) as {
                    end_session_endpoint?: string;
                };
                const endpoint = cfg.end_session_endpoint;
                if (endpoint) {
                    const u = new URL(endpoint);
                    u.searchParams.set("post_logout_redirect_uri", loginURL);

                    const userId = locals.user?.id;
                    if (userId) {
                        const acc = await db
                            .select({ idToken: account.idToken })
                            .from(account)
                            .where(
                                and(
                                    eq(account.userId, userId),
                                    eq(account.providerId, provider.providerId),
                                ),
                            )
                            .orderBy(desc(account.updatedAt))
                            .limit(1);
                        const idToken = acc[0]?.idToken;
                        if (idToken) {
                            u.searchParams.set("id_token_hint", idToken);
                        }
                    }

                    federatedLogoutURL = u.toString();
                }
            } catch {
                // ignore malformed OIDC config
            }
        }
    }

    await auth.api.signOut({
        headers: request.headers,
    });

    return redirect(307, federatedLogoutURL ?? loginURL);
};

@tnkuehne commented on GitHub (Oct 13, 2025): This is our current workaround in SvelteKit: ```ts import { auth } from "$lib/server/auth"; import { db } from "$lib/server/db"; import { account, ssoProvider } from "$lib/server/db/auth.schema"; import { redirect } from "@sveltejs/kit"; import { and, desc, eq } from "drizzle-orm"; import type { PageServerLoad } from "./$types"; export const load: PageServerLoad = async ({ request, locals, url }) => { const loginURL = new URL("/login", url.origin).toString(); let federatedLogoutURL: string | null = null; const orgId = locals.session?.activeOrganizationId; if (orgId) { const providers = await db .select({ oidcConfig: ssoProvider.oidcConfig, providerId: ssoProvider.providerId, }) .from(ssoProvider) .where(eq(ssoProvider.organizationId, orgId)) .limit(1); const provider = providers[0]; if (provider?.oidcConfig) { try { const cfg = JSON.parse(provider.oidcConfig) as { end_session_endpoint?: string; }; const endpoint = cfg.end_session_endpoint; if (endpoint) { const u = new URL(endpoint); u.searchParams.set("post_logout_redirect_uri", loginURL); const userId = locals.user?.id; if (userId) { const acc = await db .select({ idToken: account.idToken }) .from(account) .where( and( eq(account.userId, userId), eq(account.providerId, provider.providerId), ), ) .orderBy(desc(account.updatedAt)) .limit(1); const idToken = acc[0]?.idToken; if (idToken) { u.searchParams.set("id_token_hint", idToken); } } federatedLogoutURL = u.toString(); } } catch { // ignore malformed OIDC config } } } await auth.api.signOut({ headers: request.headers, }); return redirect(307, federatedLogoutURL ?? loginURL); }; ```
Author
Owner

@hmz22 commented on GitHub (Oct 27, 2025):

Related how-to article: https://kinderas.com/technology/implementing-federated-sign-out-when-using-next-auth-in-you-next-js-app. Base on that article, we'd need idToken which is stored in DB. We should be able to use hooks like:

hooks: {
async before(ctx) {
if (ctx.request?.url.endsWith("/api/auth/signout")) {
// 1. get id token
// 2. signout from provider
}
},
},
or maybe we need to redirect to provider's logout url and then redirect that back to /api/auth/signout? That's what article does.

how can do this because I have keycloak oidc and how get refresh token from account and send to sign-out keycloak?
I don't know how access session from this hook.
can add sample code

@hmz22 commented on GitHub (Oct 27, 2025): > Related how-to article: https://kinderas.com/technology/implementing-federated-sign-out-when-using-next-auth-in-you-next-js-app. Base on that article, we'd need `idToken` which is stored in DB. We should be able to use hooks like: > > hooks: { > async before(ctx) { > if (ctx.request?.url.endsWith("/api/auth/signout")) { > // 1. get id token > // 2. signout from provider > } > }, > }, > or maybe we need to redirect to provider's logout url and then redirect that back to /api/auth/signout? That's what article does. how can do this because I have keycloak oidc and how get refresh token from account and send to sign-out keycloak? I don't know how access session from this hook. can add sample code
Author
Owner

@dvanmali commented on GitHub (Dec 24, 2025):

Hi all, we released the new OAuth Provider Plugin which allows for RP-initiated logout. Note that client must be allowed for ending sessions through enable_end_session. Feel free to let us know how it works :)

@dvanmali commented on GitHub (Dec 24, 2025): Hi all, we released the new [OAuth Provider Plugin](https://www.better-auth.com/docs/plugins/oauth-provider) which allows for [RP-initiated logout](https://www.better-auth.com/docs/plugins/oauth-provider#end-session-endpoint). Note that client must be allowed for ending sessions through `enable_end_session`. Feel free to let us know how it works :)
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/better-auth#1162