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

adding authentik oauth implementation #372

Open
wants to merge 11 commits into
base: trunk
Choose a base branch
from
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "OauthProviders" ADD VALUE 'AUTHENTIK';
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
AUTHENTIK
}

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 @@ -36,6 +36,7 @@ import {
IconFolders,
IconGraph,
IconHome,
IconKey,
IconLink,
IconLogout,
IconReload,
Expand Down Expand Up @@ -137,6 +138,7 @@ export default function Layout({ children, props }) {
GitHub: IconBrandGithubFilled,
Discord: IconBrandDiscordFilled,
Google: IconBrandGoogle,
Authentik: IconKey,
};

for (const provider of oauth_providers) {
Expand Down
2 changes: 2 additions & 0 deletions src/components/pages/Manage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
IconFileZip,
IconGraph,
IconGraphOff,
IconKey,
IconPhotoMinus,
IconReload,
IconTrash,
Expand Down Expand Up @@ -76,6 +77,7 @@ export default function Manage({ oauth_registration, oauth_providers: raw_oauth_
Discord: IconBrandDiscordFilled,
GitHub: IconBrandGithubFilled,
Google: IconBrandGoogle,
Authentik: IconKey,
};

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

google_client_id?: string;
google_client_secret?: string;

authentik_client_id?: string;
authentik_client_secret?: string;
authentik_authorize_url?: string;
authentik_userinfo_url?: string;
authentik_token_url?: string;
}

export interface ConfigChunks {
Expand Down
6 changes: 6 additions & 0 deletions src/lib/config/readConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ 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_AUTHENTIK_CLIENT_ID', 'string', 'oauth.authentik_client_id'),
map('OAUTH_AUTHENTIK_CLIENT_SECRET', 'string', 'oauth.authentik_client_secret'),
map('OAUTH_AUTHENTIK_AUTHORIZE_URL', 'string', 'oauth.authentik_authorize_url'),
map('OAUTH_AUTHENTIK_USERINFO_URL', 'string', 'oauth.authentik_userinfo_url'),
map('OAUTH_AUTHENTIK_TOKEN_URL', 'string', 'oauth.authentik_token_url'),

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

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

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

authentik_client_id: s.string.nullable.default(null),
authentik_client_secret: s.string.nullable.default(null),
authentik_authorize_url: s.string.nullable.default(null),
authentik_userinfo_url: s.string.nullable.default(null),
authentik_token_url: s.string.nullable.default(null),
})
.nullish.default(null),
features: s
Expand Down
16 changes: 15 additions & 1 deletion src/lib/middleware/getServerSideProps.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import config from 'lib/config';
import { notNull } from 'lib/util';
import { notNull, notNullArray } from 'lib/util';
import { GetServerSideProps } from 'next';

export type OauthProvider = {
Expand Down Expand Up @@ -29,6 +29,13 @@ export const getServerSideProps: GetServerSideProps<ServerSideProps> = async (ct
const ghEnabled = notNull(config.oauth?.github_client_id, config.oauth?.github_client_secret);
const discEnabled = notNull(config.oauth?.discord_client_id, config.oauth?.discord_client_secret);
const googleEnabled = notNull(config.oauth?.google_client_id, config.oauth?.google_client_secret);
const authentikEnabled = notNullArray([
config.oauth?.authentik_client_id,
config.oauth?.authentik_client_secret,
config.oauth?.authentik_authorize_url,
config.oauth?.authentik_userinfo_url,
config.oauth?.authentik_token_url,
]);

const oauth_providers: OauthProvider[] = [];

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

if (authentikEnabled)
oauth_providers.push({
name: 'Authentik',
url: '/api/auth/oauth/authentik',
link_url: '/api/auth/oauth/authentik?state=link',
});

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' | 'authentik',
oauth: (query: OAuthQuery, logger: Logger) => Promise<OAuthResponse>
) =>
async (req: NextApiReq, res: NextApiRes) => {
Expand Down
17 changes: 17 additions & 0 deletions src/lib/oauth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,20 @@ export const google_auth = {
return res.json();
},
};

export const authentik_auth = {
oauth_url: (clientId: string, origin: string, authorize_url: string, state?: string) =>
`${authorize_url}?client_id=${clientId}&redirect_uri=${encodeURIComponent(
`${origin}/api/auth/oauth/authentik`
)}&response_type=code&scope=openid+email+profile${state ? `&state=${state}` : ''}`,
oauth_user: async (access_token: string, user_info_url: string) => {
const res = await fetch(user_info_url, {
headers: {
Authorization: `Bearer ${access_token}`,
},
});
if (!res.ok) return null;

return res.json();
},
};
4 changes: 4 additions & 0 deletions src/lib/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,7 @@ export async function getBase64URLFromURL(url: string) {
export function notNull(a: unknown, b: unknown) {
return a !== null && b !== null;
}

export function notNullArray(arr: unknown[]) {
return !arr.some((x) => x === null);
}
76 changes: 76 additions & 0 deletions src/pages/api/auth/oauth/authentik.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
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 { authentik_auth } from 'lib/oauth';
import { notNullArray } 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 (
!notNullArray([
config.oauth?.authentik_client_id,
config.oauth?.authentik_client_secret,
config.oauth?.authentik_authorize_url,
config.oauth?.authentik_userinfo_url,
])
) {
logger.error('Authentik OAuth is not configured');
return {
error_code: 401,
error: 'Authentik OAuth is not configured',
};
}

if (!code)
return {
redirect: authentik_auth.oauth_url(
config.oauth.authentik_client_id,
`${config.core.return_https ? 'https' : 'http'}://${host}`,
config.oauth.authentik_authorize_url,
state
),
};

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

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

const text = await resp.text();
logger.debug(`oauth ${config.oauth.authentik_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 authentik_auth.oauth_user(json.access_token, config.oauth.authentik_userinfo_url);
if (!userJson) return { error: 'invalid user request' };

return {
username: userJson.preferred_username,
user_id: userJson.sub,
access_token: json.access_token,
refresh_token: json.refresh_token,
};
}

export default withZipline(withOAuth('authentik', handler));
19 changes: 18 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 { authentik_auth, discord_auth, github_auth, google_auth } from 'lib/oauth';
import prisma from 'lib/prisma';
import { hashPassword } from 'lib/util';
import { jsonUserReplacer } from 'lib/utils/client';
Expand Down Expand Up @@ -131,6 +131,23 @@ async function handler(req: NextApiReq, res: NextApiRes, user: UserExtended) {
},
});
}
} else if (user.oauth.find((o) => o.provider === 'AUTHENTIK')) {
const resp = await authentik_auth.oauth_user(
user.oauth.find((o) => o.provider === 'AUTHENTIK').token,
zconfig.oauth.authentik_userinfo_url
);
if (!resp) {
logger.debug(`oauth expired for ${JSON.stringify(user, jsonUserReplacer)}`);

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

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, IconKey } 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,
Authentik: IconKey,
};

for (const provider of oauth_providers) {
Expand Down