Skip to content
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

feat: add oidc support #1103

Merged
merged 5 commits into from
May 31, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY="DEADBEEF"
NEXT_PRIVATE_GOOGLE_CLIENT_ID=""
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET=""

NEXT_PRIVATE_OIDC_WELL_KNOWN=""
NEXT_PRIVATE_OIDC_CLIENT_ID=""
NEXT_PRIVATE_OIDC_CLIENT_SECRET=""

# [[URLS]]
NEXT_PUBLIC_WEBAPP_URL="http://localhost:3000"
NEXT_PUBLIC_MARKETING_URL="http://localhost:3001"
Expand Down
4 changes: 4 additions & 0 deletions apps/web/process-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,9 @@ declare namespace NodeJS {

NEXT_PRIVATE_GOOGLE_CLIENT_ID: string;
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET: string;

NEXT_PRIVATE_OIDC_WELL_KNOWN: string;
NEXT_PRIVATE_OIDC_CLIENT_ID: string;
NEXT_PRIVATE_OIDC_CLIENT_SECRET: string;
}
}
9 changes: 6 additions & 3 deletions apps/web/src/app/(unauthenticated)/signin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { redirect } from 'next/navigation';

import { env } from 'next-runtime-env';

import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';

import { SignInForm } from '~/components/forms/signin';
Expand Down Expand Up @@ -37,10 +37,13 @@ export default function SignInPage({ searchParams }: SignInPageProps) {
<p className="text-muted-foreground mt-2 text-sm">
Welcome back, we are lucky to have you.
</p>

<hr className="-mx-6 my-4" />

<SignInForm initialEmail={email || undefined} isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED} />
<SignInForm
initialEmail={email || undefined}
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED}
/>

{NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (
<p className="text-muted-foreground mt-6 text-center text-sm">
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/app/(unauthenticated)/signup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { redirect } from 'next/navigation';

import { env } from 'next-runtime-env';

import { IS_GOOGLE_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { IS_GOOGLE_SSO_ENABLED, IS_OIDC_SSO_ENABLED } from '@documenso/lib/constants/auth';
import { decryptSecondaryData } from '@documenso/lib/server-only/crypto/decrypt';

import { SignUpFormV2 } from '~/components/forms/v2/signup';
Expand Down Expand Up @@ -37,6 +37,7 @@ export default function SignUpPage({ searchParams }: SignUpPageProps) {
className="w-screen max-w-screen-2xl px-4 md:px-16 lg:-my-16"
initialEmail={email || undefined}
isGoogleSSOEnabled={IS_GOOGLE_SSO_ENABLED}
isOIDCSSOEnabled={IS_OIDC_SSO_ENABLED}
/>
);
}
38 changes: 36 additions & 2 deletions apps/web/src/components/forms/signin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { browserSupportsWebAuthn, startAuthentication } from '@simplewebauthn/br
import { KeyRoundIcon } from 'lucide-react';
import { signIn } from 'next-auth/react';
import { useForm } from 'react-hook-form';
import { FaIdCardClip } from 'react-icons/fa6';
import { FcGoogle } from 'react-icons/fc';
import { match } from 'ts-pattern';
import { z } from 'zod';
Expand Down Expand Up @@ -68,9 +69,15 @@ export type SignInFormProps = {
className?: string;
initialEmail?: string;
isGoogleSSOEnabled?: boolean;
isOIDCSSOEnabled?: boolean;
};

export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: SignInFormProps) => {
export const SignInForm = ({
className,
initialEmail,
isGoogleSSOEnabled,
isOIDCSSOEnabled,
}: SignInFormProps) => {
const { toast } = useToast();
const { getFlag } = useFeatureFlags();

Expand Down Expand Up @@ -256,6 +263,19 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
}
};

const onSignInWithOIDCClick = async () => {
try {
await signIn('oidc', { callbackUrl: LOGIN_REDIRECT_PATH });
} catch (err) {
toast({
title: 'An unknown error occurred',
description:
'We encountered an unknown error while attempting to sign you In. Please try again later.',
variant: 'destructive',
});
}
};
Comment on lines +267 to +278
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct the provider key in the signIn function for OIDC.

- await signIn('oidc', { callbackUrl: LOGIN_REDIRECT_PATH });
+ await signIn('oidc', { callbackUrl: LOGIN_REDIRECT_PATH });

The onSignInWithOIDCClick function should use 'oidc' as the provider key for the OIDC sign-in to match the intended authentication method.


Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
const onSignInWithOIDCClick = async () => {
try {
await signIn('oidc', { callbackUrl: LOGIN_REDIRECT_PATH });
} catch (err) {
toast({
title: 'An unknown error occurred',
description:
'We encountered an unknown error while attempting to sign you In. Please try again later.',
variant: 'destructive',
});
}
};
const onSignInWithOIDCClick = async () => {
try {
await signIn('oidc', { callbackUrl: LOGIN_REDIRECT_PATH });
} catch (err) {
toast({
title: 'An unknown error occurred',
description:
'We encountered an unknown error while attempting to sign you In. Please try again later.',
variant: 'destructive',
});
}
};


return (
<Form {...form}>
<form
Expand Down Expand Up @@ -316,7 +336,7 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
{isSubmitting ? 'Signing in...' : 'Sign In'}
</Button>

{(isGoogleSSOEnabled || isPasskeyEnabled) && (
{(isGoogleSSOEnabled || isPasskeyEnabled || isOIDCSSOEnabled) && (
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground bg-transparent">Or continue with</span>
Expand All @@ -338,6 +358,20 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
</Button>
)}

{isOIDCSSOEnabled && (
<Button
type="button"
size="lg"
variant="outline"
className="bg-background text-muted-foreground border"
disabled={isSubmitting}
onClick={onSignInWithOIDCClick}
>
<FaIdCardClip className="mr-2 h-5 w-5" />
OIDC
</Button>
)}

{isPasskeyEnabled && (
<Button
type="button"
Expand Down
43 changes: 42 additions & 1 deletion apps/web/src/components/forms/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,9 +52,15 @@ export type SignUpFormProps = {
className?: string;
initialEmail?: string;
isGoogleSSOEnabled?: boolean;
isOIDCSSOEnabled?: boolean;
};

export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: SignUpFormProps) => {
export const SignUpForm = ({
className,
initialEmail,
isGoogleSSOEnabled,
isOIDCSSOEnabled,
}: SignUpFormProps) => {
const { toast } = useToast();
const analytics = useAnalytics();
const router = useRouter();
Expand Down Expand Up @@ -121,6 +127,19 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
}
};

const onSignUpWithOIDCClick = async () => {
try {
await signIn('oidc', { callbackUrl: SIGN_UP_REDIRECT_PATH });
} catch (err) {
toast({
title: 'An unknown error occurred',
description:
'We encountered an unknown error while attempting to sign you Up. Please try again later.',
variant: 'destructive',
});
}
};

return (
<Form {...form}>
<form
Expand Down Expand Up @@ -221,6 +240,28 @@ export const SignUpForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign
</Button>
</>
)}

{isOIDCSSOEnabled && (
<>
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground bg-transparent">Or</span>
<div className="bg-border h-px flex-1" />
</div>

<Button
type="button"
size="lg"
variant={'outline'}
className="bg-background text-muted-foreground border"
disabled={isSubmitting}
onClick={onSignUpWithOIDCClick}
>
<FcGoogle className="mr-2 h-5 w-5" />
Sign Up with OIDC
</Button>
</>
)}
</form>
</Form>
);
Expand Down
40 changes: 38 additions & 2 deletions apps/web/src/components/forms/v2/signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { zodResolver } from '@hookform/resolvers/zod';
import { AnimatePresence, motion } from 'framer-motion';
import { signIn } from 'next-auth/react';
import { useForm } from 'react-hook-form';
import { FaIdCardClip } from 'react-icons/fa6';
import { FcGoogle } from 'react-icons/fc';
import { z } from 'zod';

Expand Down Expand Up @@ -73,12 +74,14 @@ export type SignUpFormV2Props = {
className?: string;
initialEmail?: string;
isGoogleSSOEnabled?: boolean;
isOIDCSSOEnabled?: boolean;
};

export const SignUpFormV2 = ({
className,
initialEmail,
isGoogleSSOEnabled,
isOIDCSSOEnabled,
}: SignUpFormV2Props) => {
const { toast } = useToast();
const analytics = useAnalytics();
Expand Down Expand Up @@ -179,6 +182,19 @@ export const SignUpFormV2 = ({
}
};

const onSignUpWithOIDCClick = async () => {
try {
await signIn('oidc', { callbackUrl: SIGN_UP_REDIRECT_PATH });
} catch (err) {
toast({
title: 'An unknown error occurred',
description:
'We encountered an unknown error while attempting to sign you Up. Please try again later.',
variant: 'destructive',
});
}
};

return (
<div className={cn('flex justify-center gap-x-12', className)}>
<div className="border-border relative hidden flex-1 overflow-hidden rounded-xl border xl:flex">
Expand Down Expand Up @@ -255,7 +271,7 @@ export const SignUpFormV2 = ({
<fieldset
className={cn(
'flex h-[550px] w-full flex-col gap-y-4',
isGoogleSSOEnabled && 'h-[650px]',
(isGoogleSSOEnabled || isOIDCSSOEnabled) && 'h-[650px]',
)}
disabled={isSubmitting}
>
Expand Down Expand Up @@ -323,14 +339,18 @@ export const SignUpFormV2 = ({
)}
/>

{isGoogleSSOEnabled && (
{(isGoogleSSOEnabled || isOIDCSSOEnabled) && (
<>
<div className="relative flex items-center justify-center gap-x-4 py-2 text-xs uppercase">
<div className="bg-border h-px flex-1" />
<span className="text-muted-foreground bg-transparent">Or</span>
<div className="bg-border h-px flex-1" />
</div>
</>
)}

{isGoogleSSOEnabled && (
<>
<Button
type="button"
size="lg"
Expand All @@ -345,6 +365,22 @@ export const SignUpFormV2 = ({
</>
)}

{isOIDCSSOEnabled && (
<>
<Button
type="button"
size="lg"
variant={'outline'}
className="bg-background text-muted-foreground border"
disabled={isSubmitting}
onClick={onSignUpWithOIDCClick}
>
<FaIdCardClip className="mr-2 h-5 w-5" />
Sign Up with OIDC
</Button>
</>
)}
Comment on lines +368 to +382
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The implementation of the OIDC sign-up button is correct. However, consider adding an aria-label to the button for better accessibility.

- <Button type="button" size="lg" variant={'outline'} className="bg-background text-muted-foreground border" disabled={isSubmitting} onClick={onSignUpWithOIDCClick}>
+ <Button type="button" size="lg" variant={'outline'} className="bg-background text-muted-foreground border" disabled={isSubmitting} onClick={onSignUpWithOIDCClick} aria-label="Sign up with OIDC">

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
{isOIDCSSOEnabled && (
<>
<Button
type="button"
size="lg"
variant={'outline'}
className="bg-background text-muted-foreground border"
disabled={isSubmitting}
onClick={onSignUpWithOIDCClick}
>
<FaIdCardClip className="mr-2 h-5 w-5" />
Sign Up with OIDC
</Button>
</>
)}
{isOIDCSSOEnabled && (
<>
<Button
type="button"
size="lg"
variant={'outline'}
className="bg-background text-muted-foreground border"
disabled={isSubmitting}
onClick={onSignUpWithOIDCClick}
aria-label="Sign up with OIDC"
>
<FaIdCardClip className="mr-2 h-5 w-5" />
Sign Up with OIDC
</Button>
</>
)}


<p className="text-muted-foreground mt-4 text-sm">
Already have an account?{' '}
<Link href="/signin" className="text-documenso-700 duration-200 hover:opacity-70">
Expand Down
7 changes: 7 additions & 0 deletions packages/lib/constants/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,19 @@ export const SALT_ROUNDS = 12;
export const IDENTITY_PROVIDER_NAME: { [key in IdentityProvider]: string } = {
[IdentityProvider.DOCUMENSO]: 'Documenso',
[IdentityProvider.GOOGLE]: 'Google',
[IdentityProvider.OIDC]: 'OIDC',
};

export const IS_GOOGLE_SSO_ENABLED = Boolean(
process.env.NEXT_PRIVATE_GOOGLE_CLIENT_ID && process.env.NEXT_PRIVATE_GOOGLE_CLIENT_SECRET,
);

export const IS_OIDC_SSO_ENABLED = Boolean(
process.env.NEXT_PRIVATE_OIDC_WELL_KNOWN &&
process.env.NEXT_PRIVATE_OIDC_CLIENT_ID &&
process.env.NEXT_PRIVATE_OIDC_CLIENT_SECRET,
);

export const USER_SECURITY_AUDIT_LOG_MAP: { [key in UserSecurityAuditLogType]: string } = {
[UserSecurityAuditLogType.ACCOUNT_SSO_LINK]: 'Linked account to SSO',
[UserSecurityAuditLogType.ACCOUNT_PROFILE_UPDATE]: 'Profile updated',
Expand Down
19 changes: 19 additions & 0 deletions packages/lib/next-auth/auth-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,25 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = {
};
},
}),
{
id: 'oidc',
name: 'OIDC',
wellKnown: process.env.NEXT_PRIVATE_OIDC_WELL_KNOWN,
clientId: process.env.NEXT_PRIVATE_OIDC_CLIENT_ID,
clientSecret: process.env.NEXT_PRIVATE_OIDC_CLIENT_SECRET,
authorization: { params: { scope: 'openid email profile' } },
idToken: true,
checks: ['pkce', 'state'],
type: 'oauth',
allowDangerousEmailAccountLinking: true,
profile(profile) {
return {
id: Number(profile.sub),
email: profile.email,
name: profile.name || `${profile.given_name} ${profile.family_name}`.trim(),
};
},
},
CredentialsProvider({
id: 'webauthn',
name: 'Keypass',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TYPE "IdentityProvider" ADD VALUE IF NOT EXISTS 'OIDC';
1 change: 1 addition & 0 deletions packages/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ datasource db {
enum IdentityProvider {
DOCUMENSO
GOOGLE
OIDC
}

enum Role {
Expand Down
4 changes: 4 additions & 0 deletions packages/tsconfig/process-env.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ declare namespace NodeJS {
NEXT_PRIVATE_GOOGLE_CLIENT_ID?: string;
NEXT_PRIVATE_GOOGLE_CLIENT_SECRET?: string;

NEXT_PRIVATE_OIDC_WELL_KNOWN?: string;
NEXT_PRIVATE_OIDC_CLIENT_ID?: string;
NEXT_PRIVATE_OIDC_CLIENT_SECRET?: string;

NEXT_PRIVATE_DATABASE_URL: string;
NEXT_PRIVATE_ENCRYPTION_KEY: string;
NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY: string;
Expand Down
3 changes: 3 additions & 0 deletions turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@
"NEXT_PRIVATE_SIGNING_GCLOUD_APPLICATION_CREDENTIALS_CONTENTS",
"NEXT_PRIVATE_GOOGLE_CLIENT_ID",
"NEXT_PRIVATE_GOOGLE_CLIENT_SECRET",
"NEXT_PRIVATE_OIDC_WELL_KNOWN",
"NEXT_PRIVATE_OIDC_CLIENT_ID",
"NEXT_PRIVATE_OIDC_CLIENT_SECRET",
"NEXT_PUBLIC_UPLOAD_TRANSPORT",
"NEXT_PRIVATE_UPLOAD_ENDPOINT",
"NEXT_PRIVATE_UPLOAD_FORCE_PATH_STYLE",
Expand Down
Loading