Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions build/auth/idpLogout.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/**
* Build the IDP (Gatekeeper) OIDC end-session URL for federated logout.
*
* Returns null unless every part is present, INCLUDING `idTokenHint`:
* Gatekeeper's end-session endpoint requires `id_token_hint` and rejects a
* request without it, so a caller that lacks the id token must fall back to a
* local-only sign-out rather than redirect the browser into a validation error.
*/
export interface IdpLogoutConfig {
/** IDP base URL, e.g. https://identity.omni.dev */
authBaseUrl?: string;
/** OAuth client id */
clientId?: string;
/** Post-logout redirect target (the app's own base URL) */
redirectUri?: string;
/** The user's id token; required by the end-session endpoint */
idTokenHint?: string;
}
export declare const buildIdpLogoutUrl: ({ authBaseUrl, clientId, redirectUri, idTokenHint, }: IdpLogoutConfig) => string | null;
1 change: 1 addition & 0 deletions build/auth/idpLogout.test.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {};
2 changes: 2 additions & 0 deletions build/auth/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export { createAuthCache } from "./cache";
export { extractOrgClaims } from "./claims";
export { GatekeeperOrgClient, GatekeeperOrgError, formatRelativeTime, getInviteTimeInfo, isInvitationExpired, validateInvitation, } from "./gatekeeperOrg";
export { createGetAuth } from "./getAuth";
export { buildIdpLogoutUrl } from "./idpLogout";
export { verifyAccessToken } from "./jwt";
export { createOmniOAuthConfig } from "./oauth";
export { createOidcClient } from "./oidc";
Expand All @@ -11,6 +12,7 @@ export { OMNI_CLAIMS_NAMESPACE } from "./types";
export type { AuthCache, AuthCacheConfig, CachedAuthData } from "./cache";
export type { GatekeeperInvitation, GatekeeperMember, GatekeeperMemberRole, GatekeeperOrganization, InvitationValidationResult, InviteTimeInfo, ValidateInvitationParams, } from "./gatekeeperOrg";
export type { BetterAuthApi, GetAuthConfig, GetAuthSession, ResolveRowIdFn, ResolveRowIdParams, SetCookieFn, } from "./getAuth";
export type { IdpLogoutConfig } from "./idpLogout";
export type { VerifyAccessTokenConfig } from "./jwt";
export type { OmniOAuthConfig } from "./oauth";
export type { OidcClient, OidcClientConfig, OidcDiscovery, } from "./oidc";
Expand Down
17 changes: 17 additions & 0 deletions build/auth/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3400,6 +3400,21 @@ function createGetAuth(config) {
}
};
}
// src/auth/idpLogout.ts
var buildIdpLogoutUrl = ({
authBaseUrl,
clientId,
redirectUri,
idTokenHint
}) => {
if (!authBaseUrl || !clientId || !redirectUri || !idTokenHint)
return null;
const url = new URL(`${authBaseUrl}/oauth2/end-session`);
url.searchParams.set("client_id", clientId);
url.searchParams.set("post_logout_redirect_uri", redirectUri);
url.searchParams.set("id_token_hint", idTokenHint);
return url.toString();
};

// src/auth/index.ts
init_jwt();
Expand All @@ -3422,6 +3437,7 @@ function createOmniOAuthConfig(config) {
],
accessType: "offline",
pkce: true,
authorizationUrlParams: (ctx) => ctx.body?.additionalData?.screen_hint === "signup" ? { prompt: "create" } : {},
mapProfileToUser: (profile) => ({
name: profile.name,
email: profile.email,
Expand Down Expand Up @@ -3542,6 +3558,7 @@ export {
createOidcClient,
createGetAuth,
createAuthCache,
buildIdpLogoutUrl,
OMNI_CLAIMS_NAMESPACE,
GatekeeperOrgError,
GatekeeperOrgClient
Expand Down
7 changes: 7 additions & 0 deletions build/auth/oauth.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ declare function createOmniOAuthConfig(config: OmniOAuthConfig): {
scopes: string[];
accessType: "offline";
pkce: boolean;
authorizationUrlParams: (ctx: {
body?: {
additionalData?: {
screen_hint?: string;
};
};
}) => Record<string, string>;
mapProfileToUser: (profile: Record<string, unknown>) => {
name: string;
email: string;
Expand Down
1 change: 1 addition & 0 deletions build/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -78035,6 +78035,7 @@ function createOmniOAuthConfig(config) {
],
accessType: "offline",
pkce: true,
authorizationUrlParams: (ctx) => ctx.body?.additionalData?.screen_hint === "signup" ? { prompt: "create" } : {},
mapProfileToUser: (profile) => ({
name: profile.name,
email: profile.email,
Expand Down
47 changes: 47 additions & 0 deletions src/auth/idpLogout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expect, it } from "bun:test";

import { buildIdpLogoutUrl } from "./idpLogout";

const full = {
authBaseUrl: "https://identity.example.com",
clientId: "client-123",
redirectUri: "https://app.example.com",
idTokenHint: "the-id-token",
};

describe("buildIdpLogoutUrl", () => {
it("builds the end-session url with all params when everything is present", () => {
const url = new URL(buildIdpLogoutUrl(full) as string);
expect(url.origin + url.pathname).toBe(
"https://identity.example.com/oauth2/end-session",
);
expect(url.searchParams.get("client_id")).toBe("client-123");
expect(url.searchParams.get("post_logout_redirect_uri")).toBe(
"https://app.example.com",
);
expect(url.searchParams.get("id_token_hint")).toBe("the-id-token");
});

it("returns null without an id token (the endpoint requires it)", () => {
expect(buildIdpLogoutUrl({ ...full, idTokenHint: undefined })).toBeNull();
expect(buildIdpLogoutUrl({ ...full, idTokenHint: "" })).toBeNull();
});

it("returns null when any IDP config part is missing", () => {
expect(buildIdpLogoutUrl({ ...full, authBaseUrl: undefined })).toBeNull();
expect(buildIdpLogoutUrl({ ...full, clientId: undefined })).toBeNull();
expect(buildIdpLogoutUrl({ ...full, redirectUri: undefined })).toBeNull();
});

it("url-encodes the id token and redirect uri", () => {
const url = buildIdpLogoutUrl({
...full,
idTokenHint: "a b+c/d=",
redirectUri: "https://app.example.com/back?x=1",
}) as string;
expect(url).toContain("id_token_hint=a+b%2Bc%2Fd%3D");
expect(url).toContain(
"post_logout_redirect_uri=https%3A%2F%2Fapp.example.com%2Fback%3Fx%3D1",
);
});
});
34 changes: 34 additions & 0 deletions src/auth/idpLogout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Build the IDP (Gatekeeper) OIDC end-session URL for federated logout.
*
* Returns null unless every part is present, INCLUDING `idTokenHint`:
* Gatekeeper's end-session endpoint requires `id_token_hint` and rejects a
* request without it, so a caller that lacks the id token must fall back to a
* local-only sign-out rather than redirect the browser into a validation error.
*/
export interface IdpLogoutConfig {
/** IDP base URL, e.g. https://identity.omni.dev */
authBaseUrl?: string;
/** OAuth client id */
clientId?: string;
/** Post-logout redirect target (the app's own base URL) */
redirectUri?: string;
/** The user's id token; required by the end-session endpoint */
idTokenHint?: string;
}

export const buildIdpLogoutUrl = ({
authBaseUrl,
clientId,
redirectUri,
idTokenHint,
}: IdpLogoutConfig): string | null => {
if (!authBaseUrl || !clientId || !redirectUri || !idTokenHint) return null;

const url = new URL(`${authBaseUrl}/oauth2/end-session`);
url.searchParams.set("client_id", clientId);
url.searchParams.set("post_logout_redirect_uri", redirectUri);
url.searchParams.set("id_token_hint", idTokenHint);

return url.toString();
};
2 changes: 2 additions & 0 deletions src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export {
validateInvitation,
} from "./gatekeeperOrg";
export { createGetAuth } from "./getAuth";
export { buildIdpLogoutUrl } from "./idpLogout";
export { verifyAccessToken } from "./jwt";
export { createOmniOAuthConfig } from "./oauth";
export { createOidcClient } from "./oidc";
Expand Down Expand Up @@ -38,6 +39,7 @@ export type {
ResolveRowIdParams,
SetCookieFn,
} from "./getAuth";
export type { IdpLogoutConfig } from "./idpLogout";
export type { VerifyAccessTokenConfig } from "./jwt";
export type { OmniOAuthConfig } from "./oauth";
export type {
Expand Down
Loading