feat: custom redirect uris for oauth

This commit is contained in:
diced
2024-10-16 11:32:34 -07:00
parent 406a4f3fdb
commit 3d6aaa43d9
10 changed files with 68 additions and 10 deletions

View File

@@ -75,18 +75,22 @@ model Zipline {
oauthDiscordClientId String?
oauthDiscordClientSecret String?
oauthDiscordRedirectUri String?
oauthGoogleClientId String?
oauthGoogleClientSecret String?
oauthGoogleRedirectUri String?
oauthGithubClientId String?
oauthGithubClientSecret String?
oauthGithubRedirectUri String?
oauthOidcClientId String?
oauthOidcClientSecret String?
oauthOidcAuthorizeUrl String?
oauthOidcTokenUrl String?
oauthOidcUserinfoUrl String?
oauthOidcRedirectUri String?
mfaTotpEnabled Boolean @default(false)
mfaTotpIssuer String @default("Zipline")

View File

@@ -19,18 +19,22 @@ export default function ServerSettingsOauth({
oauthDiscordClientId: '',
oauthDiscordClientSecret: '',
oauthDiscordRedirectUri: '',
oauthGoogleClientId: '',
oauthGoogleClientSecret: '',
oauthGoogleRedirectUri: '',
oauthGithubClientId: '',
oauthGithubClientSecret: '',
oauthGithubRedirectUri: '',
oauthOidcClientId: '',
oauthOidcClientSecret: '',
oauthOidcAuthorizeUrl: '',
oauthOidcTokenUrl: '',
oauthOidcUserinfoUrl: '',
oauthOidcRedirectUri: '',
},
});
@@ -61,18 +65,22 @@ export default function ServerSettingsOauth({
oauthDiscordClientId: data?.oauthDiscordClientId ?? '',
oauthDiscordClientSecret: data?.oauthDiscordClientSecret ?? '',
oauthDiscordRedirectUri: data?.oauthDiscordRedirectUri ?? '',
oauthGoogleClientId: data?.oauthGoogleClientId ?? '',
oauthGoogleClientSecret: data?.oauthGoogleClientSecret ?? '',
oauthGoogleRedirectUri: data?.oauthGoogleRedirectUri ?? '',
oauthGithubClientId: data?.oauthGithubClientId ?? '',
oauthGithubClientSecret: data?.oauthGithubClientSecret ?? '',
oauthGithubRedirectUri: data?.oauthGithubRedirectUri ?? '',
oauthOidcClientId: data?.oauthOidcClientId ?? '',
oauthOidcClientSecret: data?.oauthOidcClientSecret ?? '',
oauthOidcAuthorizeUrl: data?.oauthOidcAuthorizeUrl ?? '',
oauthOidcTokenUrl: data?.oauthOidcTokenUrl ?? '',
oauthOidcUserinfoUrl: data?.oauthOidcUserinfoUrl ?? '',
oauthOidcRedirectUri: data?.oauthOidcRedirectUri ?? '',
});
}, [data]);
@@ -104,6 +112,11 @@ export default function ServerSettingsOauth({
<TextInput label='Discord Client ID' {...form.getInputProps('oauthDiscordClientId')} />
<TextInput label='Discord Client Secret' {...form.getInputProps('oauthDiscordClientSecret')} />
<TextInput
label='Discord Redirect URL'
description='The redirect URL to use instead of the host when logging in. This must end with /api/auth/oauth/discord'
{...form.getInputProps('oauthDiscordRedirectUri')}
/>
</Paper>
<Paper withBorder p='sm'>
<Title order={4} mb='sm'>
@@ -112,6 +125,11 @@ export default function ServerSettingsOauth({
<TextInput label='Google Client ID' {...form.getInputProps('oauthGoogleClientId')} />
<TextInput label='Google Client Secret' {...form.getInputProps('oauthGoogleClientSecret')} />
<TextInput
label='Google Redirect URL'
description='The redirect URL to use instead of the host when logging in. This must end with /api/auth/oauth/google'
{...form.getInputProps('oauthGoogleRedirectUri')}
/>
</Paper>
</SimpleGrid>
@@ -121,6 +139,11 @@ export default function ServerSettingsOauth({
<SimpleGrid mt='md' cols={{ base: 1, md: 2 }} spacing='lg'>
<TextInput label='GitHub Client ID' {...form.getInputProps('oauthGithubClientId')} />
<TextInput label='GitHub Client Secret' {...form.getInputProps('oauthGithubClientSecret')} />
<TextInput
label='GitHub Redirect URL'
description='The redirect URL to use instead of the host when logging in. This must end with /api/auth/oauth/github'
{...form.getInputProps('oauthGithubRedirectUri')}
/>
</SimpleGrid>
</Paper>
@@ -133,6 +156,11 @@ export default function ServerSettingsOauth({
<TextInput label='OIDC Authorize URL' {...form.getInputProps('oauthOidcAuthorizeUrl')} />
<TextInput label='OIDC Token URL' {...form.getInputProps('oauthOidcTokenUrl')} />
<TextInput label='OIDC Userinfo URL' {...form.getInputProps('oauthOidcUserinfoUrl')} />
<TextInput
label='OIDC Redirect URL'
description='The redirect URL to use instead of the host when logging in. This must end with /api/auth/oauth/oidc'
{...form.getInputProps('oauthOidcRedirectUri')}
/>
</SimpleGrid>
</Paper>

View File

@@ -223,18 +223,22 @@ export const DATABASE_TO_PROP = {
oauthDiscordClientId: 'oauth.discord.clientId',
oauthDiscordClientSecret: 'oauth.discord.clientSecret',
oauthDiscordRedirectUri: 'oauth.discord.redirectUri',
oauthGoogleClientId: 'oauth.google.clientId',
oauthGoogleClientSecret: 'oauth.google.clientSecret',
oauthGoogleRedirectUri: 'oauth.google.redirectUri',
oauthGithubClientId: 'oauth.github.clientId',
oauthGithubClientSecret: 'oauth.github.clientSecret',
oauthGithubRedirectUri: 'oauth.github.redirectUri',
oauthOidcClientId: 'oauth.oidc.clientId',
oauthOidcClientSecret: 'oauth.oidc.clientSecret',
oauthOidcAuthorizeUrl: 'oauth.oidc.authorizeUrl',
oauthOidcUserinfoUrl: 'oauth.oidc.userinfoUrl',
oauthOidcTokenUrl: 'oauth.oidc.tokenUrl',
oauthOidcRedirectUri: 'oauth.oidc.redirectUri',
mfaTotpEnabled: 'mfa.totp.enabled',
mfaTotpIssuer: 'mfa.totp.issuer',

View File

@@ -213,33 +213,39 @@ export const schema = z.object({
.object({
clientId: z.string(),
clientSecret: z.string(),
redirectUri: z.string().url().nullable().default(null),
})
.or(
z.object({
clientId: z.undefined(),
clientSecret: z.undefined(),
redirectUri: z.undefined(),
}),
),
github: z
.object({
clientId: z.string(),
clientSecret: z.string(),
redirectUri: z.string().url().nullable().default(null),
})
.or(
z.object({
clientId: z.undefined(),
clientSecret: z.undefined(),
redirectUri: z.undefined(),
}),
),
google: z
.object({
clientId: z.string(),
clientSecret: z.string(),
redirectUri: z.string().url().nullable().default(null),
})
.or(
z.object({
clientId: z.undefined(),
clientSecret: z.undefined(),
redirectUri: z.undefined(),
}),
),
oidc: z
@@ -249,6 +255,7 @@ export const schema = z.object({
authorizeUrl: z.string().url(),
userinfoUrl: z.string().url(),
tokenUrl: z.string().url(),
redirectUri: z.string().url().nullable().default(null),
})
.or(
z.object({
@@ -257,6 +264,7 @@ export const schema = z.object({
authorizeUrl: z.undefined(),
userinfoUrl: z.undefined(),
tokenUrl: z.undefined(),
redirectUri: z.undefined(),
}),
),
}),
@@ -362,7 +370,7 @@ function handleError(error: ZodIssue) {
const path =
error.path[1] === 'externalLinks'
? `WEBSITE_EXTERNAL_LINKS[${error.path[2]}]`
: PROP_TO_ENV[<keyof typeof PROP_TO_ENV>error.path.join('.')] ?? error.path.join('.');
: (PROP_TO_ENV[<keyof typeof PROP_TO_ENV>error.path.join('.')] ?? error.path.join('.'));
logger.error(`${path}: ${error.message}`);
}

View File

@@ -9,10 +9,10 @@ export function findProvider(
}
export const githubAuth = {
url: (clientId: string, state?: string) =>
url: (clientId: string, state?: string, redirectUri?: string) =>
`https://github.com/login/oauth/authorize?client_id=${clientId}&scope=read:user${
state ? `&state=${state}` : ''
}`,
}${redirectUri ? `&redirect_uri=${encodeURIComponent(redirectUri)}` : ''}`,
user: async (accessToken: string) => {
const res = await fetch('https://api.github.com/user', {
headers: {
@@ -26,9 +26,9 @@ export const githubAuth = {
};
export const discordAuth = {
url: (clientId: string, origin: string, state?: string) =>
url: (clientId: string, origin: string, state?: string, redirectUri?: string) =>
`https://discord.com/api/oauth2/authorize?client_id=${clientId}&redirect_uri=${encodeURIComponent(
`${origin}/api/auth/oauth/discord`,
redirectUri ?? `${origin}/api/auth/oauth/discord`,
)}&response_type=code&scope=identify${state ? `&state=${state}` : ''}`,
user: async (accessToken: string) => {
const res = await fetch('https://discord.com/api/users/@me', {
@@ -43,9 +43,9 @@ export const discordAuth = {
};
export const googleAuth = {
url: (clientId: string, origin: string, state?: string) =>
url: (clientId: string, origin: string, state?: string, redirectUri?: string) =>
`https://accounts.google.com/o/oauth2/auth?client_id=${clientId}&redirect_uri=${encodeURIComponent(
`${origin}/api/auth/oauth/google`,
redirectUri ?? `${origin}/api/auth/oauth/google`,
)}&response_type=code&access_type=offline&scope=https://www.googleapis.com/auth/userinfo.profile${
state ? `&state=${state}` : ''
}`,
@@ -62,9 +62,9 @@ export const googleAuth = {
};
export const oidcAuth = {
url: (clientId: string, origin: string, authorizeUrl: string, state?: string) =>
url: (clientId: string, origin: string, authorizeUrl: string, state?: string, redirectUri?: string) =>
`${authorizeUrl}?client_id=${clientId}&redirect_uri=${encodeURIComponent(
`${origin}/api/auth/oauth/oidc`,
redirectUri ?? `${origin}/api/auth/oauth/oidc`,
)}&response_type=code&scope=openid+email+profile+offline_access${state ? `&state=${state}` : ''}`,
user: async (accessToken: string, userInfoUrl: string) => {
const res = await fetch(userInfoUrl, {

View File

@@ -28,6 +28,7 @@ async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promi
config.oauth.discord.clientId!,
`${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`,
state,
config.oauth.discord.redirectUri ?? undefined,
),
};

View File

@@ -24,7 +24,11 @@ async function handler({ code, state }: OAuthQuery, logger: Logger): Promise<OAu
if (!code)
return {
redirect: githubAuth.url(config.oauth.github.clientId!, state),
redirect: githubAuth.url(
config.oauth.github.clientId!,
state,
config.oauth.github.redirectUri ?? undefined,
),
};
const body = JSON.stringify({

View File

@@ -28,6 +28,7 @@ async function handler({ code, state, host }: OAuthQuery, _logger: Logger): Prom
config.oauth.google.clientId!,
`${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`,
state,
config.oauth.google.redirectUri ?? undefined,
),
};

View File

@@ -29,6 +29,7 @@ async function handler({ code, state, host }: OAuthQuery, _logger: Logger): Prom
`${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`,
config.oauth.oidc.authorizeUrl!,
state,
config.oauth.oidc.redirectUri ?? undefined,
),
};

View File

@@ -180,15 +180,22 @@ export default fastifyPlugin(
oauthDiscordClientId: z.string().nullable(),
oauthDiscordClientSecret: z.string().nullable(),
oauthDiscordRedirectUri: z.string().url().endsWith('/api/auth/oauth/discord').nullable(),
oauthGoogleClientId: z.string().nullable(),
oauthGoogleClientSecret: z.string().nullable(),
oauthGoogleRedirectUri: z.string().url().endsWith('/api/auth/oauth/google').nullable(),
oauthGithubClientId: z.string().nullable(),
oauthGithubClientSecret: z.string().nullable(),
oauthGithubRedirectUri: z.string().url().endsWith('/api/auth/oauth/github').nullable(),
oauthOidcClientId: z.string().nullable(),
oauthOidcClientSecret: z.string().nullable(),
oauthOidcAuthorizeUrl: z.string().url().nullable(),
oauthOidcTokenUrl: z.string().url().nullable(),
oauthOidcUserinfoUrl: z.string().url().nullable(),
oauthOidcRedirectUri: z.string().url().endsWith('/api/auth/oauth/oidc').nullable(),
mfaTotpEnabled: z.boolean(),
mfaTotpIssuer: z.string(),