diff --git a/.env.example b/.env.example index 6a81f72e2..4919f0053 100644 --- a/.env.example +++ b/.env.example @@ -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" diff --git a/apps/web/process-env.d.ts b/apps/web/process-env.d.ts index 0c00cb4c1..63a341060 100644 --- a/apps/web/process-env.d.ts +++ b/apps/web/process-env.d.ts @@ -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; } } diff --git a/apps/web/src/app/(unauthenticated)/signin/page.tsx b/apps/web/src/app/(unauthenticated)/signin/page.tsx index 21136f2e6..a0599ac1a 100644 --- a/apps/web/src/app/(unauthenticated)/signin/page.tsx +++ b/apps/web/src/app/(unauthenticated)/signin/page.tsx @@ -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'; @@ -37,10 +37,13 @@ export default function SignInPage({ searchParams }: SignInPageProps) {

Welcome back, we are lucky to have you.

-
- + {NEXT_PUBLIC_DISABLE_SIGNUP !== 'true' && (

diff --git a/apps/web/src/app/(unauthenticated)/signup/page.tsx b/apps/web/src/app/(unauthenticated)/signup/page.tsx index c7284fac6..2373af770 100644 --- a/apps/web/src/app/(unauthenticated)/signup/page.tsx +++ b/apps/web/src/app/(unauthenticated)/signup/page.tsx @@ -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'; @@ -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} /> ); } diff --git a/apps/web/src/components/forms/signin.tsx b/apps/web/src/components/forms/signin.tsx index 59b8af6c7..e86ad492f 100644 --- a/apps/web/src/components/forms/signin.tsx +++ b/apps/web/src/components/forms/signin.tsx @@ -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'; @@ -69,9 +70,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(); @@ -257,6 +264,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', + }); + } + }; + return (

- {(isGoogleSSOEnabled || isPasskeyEnabled) && ( + {(isGoogleSSOEnabled || isPasskeyEnabled || isOIDCSSOEnabled) && (
Or continue with @@ -339,6 +359,20 @@ export const SignInForm = ({ className, initialEmail, isGoogleSSOEnabled }: Sign )} + {isOIDCSSOEnabled && ( + + )} + {isPasskeyEnabled && ( + + )} ); diff --git a/apps/web/src/components/forms/v2/signup.tsx b/apps/web/src/components/forms/v2/signup.tsx index b3b502993..4c177ddef 100644 --- a/apps/web/src/components/forms/v2/signup.tsx +++ b/apps/web/src/components/forms/v2/signup.tsx @@ -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'; @@ -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(); @@ -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 (
@@ -255,7 +271,7 @@ export const SignUpFormV2 = ({
@@ -323,14 +339,18 @@ export const SignUpFormV2 = ({ )} /> - {isGoogleSSOEnabled && ( + {(isGoogleSSOEnabled || isOIDCSSOEnabled) && ( <>
Or
+ + )} + {isGoogleSSOEnabled && ( + <> + + )} +

Already have an account?{' '} diff --git a/packages/lib/constants/auth.ts b/packages/lib/constants/auth.ts index 137ebe640..4df19b407 100644 --- a/packages/lib/constants/auth.ts +++ b/packages/lib/constants/auth.ts @@ -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', diff --git a/packages/lib/next-auth/auth-options.ts b/packages/lib/next-auth/auth-options.ts index 6805eedbe..107548e9b 100644 --- a/packages/lib/next-auth/auth-options.ts +++ b/packages/lib/next-auth/auth-options.ts @@ -136,6 +136,30 @@ export const NEXT_AUTH_OPTIONS: AuthOptions = { }; }, }), + { + id: 'oidc', + name: 'OIDC', + type: 'oauth', + + 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' } }, + checks: ['pkce', 'state'], + + idToken: true, + allowDangerousEmailAccountLinking: true, + + profile(profile) { + return { + id: profile.sub, + email: profile.email || profile.preferred_username, + name: profile.name || `${profile.given_name} ${profile.family_name}`.trim(), + emailVerified: profile.email_verified ? new Date().toISOString() : null, + }; + }, + }, CredentialsProvider({ id: 'webauthn', name: 'Keypass', diff --git a/packages/prisma/migrations/20240413202001_add_oidc_auth/migration.sql b/packages/prisma/migrations/20240413202001_add_oidc_auth/migration.sql new file mode 100644 index 000000000..929ae8d97 --- /dev/null +++ b/packages/prisma/migrations/20240413202001_add_oidc_auth/migration.sql @@ -0,0 +1 @@ +ALTER TYPE "IdentityProvider" ADD VALUE IF NOT EXISTS 'OIDC'; \ No newline at end of file diff --git a/packages/prisma/migrations/20240530120101_add_missing_fields_to_account_model_for_oidc/migration.sql b/packages/prisma/migrations/20240530120101_add_missing_fields_to_account_model_for_oidc/migration.sql new file mode 100644 index 000000000..6d7bc841a --- /dev/null +++ b/packages/prisma/migrations/20240530120101_add_missing_fields_to_account_model_for_oidc/migration.sql @@ -0,0 +1,3 @@ +-- AlterTable +ALTER TABLE "Account" ADD COLUMN "created_at" INTEGER, +ADD COLUMN "ext_expires_in" INTEGER; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index fca01f568..908bb10c1 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -15,6 +15,7 @@ datasource db { enum IdentityProvider { DOCUMENSO GOOGLE + OIDC } enum Role { @@ -232,6 +233,10 @@ model Account { refresh_token String? @db.Text access_token String? @db.Text expires_at Int? + // Some providers return created_at so we need to make it optional + created_at Int? + // Stops next-auth from crashing when dealing with AzureAD + ext_expires_in Int? token_type String? scope String? id_token String? @db.Text diff --git a/packages/tsconfig/process-env.d.ts b/packages/tsconfig/process-env.d.ts index 13e3973db..9eb5b2594 100644 --- a/packages/tsconfig/process-env.d.ts +++ b/packages/tsconfig/process-env.d.ts @@ -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; diff --git a/turbo.json b/turbo.json index 8f89e97a6..2b84e14ae 100644 --- a/turbo.json +++ b/turbo.json @@ -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",