mirror of
https://github.com/diced/zipline.git
synced 2025-05-11 10:26:05 +02:00
fix: sessions & authentik -> oidc & #603
This commit is contained in:
parent
708b130002
commit
67a7d44198
20 changed files with 119 additions and 107 deletions
10
README.md
10
README.md
|
@ -118,11 +118,11 @@ OAUTH_GITHUB_CLIENT_SECRET=x
|
||||||
OAUTH_GOOGLE_CLIENT_ID=x-x.apps.googleusercontent.com
|
OAUTH_GOOGLE_CLIENT_ID=x-x.apps.googleusercontent.com
|
||||||
OAUTH_GOOGLE_CLIENT_SECRET=x-x-x
|
OAUTH_GOOGLE_CLIENT_SECRET=x-x-x
|
||||||
|
|
||||||
OAUTH_AUTHENTIK_CLIENT_ID=x
|
OAUTH_OIDC_CLIENT_ID=x
|
||||||
OAUTH_AUTHENTIK_CLIENT_SECRET=x
|
OAUTH_OIDC_CLIENT_SECRET=x
|
||||||
OAUTH_AUTHENTIK_AUTHORIZE_URL=http://localhost:9000/application/o/authorize/
|
OAUTH_OIDC_AUTHORIZE_URL=http://localhost:9000/application/o/authorize/
|
||||||
OAUTH_AUTHENTIK_USERINFO_URL=http://localhost:9000/application/o/userinfo/
|
OAUTH_OIDC_USERINFO_URL=http://localhost:9000/application/o/userinfo/
|
||||||
OAUTH_AUTHENTIK_TOKEN_URL=http://localhost:9000/application/o/token/
|
OAUTH_OIDC_TOKEN_URL=http://localhost:9000/application/o/token/
|
||||||
|
|
||||||
FEATURES_OAUTH_REGISTRATION=true
|
FEATURES_OAUTH_REGISTRATION=true
|
||||||
FEATURES_USER_REGISTRATION=true
|
FEATURES_USER_REGISTRATION=true
|
||||||
|
|
|
@ -29,6 +29,7 @@ model User {
|
||||||
|
|
||||||
totpSecret String?
|
totpSecret String?
|
||||||
passkeys UserPasskey[]
|
passkeys UserPasskey[]
|
||||||
|
sessions String[]
|
||||||
|
|
||||||
quota UserQuota?
|
quota UserQuota?
|
||||||
|
|
||||||
|
@ -103,7 +104,7 @@ enum OAuthProviderType {
|
||||||
DISCORD
|
DISCORD
|
||||||
GOOGLE
|
GOOGLE
|
||||||
GITHUB
|
GITHUB
|
||||||
AUTHENTIK
|
OIDC
|
||||||
}
|
}
|
||||||
|
|
||||||
model File {
|
model File {
|
||||||
|
@ -112,7 +113,7 @@ model File {
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
deletesAt DateTime?
|
deletesAt DateTime?
|
||||||
|
|
||||||
name String // name & file saved on datasource
|
name String // name & file saved on datasource
|
||||||
originalName String? // original name of file when uploaded
|
originalName String? // original name of file when uploaded
|
||||||
size BigInt
|
size BigInt
|
||||||
type String
|
type String
|
||||||
|
@ -232,4 +233,4 @@ model Invite {
|
||||||
|
|
||||||
inviter User @relation(fields: [inviterId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
inviter User @relation(fields: [inviterId], references: [id], onDelete: Cascade, onUpdate: Cascade)
|
||||||
inviterId String
|
inviterId String
|
||||||
}
|
}
|
|
@ -24,14 +24,14 @@ const icons = {
|
||||||
DISCORD: <IconBrandDiscordFilled size='1rem' />,
|
DISCORD: <IconBrandDiscordFilled size='1rem' />,
|
||||||
GITHUB: <IconBrandGithubFilled size='1rem' />,
|
GITHUB: <IconBrandGithubFilled size='1rem' />,
|
||||||
GOOGLE: <IconBrandGoogleFilled size='1rem' stroke={4} />,
|
GOOGLE: <IconBrandGoogleFilled size='1rem' stroke={4} />,
|
||||||
AUTHENTIK: <IconCircleKeyFilled size='1rem' />,
|
OIDC: <IconCircleKeyFilled size='1rem' />,
|
||||||
};
|
};
|
||||||
|
|
||||||
const names = {
|
const names = {
|
||||||
DISCORD: 'Discord',
|
DISCORD: 'Discord',
|
||||||
GITHUB: 'GitHub',
|
GITHUB: 'GitHub',
|
||||||
GOOGLE: 'Google',
|
GOOGLE: 'Google',
|
||||||
AUTHENTIK: 'Authentik',
|
OIDC: 'OpenID Connect',
|
||||||
};
|
};
|
||||||
|
|
||||||
function OAuthButton({ provider, linked }: { provider: OAuthProviderType; linked: boolean }) {
|
function OAuthButton({ provider, linked }: { provider: OAuthProviderType; linked: boolean }) {
|
||||||
|
@ -90,7 +90,7 @@ export default function SettingsOAuth() {
|
||||||
const discordLinked = findProvider('DISCORD', user?.oauthProviders ?? []);
|
const discordLinked = findProvider('DISCORD', user?.oauthProviders ?? []);
|
||||||
const githubLinked = findProvider('GITHUB', user?.oauthProviders ?? []);
|
const githubLinked = findProvider('GITHUB', user?.oauthProviders ?? []);
|
||||||
const googleLinked = findProvider('GOOGLE', user?.oauthProviders ?? []);
|
const googleLinked = findProvider('GOOGLE', user?.oauthProviders ?? []);
|
||||||
const authentikLinked = findProvider('AUTHENTIK', user?.oauthProviders ?? []);
|
const oidcLinked = findProvider('OIDC', user?.oauthProviders ?? []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Paper withBorder p='sm'>
|
<Paper withBorder p='sm'>
|
||||||
|
@ -103,7 +103,7 @@ export default function SettingsOAuth() {
|
||||||
{config.oauthEnabled.discord && <OAuthButton provider='DISCORD' linked={!!discordLinked} />}
|
{config.oauthEnabled.discord && <OAuthButton provider='DISCORD' linked={!!discordLinked} />}
|
||||||
{config.oauthEnabled.github && <OAuthButton provider='GITHUB' linked={!!githubLinked} />}
|
{config.oauthEnabled.github && <OAuthButton provider='GITHUB' linked={!!githubLinked} />}
|
||||||
{config.oauthEnabled.google && <OAuthButton provider='GOOGLE' linked={!!googleLinked} />}
|
{config.oauthEnabled.google && <OAuthButton provider='GOOGLE' linked={!!googleLinked} />}
|
||||||
{config.oauthEnabled.authentik && <OAuthButton provider='AUTHENTIK' linked={!!authentikLinked} />}
|
{config.oauthEnabled.oidc && <OAuthButton provider='OIDC' linked={!!oidcLinked} />}
|
||||||
</Group>
|
</Group>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
|
|
|
@ -102,7 +102,7 @@ export const rawConfig: any = {
|
||||||
clientId: undefined,
|
clientId: undefined,
|
||||||
clientSecret: undefined,
|
clientSecret: undefined,
|
||||||
},
|
},
|
||||||
authentik: {
|
oidc: {
|
||||||
clientId: undefined,
|
clientId: undefined,
|
||||||
clientSecret: undefined,
|
clientSecret: undefined,
|
||||||
authorizeUrl: undefined,
|
authorizeUrl: undefined,
|
||||||
|
@ -211,11 +211,11 @@ export const PROP_TO_ENV = {
|
||||||
'oauth.github.clientSecret': 'OAUTH_GITHUB_CLIENT_SECRET',
|
'oauth.github.clientSecret': 'OAUTH_GITHUB_CLIENT_SECRET',
|
||||||
'oauth.google.clientId': 'OAUTH_GOOGLE_CLIENT_ID',
|
'oauth.google.clientId': 'OAUTH_GOOGLE_CLIENT_ID',
|
||||||
'oauth.google.clientSecret': 'OAUTH_GOOGLE_CLIENT_SECRET',
|
'oauth.google.clientSecret': 'OAUTH_GOOGLE_CLIENT_SECRET',
|
||||||
'oauth.authentik.clientId': 'OAUTH_AUTHENTIK_CLIENT_ID',
|
'oauth.oidc.clientId': 'OAUTH_OIDC_CLIENT_ID',
|
||||||
'oauth.authentik.clientSecret': 'OAUTH_AUTHENTIK_CLIENT_SECRET',
|
'oauth.oidc.clientSecret': 'OAUTH_OIDC_CLIENT_SECRET',
|
||||||
'oauth.authentik.authorizeUrl': 'OAUTH_AUTHENTIK_AUTHORIZE_URL',
|
'oauth.oidc.authorizeUrl': 'OAUTH_OIDC_AUTHORIZE_URL',
|
||||||
'oauth.authentik.userinfoUrl': 'OAUTH_AUTHENTIK_USERINFO_URL',
|
'oauth.oidc.userinfoUrl': 'OAUTH_OIDC_USERINFO_URL',
|
||||||
'oauth.authentik.tokenUrl': 'OAUTH_AUTHENTIK_TOKEN_URL',
|
'oauth.oidc.tokenUrl': 'OAUTH_OIDC_TOKEN_URL',
|
||||||
|
|
||||||
'discord.webhookUrl': 'DISCORD_WEBHOOK_URL',
|
'discord.webhookUrl': 'DISCORD_WEBHOOK_URL',
|
||||||
'discord.username': 'DISCORD_USERNAME',
|
'discord.username': 'DISCORD_USERNAME',
|
||||||
|
@ -339,11 +339,11 @@ export function readEnv() {
|
||||||
env('oauth.github.clientSecret', 'string'),
|
env('oauth.github.clientSecret', 'string'),
|
||||||
env('oauth.google.clientId', 'string'),
|
env('oauth.google.clientId', 'string'),
|
||||||
env('oauth.google.clientSecret', 'string'),
|
env('oauth.google.clientSecret', 'string'),
|
||||||
env('oauth.authentik.clientId', 'string'),
|
env('oauth.oidc.clientId', 'string'),
|
||||||
env('oauth.authentik.clientSecret', 'string'),
|
env('oauth.oidc.clientSecret', 'string'),
|
||||||
env('oauth.authentik.authorizeUrl', 'string'),
|
env('oauth.oidc.authorizeUrl', 'string'),
|
||||||
env('oauth.authentik.userinfoUrl', 'string'),
|
env('oauth.oidc.userinfoUrl', 'string'),
|
||||||
env('oauth.authentik.tokenUrl', 'string'),
|
env('oauth.oidc.tokenUrl', 'string'),
|
||||||
|
|
||||||
env('discord.webhookUrl', 'string'),
|
env('discord.webhookUrl', 'string'),
|
||||||
env('discord.username', 'string'),
|
env('discord.username', 'string'),
|
||||||
|
|
|
@ -242,7 +242,7 @@ export const schema = z.object({
|
||||||
clientSecret: z.undefined(),
|
clientSecret: z.undefined(),
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
authentik: z
|
oidc: z
|
||||||
.object({
|
.object({
|
||||||
clientId: z.string(),
|
clientId: z.string(),
|
||||||
clientSecret: z.string(),
|
clientSecret: z.string(),
|
||||||
|
|
|
@ -9,6 +9,8 @@ export type User = {
|
||||||
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
|
role: 'USER' | 'ADMIN' | 'SUPERADMIN';
|
||||||
view: UserViewSettings;
|
view: UserViewSettings;
|
||||||
|
|
||||||
|
sessions: string[];
|
||||||
|
|
||||||
oauthProviders: OAuthProvider[];
|
oauthProviders: OAuthProvider[];
|
||||||
|
|
||||||
totpSecret?: string | null;
|
totpSecret?: string | null;
|
||||||
|
@ -32,6 +34,7 @@ export const userSelect = {
|
||||||
totpSecret: true,
|
totpSecret: true,
|
||||||
passkeys: true,
|
passkeys: true,
|
||||||
quota: true,
|
quota: true,
|
||||||
|
sessions: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UserViewSettings = z.infer<typeof userViewSchema>;
|
export type UserViewSettings = z.infer<typeof userViewSchema>;
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { User, userSelect } from '../db/models/user';
|
||||||
import { NextApiReq, NextApiRes } from '../response';
|
import { NextApiReq, NextApiRes } from '../response';
|
||||||
import { Handler } from './combine';
|
import { Handler } from './combine';
|
||||||
import { isAdministrator } from '../role';
|
import { isAdministrator } from '../role';
|
||||||
|
import { getSession } from '@/server/session';
|
||||||
|
|
||||||
export type ZiplineAuthOptions = {
|
export type ZiplineAuthOptions = {
|
||||||
administratorOnly?: boolean;
|
administratorOnly?: boolean;
|
||||||
|
@ -47,28 +48,19 @@ export function parseUserToken(
|
||||||
export function ziplineAuth(options?: ZiplineAuthOptions) {
|
export function ziplineAuth(options?: ZiplineAuthOptions) {
|
||||||
return (handler: Handler) => {
|
return (handler: Handler) => {
|
||||||
return async (req: NextApiReq, res: NextApiRes) => {
|
return async (req: NextApiReq, res: NextApiRes) => {
|
||||||
let rawToken: string | undefined;
|
const session = await getSession(req, res);
|
||||||
|
if (!session.id || !session.sessionId) return res.unauthorized('invalid session, not logged in');
|
||||||
if (req.cookies.zipline_token) rawToken = req.cookies.zipline_token;
|
|
||||||
else if (req.headers.authorization) rawToken = req.headers.authorization;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
var token = parseUserToken(rawToken);
|
|
||||||
} catch (e) {
|
|
||||||
return res.unauthorized((e as { error: string }).error);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
token,
|
sessions: {
|
||||||
},
|
has: session.sessionId,
|
||||||
select: {
|
},
|
||||||
...userSelect,
|
|
||||||
...(options?.select && options.select),
|
|
||||||
},
|
},
|
||||||
|
select: userSelect,
|
||||||
});
|
});
|
||||||
if (!user) return res.unauthorized();
|
|
||||||
|
if (!user) return res.unauthorized('invalid login session');
|
||||||
|
|
||||||
req.user = user;
|
req.user = user;
|
||||||
|
|
||||||
|
|
|
@ -20,12 +20,12 @@ export default function enabled(config: Config) {
|
||||||
config.features.oauthRegistration,
|
config.features.oauthRegistration,
|
||||||
);
|
);
|
||||||
|
|
||||||
const authentikEnabled = isTruthy(
|
const oidcEnabled = isTruthy(
|
||||||
config.oauth?.authentik?.clientId,
|
config.oauth?.oidc?.clientId,
|
||||||
config.oauth?.authentik?.clientSecret,
|
config.oauth?.oidc?.clientSecret,
|
||||||
config.oauth?.authentik?.authorizeUrl,
|
config.oauth?.oidc?.authorizeUrl,
|
||||||
config.oauth?.authentik?.tokenUrl,
|
config.oauth?.oidc?.tokenUrl,
|
||||||
config.oauth?.authentik?.userinfoUrl,
|
config.oauth?.oidc?.userinfoUrl,
|
||||||
config.features.oauthRegistration,
|
config.features.oauthRegistration,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -33,6 +33,6 @@ export default function enabled(config: Config) {
|
||||||
discord: discordEnabled,
|
discord: discordEnabled,
|
||||||
github: githubEnabled,
|
github: githubEnabled,
|
||||||
google: googleEnabled,
|
google: googleEnabled,
|
||||||
authentik: authentikEnabled,
|
oidc: oidcEnabled,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -61,10 +61,10 @@ export const googleAuth = {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const authentikAuth = {
|
export const oidcAuth = {
|
||||||
url: (clientId: string, origin: string, authorizeUrl: string, state?: string) =>
|
url: (clientId: string, origin: string, authorizeUrl: string, state?: string) =>
|
||||||
`${authorizeUrl}?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
`${authorizeUrl}?client_id=${clientId}&redirect_uri=${encodeURIComponent(
|
||||||
`${origin}/api/auth/oauth/authentik`,
|
`${origin}/api/auth/oauth/oidc`,
|
||||||
)}&response_type=code&scope=openid+email+profile+offline_access${state ? `&state=${state}` : ''}`,
|
)}&response_type=code&scope=openid+email+profile+offline_access${state ? `&state=${state}` : ''}`,
|
||||||
user: async (accessToken: string, userInfoUrl: string) => {
|
user: async (accessToken: string, userInfoUrl: string) => {
|
||||||
const res = await fetch(userInfoUrl, {
|
const res = await fetch(userInfoUrl, {
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import { NextApiReq, NextApiRes } from '@/lib/response';
|
import { NextApiReq, NextApiRes } from '@/lib/response';
|
||||||
import { OAuthProviderType } from '@prisma/client';
|
import { OAuthProviderType } from '@prisma/client';
|
||||||
import { prisma } from '../db';
|
import { prisma } from '../db';
|
||||||
import { parseUserToken } from '../middleware/ziplineAuth';
|
|
||||||
import { findProvider } from './providerUtil';
|
import { findProvider } from './providerUtil';
|
||||||
import { createToken } from '../crypto';
|
import { createToken } from '../crypto';
|
||||||
import { config } from '../config';
|
import { config } from '../config';
|
||||||
import { User } from '../db/models/user';
|
import { User } from '../db/models/user';
|
||||||
import Logger, { log } from '../logger';
|
import Logger, { log } from '../logger';
|
||||||
import { getSession } from '@/server/session';
|
import { getSession, saveSession } from '@/server/session';
|
||||||
|
|
||||||
export interface OAuthQuery {
|
export interface OAuthQuery {
|
||||||
state?: string;
|
state?: string;
|
||||||
|
@ -76,15 +75,11 @@ export const withOAuth =
|
||||||
|
|
||||||
const { state } = req.query as OAuthQuery;
|
const { state } = req.query as OAuthQuery;
|
||||||
|
|
||||||
let rawToken: string | undefined;
|
|
||||||
|
|
||||||
if (req.cookies.zipline_token) rawToken = req.cookies.zipline_token;
|
|
||||||
else if (req.headers.authorization) rawToken = req.headers.authorization;
|
|
||||||
const token = parseUserToken(rawToken, true);
|
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
token: token ?? '',
|
sessions: {
|
||||||
|
has: session.sessionId ?? '',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
oauthProviders: true,
|
oauthProviders: true,
|
||||||
|
@ -94,7 +89,7 @@ export const withOAuth =
|
||||||
const userOauth = findProvider(provider, user?.oauthProviders ?? []);
|
const userOauth = findProvider(provider, user?.oauthProviders ?? []);
|
||||||
|
|
||||||
if (state === 'link') {
|
if (state === 'link') {
|
||||||
if (!user) return res.unauthorized();
|
if (!user) return res.unauthorized('invalid session');
|
||||||
|
|
||||||
if (findProvider(provider, user.oauthProviders))
|
if (findProvider(provider, user.oauthProviders))
|
||||||
return res.badRequest('This account is already linked to this provider');
|
return res.badRequest('This account is already linked to this provider');
|
||||||
|
@ -122,8 +117,7 @@ export const withOAuth =
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
session.user = user;
|
await saveSession(session, <User>user);
|
||||||
await session.save();
|
|
||||||
|
|
||||||
logger.info('linked oauth account', {
|
logger.info('linked oauth account', {
|
||||||
provider,
|
provider,
|
||||||
|
@ -153,8 +147,7 @@ export const withOAuth =
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
session.user = user;
|
await saveSession(session, <User>user);
|
||||||
await session.save();
|
|
||||||
|
|
||||||
return res.redirect('/dashboard');
|
return res.redirect('/dashboard');
|
||||||
} else if (existingOauth) {
|
} else if (existingOauth) {
|
||||||
|
@ -173,8 +166,7 @@ export const withOAuth =
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
session.user = login.user! as User;
|
await saveSession(session, <User>login.user!);
|
||||||
await session.save();
|
|
||||||
|
|
||||||
logger.info('logged in with oauth', {
|
logger.info('logged in with oauth', {
|
||||||
provider,
|
provider,
|
||||||
|
@ -206,8 +198,7 @@ export const withOAuth =
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
session.user = nuser as User;
|
await saveSession(session, <User>nuser);
|
||||||
await session.save();
|
|
||||||
|
|
||||||
logger.info('created user with oauth', {
|
logger.info('created user with oauth', {
|
||||||
provider,
|
provider,
|
||||||
|
|
|
@ -69,7 +69,7 @@ export async function handleOverrideColors(theme: ZiplineTheme) {
|
||||||
...theme.colors,
|
...theme.colors,
|
||||||
google: theme.colors?.google || Array(10).fill('#4285F4'),
|
google: theme.colors?.google || Array(10).fill('#4285F4'),
|
||||||
github: theme.colors?.github || Array(10).fill('#24292E'),
|
github: theme.colors?.github || Array(10).fill('#24292E'),
|
||||||
authentik: theme.colors?.authentik || Array(10).fill('#FD4B2D'),
|
oidc: theme.colors?.oidc || Array(10).fill('#72abcf'),
|
||||||
discord: theme.colors?.discord || Array(10).fill('#5865F2'),
|
discord: theme.colors?.discord || Array(10).fill('#5865F2'),
|
||||||
},
|
},
|
||||||
} as ZiplineTheme;
|
} as ZiplineTheme;
|
||||||
|
|
26
src/pages/api/auth/oauth/authentik.ts → src/pages/api/auth/oauth/oidc.ts
Executable file → Normal file
26
src/pages/api/auth/oauth/authentik.ts → src/pages/api/auth/oauth/oidc.ts
Executable file → Normal file
|
@ -3,7 +3,7 @@ import Logger from '@/lib/logger';
|
||||||
import { combine } from '@/lib/middleware/combine';
|
import { combine } from '@/lib/middleware/combine';
|
||||||
import { method } from '@/lib/middleware/method';
|
import { method } from '@/lib/middleware/method';
|
||||||
import enabled from '@/lib/oauth/enabled';
|
import enabled from '@/lib/oauth/enabled';
|
||||||
import { authentikAuth } from '@/lib/oauth/providerUtil';
|
import { oidcAuth } from '@/lib/oauth/providerUtil';
|
||||||
import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth';
|
import { OAuthQuery, OAuthResponse, withOAuth } from '@/lib/oauth/withOAuth';
|
||||||
|
|
||||||
// thanks to @danejur for this https://github.com/diced/zipline/pull/372
|
// thanks to @danejur for this https://github.com/diced/zipline/pull/372
|
||||||
|
@ -14,33 +14,33 @@ async function handler({ code, state, host }: OAuthQuery, _logger: Logger): Prom
|
||||||
error_code: 403,
|
error_code: 403,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { authentik: authentikEnabled } = enabled(config);
|
const { oidc: oidcEnabled } = enabled(config);
|
||||||
|
|
||||||
if (!authentikEnabled)
|
if (!oidcEnabled)
|
||||||
return {
|
return {
|
||||||
error: 'Authentik OAuth is not configured.',
|
error: 'OpenID Connect OAuth is not configured.',
|
||||||
error_code: 401,
|
error_code: 401,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!code)
|
if (!code)
|
||||||
return {
|
return {
|
||||||
redirect: authentikAuth.url(
|
redirect: oidcAuth.url(
|
||||||
config.oauth.authentik.clientId!,
|
config.oauth.oidc.clientId!,
|
||||||
`${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`,
|
`${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}`,
|
||||||
config.oauth.authentik.authorizeUrl!,
|
config.oauth.oidc.authorizeUrl!,
|
||||||
state,
|
state,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
const body = new URLSearchParams({
|
const body = new URLSearchParams({
|
||||||
client_id: config.oauth.authentik.clientId!,
|
client_id: config.oauth.oidc.clientId!,
|
||||||
client_secret: config.oauth.authentik.clientSecret!,
|
client_secret: config.oauth.oidc.clientSecret!,
|
||||||
grant_type: 'authorization_code',
|
grant_type: 'authorization_code',
|
||||||
code,
|
code,
|
||||||
redirect_uri: `${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}/api/auth/oauth/authentik`,
|
redirect_uri: `${config.core.returnHttpsUrls ? 'https' : 'http'}://${host}/api/auth/oauth/oidc`,
|
||||||
});
|
});
|
||||||
|
|
||||||
const res = await fetch(config.oauth.authentik.tokenUrl!, {
|
const res = await fetch(config.oauth.oidc.tokenUrl!, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body,
|
body,
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -57,7 +57,7 @@ async function handler({ code, state, host }: OAuthQuery, _logger: Logger): Prom
|
||||||
if (!json.access_token) return { error: 'No access token in response' };
|
if (!json.access_token) return { error: 'No access token in response' };
|
||||||
if (!json.refresh_token) return { error: 'No refresh token in response' };
|
if (!json.refresh_token) return { error: 'No refresh token in response' };
|
||||||
|
|
||||||
const userJson = await authentikAuth.user(json.access_token, config.oauth.authentik.userinfoUrl!);
|
const userJson = await oidcAuth.user(json.access_token, config.oauth.oidc.userinfoUrl!);
|
||||||
if (!userJson) return { error: 'Failed to fetch user' };
|
if (!userJson) return { error: 'Failed to fetch user' };
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
@ -68,4 +68,4 @@ async function handler({ code, state, host }: OAuthQuery, _logger: Logger): Prom
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default combine([method(['GET'])], withOAuth('AUTHENTIK', handler));
|
export default combine([method(['GET'])], withOAuth('OIDC', handler));
|
|
@ -332,8 +332,8 @@ export default function Login({ config }: InferGetServerSidePropsType<typeof get
|
||||||
leftSection={<IconBrandGoogleFilled stroke={4} />}
|
leftSection={<IconBrandGoogleFilled stroke={4} />}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{config.oauthEnabled.authentik && (
|
{config.oauthEnabled.oidc && (
|
||||||
<ExternalAuthButton provider='Authentik' alpha={0.2} leftSection={<IconCircleKeyFilled />} />
|
<ExternalAuthButton provider='OIDC' alpha={0.2} leftSection={<IconCircleKeyFilled />} />
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
|
@ -41,11 +41,13 @@ export function parseUserToken(
|
||||||
export async function userMiddleware(req: FastifyRequest, res: FastifyReply) {
|
export async function userMiddleware(req: FastifyRequest, res: FastifyReply) {
|
||||||
const session = await getSession(req, res);
|
const session = await getSession(req, res);
|
||||||
|
|
||||||
if (!session.user) return res.unauthorized('not logged in');
|
if (!session.id || !session.sessionId) return res.unauthorized('not logged in');
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
const user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
password: session.user.password,
|
sessions: {
|
||||||
|
has: session.sessionId,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
select: userSelect,
|
select: userSelect,
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { verifyPassword } from '@/lib/crypto';
|
||||||
import { prisma } from '@/lib/db';
|
import { prisma } from '@/lib/db';
|
||||||
import { User, userSelect } from '@/lib/db/models/user';
|
import { User, userSelect } from '@/lib/db/models/user';
|
||||||
import { verifyTotpCode } from '@/lib/totp';
|
import { verifyTotpCode } from '@/lib/totp';
|
||||||
import { getSession } from '@/server/session';
|
import { getSession, saveSession } from '@/server/session';
|
||||||
import fastifyPlugin from 'fastify-plugin';
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
|
|
||||||
export type ApiLoginResponse = {
|
export type ApiLoginResponse = {
|
||||||
|
@ -27,7 +27,8 @@ export default fastifyPlugin(
|
||||||
handler: async (req, res) => {
|
handler: async (req, res) => {
|
||||||
const session = await getSession(req, res);
|
const session = await getSession(req, res);
|
||||||
|
|
||||||
session.user = null;
|
session.id = null;
|
||||||
|
session.sessionId = null;
|
||||||
|
|
||||||
const { username, password, code } = req.body;
|
const { username, password, code } = req.body;
|
||||||
|
|
||||||
|
@ -60,8 +61,7 @@ export default fastifyPlugin(
|
||||||
totp: true,
|
totp: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
session.user = user;
|
await saveSession(session, user as User, false);
|
||||||
await session.save();
|
|
||||||
|
|
||||||
delete (user as any).password;
|
delete (user as any).password;
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import { config } from '@/lib/config';
|
import { config } from '@/lib/config';
|
||||||
import { createToken, hashPassword } from '@/lib/crypto';
|
import { createToken, hashPassword } from '@/lib/crypto';
|
||||||
import { prisma } from '@/lib/db';
|
import { prisma } from '@/lib/db';
|
||||||
import { userSelect } from '@/lib/db/models/user';
|
import { User, userSelect } from '@/lib/db/models/user';
|
||||||
import { getSession } from '@/server/session';
|
import { getSession, saveSession } from '@/server/session';
|
||||||
import fastifyPlugin from 'fastify-plugin';
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
import { ApiLoginResponse } from './login';
|
import { ApiLoginResponse } from './login';
|
||||||
|
|
||||||
|
@ -76,8 +76,7 @@ export default fastifyPlugin(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
session.user = user;
|
await saveSession(session, <User>user);
|
||||||
await session.save();
|
|
||||||
|
|
||||||
delete (user as any).password;
|
delete (user as any).password;
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { config } from '@/lib/config';
|
import { config } from '@/lib/config';
|
||||||
import { prisma } from '@/lib/db';
|
import { prisma } from '@/lib/db';
|
||||||
import { User, userSelect } from '@/lib/db/models/user';
|
import { User, userSelect } from '@/lib/db/models/user';
|
||||||
import { getSession } from '@/server/session';
|
import { getSession, saveSession } from '@/server/session';
|
||||||
import { AuthenticationResponseJSON } from '@github/webauthn-json/dist/types/browser-ponyfill';
|
import { AuthenticationResponseJSON } from '@github/webauthn-json/dist/types/browser-ponyfill';
|
||||||
import fastifyPlugin from 'fastify-plugin';
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
|
|
||||||
|
@ -47,8 +47,7 @@ export default fastifyPlugin(
|
||||||
});
|
});
|
||||||
if (!user) return res.badRequest('Invalid passkey');
|
if (!user) return res.badRequest('Invalid passkey');
|
||||||
|
|
||||||
session.user = user;
|
await saveSession(session, <User>user);
|
||||||
await session.save();
|
|
||||||
|
|
||||||
delete (user as any).password;
|
delete (user as any).password;
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { hashPassword } from '@/lib/crypto';
|
||||||
import { prisma } from '@/lib/db';
|
import { prisma } from '@/lib/db';
|
||||||
import { User, userSelect } from '@/lib/db/models/user';
|
import { User, userSelect } from '@/lib/db/models/user';
|
||||||
import { userMiddleware } from '@/server/middleware/user';
|
import { userMiddleware } from '@/server/middleware/user';
|
||||||
import { getSession } from '@/server/session';
|
import { getSession, saveSession } from '@/server/session';
|
||||||
import fastifyPlugin from 'fastify-plugin';
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
|
|
||||||
export type ApiUserResponse = {
|
export type ApiUserResponse = {
|
||||||
|
@ -75,12 +75,13 @@ export default fastifyPlugin(
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
...userSelect,
|
...userSelect,
|
||||||
|
password: true,
|
||||||
|
token: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const session = await getSession(req, res);
|
const session = await getSession(req, res);
|
||||||
session.user = user;
|
await saveSession(session, <User>user);
|
||||||
await session.save();
|
|
||||||
|
|
||||||
delete (user as any).password;
|
delete (user as any).password;
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,6 @@ import { createToken, encryptToken } from '@/lib/crypto';
|
||||||
import { prisma } from '@/lib/db';
|
import { prisma } from '@/lib/db';
|
||||||
import { User, userSelect } from '@/lib/db/models/user';
|
import { User, userSelect } from '@/lib/db/models/user';
|
||||||
import { userMiddleware } from '@/server/middleware/user';
|
import { userMiddleware } from '@/server/middleware/user';
|
||||||
import { getSession } from '@/server/session';
|
|
||||||
import fastifyPlugin from 'fastify-plugin';
|
import fastifyPlugin from 'fastify-plugin';
|
||||||
|
|
||||||
export type ApiUserTokenResponse = {
|
export type ApiUserTokenResponse = {
|
||||||
|
@ -32,8 +31,6 @@ export default fastifyPlugin(
|
||||||
});
|
});
|
||||||
|
|
||||||
server.patch(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
server.patch(PATH, { preHandler: [userMiddleware] }, async (req, res) => {
|
||||||
const session = await getSession(req, res);
|
|
||||||
|
|
||||||
const user = await prisma.user.update({
|
const user = await prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
id: req.user.id,
|
id: req.user.id,
|
||||||
|
@ -47,8 +44,6 @@ export default fastifyPlugin(
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
session.user!.token = user.token;
|
|
||||||
|
|
||||||
delete (user as any).password;
|
delete (user as any).password;
|
||||||
|
|
||||||
return res.send({
|
return res.send({
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
import { config } from '@/lib/config';
|
import { config } from '@/lib/config';
|
||||||
|
import { prisma } from '@/lib/db';
|
||||||
import { User } from '@/lib/db/models/user';
|
import { User } from '@/lib/db/models/user';
|
||||||
|
import { randomCharacters } from '@/lib/random';
|
||||||
import { FastifyReply, FastifyRequest } from 'fastify';
|
import { FastifyReply, FastifyRequest } from 'fastify';
|
||||||
import { IncomingMessage, ServerResponse } from 'http';
|
import { IncomingMessage, ServerResponse } from 'http';
|
||||||
import { getIronSession } from 'iron-session';
|
import { getIronSession } from 'iron-session';
|
||||||
|
@ -12,12 +14,17 @@ const cookieOptions = {
|
||||||
sameSite: 'lax',
|
sameSite: 'lax',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ZiplineSession = {
|
||||||
|
id: string | null;
|
||||||
|
sessionId: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export async function getSession(
|
export async function getSession(
|
||||||
req: FastifyRequest | IncomingMessage,
|
req: FastifyRequest | IncomingMessage,
|
||||||
reply: FastifyReply | ServerResponse<IncomingMessage>,
|
reply: FastifyReply | ServerResponse<IncomingMessage>,
|
||||||
) {
|
) {
|
||||||
if (!(req as any).raw || !(req as any).raw) {
|
if (!(req as any).raw || !(req as any).raw) {
|
||||||
const session = await getIronSession<{ user: User | null }>(
|
const session = await getIronSession<ZiplineSession>(
|
||||||
req as IncomingMessage,
|
req as IncomingMessage,
|
||||||
reply as ServerResponse<IncomingMessage>,
|
reply as ServerResponse<IncomingMessage>,
|
||||||
{
|
{
|
||||||
|
@ -30,7 +37,7 @@ export async function getSession(
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
const session = await getIronSession<{ user: User | null }>(
|
const session = await getIronSession<ZiplineSession>(
|
||||||
(req as FastifyRequest).raw,
|
(req as FastifyRequest).raw,
|
||||||
(reply as FastifyReply).raw,
|
(reply as FastifyReply).raw,
|
||||||
{
|
{
|
||||||
|
@ -42,3 +49,25 @@ export async function getSession(
|
||||||
|
|
||||||
return session;
|
return session;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function saveSession(
|
||||||
|
session: Awaited<ReturnType<typeof getSession>>,
|
||||||
|
user: User,
|
||||||
|
overwriteSessions = true,
|
||||||
|
) {
|
||||||
|
session.id = user.id;
|
||||||
|
|
||||||
|
const sessionId = randomCharacters(32);
|
||||||
|
session.sessionId = sessionId;
|
||||||
|
|
||||||
|
await prisma.user.update({
|
||||||
|
where: {
|
||||||
|
id: user.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
sessions: overwriteSessions ? { set: [sessionId] } : { push: sessionId },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await session.save();
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue