Skip to content
Open
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
4 changes: 2 additions & 2 deletions src/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { decodeJwt } from 'jose';
import { revalidatePath, revalidateTag } from 'next/cache';
import { cookies, headers } from 'next/headers';
import { redirect } from 'next/navigation';
import { WORKOS_COOKIE_NAME } from './env-variables.js';
import { config } from './config.js';
import { getCookieOptions, getPKCECookieOptions } from './cookie.js';
import { getAuthorizationUrl } from './get-authorization-url.js';
import type { AccessToken, GetAuthURLOptions, SwitchToOrganizationOptions, UserInfo } from './interfaces.js';
Expand Down Expand Up @@ -71,7 +71,7 @@ export async function signOut({ returnTo }: { returnTo?: string } = {}) {
}
} finally {
const nextCookies = await cookies();
const cookieName = WORKOS_COOKIE_NAME || 'wos-session';
const cookieName = config.cookieName || 'wos-session';
const { domain, path, sameSite, secure } = getCookieOptions();
try {
nextCookies.delete({ name: cookieName, domain, path, sameSite, secure });
Expand Down
4 changes: 2 additions & 2 deletions src/authkit-callback-route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextRequest } from 'next/server';
import { getPKCECookieOptions } from './cookie.js';
import { WORKOS_CLIENT_ID } from './env-variables.js';
import { config } from './config.js';
import { HandleAuthOptions } from './interfaces.js';
import { PKCE_COOKIE_NAME, getPKCECookieNameForState, getStateFromPKCECookieValue } from './pkce.js';
import { saveSession } from './session.js';
Expand Down Expand Up @@ -66,7 +66,7 @@ export function handleAuth(options: HandleAuthOptions = {}) {
// Use the code returned to us by AuthKit and authenticate the user with WorkOS
const { accessToken, refreshToken, user, impersonator, oauthTokens, authenticationMethod, organizationId } =
await getWorkOS().userManagement.authenticateWithCode({
clientId: WORKOS_CLIENT_ID,
clientId: config.clientId,
code,
codeVerifier,
});
Expand Down
116 changes: 116 additions & 0 deletions src/config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { config, initAuthKit } from './config.js';

describe('config', () => {
beforeEach(() => {
vi.resetModules();
delete (globalThis as Record<symbol, unknown>)[Symbol.for('workos.authkit.overrides')];
});

describe('env variable fallbacks', () => {
it('reads apiKey from WORKOS_API_KEY', () => {
expect(config.apiKey).toBe(process.env.WORKOS_API_KEY);
});

it('reads clientId from WORKOS_CLIENT_ID', () => {
expect(config.clientId).toBe(process.env.WORKOS_CLIENT_ID);
});

it('reads cookiePassword from WORKOS_COOKIE_PASSWORD', () => {
expect(config.cookiePassword).toBe(process.env.WORKOS_COOKIE_PASSWORD);
});

it('reads redirectUri from NEXT_PUBLIC_WORKOS_REDIRECT_URI', () => {
expect(config.redirectUri).toBe(process.env.NEXT_PUBLIC_WORKOS_REDIRECT_URI);
});

it('reads cookieDomain from WORKOS_COOKIE_DOMAIN', () => {
expect(config.cookieDomain).toBe(process.env.WORKOS_COOKIE_DOMAIN);
});

it('returns undefined for unset optional values', () => {
expect(config.cookieName).toBeUndefined();
expect(config.cookieMaxAge).toBeUndefined();
expect(config.cookieSameSite).toBeUndefined();
expect(config.claimToken).toBeUndefined();
expect(config.apiHostname).toBeUndefined();
expect(config.apiPort).toBeUndefined();
});

it('defaults apiHttps to true when WORKOS_API_HTTPS is not set', () => {
expect(config.apiHttps).toBe(true);
});

it('parses WORKOS_API_HTTPS from env', () => {
process.env.WORKOS_API_HTTPS = 'false';
expect(config.apiHttps).toBe(false);
delete process.env.WORKOS_API_HTTPS;
});

it('parses WORKOS_COOKIE_MAX_AGE from env', () => {
process.env.WORKOS_COOKIE_MAX_AGE = '3600';
expect(config.cookieMaxAge).toBe(3600);
delete process.env.WORKOS_COOKIE_MAX_AGE;
});

it('returns undefined for an invalid WORKOS_COOKIE_MAX_AGE', () => {
process.env.WORKOS_COOKIE_MAX_AGE = 'not-a-number';
expect(config.cookieMaxAge).toBeUndefined();
delete process.env.WORKOS_COOKIE_MAX_AGE;
});

it('parses WORKOS_API_PORT from env', () => {
process.env.WORKOS_API_PORT = '8080';
expect(config.apiPort).toBe(8080);
delete process.env.WORKOS_API_PORT;
});
});

describe('initAuthKit overrides', () => {
it('takes precedence over env variables', async () => {
const { config: freshConfig, initAuthKit: freshInit } = await import('./config.js');
freshInit({ apiKey: 'override-key', cookiePassword: 'override-password-with-enough-length' });

expect(freshConfig.apiKey).toBe('override-key');
expect(freshConfig.cookiePassword).toBe('override-password-with-enough-length');
});

it('falls back to env for keys not in the override', async () => {
const { config: freshConfig, initAuthKit: freshInit } = await import('./config.js');
freshInit({ apiKey: 'custom-key' });

expect(freshConfig.clientId).toBe(process.env.WORKOS_CLIENT_ID);
});

it('merges successive calls', async () => {
const { config: freshConfig, initAuthKit: freshInit } = await import('./config.js');
freshInit({ apiKey: 'key1' });
freshInit({ cookiePassword: 'password-long-enough-to-pass-32-chars' });

expect(freshConfig.apiKey).toBe('key1');
expect(freshConfig.cookiePassword).toBe('password-long-enough-to-pass-32-chars');
});

it('overrides cookieMaxAge as a number directly', async () => {
const { config: freshConfig, initAuthKit: freshInit } = await import('./config.js');
freshInit({ cookieMaxAge: 7200 });

expect(freshConfig.cookieMaxAge).toBe(7200);
});

it('overrides apiHttps as a boolean directly', async () => {
const { config: freshConfig, initAuthKit: freshInit } = await import('./config.js');
freshInit({ apiHttps: false });

expect(freshConfig.apiHttps).toBe(false);
});

it('restores env fallback when override is cleared with undefined', async () => {
const { config: freshConfig, initAuthKit: freshInit } = await import('./config.js');
freshInit({ apiKey: 'custom' });
expect(freshConfig.apiKey).toBe('custom');

freshInit({ apiKey: undefined });
expect(freshConfig.apiKey).toBe(process.env.WORKOS_API_KEY);
});
});
});
106 changes: 106 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
export interface AuthKitConfig {
/** WorkOS API key. Overrides `WORKOS_API_KEY`. Applied once at client creation. */
apiKey?: string;
/** WorkOS client ID. Overrides `WORKOS_CLIENT_ID`. */
clientId?: string;
/** Secret used to seal/unseal session cookies. At least 32 characters. Overrides `WORKOS_COOKIE_PASSWORD`. */
cookiePassword?: string;
/** OAuth redirect URI. Overrides `NEXT_PUBLIC_WORKOS_REDIRECT_URI`. */
redirectUri?: string;
/** Session cookie name. Defaults to `wos-session`. Overrides `WORKOS_COOKIE_NAME`. */
cookieName?: string;
/** Cookie `Domain` attribute. Overrides `WORKOS_COOKIE_DOMAIN`. */
cookieDomain?: string;
/** Cookie `Max-Age` in seconds. Defaults to 400 days. Overrides `WORKOS_COOKIE_MAX_AGE`. */
cookieMaxAge?: number;
/** Cookie `SameSite` attribute. Defaults to `lax`. Overrides `WORKOS_COOKIE_SAMESITE`. */
cookieSameSite?: 'lax' | 'strict' | 'none';
/** One-shot environment claim token. Overrides `WORKOS_CLAIM_TOKEN`. */
claimToken?: string;
/** Custom WorkOS API hostname. Overrides `WORKOS_API_HOSTNAME`. Applied once at client creation. */
apiHostname?: string;
/** Whether to use HTTPS for the WorkOS API. Defaults to `true`. Overrides `WORKOS_API_HTTPS`. Applied once at client creation. */
apiHttps?: boolean;
/** Custom WorkOS API port. Overrides `WORKOS_API_PORT`. Applied once at client creation. */
apiPort?: number;
}

const OVERRIDES_KEY = Symbol.for('workos.authkit.overrides');

const _overrides: AuthKitConfig = ((globalThis as Record<symbol, AuthKitConfig | undefined>)[OVERRIDES_KEY] ??= {});

/**
* Configure AuthKit with values from any source (secrets manager, vault, etc.)
* as an alternative to environment variables. Successive calls are merged, so
* you can call this multiple times to set different groups of values.
*
* **Call this before any other AuthKit function.** API connection settings
* (`apiKey`, `apiHostname`, `apiHttps`, `apiPort`) are applied when the WorkOS
* client is first created and cannot be updated afterwards. All other settings
* are read on every request, so they can technically be set later, but
* calling this once at startup is the intended usage.
*
* @example
* ```ts
* import { initAuthKit } from '@workos-inc/authkit-nextjs';
*
* initAuthKit({
* apiKey: await secrets.get('WORKOS_API_KEY'),
* clientId: await secrets.get('WORKOS_CLIENT_ID'),
* cookiePassword: await secrets.get('WORKOS_COOKIE_PASSWORD'),
* redirectUri: 'https://myapp.com/callback',
* });
* ```
*/
export function initAuthKit(overrides: AuthKitConfig): void {
Object.assign(_overrides, overrides);
}

export const config = {
get apiKey(): string {
return _overrides.apiKey ?? process.env.WORKOS_API_KEY ?? '';
},
get clientId(): string {
return _overrides.clientId ?? process.env.WORKOS_CLIENT_ID ?? '';
},
get cookiePassword(): string {
return _overrides.cookiePassword ?? process.env.WORKOS_COOKIE_PASSWORD ?? '';
},
get redirectUri(): string {
return _overrides.redirectUri ?? process.env.NEXT_PUBLIC_WORKOS_REDIRECT_URI ?? '';
},
get cookieName(): string | undefined {
return _overrides.cookieName ?? process.env.WORKOS_COOKIE_NAME;
},
get cookieDomain(): string | undefined {
return _overrides.cookieDomain ?? process.env.WORKOS_COOKIE_DOMAIN;
},
get cookieMaxAge(): number | undefined {
if (_overrides.cookieMaxAge !== undefined) return _overrides.cookieMaxAge;
const raw = process.env.WORKOS_COOKIE_MAX_AGE;
if (!raw) return undefined;
const parsed = parseInt(raw, 10);
return Number.isFinite(parsed) ? parsed : undefined;
},
get cookieSameSite(): 'lax' | 'strict' | 'none' | undefined {
return _overrides.cookieSameSite ?? (process.env.WORKOS_COOKIE_SAMESITE as 'lax' | 'strict' | 'none' | undefined);
},
get claimToken(): string | undefined {
return _overrides.claimToken ?? process.env.WORKOS_CLAIM_TOKEN;
},
get apiHostname(): string | undefined {
return _overrides.apiHostname ?? process.env.WORKOS_API_HOSTNAME;
},
get apiHttps(): boolean {
if (_overrides.apiHttps !== undefined) return _overrides.apiHttps;
const raw = process.env.WORKOS_API_HTTPS;
return raw ? raw === 'true' : true;
},
get apiPort(): number | undefined {
if (_overrides.apiPort !== undefined) return _overrides.apiPort;
const raw = process.env.WORKOS_API_PORT;
if (!raw) return undefined;
const parsed = parseInt(raw, 10);
return Number.isFinite(parsed) ? parsed : undefined;
},
};
Loading