From 17892e64adda3ed2a5d6523a66d774978433dc28 Mon Sep 17 00:00:00 2001 From: DrummyFloyd Date: Fri, 31 Jan 2025 16:21:44 +0100 Subject: [PATCH] feat(oidc): use generic implementation --- .env.example | 21 ++++---- ...uthentik.provider.ts => oauth.provider.ts} | 51 ++++++++++++------- .../auth/providers/providers.factory.ts | 6 +-- apps/frontend/public/icons/authentik.svg | 1 - apps/frontend/public/icons/generic-oauth.svg | 9 ++++ apps/frontend/src/app/layout.tsx | 4 +- apps/frontend/src/components/auth/login.tsx | 10 ++-- ...hentik.provider.tsx => oauth.provider.tsx} | 14 ++--- apps/frontend/src/middleware.ts | 4 +- .../src/database/prisma/schema.prisma | 2 +- .../src/helpers/variable.context.tsx | 8 ++- 11 files changed, 82 insertions(+), 48 deletions(-) rename apps/backend/src/services/auth/providers/{authentik.provider.ts => oauth.provider.ts} (54%) delete mode 100644 apps/frontend/public/icons/authentik.svg create mode 100644 apps/frontend/public/icons/generic-oauth.svg rename apps/frontend/src/components/auth/providers/{authentik.provider.tsx => oauth.provider.tsx} (65%) diff --git a/.env.example b/.env.example index 64252ba1..34eab615 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,6 @@ # Configuration reference: http://docs.postiz.com/configuration/reference -# === Required Settings +# === Required Settings DATABASE_URL="postgresql://postiz-user:postiz-password@localhost:5432/postiz-db-local" REDIS_URL="redis://localhost:6379" JWT_SECRET="random string for your JWT secret, make it long" @@ -20,7 +20,6 @@ CLOUDFLARE_BUCKETNAME="your-bucket-name" CLOUDFLARE_BUCKET_URL="https://your-bucket-url.r2.cloudflarestorage.com/" CLOUDFLARE_REGION="auto" - # === Common optional Settings ## This is a dummy key, you must create your own from Resend. @@ -32,7 +31,7 @@ CLOUDFLARE_REGION="auto" #DISABLE_REGISTRATION=false # Where will social media icons be saved - local or cloudflare. -STORAGE_PROVIDER="local" +STORAGE_PROVIDER="local" # Your upload directory path if you host your files locally, otherwise Cloudflare will be used. #UPLOAD_DIRECTORY="" @@ -40,7 +39,6 @@ STORAGE_PROVIDER="local" # Your upload directory path if you host your files locally, otherwise Cloudflare will be used. #NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY="" - # Social Media API Settings X_API_KEY="" X_API_SECRET="" @@ -91,8 +89,13 @@ 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="" +NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME="Authentik" +NEXT_PUBLIC_POSTIZ_OAUTH_LOGO_URL="https://raw.githubusercontent.com/walkxcode/dashboard-icons/master/png/authentik.png" +POSTIZ_GENERIC_OAUTH="false" +POSTIZ_OAUTH_URL="https://auth.example.com" +POSTIZ_OAUTH_AUTH_URL="https://auth.example.com/application/o/authorize" +POSTIZ_OAUTH_TOKEN_URL="https://auth.example.com/application/o/token" +POSTIZ_OAUTH_USERINFO_URL="https://authentik.example.com/application/o/userinfo" +POSTIZ_OAUTH_CLIENT_ID="" +POSTIZ_OAUTH_CLIENT_SECRET="" +# POSTIZ_OAUTH_SCOPE="openid profile email" # default values diff --git a/apps/backend/src/services/auth/providers/authentik.provider.ts b/apps/backend/src/services/auth/providers/oauth.provider.ts similarity index 54% rename from apps/backend/src/services/auth/providers/authentik.provider.ts rename to apps/backend/src/services/auth/providers/oauth.provider.ts index 88f1c1d1..9ce6f278 100644 --- a/apps/backend/src/services/auth/providers/authentik.provider.ts +++ b/apps/backend/src/services/auth/providers/oauth.provider.ts @@ -1,36 +1,51 @@ import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface'; -export class AuthentikProvider implements ProvidersInterface { +export class OauthProvider implements ProvidersInterface { + private readonly authUrl: string; private readonly baseUrl: string; private readonly clientId: string; private readonly clientSecret: string; private readonly frontendUrl: string; + private readonly tokenUrl: string; + private readonly userInfoUrl: string; constructor() { const { - AUTHENTIK_URL, - AUTHENTIK_CLIENT_ID, - AUTHENTIK_CLIENT_SECRET, + POSTIZ_OAUTH_AUTH_URL, + POSTIZ_OAUTH_CLIENT_ID, + POSTIZ_OAUTH_CLIENT_SECRET, + POSTIZ_OAUTH_TOKEN_URL, + POSTIZ_OAUTH_URL, + POSTIZ_OAUTH_USERINFO_URL, 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) + if (!POSTIZ_OAUTH_USERINFO_URL) throw new Error( - 'AUTHENTIK_CLIENT_SECRET environment variable is not set' + 'POSTIZ_OAUTH_USERINFO_URL environment variable is not set' ); + if (!POSTIZ_OAUTH_URL) + throw new Error('POSTIZ_OAUTH_URL environment variable is not set'); + if (!POSTIZ_OAUTH_TOKEN_URL) + throw new Error('POSTIZ_OAUTH_TOKEN_URL environment variable is not set'); + if (!POSTIZ_OAUTH_CLIENT_ID) + throw new Error('POSTIZ_OAUTH_CLIENT_ID environment variable is not set'); + if (!POSTIZ_OAUTH_CLIENT_SECRET) + throw new Error( + 'POSTIZ_OAUTH_CLIENT_SECRET environment variable is not set' + ); + if (!POSTIZ_OAUTH_AUTH_URL) + throw new Error('POSTIZ_OAUTH_AUTH_URL 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.authUrl = POSTIZ_OAUTH_AUTH_URL; + this.baseUrl = POSTIZ_OAUTH_URL; + this.clientId = POSTIZ_OAUTH_CLIENT_ID; + this.clientSecret = POSTIZ_OAUTH_CLIENT_SECRET; this.frontendUrl = FRONTEND_URL; + this.tokenUrl = POSTIZ_OAUTH_TOKEN_URL; + this.userInfoUrl = POSTIZ_OAUTH_USERINFO_URL; } generateLink(): string { @@ -41,11 +56,11 @@ export class AuthentikProvider implements ProvidersInterface { redirect_uri: `${this.frontendUrl}/settings`, }); - return `${this.baseUrl}/application/o/authorize/?${params.toString()}`; + return `${this.authUrl}/?${params.toString()}`; } async getToken(code: string): Promise { - const response = await fetch(`${this.baseUrl}/application/o/token/`, { + const response = await fetch(`${this.tokenUrl}/`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', @@ -70,7 +85,7 @@ export class AuthentikProvider implements ProvidersInterface { } async getUser(access_token: string): Promise<{ email: string; id: string }> { - const response = await fetch(`${this.baseUrl}/application/o/userinfo/`, { + const response = await fetch(`${this.userInfoUrl}/`, { headers: { Authorization: `Bearer ${access_token}`, Accept: 'application/json', diff --git a/apps/backend/src/services/auth/providers/providers.factory.ts b/apps/backend/src/services/auth/providers/providers.factory.ts index 01bfa483..2815bc3e 100644 --- a/apps/backend/src/services/auth/providers/providers.factory.ts +++ b/apps/backend/src/services/auth/providers/providers.factory.ts @@ -4,7 +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'; +import { OauthProvider } from '@gitroom/backend/services/auth/providers/oauth.provider'; export class ProvidersFactory { static loadProvider(provider: Provider): ProvidersInterface { @@ -17,8 +17,8 @@ export class ProvidersFactory { return new FarcasterProvider(); case Provider.WALLET: return new WalletProvider(); - case Provider.AUTHENTIK: - return new AuthentikProvider(); + case Provider.GENERIC: + return new OauthProvider(); } } } diff --git a/apps/frontend/public/icons/authentik.svg b/apps/frontend/public/icons/authentik.svg deleted file mode 100644 index c839ddab..00000000 --- a/apps/frontend/public/icons/authentik.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/apps/frontend/public/icons/generic-oauth.svg b/apps/frontend/public/icons/generic-oauth.svg new file mode 100644 index 00000000..b06d1498 --- /dev/null +++ b/apps/frontend/public/icons/generic-oauth.svg @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/apps/frontend/src/app/layout.tsx b/apps/frontend/src/app/layout.tsx index b932d3ac..68d46402 100644 --- a/apps/frontend/src/app/layout.tsx +++ b/apps/frontend/src/app/layout.tsx @@ -39,7 +39,9 @@ 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} + genericOauth={!!process.env.POSTIZ_GENERIC_OAUTH} + oauthLogoUrl={process.env.NEXT_PUBLIC_POSTIZ_OAUTH_LOGO_URL!} + oauthDisplayName={process.env.NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME!} uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!} tolt={process.env.NEXT_PUBLIC_TOLT!} facebookPixel={process.env.NEXT_PUBLIC_FACEBOOK_PIXEL!} diff --git a/apps/frontend/src/components/auth/login.tsx b/apps/frontend/src/components/auth/login.tsx index 9e52e429..00a1ed88 100644 --- a/apps/frontend/src/components/auth/login.tsx +++ b/apps/frontend/src/components/auth/login.tsx @@ -9,7 +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 { OauthProvider } from '@gitroom/frontend/components/auth/providers/oauth.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,8 +25,8 @@ type Inputs = { export function Login() { const [loading, setLoading] = useState(false); - const { isGeneral, neynarClientId, billingEnabled } = useVariables(); - const { isGeneral, neynarClientId, authentikOIDC } = useVariables(); + const { isGeneral, neynarClientId, billingEnabled, genericOauth } = + useVariables(); const resolver = useMemo(() => { return classValidatorResolver(LoginUserDto); }, []); @@ -65,8 +65,8 @@ export function Login() { Sign In - {isGeneral && authentikOIDC ? ( - + {isGeneral && genericOauth ? ( + ) : !isGeneral ? ( ) : ( diff --git a/apps/frontend/src/components/auth/providers/authentik.provider.tsx b/apps/frontend/src/components/auth/providers/oauth.provider.tsx similarity index 65% rename from apps/frontend/src/components/auth/providers/authentik.provider.tsx rename to apps/frontend/src/components/auth/providers/oauth.provider.tsx index 985825ad..69b15210 100644 --- a/apps/frontend/src/components/auth/providers/authentik.provider.tsx +++ b/apps/frontend/src/components/auth/providers/oauth.provider.tsx @@ -2,13 +2,15 @@ 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'; +import { useVariables } from '@gitroom/react/helpers/variable.context'; -export const AuthentikProvider = () => { +export const OauthProvider = () => { const fetch = useFetch(); + const { oauthLogoUrl, oauthDisplayName } = useVariables(); const gotoLogin = useCallback(async () => { try { - const response = await fetch('/auth/oauth/AUTHENTIK'); + const response = await fetch('/auth/oauth/GENERIC'); if (!response.ok) { throw new Error( `Login link request failed with status ${response.status}` @@ -17,7 +19,7 @@ export const AuthentikProvider = () => { const link = await response.text(); window.location.href = link; } catch (error) { - console.error('Failed to get Authentik login link:', error); + console.error('Failed to get generic oauth login link:', error); } }, []); @@ -28,13 +30,13 @@ export const AuthentikProvider = () => { >
Authentik
-
Sign in with Authentik
+
Sign in with {oauthDisplayName || 'OAuth'}
); }; diff --git a/apps/frontend/src/middleware.ts b/apps/frontend/src/middleware.ts index 01aae290..a7f7076d 100644 --- a/apps/frontend/src/middleware.ts +++ b/apps/frontend/src/middleware.ts @@ -44,8 +44,8 @@ export async function middleware(request: NextRequest) { ? '' : (url.indexOf('?') > -1 ? '&' : '?') + `provider=${(findIndex === 'settings' - ? process.env.AUTHENTIK_OIDC - ? 'authentik' + ? process.env.POSTIZ_GENERIC_OAUTH + ? 'generic' : 'github' : findIndex ).toUpperCase()}`; diff --git a/libraries/nestjs-libraries/src/database/prisma/schema.prisma b/libraries/nestjs-libraries/src/database/prisma/schema.prisma index ae7fe520..3b5efbcf 100644 --- a/libraries/nestjs-libraries/src/database/prisma/schema.prisma +++ b/libraries/nestjs-libraries/src/database/prisma/schema.prisma @@ -636,7 +636,7 @@ enum Provider { GOOGLE FARCASTER WALLET - AUTHENTIK + GENERIC } enum Role { diff --git a/libraries/react-shared-libraries/src/helpers/variable.context.tsx b/libraries/react-shared-libraries/src/helpers/variable.context.tsx index bf2ae9a3..a6716c2e 100644 --- a/libraries/react-shared-libraries/src/helpers/variable.context.tsx +++ b/libraries/react-shared-libraries/src/helpers/variable.context.tsx @@ -5,7 +5,9 @@ import { createContext, FC, ReactNode, useContext, useEffect } from 'react'; interface VariableContextInterface { billingEnabled: boolean; isGeneral: boolean; - authentikOIDC: boolean; + genericOauth: boolean; + oauthLogoUrl: string; + oauthDisplayName: string; frontEndUrl: string; plontoKey: string; storageProvider: 'local' | 'cloudflare'; @@ -21,7 +23,9 @@ interface VariableContextInterface { const VariableContext = createContext({ billingEnabled: false, isGeneral: true, - authentikOIDC: false, + genericOauth: false, + oauthLogoUrl: '', + oauthDisplayName: '', frontEndUrl: '', storageProvider: 'local', plontoKey: '',