feat(oidc): add authentik as oidc
chore(env): missing var in .env.example chore: typo chore: add reviewed stuff fix: rebased stuff fix(authentik-oidc): redirect corect oidc provider
This commit is contained in:
parent
b8a0e1cbf2
commit
f9aa278883
|
|
@ -91,3 +91,8 @@ STRIPE_SIGNING_KEY_CONNECT=""
|
|||
# Developer Settings
|
||||
NX_ADD_PLUGINS=false
|
||||
IS_GENERAL="true" # required for now
|
||||
AUTHENTIK_OIDC="false"
|
||||
AUTHENTIK_URL=""
|
||||
AUTHENTIK_CLIENT_ID=""
|
||||
AUTHENTIK_CLIENT_SECRET=""
|
||||
AUTHENTIK_SCOPE=""
|
||||
|
|
|
|||
|
|
@ -0,0 +1,88 @@
|
|||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
|
||||
|
||||
export class AuthentikProvider implements ProvidersInterface {
|
||||
private readonly baseUrl: string;
|
||||
private readonly clientId: string;
|
||||
private readonly clientSecret: string;
|
||||
private readonly frontendUrl: string;
|
||||
|
||||
constructor() {
|
||||
const {
|
||||
AUTHENTIK_URL,
|
||||
AUTHENTIK_CLIENT_ID,
|
||||
AUTHENTIK_CLIENT_SECRET,
|
||||
FRONTEND_URL,
|
||||
} = process.env;
|
||||
|
||||
if (!AUTHENTIK_URL)
|
||||
throw new Error('AUTHENTIK_URL environment variable is not set');
|
||||
if (!AUTHENTIK_CLIENT_ID)
|
||||
throw new Error('AUTHENTIK_CLIENT_ID environment variable is not set');
|
||||
if (!AUTHENTIK_CLIENT_SECRET)
|
||||
throw new Error(
|
||||
'AUTHENTIK_CLIENT_SECRET environment variable is not set'
|
||||
);
|
||||
if (!FRONTEND_URL)
|
||||
throw new Error('FRONTEND_URL environment variable is not set');
|
||||
|
||||
this.baseUrl = AUTHENTIK_URL.endsWith('/')
|
||||
? AUTHENTIK_URL.slice(0, -1)
|
||||
: AUTHENTIK_URL;
|
||||
this.clientId = AUTHENTIK_CLIENT_ID;
|
||||
this.clientSecret = AUTHENTIK_CLIENT_SECRET;
|
||||
this.frontendUrl = FRONTEND_URL;
|
||||
}
|
||||
|
||||
generateLink(): string {
|
||||
const params = new URLSearchParams({
|
||||
client_id: this.clientId,
|
||||
scope: 'openid profile email',
|
||||
response_type: 'code',
|
||||
redirect_uri: `${this.frontendUrl}/settings`,
|
||||
});
|
||||
|
||||
return `${this.baseUrl}/application/o/authorize/?${params.toString()}`;
|
||||
}
|
||||
|
||||
async getToken(code: string): Promise<string> {
|
||||
const response = await fetch(`${this.baseUrl}/application/o/token/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: new URLSearchParams({
|
||||
grant_type: 'authorization_code',
|
||||
client_id: this.clientId,
|
||||
client_secret: this.clientSecret,
|
||||
code,
|
||||
redirect_uri: `${this.frontendUrl}/settings`,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`Token request failed: ${error}`);
|
||||
}
|
||||
|
||||
const { access_token } = await response.json();
|
||||
return access_token;
|
||||
}
|
||||
|
||||
async getUser(access_token: string): Promise<{ email: string; id: string }> {
|
||||
const response = await fetch(`${this.baseUrl}/application/o/userinfo/`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${access_token}`,
|
||||
Accept: 'application/json',
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const error = await response.text();
|
||||
throw new Error(`User info request failed: ${error}`);
|
||||
}
|
||||
|
||||
const { email, sub: id } = await response.json();
|
||||
return { email, id };
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.int
|
|||
import { GoogleProvider } from '@gitroom/backend/services/auth/providers/google.provider';
|
||||
import { FarcasterProvider } from '@gitroom/backend/services/auth/providers/farcaster.provider';
|
||||
import { WalletProvider } from '@gitroom/backend/services/auth/providers/wallet.provider';
|
||||
import { AuthentikProvider } from '@gitroom/backend/services/auth/providers/authentik.provider';
|
||||
|
||||
export class ProvidersFactory {
|
||||
static loadProvider(provider: Provider): ProvidersInterface {
|
||||
|
|
@ -16,6 +17,8 @@ export class ProvidersFactory {
|
|||
return new FarcasterProvider();
|
||||
case Provider.WALLET:
|
||||
return new WalletProvider();
|
||||
case Provider.AUTHENTIK:
|
||||
return new AuthentikProvider();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.0 KiB |
|
|
@ -39,6 +39,7 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
|
|||
discordUrl={process.env.NEXT_PUBLIC_DISCORD_SUPPORT!}
|
||||
frontEndUrl={process.env.FRONTEND_URL!}
|
||||
isGeneral={!!process.env.IS_GENERAL}
|
||||
authentikOIDC={!!process.env.AUTHENTIK_OIDC}
|
||||
uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!}
|
||||
tolt={process.env.NEXT_PUBLIC_TOLT!}
|
||||
facebookPixel={process.env.NEXT_PUBLIC_FACEBOOK_PIXEL!}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import { useMemo, useState } from 'react';
|
|||
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
|
||||
import { LoginUserDto } from '@gitroom/nestjs-libraries/dtos/auth/login.user.dto';
|
||||
import { GithubProvider } from '@gitroom/frontend/components/auth/providers/github.provider';
|
||||
import { AuthentikProvider } from '@gitroom/frontend/components/auth/providers/authentik.provider';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
import { GoogleProvider } from '@gitroom/frontend/components/auth/providers/google.provider';
|
||||
import { useVariables } from '@gitroom/react/helpers/variable.context';
|
||||
|
|
@ -25,6 +26,7 @@ type Inputs = {
|
|||
export function Login() {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const { isGeneral, neynarClientId, billingEnabled } = useVariables();
|
||||
const { isGeneral, neynarClientId, authentikOIDC } = useVariables();
|
||||
const resolver = useMemo(() => {
|
||||
return classValidatorResolver(LoginUserDto);
|
||||
}, []);
|
||||
|
|
@ -63,8 +65,9 @@ export function Login() {
|
|||
Sign In
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{!isGeneral ? (
|
||||
{isGeneral && authentikOIDC ? (
|
||||
<AuthentikProvider />
|
||||
) : !isGeneral ? (
|
||||
<GithubProvider />
|
||||
) : (
|
||||
<div className="gap-[5px] flex flex-col">
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import { useCallback } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
|
||||
import interClass from '@gitroom/react/helpers/inter.font';
|
||||
|
||||
export const AuthentikProvider = () => {
|
||||
const fetch = useFetch();
|
||||
|
||||
const gotoLogin = useCallback(async () => {
|
||||
try {
|
||||
const response = await fetch('/auth/oauth/AUTHENTIK');
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Login link request failed with status ${response.status}`
|
||||
);
|
||||
}
|
||||
const link = await response.text();
|
||||
window.location.href = link;
|
||||
} catch (error) {
|
||||
console.error('Failed to get Authentik login link:', error);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={gotoLogin}
|
||||
className={`cursor-pointer bg-white h-[44px] rounded-[4px] flex justify-center items-center text-customColor16 ${interClass} gap-[4px]`}
|
||||
>
|
||||
<div>
|
||||
<Image
|
||||
src="/icons/authentik.svg"
|
||||
alt="Authentik"
|
||||
width={40}
|
||||
height={40}
|
||||
/>
|
||||
</div>
|
||||
<div>Sign in with Authentik </div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -44,7 +44,9 @@ export async function middleware(request: NextRequest) {
|
|||
? ''
|
||||
: (url.indexOf('?') > -1 ? '&' : '?') +
|
||||
`provider=${(findIndex === 'settings'
|
||||
? 'github'
|
||||
? process.env.AUTHENTIK_OIDC
|
||||
? 'authentik'
|
||||
: 'github'
|
||||
: findIndex
|
||||
).toUpperCase()}`;
|
||||
return NextResponse.redirect(
|
||||
|
|
|
|||
|
|
@ -636,6 +636,7 @@ enum Provider {
|
|||
GOOGLE
|
||||
FARCASTER
|
||||
WALLET
|
||||
AUTHENTIK
|
||||
}
|
||||
|
||||
enum Role {
|
||||
|
|
@ -648,4 +649,4 @@ enum APPROVED_SUBMIT_FOR_ORDER {
|
|||
NO
|
||||
WAITING_CONFIRMATION
|
||||
YES
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ import { createContext, FC, ReactNode, useContext, useEffect } from 'react';
|
|||
interface VariableContextInterface {
|
||||
billingEnabled: boolean;
|
||||
isGeneral: boolean;
|
||||
authentikOIDC: boolean;
|
||||
frontEndUrl: string;
|
||||
plontoKey: string;
|
||||
storageProvider: 'local' | 'cloudflare',
|
||||
storageProvider: 'local' | 'cloudflare';
|
||||
backendUrl: string;
|
||||
discordUrl: string;
|
||||
uploadDirectory: string;
|
||||
|
|
@ -20,6 +21,7 @@ interface VariableContextInterface {
|
|||
const VariableContext = createContext({
|
||||
billingEnabled: false,
|
||||
isGeneral: true,
|
||||
authentikOIDC: false,
|
||||
frontEndUrl: '',
|
||||
storageProvider: 'local',
|
||||
plontoKey: '',
|
||||
|
|
@ -52,9 +54,9 @@ export const VariableContextComponent: FC<
|
|||
|
||||
export const useVariables = () => {
|
||||
return useContext(VariableContext);
|
||||
}
|
||||
};
|
||||
|
||||
export const loadVars = () => {
|
||||
// @ts-ignore
|
||||
return window.vars as VariableContextInterface;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue