Fix redirect OIDC (#1911)

* fix: redirect oidc not defined

* fix: construct redirect url from x-forwarded prot and host

* fix: redirect method does not support comma seperated protocol list and url starting with http(s)://

* fix: redirect_uri not specified for oidc as authorization parameter

* fix: unit test not modified

* docs: add comment why the redirect_uri is constructed

* fix: add redirect callback with forwarded headers as redirect url host and protocol

* Apply suggestions from code review
This commit is contained in:
Meier Lukas
2024-02-20 20:34:57 +01:00
committed by GitHub
parent 1bc19e7857
commit 5cd940f3cc
6 changed files with 140 additions and 19 deletions

View File

@@ -2,6 +2,8 @@ import { DefaultSession } from 'next-auth';
import { CredentialsConfig, OAuthConfig } from 'next-auth/providers';
import { env } from '~/env';
import { OidcRedirectCallbackHeaders } from './oidc';
export { default as adapter, onCreateUser } from './adapter';
/**
@@ -38,9 +40,16 @@ declare module 'next-auth/jwt' {
}
}
export const providers: (CredentialsConfig | OAuthConfig<any>)[] = [];
export const getProviders = async (headers: OidcRedirectCallbackHeaders) => {
const providers: (CredentialsConfig | OAuthConfig<any>)[] = [];
if (env.AUTH_PROVIDER?.includes('ldap')) providers.push((await import('./ldap')).default);
if (env.AUTH_PROVIDER?.includes('credentials'))
providers.push((await import('./credentials')).default);
if (env.AUTH_PROVIDER?.includes('oidc')) providers.push((await import('./oidc')).default);
if (env.AUTH_PROVIDER?.includes('ldap')) providers.push((await import('./ldap')).default);
if (env.AUTH_PROVIDER?.includes('credentials'))
providers.push((await import('./credentials')).default);
if (env.AUTH_PROVIDER?.includes('oidc')) {
const createProvider = (await import('./oidc')).default;
providers.push(createProvider(headers));
}
return providers;
};

View File

@@ -0,0 +1,60 @@
import { describe, expect, test } from 'vitest';
import { createRedirectUri } from './oidc';
describe('redirect', () => {
test('Callback should return http url when not defining protocol', async () => {
// Arrange
const headers = {
'x-forwarded-host': 'localhost:3000',
};
// Act
const result = await createRedirectUri(headers, '/api/auth/callback/oidc');
// Assert
expect(result).toBe('http://localhost:3000/api/auth/callback/oidc');
});
test('Callback should return https url when defining protocol', async () => {
// Arrange
const headers = {
'x-forwarded-proto': 'https',
'x-forwarded-host': 'localhost:3000',
};
// Act
const result = await createRedirectUri(headers, '/api/auth/callback/oidc');
// Assert
expect(result).toBe('https://localhost:3000/api/auth/callback/oidc');
});
test('Callback should return https url when defining protocol and host', async () => {
// Arrange
const headers = {
'x-forwarded-proto': 'https',
host: 'something.else',
};
// Act
const result = await createRedirectUri(headers, '/api/auth/callback/oidc');
// Assert
expect(result).toBe('https://something.else/api/auth/callback/oidc');
});
test('Callback should return https url when defining protocol as http,https and host', async () => {
// Arrange
const headers = {
'x-forwarded-proto': 'http,https',
'x-forwarded-host': 'hello.world',
};
// Act
const result = await createRedirectUri(headers, '/api/auth/callback/oidc');
// Assert
expect(result).toBe('https://hello.world/api/auth/callback/oidc');
});
});

View File

@@ -13,14 +13,42 @@ type Profile = {
email_verified: boolean;
};
const provider: OAuthConfig<Profile> = {
export type OidcRedirectCallbackHeaders = {
'x-forwarded-proto'?: string;
'x-forwarded-host'?: string;
host?: string;
};
// The redirect_uri is constructed to work behind a reverse proxy. It is constructed from the headers x-forwarded-proto and x-forwarded-host.
export const createRedirectUri = (headers: OidcRedirectCallbackHeaders, pathname: string) => {
let protocol = headers['x-forwarded-proto'] ?? 'http';
// @see https://support.glitch.com/t/x-forwarded-proto-contains-multiple-protocols/17219
if (protocol.includes(',')) {
protocol = protocol.includes('https') ? 'https' : 'http';
}
const path = pathname.startsWith('/') ? pathname : `/${pathname}`;
const host = headers['x-forwarded-host'] ?? headers.host;
return `${protocol}://${host}${path}`;
};
const createProvider = (headers: OidcRedirectCallbackHeaders): OAuthConfig<Profile> => ({
id: 'oidc',
name: env.AUTH_OIDC_CLIENT_NAME,
type: 'oauth',
clientId: env.AUTH_OIDC_CLIENT_ID,
clientSecret: env.AUTH_OIDC_CLIENT_SECRET,
wellKnown: `${env.AUTH_OIDC_URI}/.well-known/openid-configuration`,
authorization: { params: { scope: env.AUTH_OIDC_SCOPE_OVERWRITE } },
authorization: {
params: {
scope: env.AUTH_OIDC_SCOPE_OVERWRITE,
redirect_uri: createRedirectUri(headers, '/api/auth/callback/oidc'),
},
},
idToken: true,
async profile(profile) {
const user = await adapter.getUserByEmail!(profile.email);
@@ -50,6 +78,6 @@ const provider: OAuthConfig<Profile> = {
isOwner,
};
},
};
});
export default provider;
export default createProvider;