Skip to content

Commit 7d2c6a5

Browse files
authored
Merge pull request #701 from DrummyFloyd/add-authentik-sso
Add Generic sso/OIDC
2 parents 4ac4bc4 + 38ad812 commit 7d2c6a5

File tree

10 files changed

+192
-12
lines changed

10 files changed

+192
-12
lines changed

.env.example

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Configuration reference: http://docs.postiz.com/configuration/reference
22

3-
# === Required Settings
3+
# === Required Settings
44
DATABASE_URL="postgresql://postiz-user:postiz-password@localhost:5432/postiz-db-local"
55
REDIS_URL="redis://localhost:6379"
66
JWT_SECRET="random string for your JWT secret, make it long"
@@ -20,7 +20,6 @@ CLOUDFLARE_BUCKETNAME="your-bucket-name"
2020
CLOUDFLARE_BUCKET_URL="https://your-bucket-url.r2.cloudflarestorage.com/"
2121
CLOUDFLARE_REGION="auto"
2222

23-
2423
# === Common optional Settings
2524

2625
## This is a dummy key, you must create your own from Resend.
@@ -32,15 +31,14 @@ CLOUDFLARE_REGION="auto"
3231
#DISABLE_REGISTRATION=false
3332

3433
# Where will social media icons be saved - local or cloudflare.
35-
STORAGE_PROVIDER="local"
34+
STORAGE_PROVIDER="local"
3635

3736
# Your upload directory path if you host your files locally, otherwise Cloudflare will be used.
3837
#UPLOAD_DIRECTORY=""
3938

4039
# Your upload directory path if you host your files locally, otherwise Cloudflare will be used.
4140
#NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY=""
4241

43-
4442
# Social Media API Settings
4543
X_API_KEY=""
4644
X_API_SECRET=""
@@ -92,3 +90,13 @@ STRIPE_SIGNING_KEY_CONNECT=""
9290
# Developer Settings
9391
NX_ADD_PLUGINS=false
9492
IS_GENERAL="true" # required for now
93+
NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME="Authentik"
94+
NEXT_PUBLIC_POSTIZ_OAUTH_LOGO_URL="https://raw.githubusercontent.com/walkxcode/dashboard-icons/master/png/authentik.png"
95+
POSTIZ_GENERIC_OAUTH="false"
96+
POSTIZ_OAUTH_URL="https://auth.example.com"
97+
POSTIZ_OAUTH_AUTH_URL="https://auth.example.com/application/o/authorize"
98+
POSTIZ_OAUTH_TOKEN_URL="https://auth.example.com/application/o/token"
99+
POSTIZ_OAUTH_USERINFO_URL="https://authentik.example.com/application/o/userinfo"
100+
POSTIZ_OAUTH_CLIENT_ID=""
101+
POSTIZ_OAUTH_CLIENT_SECRET=""
102+
# POSTIZ_OAUTH_SCOPE="openid profile email" # default values
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface';
2+
3+
export class OauthProvider implements ProvidersInterface {
4+
private readonly authUrl: string;
5+
private readonly baseUrl: string;
6+
private readonly clientId: string;
7+
private readonly clientSecret: string;
8+
private readonly frontendUrl: string;
9+
private readonly tokenUrl: string;
10+
private readonly userInfoUrl: string;
11+
12+
constructor() {
13+
const {
14+
POSTIZ_OAUTH_AUTH_URL,
15+
POSTIZ_OAUTH_CLIENT_ID,
16+
POSTIZ_OAUTH_CLIENT_SECRET,
17+
POSTIZ_OAUTH_TOKEN_URL,
18+
POSTIZ_OAUTH_URL,
19+
POSTIZ_OAUTH_USERINFO_URL,
20+
FRONTEND_URL,
21+
} = process.env;
22+
23+
if (!POSTIZ_OAUTH_USERINFO_URL)
24+
throw new Error(
25+
'POSTIZ_OAUTH_USERINFO_URL environment variable is not set'
26+
);
27+
if (!POSTIZ_OAUTH_URL)
28+
throw new Error('POSTIZ_OAUTH_URL environment variable is not set');
29+
if (!POSTIZ_OAUTH_TOKEN_URL)
30+
throw new Error('POSTIZ_OAUTH_TOKEN_URL environment variable is not set');
31+
if (!POSTIZ_OAUTH_CLIENT_ID)
32+
throw new Error('POSTIZ_OAUTH_CLIENT_ID environment variable is not set');
33+
if (!POSTIZ_OAUTH_CLIENT_SECRET)
34+
throw new Error(
35+
'POSTIZ_OAUTH_CLIENT_SECRET environment variable is not set'
36+
);
37+
if (!POSTIZ_OAUTH_AUTH_URL)
38+
throw new Error('POSTIZ_OAUTH_AUTH_URL environment variable is not set');
39+
if (!FRONTEND_URL)
40+
throw new Error('FRONTEND_URL environment variable is not set');
41+
42+
this.authUrl = POSTIZ_OAUTH_AUTH_URL;
43+
this.baseUrl = POSTIZ_OAUTH_URL;
44+
this.clientId = POSTIZ_OAUTH_CLIENT_ID;
45+
this.clientSecret = POSTIZ_OAUTH_CLIENT_SECRET;
46+
this.frontendUrl = FRONTEND_URL;
47+
this.tokenUrl = POSTIZ_OAUTH_TOKEN_URL;
48+
this.userInfoUrl = POSTIZ_OAUTH_USERINFO_URL;
49+
}
50+
51+
generateLink(): string {
52+
const params = new URLSearchParams({
53+
client_id: this.clientId,
54+
scope: 'openid profile email',
55+
response_type: 'code',
56+
redirect_uri: `${this.frontendUrl}/settings`,
57+
});
58+
59+
return `${this.authUrl}/?${params.toString()}`;
60+
}
61+
62+
async getToken(code: string): Promise<string> {
63+
const response = await fetch(`${this.tokenUrl}/`, {
64+
method: 'POST',
65+
headers: {
66+
'Content-Type': 'application/x-www-form-urlencoded',
67+
Accept: 'application/json',
68+
},
69+
body: new URLSearchParams({
70+
grant_type: 'authorization_code',
71+
client_id: this.clientId,
72+
client_secret: this.clientSecret,
73+
code,
74+
redirect_uri: `${this.frontendUrl}/settings`,
75+
}),
76+
});
77+
78+
if (!response.ok) {
79+
const error = await response.text();
80+
throw new Error(`Token request failed: ${error}`);
81+
}
82+
83+
const { access_token } = await response.json();
84+
return access_token;
85+
}
86+
87+
async getUser(access_token: string): Promise<{ email: string; id: string }> {
88+
const response = await fetch(`${this.userInfoUrl}/`, {
89+
headers: {
90+
Authorization: `Bearer ${access_token}`,
91+
Accept: 'application/json',
92+
},
93+
});
94+
95+
if (!response.ok) {
96+
const error = await response.text();
97+
throw new Error(`User info request failed: ${error}`);
98+
}
99+
100+
const { email, sub: id } = await response.json();
101+
return { email, id };
102+
}
103+
}

apps/backend/src/services/auth/providers/providers.factory.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.int
44
import { GoogleProvider } from '@gitroom/backend/services/auth/providers/google.provider';
55
import { FarcasterProvider } from '@gitroom/backend/services/auth/providers/farcaster.provider';
66
import { WalletProvider } from '@gitroom/backend/services/auth/providers/wallet.provider';
7+
import { OauthProvider } from '@gitroom/backend/services/auth/providers/oauth.provider';
78

89
export class ProvidersFactory {
910
static loadProvider(provider: Provider): ProvidersInterface {
@@ -16,6 +17,8 @@ export class ProvidersFactory {
1617
return new FarcasterProvider();
1718
case Provider.WALLET:
1819
return new WalletProvider();
20+
case Provider.GENERIC:
21+
return new OauthProvider();
1922
}
2023
}
2124
}
Lines changed: 9 additions & 0 deletions
Loading

apps/frontend/src/app/layout.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ export default async function AppLayout({ children }: { children: ReactNode }) {
3939
discordUrl={process.env.NEXT_PUBLIC_DISCORD_SUPPORT!}
4040
frontEndUrl={process.env.FRONTEND_URL!}
4141
isGeneral={!!process.env.IS_GENERAL}
42+
genericOauth={!!process.env.POSTIZ_GENERIC_OAUTH}
43+
oauthLogoUrl={process.env.NEXT_PUBLIC_POSTIZ_OAUTH_LOGO_URL!}
44+
oauthDisplayName={process.env.NEXT_PUBLIC_POSTIZ_OAUTH_DISPLAY_NAME!}
4245
uploadDirectory={process.env.NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY!}
4346
tolt={process.env.NEXT_PUBLIC_TOLT!}
4447
facebookPixel={process.env.NEXT_PUBLIC_FACEBOOK_PIXEL!}

apps/frontend/src/components/auth/login.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { useMemo, useState } from 'react';
99
import { classValidatorResolver } from '@hookform/resolvers/class-validator';
1010
import { LoginUserDto } from '@gitroom/nestjs-libraries/dtos/auth/login.user.dto';
1111
import { GithubProvider } from '@gitroom/frontend/components/auth/providers/github.provider';
12+
import { OauthProvider } from '@gitroom/frontend/components/auth/providers/oauth.provider';
1213
import interClass from '@gitroom/react/helpers/inter.font';
1314
import { GoogleProvider } from '@gitroom/frontend/components/auth/providers/google.provider';
1415
import { useVariables } from '@gitroom/react/helpers/variable.context';
@@ -24,7 +25,8 @@ type Inputs = {
2425

2526
export function Login() {
2627
const [loading, setLoading] = useState(false);
27-
const { isGeneral, neynarClientId, billingEnabled } = useVariables();
28+
const { isGeneral, neynarClientId, billingEnabled, genericOauth } =
29+
useVariables();
2830
const resolver = useMemo(() => {
2931
return classValidatorResolver(LoginUserDto);
3032
}, []);
@@ -63,8 +65,9 @@ export function Login() {
6365
Sign In
6466
</h1>
6567
</div>
66-
67-
{!isGeneral ? (
68+
{isGeneral && genericOauth ? (
69+
<OauthProvider />
70+
) : !isGeneral ? (
6871
<GithubProvider />
6972
) : (
7073
<div className="gap-[5px] flex flex-col">
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import { useCallback } from 'react';
2+
import Image from 'next/image';
3+
import { useFetch } from '@gitroom/helpers/utils/custom.fetch';
4+
import interClass from '@gitroom/react/helpers/inter.font';
5+
import { useVariables } from '@gitroom/react/helpers/variable.context';
6+
7+
export const OauthProvider = () => {
8+
const fetch = useFetch();
9+
const { oauthLogoUrl, oauthDisplayName } = useVariables();
10+
11+
const gotoLogin = useCallback(async () => {
12+
try {
13+
const response = await fetch('/auth/oauth/GENERIC');
14+
if (!response.ok) {
15+
throw new Error(
16+
`Login link request failed with status ${response.status}`
17+
);
18+
}
19+
const link = await response.text();
20+
window.location.href = link;
21+
} catch (error) {
22+
console.error('Failed to get generic oauth login link:', error);
23+
}
24+
}, []);
25+
26+
return (
27+
<div
28+
onClick={gotoLogin}
29+
className={`cursor-pointer bg-white h-[44px] rounded-[4px] flex justify-center items-center text-customColor16 ${interClass} gap-[4px]`}
30+
>
31+
<div>
32+
<Image
33+
src={oauthLogoUrl || '/icons/generic-oauth.svg'}
34+
alt="genericOauth"
35+
width={40}
36+
height={40}
37+
/>
38+
</div>
39+
<div>Sign in with {oauthDisplayName || 'OAuth'}</div>
40+
</div>
41+
);
42+
};

apps/frontend/src/middleware.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,9 @@ export async function middleware(request: NextRequest) {
4444
? ''
4545
: (url.indexOf('?') > -1 ? '&' : '?') +
4646
`provider=${(findIndex === 'settings'
47-
? 'github'
47+
? process.env.POSTIZ_GENERIC_OAUTH
48+
? 'generic'
49+
: 'github'
4850
: findIndex
4951
).toUpperCase()}`;
5052
return NextResponse.redirect(

libraries/nestjs-libraries/src/database/prisma/schema.prisma

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -636,6 +636,7 @@ enum Provider {
636636
GOOGLE
637637
FARCASTER
638638
WALLET
639+
GENERIC
639640
}
640641

641642
enum Role {
@@ -648,4 +649,4 @@ enum APPROVED_SUBMIT_FOR_ORDER {
648649
NO
649650
WAITING_CONFIRMATION
650651
YES
651-
}
652+
}

libraries/react-shared-libraries/src/helpers/variable.context.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import { createContext, FC, ReactNode, useContext, useEffect } from 'react';
55
interface VariableContextInterface {
66
billingEnabled: boolean;
77
isGeneral: boolean;
8+
genericOauth: boolean;
9+
oauthLogoUrl: string;
10+
oauthDisplayName: string;
811
frontEndUrl: string;
912
plontoKey: string;
10-
storageProvider: 'local' | 'cloudflare',
13+
storageProvider: 'local' | 'cloudflare';
1114
backendUrl: string;
1215
discordUrl: string;
1316
uploadDirectory: string;
@@ -20,6 +23,9 @@ interface VariableContextInterface {
2023
const VariableContext = createContext({
2124
billingEnabled: false,
2225
isGeneral: true,
26+
genericOauth: false,
27+
oauthLogoUrl: '',
28+
oauthDisplayName: '',
2329
frontEndUrl: '',
2430
storageProvider: 'local',
2531
plontoKey: '',
@@ -52,9 +58,9 @@ export const VariableContextComponent: FC<
5258

5359
export const useVariables = () => {
5460
return useContext(VariableContext);
55-
}
61+
};
5662

5763
export const loadVars = () => {
5864
// @ts-ignore
5965
return window.vars as VariableContextInterface;
60-
}
66+
};

0 commit comments

Comments
 (0)