feat: custom OIDC providers (#2109)

This commit is contained in:
Shubham Palriwala
2024-02-22 20:01:48 +05:30
committed by GitHub
parent cdf8e91dad
commit 7f21e65625
13 changed files with 208 additions and 46 deletions

View File

@@ -2,7 +2,6 @@
# ------------ MANDATORY (CHANGE ACCORDING TO YOUR SETUP) ------------ #
########################################################################
############
# BASICS #
############
@@ -51,7 +50,6 @@ SMTP_SECURE_ENABLED=0
SMTP_USER=smtpUser
SMTP_PASSWORD=smtpPassword
########################################################################
# ------------------------------ OPTIONAL -----------------------------#
########################################################################
@@ -99,6 +97,13 @@ AZUREAD_CLIENT_ID=
AZUREAD_CLIENT_SECRET=
AZUREAD_TENANT_ID=
# OpenID Connect (OIDC) configuration
# OIDC_CLIENT_ID=
# OIDC_CLIENT_SECRET=
# OIDC_ISSUER=
# OIDC_DISPLAY_NAME=
# OIDC_SIGNING_ALGORITHM=
# Cron Secret
CRON_SECRET=
@@ -118,12 +123,12 @@ NEXT_PUBLIC_FORMBRICKS_API_HOST=
NEXT_PUBLIC_FORMBRICKS_ENVIRONMENT_ID=
NEXT_PUBLIC_FORMBRICKS_ONBOARDING_SURVEY_ID=
# Oauth credentials for Google sheet integration
# Oauth credentials for Google sheet integration
GOOGLE_SHEETS_CLIENT_ID=
GOOGLE_SHEETS_CLIENT_SECRET=
GOOGLE_SHEETS_REDIRECT_URL=
# Oauth credentials for Airtable integration
# Oauth credentials for Airtable integration
AIRTABLE_CLIENT_ID=
# Enterprise License Key
@@ -143,4 +148,4 @@ ENTERPRISE_LICENSE_KEY=
# CUSTOMER_IO_SITE_ID=
# Ignore Rate Limiting across the Formbricks app
# RATE_LIMITING_DISABLED=1
# RATE_LIMITING_DISABLED=1

View File

@@ -76,46 +76,86 @@ GOOGLE_CLIENT_SECRET=your-client-secret-here
- Navigate to your Docker setup directory where your `docker-compose.yml` file is located.
- Run the following command to bring down your current Docker containers and then bring them back up with the updated environment configuration:
## OpenID Integration
Integrating your own OIDC (OpenID Connect) instance with your Formbricks instance allows users to log in using their OIDC credentials, ensuring a secure and streamlined user experience. Please follow the steps below to set up OIDC for your Formbricks instance.
1. Configure your OIDC provider & get the following variables:
- `OIDC_CLIENT_ID`
- `OIDC_CLIENT_SECRET`
- `OIDC_ISSUER`
- `OIDC_SIGNING_ALGORITHM`
<Note>
Make sure the Redirect URI for your OIDC Client is set to `{WEBAPP_URL}/api/auth/callback/openid`.
</Note>
2. Update these environment variables in your `docker-compose.yml` or pass it directly to the running container.
An example configuration for a FusionAuth OpenID Connect in Formbricks would look like:
<Col>
<CodeGroup title="Formbricks Env for FusionAuth OIDC">
```yml {{ title: '.env' }}
OIDC_CLIENT_ID=59cada54-56d4-4aa8-a5e7-5823bbe0e5b7
OIDC_CLIENT_SECRET=4f4dwP0ZoOAqMW8fM9290A7uIS3E8Xg29xe1umhlB_s
OIDC_ISSUER=http://localhost:9011
OIDC_DISPLAY_NAME=FusionAuth
OIDC_SIGNING_ALGORITHM=HS256
```
</CodeGroup>
</Col>
3. Set an environment variable `OIDC_DISPLAY_NAME` to the display name of your OIDC provider.
4. Restart your Formbricks instance.
5. You're all set! Users can now signup & log in using their OIDC credentials.
## Important Run-time Variables
These variables can be provided at the runtime i.e. in your docker-compose file.
| Variable | Description | Required | Default |
| --------------------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------- |
| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
| DATABASE_URL | Database URL with credentials. | required | |
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
| ENCRYPTION_KEY | Secret for used by Formbricks for data encryption | required | (Generated by the user) |
| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` |
| PRIVACY_URL | URL for privacy policy. | optional | |
| TERMS_URL | URL for terms of service. | optional | |
| IMPRINT_URL | URL for imprint. | optional | |
| SIGNUP_DISABLED | Disables the ability for new users to create an account if set to `1`. | optional | |
| EMAIL_AUTH_DISABLED | Disables the ability for users to signup or login via email and password if set to `1`. | optional | |
| PASSWORD_RESET_DISABLED | Disables password reset functionality if set to `1`. | optional | |
| EMAIL_VERIFICATION_DISABLED | Disables email verification if set to `1`. | optional | |
| RATE_LIMITING_DISABLED | Disables rate limiting if set to `1`. | optional | |
| INVITE_DISABLED | Disables the ability for invited users to create an account if set to `1`. | optional | |
| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | |
| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | |
| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | |
| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | |
| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | |
| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to `1` else to `0`. | optional (required if email services are to be enabled) | |
| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | |
| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | |
| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | |
| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | |
| CRON_SECRET | API Secret for running cron jobs. | optional | |
| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | |
| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | |
| TELEMETRY_DISABLED | Disables telemetry if set to `1`. | optional | |
| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | |
| INTERNAL_SECRET | Internal Secret (Currently we overwrite the value with a random value). | optional | |
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | `#64748b` |
| DEFAULT_TEAM_ID | Automatically assign new users to a specific team when joining | optional | |
| DEFAULT_TEAM_ROLE | Role of the user in the default team. | optional | `admin` |
| ONBOARDING_DISABLED | Disables onboarding for new users if set to `1` | optional | |
| Variable | Description | Required | Default |
| --------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------- | ----------------------- |
| WEBAPP_URL | Base URL of the site. | required | `http://localhost:3000` |
| DATABASE_URL | Database URL with credentials. | required | |
| NEXTAUTH_SECRET | Secret for NextAuth, used for session signing and encryption. | required | (Generated by the user) |
| ENCRYPTION_KEY | Secret for used by Formbricks for data encryption | required | (Generated by the user) |
| NEXTAUTH_URL | Location of the auth server. By default, this is the Formbricks docker instance itself. | required | `http://localhost:3000` |
| PRIVACY_URL | URL for privacy policy. | optional | |
| TERMS_URL | URL for terms of service. | optional | |
| IMPRINT_URL | URL for imprint. | optional | |
| SIGNUP_DISABLED | Disables the ability for new users to create an account if set to `1`. | optional | |
| EMAIL_AUTH_DISABLED | Disables the ability for users to signup or login via email and password if set to `1`. | optional | |
| PASSWORD_RESET_DISABLED | Disables password reset functionality if set to `1`. | optional | |
| EMAIL_VERIFICATION_DISABLED | Disables email verification if set to `1`. | optional | |
| RATE_LIMITING_DISABLED | Disables rate limiting if set to `1`. | optional | |
| INVITE_DISABLED | Disables the ability for invited users to create an account if set to `1`. | optional | |
| MAIL_FROM | Email address to send emails from. | optional (required if email services are to be enabled) | |
| SMTP_HOST | Host URL of your SMTP server. | optional (required if email services are to be enabled) | |
| SMTP_PORT | Host Port of your SMTP server. | optional (required if email services are to be enabled) | |
| SMTP_USER | Username for your SMTP Server. | optional (required if email services are to be enabled) | |
| SMTP_PASSWORD | Password for your SMTP Server. | optional (required if email services are to be enabled) | |
| SMTP_SECURE_ENABLED | SMTP secure connection. For using TLS, set to `1` else to `0`. | optional (required if email services are to be enabled) | |
| GITHUB_ID | Client ID for GitHub. | optional (required if GitHub auth is enabled) | |
| GITHUB_SECRET | Secret for GitHub. | optional (required if GitHub auth is enabled) | |
| GOOGLE_CLIENT_ID | Client ID for Google. | optional (required if Google auth is enabled) | |
| GOOGLE_CLIENT_SECRET | Secret for Google. | optional (required if Google auth is enabled) | |
| CRON_SECRET | API Secret for running cron jobs. | optional | |
| STRIPE_SECRET_KEY | Secret key for Stripe integration. | optional | |
| STRIPE_WEBHOOK_SECRET | Webhook secret for Stripe integration. | optional | |
| TELEMETRY_DISABLED | Disables telemetry if set to `1`. | optional | |
| INSTANCE_ID | Instance ID for Formbricks Cloud to be sent to Telemetry. | optional | |
| INTERNAL_SECRET | Internal Secret (Currently we overwrite the value with a random value). | optional | |
| DEFAULT_BRAND_COLOR | Default brand color for your app (Can be overwritten from the UI as well). | optional | `#64748b` |
| DEFAULT_TEAM_ID | Automatically assign new users to a specific team when joining | optional | |
| DEFAULT_TEAM_ROLE | Role of the user in the default team. | optional | `admin` |
| ONBOARDING_DISABLED | Disables onboarding for new users if set to `1` | optional | |
| OIDC_DISPLAY_NAME | Display name for Custom OpenID Connect Provider | optional | |
| OIDC_CLIENT_ID | Client ID for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
| OIDC_CLIENT_SECRET | Secret for Custom OpenID Connect Provider | optional (required if OIDC auth is enabled) | |
| OIDC_ISSUER | Issuer URL for Custom OpenID Connect Provider (should have `.well-known` configured at this) | optional (required if OIDC auth is enabled) | |
| OIDC_SIGNING_ALGORITHM | Signing Algorithm for Custom OpenID Connect Provider | optional | `RS256` |
## Build-time Variables

View File

@@ -0,0 +1,38 @@
import { signIn } from "next-auth/react";
import { useCallback, useEffect } from "react";
import { Button } from "@formbricks/ui/Button";
export const OpenIdButton = ({
text = "Continue with OpenId Connect",
inviteUrl,
directRedirect = false,
}: {
text?: string;
inviteUrl?: string | null;
directRedirect?: boolean;
}) => {
const handleLogin = useCallback(async () => {
await signIn("openid", {
redirect: true,
callbackUrl: inviteUrl ? inviteUrl : "/",
});
}, [inviteUrl]);
useEffect(() => {
if (directRedirect) {
handleLogin();
}
}, [directRedirect, handleLogin]);
return (
<Button
type="button"
startIconClassName="ml-2"
onClick={handleLogin}
variant="secondary"
className="w-full justify-center">
{text}
</Button>
);
};

View File

@@ -3,6 +3,7 @@
import { AzureButton } from "@/app/(auth)/auth/components/AzureButton";
import { GithubButton } from "@/app/(auth)/auth/components/GithubButton";
import { GoogleButton } from "@/app/(auth)/auth/components/GoogleButton";
import { OpenIdButton } from "@/app/(auth)/auth/components/OpenIdButton";
import TwoFactor from "@/app/(auth)/auth/login/components/TwoFactor";
import TwoFactorBackup from "@/app/(auth)/auth/login/components/TwoFactorBackup";
import { XCircleIcon } from "@heroicons/react/24/solid";
@@ -30,6 +31,8 @@ export const SigninForm = ({
googleOAuthEnabled,
githubOAuthEnabled,
azureOAuthEnabled,
oidcOAuthEnabled,
oidcDisplayName,
}: {
emailAuthEnabled: boolean;
publicSignUpEnabled: boolean;
@@ -37,6 +40,8 @@ export const SigninForm = ({
googleOAuthEnabled: boolean;
githubOAuthEnabled: boolean;
azureOAuthEnabled: boolean;
oidcOAuthEnabled: boolean;
oidcDisplayName?: string;
}) => {
const router = useRouter();
const searchParams = useSearchParams();
@@ -223,6 +228,12 @@ export const SigninForm = ({
<AzureButton inviteUrl={callbackUrl} />
</>
)}
{oidcOAuthEnabled && !totpLogin && (
<>
<OpenIdButton inviteUrl={callbackUrl} text={`Continue with ${oidcDisplayName}`} />
</>
)}
</div>
{publicSignUpEnabled && !totpLogin && (

View File

@@ -8,6 +8,8 @@ import {
EMAIL_AUTH_ENABLED,
GITHUB_OAUTH_ENABLED,
GOOGLE_OAUTH_ENABLED,
OIDC_DISPLAY_NAME,
OIDC_OAUTH_ENABLED,
PASSWORD_RESET_DISABLED,
SIGNUP_ENABLED,
} from "@formbricks/lib/constants";
@@ -32,6 +34,8 @@ export default function SignInPage() {
googleOAuthEnabled={GOOGLE_OAUTH_ENABLED}
githubOAuthEnabled={GITHUB_OAUTH_ENABLED}
azureOAuthEnabled={AZURE_OAUTH_ENABLED}
oidcOAuthEnabled={OIDC_OAUTH_ENABLED}
oidcDisplayName={OIDC_DISPLAY_NAME}
/>
</FormWrapper>
</div>

View File

@@ -4,6 +4,7 @@ import { AzureButton } from "@/app/(auth)/auth/components/AzureButton";
import { GithubButton } from "@/app/(auth)/auth/components/GithubButton";
import { GoogleButton } from "@/app/(auth)/auth/components/GoogleButton";
import IsPasswordValid from "@/app/(auth)/auth/components/IsPasswordValid";
import { OpenIdButton } from "@/app/(auth)/auth/components/OpenIdButton";
import { createUser } from "@/app/lib/users/users";
import { XCircleIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
@@ -23,6 +24,8 @@ interface SignupFormProps {
googleOAuthEnabled: boolean;
githubOAuthEnabled: boolean;
azureOAuthEnabled: boolean;
oidcOAuthEnabled: boolean;
oidcDisplayName?: string;
}
export const SignupForm = ({
@@ -35,6 +38,8 @@ export const SignupForm = ({
googleOAuthEnabled,
githubOAuthEnabled,
azureOAuthEnabled,
oidcOAuthEnabled,
oidcDisplayName,
}: SignupFormProps) => {
const searchParams = useSearchParams();
const router = useRouter();
@@ -213,6 +218,11 @@ export const SignupForm = ({
<AzureButton inviteUrl={callbackUrl} />
</>
)}
{oidcOAuthEnabled && (
<>
<OpenIdButton inviteUrl={callbackUrl} text={`Continue with ${oidcDisplayName}`} />
</>
)}
</div>
{(termsUrl || privacyUrl) && (

View File

@@ -10,6 +10,8 @@ import {
GITHUB_OAUTH_ENABLED,
GOOGLE_OAUTH_ENABLED,
INVITE_DISABLED,
OIDC_DISPLAY_NAME,
OIDC_OAUTH_ENABLED,
PASSWORD_RESET_DISABLED,
PRIVACY_URL,
SIGNUP_ENABLED,
@@ -56,6 +58,8 @@ export default function SignUpPage({
googleOAuthEnabled={GOOGLE_OAUTH_ENABLED}
githubOAuthEnabled={GITHUB_OAUTH_ENABLED}
azureOAuthEnabled={AZURE_OAUTH_ENABLED}
oidcOAuthEnabled={OIDC_OAUTH_ENABLED}
oidcDisplayName={OIDC_DISPLAY_NAME}
/>
)}
</FormWrapper>

View File

@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "IdentityProvider" ADD VALUE 'openid';

View File

@@ -495,6 +495,7 @@ enum IdentityProvider {
github
google
azuread
openid
}
model Account {

View File

@@ -9,7 +9,14 @@ import { prisma } from "@formbricks/database";
import { createAccount } from "./account/service";
import { verifyPassword } from "./auth/util";
import { EMAIL_VERIFICATION_DISABLED } from "./constants";
import {
EMAIL_VERIFICATION_DISABLED,
OIDC_CLIENT_ID,
OIDC_CLIENT_SECRET,
OIDC_DISPLAY_NAME,
OIDC_ISSUER,
OIDC_SIGNING_ALGORITHM,
} from "./constants";
import { env } from "./env.mjs";
import { verifyToken } from "./jwt";
import { createMembership } from "./membership/service";
@@ -131,6 +138,28 @@ export const authOptions: NextAuthOptions = {
clientSecret: env.AZUREAD_CLIENT_SECRET || "",
tenantId: env.AZUREAD_TENANT_ID || "",
}),
{
id: "openid",
name: OIDC_DISPLAY_NAME || "OpenId",
type: "oauth",
clientId: OIDC_CLIENT_ID || "",
clientSecret: OIDC_CLIENT_SECRET || "",
wellKnown: `${OIDC_ISSUER}/.well-known/openid-configuration`,
authorization: { params: { scope: "openid email profile" } },
idToken: true,
client: {
id_token_signed_response_alg: OIDC_SIGNING_ALGORITHM || "RS256",
},
checks: ["pkce", "state"],
profile: (profile) => {
return {
id: profile.sub,
name: profile.name,
email: profile.email,
image: profile.picture,
};
},
},
],
callbacks: {
async jwt({ token }) {
@@ -161,7 +190,7 @@ export const authOptions: NextAuthOptions = {
return true;
}
if (!user.email || !user.name || account.type !== "oauth") {
if (!user.email || account.type !== "oauth") {
return false;
}
@@ -214,7 +243,7 @@ export const authOptions: NextAuthOptions = {
}
const userProfile = await createUser({
name: user.name,
name: user.name || user.email.split("@")[0],
email: user.email,
emailVerified: new Date(Date.now()),
onboardingCompleted: false,

View File

@@ -33,6 +33,8 @@ export const GOOGLE_OAUTH_ENABLED = env.GOOGLE_CLIENT_ID && env.GOOGLE_CLIENT_SE
export const GITHUB_OAUTH_ENABLED = env.GITHUB_ID && env.GITHUB_SECRET ? true : false;
export const AZURE_OAUTH_ENABLED =
env.AZUREAD_CLIENT_ID && env.AZUREAD_CLIENT_SECRET && env.AZUREAD_TENANT_ID ? true : false;
export const OIDC_OAUTH_ENABLED =
env.OIDC_CLIENT_ID && env.OIDC_CLIENT_SECRET && env.OIDC_ISSUER ? true : false;
export const GITHUB_ID = env.GITHUB_ID;
export const GITHUB_SECRET = env.GITHUB_SECRET;
@@ -43,6 +45,12 @@ export const AZUREAD_CLIENT_ID = env.AZUREAD_CLIENT_ID;
export const AZUREAD_CLIENT_SECRET = env.AZUREAD_CLIENT_SECRET;
export const AZUREAD_TENANT_ID = env.AZUREAD_TENANT_ID;
export const OIDC_CLIENT_ID = env.OIDC_CLIENT_ID;
export const OIDC_CLIENT_SECRET = env.OIDC_CLIENT_SECRET;
export const OIDC_ISSUER = env.OIDC_ISSUER;
export const OIDC_DISPLAY_NAME = env.OIDC_DISPLAY_NAME;
export const OIDC_SIGNING_ALGORITHM = env.OIDC_SIGNING_ALGORITHM;
export const SIGNUP_ENABLED = env.SIGNUP_DISABLED !== "1";
export const EMAIL_AUTH_ENABLED = env.EMAIL_AUTH_DISABLED !== "1";
export const INVITE_DISABLED = env.INVITE_DISABLED === "1";

View File

@@ -72,6 +72,11 @@ export const env = createEnv({
ONBOARDING_DISABLED: z.string().optional(),
ENTERPRISE_LICENSE_KEY: z.string().optional(),
RATE_LIMITING_DISABLED: z.enum(["1", "0"]).optional(),
OIDC_DISPLAY_NAME: z.string().optional(),
OIDC_CLIENT_ID: z.string().optional(),
OIDC_CLIENT_SECRET: z.string().optional(),
OIDC_ISSUER: z.string().optional(),
OIDC_SIGNING_ALGORITHM: z.string().optional(),
},
/*
@@ -154,5 +159,10 @@ export const env = createEnv({
ONBOARDING_DISABLED: process.env.ONBOARDING_DISABLED,
ENTERPRISE_LICENSE_KEY: process.env.ENTERPRISE_LICENSE_KEY,
RATE_LIMITING_DISABLED: process.env.RATE_LIMITING_DISABLED,
OIDC_DISPLAY_NAME: process.env.OIDC_DISPLAY_NAME,
OIDC_CLIENT_ID: process.env.OIDC_CLIENT_ID,
OIDC_CLIENT_SECRET: process.env.OIDC_CLIENT_SECRET,
OIDC_ISSUER: process.env.OIDC_ISSUER,
OIDC_SIGNING_ALGORITHM: process.env.OIDC_SIGNING_ALGORITHM,
},
});

View File

@@ -28,7 +28,7 @@ export const ZUser = z.object({
emailVerified: z.date().nullable(),
imageUrl: z.string().url().nullable(),
twoFactorEnabled: z.boolean(),
identityProvider: z.enum(["email", "google", "github", "azuread"]),
identityProvider: z.enum(["email", "google", "github", "azuread", "openid"]),
createdAt: z.date(),
updatedAt: z.date(),
onboardingCompleted: z.boolean(),
@@ -58,7 +58,7 @@ export const ZUserCreateInput = z.object({
onboardingCompleted: z.boolean().optional(),
role: ZRole.optional(),
objective: ZUserObjective.nullish(),
identityProvider: z.enum(["email", "google", "github", "azuread"]).optional(),
identityProvider: z.enum(["email", "google", "github", "azuread", "openid"]).optional(),
identityProviderAccountId: z.string().optional(),
});