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
10 changes: 7 additions & 3 deletions src/auth0-session/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,12 @@ export default function get(config: Config, { name, version }: Telemetry): Clien
applyHttpOptionsCustom(client);
client[custom.clock_tolerance] = config.clockTolerance;

if (config.idpLogout && !issuer.end_session_endpoint) {
if (config.auth0Logout || (url.parse(issuer.metadata.issuer).hostname as string).match('\\.auth0\\.com$')) {
if (config.idpLogout) {
if (
config.auth0Logout ||
((url.parse(issuer.metadata.issuer).hostname as string).match('\\.auth0\\.com$') &&
config.auth0Logout !== false)
) {
Object.defineProperty(client, 'endSessionUrl', {
value(params: EndSessionParameters) {
const parsedUrl = url.parse(urlJoin(issuer.metadata.issuer, '/v2/logout'));
Expand All @@ -125,7 +129,7 @@ export default function get(config: Config, { name, version }: Telemetry): Clien
return url.format(parsedUrl);
}
});
} else {
} else if (!issuer.end_session_endpoint) {
debug('the issuer does not support RP-Initiated Logout');
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/auth0-session/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface Config {
/**
* Boolean value to enable Auth0's logout feature.
*/
auth0Logout: boolean;
auth0Logout?: boolean;

/**
* URL parameters used when redirecting users to the authorization server to log in.
Expand Down
2 changes: 1 addition & 1 deletion src/auth0-session/get-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const paramsSchema = Joi.object({
})
.default()
.unknown(false),
auth0Logout: Joi.boolean().optional().default(false),
auth0Logout: Joi.boolean().optional(),
authorizationParams: Joi.object({
response_type: Joi.string().optional().valid('id_token', 'code id_token', 'code').default('id_token'),
scope: Joi.string()
Expand Down
2 changes: 1 addition & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export interface BaseConfig {
* Boolean value to enable Auth0's proprietary logout feature.
* Since this SDK is for Auth0, it's set to `true`by default.
*/
auth0Logout: boolean;
auth0Logout?: boolean;

/**
* URL parameters used when redirecting users to the authorization server to log in.
Expand Down
60 changes: 60 additions & 0 deletions tests/auth0-session/client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,66 @@ describe('clientFactory', function () {
expect(client.id_token_signed_response_alg).toEqual('RS256');
});

it('should use discovered logout endpoint by default', async function () {
const client = await getClient({ ...defaultConfig, idpLogout: true });
expect(client.endSessionUrl({})).toEqual('https://op.example.com/session/end');
});

it('should use auth0 logout endpoint if configured', async function () {
const client = await getClient({ ...defaultConfig, idpLogout: true, auth0Logout: true });
expect(client.endSessionUrl({})).toEqual('https://op.example.com/v2/logout?returnTo=&client_id=__test_client_id__');
});

it('should use auth0 logout endpoint if domain is auth0.com', async function () {
nock('https://foo.auth0.com')
.get('/.well-known/openid-configuration')
.reply(200, { ...wellKnown, issuer: 'https://foo.auth0.com/' });
const client = await getClient({ ...defaultConfig, idpLogout: true, issuerBaseURL: 'https://foo.auth0.com' });
expect(client.endSessionUrl({})).toEqual('https://foo.auth0.com/v2/logout?returnTo=&client_id=__test_client_id__');
});

it('should use auth0 logout endpoint if domain is auth0.com and configured', async function () {
nock('https://foo.auth0.com')
.get('/.well-known/openid-configuration')
.reply(200, { ...wellKnown, issuer: 'https://foo.auth0.com/' });
const client = await getClient({
...defaultConfig,
issuerBaseURL: 'https://foo.auth0.com',
idpLogout: true,
auth0Logout: true
});
expect(client.endSessionUrl({})).toEqual('https://foo.auth0.com/v2/logout?returnTo=&client_id=__test_client_id__');
});

it('should use discovered logout endpoint if domain is auth0.com but configured with auth0logout false', async function () {
nock('https://foo.auth0.com')
.get('/.well-known/openid-configuration')
.reply(200, {
...wellKnown,
issuer: 'https://foo.auth0.com/',
end_session_endpoint: 'https://foo.auth0.com/oidc/logout'
});
const client = await getClient({
...defaultConfig,
issuerBaseURL: 'https://foo.auth0.com',
idpLogout: true,
auth0Logout: false
});
expect(client.endSessionUrl({})).toEqual('https://foo.auth0.com/oidc/logout');
});

it('should create client with no end_session_endpoint', async function () {
nock('https://op2.example.com')
.get('/.well-known/openid-configuration')
.reply(200, {
...wellKnown,
issuer: 'https://op2.example.com',
end_session_endpoint: undefined
});
const client = await getClient({ ...defaultConfig, issuerBaseURL: 'https://op2.example.com' });
expect(() => client.endSessionUrl({})).toThrowError();
});

it('should create custom logout for auth0', async function () {
nock('https://test.eu.auth0.com')
.get('/.well-known/openid-configuration')
Expand Down
38 changes: 17 additions & 21 deletions tests/auth0-session/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,23 +64,19 @@ describe('Config', () => {
});
});

it('auth0Logout and idpLogout should default to false', () => {
it('auth0Logout should default to undefined and idpLogout should default to false', () => {
const config = getConfig(defaultConfig);
expect(config).toMatchObject({
auth0Logout: false,
idpLogout: false
});
expect(config.idpLogout).toBe(false);
expect(config.auth0Logout).toBeUndefined();
});

it('should not set auth0Logout to true when idpLogout is true', () => {
const config = getConfig({
...defaultConfig,
idpLogout: true
});
expect(config).toMatchObject({
auth0Logout: false,
idpLogout: true
});
expect(config.idpLogout).toBe(true);
expect(config.auth0Logout).toBeUndefined();
});

it('should set default route paths', () => {
Expand Down Expand Up @@ -229,7 +225,7 @@ describe('Config', () => {
...defaultConfig,
session: {
rolling: true,
rollingDuration: (false as unknown) as undefined // testing invalid configuration
rollingDuration: false as unknown as undefined // testing invalid configuration
}
})
).toThrow('"session.rollingDuration" must be provided an integer value when "session.rolling" is true');
Expand All @@ -251,7 +247,7 @@ describe('Config', () => {
expect(() =>
getConfig({
...defaultConfig,
secret: ({ key: '__test_session_secret__' } as unknown) as string // testing invalid configuration
secret: { key: '__test_session_secret__' } as unknown as string // testing invalid configuration
})
).toThrow('"secret" must be one of [string, binary, array]');
});
Expand All @@ -262,7 +258,7 @@ describe('Config', () => {
...defaultConfig,
session: {
cookie: {
httpOnly: ('__invalid_httponly__' as unknown) as boolean // testing invalid configuration
httpOnly: '__invalid_httponly__' as unknown as boolean // testing invalid configuration
}
}
})
Expand All @@ -276,7 +272,7 @@ describe('Config', () => {
secret: '__test_session_secret__',
session: {
cookie: {
secure: ('__invalid_secure__' as unknown) as boolean // testing invalid configuration
secure: '__invalid_secure__' as unknown as boolean // testing invalid configuration
}
}
})
Expand All @@ -290,7 +286,7 @@ describe('Config', () => {
secret: '__test_session_secret__',
session: {
cookie: {
sameSite: ('__invalid_samesite__' as unknown) as any // testing invalid configuration
sameSite: '__invalid_samesite__' as unknown as any // testing invalid configuration
}
}
})
Expand All @@ -304,7 +300,7 @@ describe('Config', () => {
secret: '__test_session_secret__',
session: {
cookie: {
domain: (false as unknown) as string // testing invalid configuration
domain: false as unknown as string // testing invalid configuration
}
}
})
Expand Down Expand Up @@ -399,7 +395,7 @@ describe('Config', () => {
});

it('should not allow empty scope', () => {
expect(() => validateAuthorizationParams({ scope: (null as unknown) as undefined })).toThrowError(
expect(() => validateAuthorizationParams({ scope: null as unknown as undefined })).toThrowError(
new TypeError('"authorizationParams.scope" must be a string')
);
expect(() => validateAuthorizationParams({ scope: '' })).toThrowError(
Expand All @@ -420,10 +416,10 @@ describe('Config', () => {
});

it('should not allow empty response_type', () => {
expect(() => validateAuthorizationParams({ response_type: (null as unknown) as undefined })).toThrowError(
expect(() => validateAuthorizationParams({ response_type: null as unknown as undefined })).toThrowError(
new TypeError('"authorizationParams.response_type" must be one of [id_token, code id_token, code]')
);
expect(() => validateAuthorizationParams({ response_type: ('' as unknown) as undefined })).toThrowError(
expect(() => validateAuthorizationParams({ response_type: '' as unknown as undefined })).toThrowError(
new TypeError('"authorizationParams.response_type" must be one of [id_token, code id_token, code]')
);
});
Expand Down Expand Up @@ -452,16 +448,16 @@ describe('Config', () => {
});

it('should not allow empty response_mode', () => {
expect(() => validateAuthorizationParams({ response_mode: (null as unknown) as undefined })).toThrowError(
expect(() => validateAuthorizationParams({ response_mode: null as unknown as undefined })).toThrowError(
new TypeError('"authorizationParams.response_mode" must be [form_post]')
);
expect(() => validateAuthorizationParams({ response_mode: ('' as unknown) as undefined })).toThrowError(
expect(() => validateAuthorizationParams({ response_mode: '' as unknown as undefined })).toThrowError(
new TypeError('"authorizationParams.response_mode" must be [form_post]')
);
expect(() =>
validateAuthorizationParams({
response_type: 'code',
response_mode: ('' as unknown) as undefined
response_mode: '' as unknown as undefined
})
).toThrowError(new TypeError('"authorizationParams.response_mode" must be one of [query, form_post]'));
});
Expand Down
32 changes: 27 additions & 5 deletions tests/handlers/logout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jest.mock('../../src/utils/assert', () => ({
describe('logout handler', () => {
afterEach(teardown);

test('should redirect to the identity provider', async () => {
test('should redirect to auth0', async () => {
const baseUrl = await setup(withoutApi);
const cookieJar = await login(baseUrl);

Expand Down Expand Up @@ -59,10 +59,13 @@ describe('logout handler', () => {
});
});

test('should use end_session_endpoint if available', async () => {
const baseUrl = await setup(withoutApi, {
discoveryOptions: { end_session_endpoint: 'https://my-end-session-endpoint/logout' }
});
test('should use end_session_endpoint when configured', async () => {
const baseUrl = await setup(
{ ...withoutApi, auth0Logout: false },
{
discoveryOptions: { end_session_endpoint: 'https://my-end-session-endpoint/logout' }
}
);
const cookieJar = await login(baseUrl);

const { status, headers } = await fetch(`${baseUrl}/api/auth/logout`, {
Expand All @@ -79,6 +82,25 @@ describe('logout handler', () => {
});
});

test('should use auth0 logout by default even when end_session_endpoint is discovered', async () => {
const baseUrl = await setup(withoutApi, {
discoveryOptions: { end_session_endpoint: 'https://my-end-session-endpoint/logout' }
});
const cookieJar = await login(baseUrl);
const { status, headers } = await fetch(`${baseUrl}/api/auth/logout`, {
redirect: 'manual',
headers: {
cookie: cookieJar.getCookieStringSync(baseUrl)
}
});

expect(status).toBe(302);
expect(parseUrl(headers.get('location') as string)).toMatchObject({
host: 'acme.auth0.local',
pathname: '/v2/logout'
});
});

test('should delete the session', async () => {
const baseUrl = await setup(withoutApi, {
discoveryOptions: { end_session_endpoint: 'https://my-end-session-endpoint/logout' }
Expand Down