-
-
Notifications
You must be signed in to change notification settings - Fork 3.9k
Add Generic sso/OIDC #701
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
Add Generic sso/OIDC #701
Changes from all commits
f9aa278
17892e6
a0ad28e
a9ffcec
a14c950
8a41e20
0f5379f
b111dd0
c66b7c0
aa6322a
9d1c762
2a87ba1
38ad812
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { ProvidersInterface } from '@gitroom/backend/services/auth/providers.interface'; | ||
|
||
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 { | ||
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 (!POSTIZ_OAUTH_USERINFO_URL) | ||
throw new Error( | ||
'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.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 { | ||
const params = new URLSearchParams({ | ||
client_id: this.clientId, | ||
scope: 'openid profile email', | ||
response_type: 'code', | ||
redirect_uri: `${this.frontendUrl}/settings`, | ||
}); | ||
|
||
return `${this.authUrl}/?${params.toString()}`; | ||
} | ||
|
||
async getToken(code: string): Promise<string> { | ||
const response = await fetch(`${this.tokenUrl}/`, { | ||
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.userInfoUrl}/`, { | ||
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 }; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,42 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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 OauthProvider = () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const fetch = useFetch(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const { oauthLogoUrl, oauthDisplayName } = useVariables(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const gotoLogin = useCallback(async () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
const response = await fetch('/auth/oauth/GENERIC'); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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 generic oauth login link:', error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+11
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Add user-facing error handling The error handling currently only logs to the console, but there's no feedback mechanism for the user if the OAuth login link request fails. Consider implementing a state variable to track errors and display a user-friendly message: import { useCallback } from 'react';
+import { useState } 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 OauthProvider = () => {
const fetch = useFetch();
const { oauthLogoUrl, oauthDisplayName } = useVariables();
+ const [error, setError] = useState<string | null>(null);
const gotoLogin = useCallback(async () => {
try {
+ setError(null);
const response = await fetch('/auth/oauth/GENERIC');
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 generic oauth login link:', error);
+ setError('Failed to initiate login. Please try again later.');
}
}, []); Then display the error message in the UI: 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={oauthLogoUrl || '/icons/generic-oauth.svg'}
alt="genericOauth"
width={40}
height={40}
/>
</div>
<div>Sign in with {oauthDisplayName || 'OAuth'}</div>
+ {error && <div className="text-red-500 text-sm mt-2">{error}</div>}
</div>
); 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
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={oauthLogoUrl || '/icons/generic-oauth.svg'} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
alt="genericOauth" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
width={40} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
height={40} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
<div>Sign in with {oauthDisplayName || 'OAuth'}</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
</div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
}; |
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -44,7 +44,9 @@ export async function middleware(request: NextRequest) { | |||||||||||||
? '' | ||||||||||||||
: (url.indexOf('?') > -1 ? '&' : '?') + | ||||||||||||||
`provider=${(findIndex === 'settings' | ||||||||||||||
? 'github' | ||||||||||||||
? process.env.POSTIZ_GENERIC_OAUTH | ||||||||||||||
? 'generic' | ||||||||||||||
: 'github' | ||||||||||||||
Comment on lines
+47
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Enhance type safety for environment variable check The condition checks - ? process.env.POSTIZ_GENERIC_OAUTH
+ ? process.env.POSTIZ_GENERIC_OAUTH === 'true'
? 'generic'
: 'github' Additionally, consider renaming 'generic' to 'oauth' or 'authentik' to better reflect the PR's purpose of adding Authentik SSO support. 📝 Committable suggestion
Suggested change
|
||||||||||||||
: findIndex | ||||||||||||||
).toUpperCase()}`; | ||||||||||||||
return NextResponse.redirect( | ||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Added support for the GENERIC provider type.
The case for
Provider.GENERIC
correctly instantiates theOauthProvider
class when requested, following the factory pattern consistently with other providers. Based on the relevant code snippet, make sure all required environment variables (POSTIZ_OAUTH_*
) are documented in the deployment guide.🏁 Script executed:
Length of output: 181
🏁 Script executed:
Length of output: 3939
Please document the new POSTIZ_OAUTH_ variables in your environment examples and deployment guide*
The
OauthProvider
now relies on sixPOSTIZ_OAUTH_*
variables, but I couldn’t find any.env
-style example or docs entries listing them. Before merging, please:• Add the following keys to your
.env.example
(or equivalent) with placeholder values:• Update your deployment or onboarding documentation (e.g., in your docs/ directory or README) to explain each variable’s purpose and expected format.
That will ensure users know how to configure the new GENERIC provider correctly.