Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for generic OIDC OAuth provider like Authentik #466

Closed
wants to merge 1 commit into from
Closed
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "OauthProviders" ADD VALUE 'OIDC';
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@ enum OauthProviders {
DISCORD
GITHUB
GOOGLE
OIDC
}

model IncompleteFile {
Expand Down
2 changes: 2 additions & 0 deletions src/components/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
IconGraph,
IconHome,
IconLink,
IconLogin,
IconLogout,
IconReload,
IconSettings,
Expand Down Expand Up @@ -137,6 +138,7 @@ export default function Layout({ children, props }) {
GitHub: IconBrandGithubFilled,
Discord: IconBrandDiscordFilled,
Google: IconBrandGoogle,
OIDC: IconLogin,
};

for (const provider of oauth_providers) {
Expand Down
10 changes: 10 additions & 0 deletions src/lib/config/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,16 @@ export interface ConfigOAuth {

google_client_id?: string;
google_client_secret?: string;

oidc_client_id?: string;
oidc_client_secret?: string;
oidc_authorize_url?: string;
oidc_token_url?: string;
oidc_userinfo_url?: string;
oidc_scopes?: string;
oidc_name_field?: string;
oidc_user_id_field?: string;
oidc_provider_display_name?: string;
}

export interface ConfigChunks {
Expand Down
10 changes: 10 additions & 0 deletions src/lib/config/readConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,16 @@ export default function readConfig() {
map('OAUTH_GOOGLE_CLIENT_ID', 'string', 'oauth.google_client_id'),
map('OAUTH_GOOGLE_CLIENT_SECRET', 'string', 'oauth.google_client_secret'),

map('OAUTH_OIDC_CLIENT_ID', 'string', 'oauth.oidc_client_id'),
map('OAUTH_OIDC_CLIENT_SECRET', 'string', 'oauth.oidc_client_secret'),
map('OAUTH_OIDC_AUTHORIZE_URL', 'string', 'oauth.oidc_authorize_url'),
map('OAUTH_OIDC_TOKEN_URL', 'string', 'oauth.oidc_token_url'),
map('OAUTH_OIDC_USERINFO_URL', 'string', 'oauth.oidc_userinfo_url'),
map('OAUTH_OIDC_SCOPES', 'string', 'oauth.oidc_scopes'),
map('OAUTH_OIDC_NAME_FIELD', 'string', 'oauth.oidc_name_field'),
map('OAUTH_OIDC_USER_ID_FIELD', 'string', 'oauth.oidc_user_id_field'),
map('OAUTH_OIDC_PROVIDER_DISPLAY_NAME', 'string', 'oauth.oidc_provider_display_name'),

map('FEATURES_INVITES', 'boolean', 'features.invites'),
map('FEATURES_INVITES_LENGTH', 'number', 'features.invites_length'),

Expand Down
10 changes: 10 additions & 0 deletions src/lib/config/validateConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,16 @@ const validator = s.object({

google_client_id: s.string.nullable.default(null),
google_client_secret: s.string.nullable.default(null),

oidc_client_id: s.string.nullable.default(null),
oidc_client_secret: s.string.nullable.default(null),
oidc_authorize_url: s.string.nullable.default(null),
oidc_token_url: s.string.nullable.default(null),
oidc_userinfo_url: s.string.nullable.default(null),
oidc_scopes: s.string.nullable.default(null),
oidc_name_field: s.string.nullable.default(null),
oidc_user_id_field: s.string.nullable.default(null),
oidc_provider_display_name: s.string.nullable.default(null),
})
.nullish.default(null),
features: s
Expand Down
20 changes: 20 additions & 0 deletions src/lib/middleware/getServerSideProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (ct
isNotNullOrUndefined(config.oauth?.google_client_id) &&
isNotNullOrUndefined(config.oauth?.google_client_secret);

const oidcEnabled =
isNotNullOrUndefined(config.oauth.oidc_client_id) &&
isNotNullOrUndefined(config.oauth.oidc_client_secret) &&
isNotNullOrUndefined(config.oauth.oidc_authorize_url) &&
isNotNullOrUndefined(config.oauth.oidc_token_url) &&
isNotNullOrUndefined(config.oauth.oidc_userinfo_url) &&
isNotNullOrUndefined(config.oauth.oidc_scopes) &&
isNotNullOrUndefined(config.oauth.oidc_name_field) &&
isNotNullOrUndefined(config.oauth.oidc_user_id_field) &&
isNotNullOrUndefined(config.oauth.oidc_provider_display_name);

const oauth_providers: OauthProvider[] = [];

if (ghEnabled)
Expand All @@ -59,6 +70,15 @@ export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (ct
link_url: '/api/auth/oauth/google?state=link',
});

if (oidcEnabled)
oauth_providers.push({
name: config.oauth.oidc_provider_display_name,
url: '/api/auth/oauth/oidc',
link_url: '/api/auth/oauth/oidc?state=link',
});

console.log(oauth_providers);

const obj = {
props: {
title: config.website.title,
Expand Down
2 changes: 1 addition & 1 deletion src/lib/middleware/withOAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export interface OAuthResponse {

export const withOAuth =
(
provider: 'discord' | 'github' | 'google',
provider: 'discord' | 'github' | 'google' | 'oidc',
oauth: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>
) =>
async (req: NextApiReq, res: NextApiRes) => {
Expand Down
18 changes: 18 additions & 0 deletions src/lib/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,21 @@ export const google_auth = {
return res.json();
},
};

export const oidc_auth = {
oauth_url: (authorizeUrl: string, clientId: string, origin: string, scope: string, state?: string) =>
`${authorizeUrl}?client_id=${clientId}&redirect_uri=${encodeURIComponent(
`${origin}/api/auth/oauth/oidc`
)}&response_type=code&access_type=offline&scope=${scope}${state ? `&state=${state}` : ''}`,
oauth_user: async (userinfoUrl: string, access_token: string) => {
const res = await fetch(`${userinfoUrl}`, {
headers: {
Authorization: `Bearer ${access_token}`,
},
});
console.log('userinfo', res);
if (!res.ok) return null;

return res.json();
},
};
82 changes: 82 additions & 0 deletions src/pages/api/auth/oauth/oidc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import config from 'lib/config';
import Logger from 'lib/logger';
import { OAuthQuery, OAuthResponse, withOAuth } from 'lib/middleware/withOAuth';
import { withZipline } from 'lib/middleware/withZipline';
import { oidc_auth } from 'lib/oauth';
import { isNotNullOrUndefined } from 'lib/util';

async function handler({ code, state, host }: OAuthQuery, logger: Logger): Promise<OAuthResponse> {
if (!config.features.oauth_registration)
return {
error_code: 403,
error: 'oauth registration is disabled',
};

if (
!isNotNullOrUndefined(config.oauth.oidc_client_id) &&
!isNotNullOrUndefined(config.oauth.oidc_client_secret) &&
!isNotNullOrUndefined(config.oauth.oidc_authorize_url) &&
!isNotNullOrUndefined(config.oauth.oidc_token_url) &&
!isNotNullOrUndefined(config.oauth.oidc_userinfo_url) &&
!isNotNullOrUndefined(config.oauth.oidc_scopes) &&
!isNotNullOrUndefined(config.oauth.oidc_name_field) &&
!isNotNullOrUndefined(config.oauth.oidc_user_id_field) &&
!isNotNullOrUndefined(config.oauth.oidc_provider_display_name)
) {
logger.error('OIDC OAuth is not configured');
return {
error_code: 401,
error: 'OIDC OAuth is not configured',
};
}

if (!code) {
return {
redirect: oidc_auth.oauth_url(
config.oauth.oidc_authorize_url,
config.oauth.oidc_client_id,
`${config.core.return_https ? 'https' : 'http'}://${host}`,
config.oauth.oidc_scopes,
state
),
};
}

const body = new URLSearchParams({
code,
client_id: config.oauth.oidc_client_id,
client_secret: config.oauth.oidc_client_secret,
redirect_uri: `${config.core.return_https ? 'https' : 'http'}://${host}/api/auth/oauth/oidc`,
grant_type: 'authorization_code',
});

const resp = await fetch(config.oauth.oidc_token_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body,
});

const text = await resp.text();
console.log(`oauth ${config.oauth.oidc_token_url} -> body(${body}) resp(${text})`);

if (!resp.ok) return { error: 'invalid request' };

const json = JSON.parse(text);

if (!json.access_token) return { error: 'no access_token in response' };

const userJson = await oidc_auth.oauth_user(config.oauth.oidc_userinfo_url, json.access_token);

if (!userJson) return { error: 'invalid user request' };

return {
username: userJson[config.oauth.oidc_name_field],
user_id: userJson[config.oauth.oidc_user_id_field],
access_token: json.access_token,
refresh_token: json.refresh_token,
};
}

export default withZipline(withOAuth('oidc', handler));
61 changes: 60 additions & 1 deletion src/pages/api/user/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import zconfig from 'lib/config';
import Logger from 'lib/logger';
import { discord_auth, github_auth, google_auth } from 'lib/oauth';
import { discord_auth, github_auth, google_auth, oidc_auth } from 'lib/oauth';
import prisma from 'lib/prisma';
import { hashPassword } from 'lib/util';
import { jsonUserReplacer } from 'lib/utils/client';
Expand Down Expand Up @@ -121,6 +121,65 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {

const json = await resp2.json();

await prisma.oAuth.update({
where: {
id: provider.id,
},
data: {
token: json.access_token,
refresh: json.refresh_token,
},
});
}
} else if (user.oauth.find((o) => o.provider === 'OIDC')) {
const resp = await fetch(
`${zconfig.oauth.oidc_userinfo_url}?access_token=${
user.oauth.find((o) => o.provider === 'OIDC').token
}`
);
if (!resp.ok) {
const provider = user.oauth.find((o) => o.provider === 'OIDC');
if (!provider.refresh) {
logger.debug(`couldn't find a refresh token for ${JSON.stringify(user, jsonUserReplacer)}`);

return res.json({
error: 'oauth token expired',
redirect_uri: oidc_auth.oauth_url(
zconfig.oauth.oidc_authorize_url,
zconfig.oauth.oidc_client_id,
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`,
zconfig.oauth.oidc_scopes
),
});
}
const resp2 = await fetch(zconfig.oauth.oidc_token_url, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: zconfig.oauth.oidc_client_id,
client_secret: zconfig.oauth.oidc_client_secret,
grant_type: 'refresh_token',
refresh_token: provider.refresh,
}),
});
if (!resp2.ok) {
logger.debug(`oauth expired for ${JSON.stringify(user, jsonUserReplacer)}`);

return res.json({
error: 'oauth token expired',
redirect_uri: oidc_auth.oauth_url(
zconfig.oauth.oidc_authorize_url,
zconfig.oauth.oidc_client_id,
`${zconfig.core.return_https ? 'https' : 'http'}://${req.headers.host}`,
zconfig.oauth.oidc_scopes
),
});
}

const json = await resp2.json();

await prisma.oAuth.update({
where: {
id: provider.id,
Expand Down
3 changes: 2 additions & 1 deletion src/pages/auth/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
Title,
} from '@mantine/core';
import { useForm } from '@mantine/form';
import { IconBrandDiscordFilled, IconBrandGithub, IconBrandGoogle } from '@tabler/icons-react';
import { IconBrandDiscordFilled, IconBrandGithub, IconBrandGoogle, IconLogin } from '@tabler/icons-react';
import useFetch from 'hooks/useFetch';
import Head from 'next/head';
import Link from 'next/link';
Expand Down Expand Up @@ -47,6 +47,7 @@ export default function Login({
GitHub: IconBrandGithub,
Discord: IconBrandDiscordFilled,
Google: IconBrandGoogle,
OIDC: IconLogin,
};

for (const provider of oauth_providers) {
Expand Down
Loading