feat: added oidc auth plugin for better auth (#147)
* feat: added oidc auth plugin for better auth * chore: remove unused code * chore: revert change to pnpm workspace file * chore: remove unused code * chore: remove unused code * chore: remove unused code * chore: added requested changes
This commit is contained in:
committed by
GitHub
parent
c7f7531b11
commit
ba2fd7f695
65
README.md
65
README.md
@@ -138,37 +138,40 @@ pnpm dev
|
|||||||
|
|
||||||
## Environment Variables 🔐
|
## Environment Variables 🔐
|
||||||
|
|
||||||
| Variable | Description | Required | Example |
|
| Variable | Description | Required | Example |
|
||||||
| ----------------------------------------- | -------------------------------------------------------- | ------------------------ | --------------------------------------------- |
|
| ----------------------------------------- | -------------------------------------------------------- | ------------------------ | ----------------------------------------------------------- |
|
||||||
| `POSTGRES_URL` | PostgreSQL connection URL | To use external database | `postgres://user:pass@localhost:5432/db` |
|
| `POSTGRES_URL` | PostgreSQL connection URL | To use external database | `postgres://user:pass@localhost:5432/db` |
|
||||||
| `EMAIL_FROM` | Sender email address | For Email | `"Kan <hello@mail.kan.bn>"` |
|
| `EMAIL_FROM` | Sender email address | For Email | `"Kan <hello@mail.kan.bn>"` |
|
||||||
| `SMTP_HOST` | SMTP server hostname | For Email | `smtp.resend.com` |
|
| `SMTP_HOST` | SMTP server hostname | For Email | `smtp.resend.com` |
|
||||||
| `SMTP_PORT` | SMTP server port | For Email | `465` |
|
| `SMTP_PORT` | SMTP server port | For Email | `465` |
|
||||||
| `SMTP_USER` | SMTP username/email | No | `resend` |
|
| `SMTP_USER` | SMTP username/email | No | `resend` |
|
||||||
| `SMTP_PASSWORD` | SMTP password/token | No | `re_xxxx` |
|
| `SMTP_PASSWORD` | SMTP password/token | No | `re_xxxx` |
|
||||||
| `SMTP_SECURE` | Use secure SMTP connection (defaults to true if not set) | For Email | `true` |
|
| `SMTP_SECURE` | Use secure SMTP connection (defaults to true if not set) | For Email | `true` |
|
||||||
| `NEXT_PUBLIC_BASE_URL` | Base URL of your installation | Yes | `http://localhost:3000` |
|
| `NEXT_PUBLIC_BASE_URL` | Base URL of your installation | Yes | `http://localhost:3000` |
|
||||||
| `BETTER_AUTH_SECRET` | Auth encryption secret | Yes | Random 32+ char string |
|
| `BETTER_AUTH_SECRET` | Auth encryption secret | Yes | Random 32+ char string |
|
||||||
| `BETTER_AUTH_TRUSTED_ORIGINS` | Allowed callback origins | No | `http://localhost:3000,http://localhost:3001` |
|
| `BETTER_AUTH_TRUSTED_ORIGINS` | Allowed callback origins | No | `http://localhost:3000,http://localhost:3001` |
|
||||||
| `GOOGLE_CLIENT_ID` | Google OAuth client ID | For Google login | `xxx.apps.googleusercontent.com` |
|
| `GOOGLE_CLIENT_ID` | Google OAuth client ID | For Google login | `xxx.apps.googleusercontent.com` |
|
||||||
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | For Google login | `xxx` |
|
| `GOOGLE_CLIENT_SECRET` | Google OAuth client secret | For Google login | `xxx` |
|
||||||
| `DISCORD_CLIENT_ID` | Discord OAuth client ID | For Discord login | `xxx` |
|
| `DISCORD_CLIENT_ID` | Discord OAuth client ID | For Discord login | `xxx` |
|
||||||
| `DISCORD_CLIENT_SECRET` | Discord OAuth client secret | For Discord login | `xxx` |
|
| `DISCORD_CLIENT_SECRET` | Discord OAuth client secret | For Discord login | `xxx` |
|
||||||
| `GITHUB_CLIENT_ID` | GitHub OAuth client ID | For GitHub login | `xxx` |
|
| `GITHUB_CLIENT_ID` | GitHub OAuth client ID | For GitHub login | `xxx` |
|
||||||
| `GITHUB_CLIENT_SECRET` | GitHub OAuth client secret | For GitHub login | `xxx` |
|
| `GITHUB_CLIENT_SECRET` | GitHub OAuth client secret | For GitHub login | `xxx` |
|
||||||
| `TRELLO_APP_API_KEY` | Trello app API key | For Trello import | `xxx` |
|
| `OIDC_CLIENT_ID` | Generic OIDC client ID | For OIDC login | `xxx` |
|
||||||
| `TRELLO_APP_API_SECRET` | Trello app API secret | For Trello import | `xxx` |
|
| `OIDC_CLIENT_SECRET` | Generic OIDC client secret | For OIDC login | `xxx` |
|
||||||
| `S3_REGION` | S3 storage region | For file uploads | `WEUR` |
|
| `OIDC_DISCOVERY_URL` | OIDC discovery URL | For OIDC login | `https://auth.example.com/.well-known/openid-configuration` |
|
||||||
| `S3_ENDPOINT` | S3 endpoint URL | For file uploads | `https://xxx.r2.cloudflarestorage.com` |
|
| `TRELLO_APP_API_KEY` | Trello app API key | For Trello import | `xxx` |
|
||||||
| `S3_ACCESS_KEY_ID` | S3 access key | For file uploads | `xxx` |
|
| `TRELLO_APP_API_SECRET` | Trello app API secret | For Trello import | `xxx` |
|
||||||
| `S3_SECRET_ACCESS_KEY` | S3 secret key | For file uploads | `xxx` |
|
| `S3_REGION` | S3 storage region | For file uploads | `WEUR` |
|
||||||
| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 | For file uploads | `true` |
|
| `S3_ENDPOINT` | S3 endpoint URL | For file uploads | `https://xxx.r2.cloudflarestorage.com` |
|
||||||
| `NEXT_PUBLIC_STORAGE_URL` | Storage service URL | For file uploads | `https://storage.kanbn.com` |
|
| `S3_ACCESS_KEY_ID` | S3 access key | For file uploads | `xxx` |
|
||||||
| `NEXT_PUBLIC_STORAGE_DOMAIN` | Storage domain name | For file uploads | `kanbn.com` |
|
| `S3_SECRET_ACCESS_KEY` | S3 secret key | For file uploads | `xxx` |
|
||||||
| `NEXT_PUBLIC_AVATAR_BUCKET_NAME` | S3 bucket name for avatars | For file uploads | `avatars` |
|
| `S3_FORCE_PATH_STYLE` | Use path-style URLs for S3 | For file uploads | `true` |
|
||||||
| `NEXT_PUBLIC_ALLOW_CREDENTIALS` | Allow email & password login | For authentication | `true` |
|
| `NEXT_PUBLIC_STORAGE_URL` | Storage service URL | For file uploads | `https://storage.kanbn.com` |
|
||||||
| `NEXT_PUBLIC_DISABLE_SIGN_UP` | Disable sign up | For authentication | `false` |
|
| `NEXT_PUBLIC_STORAGE_DOMAIN` | Storage domain name | For file uploads | `kanbn.com` |
|
||||||
| `NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY` | Hide “Powered by kan.bn” on public boards (self-host) | For white labelling | `true` |
|
| `NEXT_PUBLIC_AVATAR_BUCKET_NAME` | S3 bucket name for avatars | For file uploads | `avatars` |
|
||||||
|
| `NEXT_PUBLIC_ALLOW_CREDENTIALS` | Allow email & password login | For authentication | `true` |
|
||||||
|
| `NEXT_PUBLIC_DISABLE_SIGN_UP` | Disable sign up | For authentication | `false` |
|
||||||
|
| `NEXT_PUBLIC_WHITE_LABEL_HIDE_POWERED_BY` | Hide “Powered by kan.bn” on public boards (self-host) | For white labelling | `true` |
|
||||||
|
|
||||||
See `.env.example` for a complete list of supported environment variables.
|
See `.env.example` for a complete list of supported environment variables.
|
||||||
|
|
||||||
|
|||||||
@@ -31,12 +31,12 @@ const config = {
|
|||||||
|
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
|
|
||||||
{
|
{
|
||||||
protocol: "https",
|
protocol: "https",
|
||||||
hostname: process.env.S3_FORCE_PATH_STYLE === "true"
|
hostname:
|
||||||
? `${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`
|
env("S3_FORCE_PATH_STYLE") === "true"
|
||||||
: `*.${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`,
|
? `${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`
|
||||||
|
: `*.${env("NEXT_PUBLIC_STORAGE_DOMAIN")}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
protocol: "http",
|
protocol: "http",
|
||||||
@@ -54,4 +54,16 @@ const config = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Only allow external images when OIDC is configured (for OIDC provider avatars)
|
||||||
|
if (
|
||||||
|
env("OIDC_CLIENT_ID") &&
|
||||||
|
env("OIDC_CLIENT_SECRET") &&
|
||||||
|
env("OIDC_DISCOVERY_URL")
|
||||||
|
) {
|
||||||
|
config.images?.remotePatterns?.push({
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "**",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { t } from "@lingui/core/macro";
|
|||||||
import { Trans } from "@lingui/react/macro";
|
import { Trans } from "@lingui/react/macro";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { env } from "next-runtime-env";
|
import { env } from "next-runtime-env";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
FaApple,
|
FaApple,
|
||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
FaGoogle,
|
FaGoogle,
|
||||||
FaLinkedin,
|
FaLinkedin,
|
||||||
FaMicrosoft,
|
FaMicrosoft,
|
||||||
|
FaOpenid,
|
||||||
FaReddit,
|
FaReddit,
|
||||||
FaSpotify,
|
FaSpotify,
|
||||||
FaTiktok,
|
FaTiktok,
|
||||||
@@ -33,6 +34,8 @@ import Button from "~/components/Button";
|
|||||||
import Input from "~/components/Input";
|
import Input from "~/components/Input";
|
||||||
import { usePopup } from "~/providers/popup";
|
import { usePopup } from "~/providers/popup";
|
||||||
|
|
||||||
|
type AuthProvider = SocialProvider | "oidc";
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
name?: string;
|
name?: string;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -141,16 +144,28 @@ const availableSocialProviders = {
|
|||||||
name: "Zoom",
|
name: "Zoom",
|
||||||
icon: SiZoom,
|
icon: SiZoom,
|
||||||
},
|
},
|
||||||
|
oidc: {
|
||||||
|
id: "oidc",
|
||||||
|
name: "OIDC",
|
||||||
|
icon: FaOpenid,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
||||||
const [isLoginWithProviderPending, setIsLoginWithProviderPending] =
|
const [isLoginWithProviderPending, setIsLoginWithProviderPending] =
|
||||||
useState<null | SocialProvider>(null);
|
useState<null | AuthProvider>(null);
|
||||||
const isCredentialsEnabled =
|
const [isCredentialsEnabled, setIsCredentialsEnabled] = useState(false);
|
||||||
env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true";
|
|
||||||
const [isLoginWithEmailPending, setIsLoginWithEmailPending] = useState(false);
|
const [isLoginWithEmailPending, setIsLoginWithEmailPending] = useState(false);
|
||||||
const [loginError, setLoginError] = useState<string | null>(null);
|
const [loginError, setLoginError] = useState<string | null>(null);
|
||||||
const { showPopup } = usePopup();
|
const { showPopup } = usePopup();
|
||||||
|
const oidcProviderName = "OIDC";
|
||||||
|
|
||||||
|
// Safely get environment variables on client side to avoid hydration mismatch
|
||||||
|
useEffect(() => {
|
||||||
|
const credentialsAllowed =
|
||||||
|
env("NEXT_PUBLIC_ALLOW_CREDENTIALS")?.toLowerCase() === "true";
|
||||||
|
setIsCredentialsEnabled(credentialsAllowed);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
@@ -226,13 +241,26 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
|||||||
setIsLoginWithEmailPending(false);
|
setIsLoginWithEmailPending(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLoginWithProvider = async (provider: SocialProvider) => {
|
const handleLoginWithProvider = async (provider: AuthProvider) => {
|
||||||
setIsLoginWithProviderPending(provider);
|
setIsLoginWithProviderPending(provider);
|
||||||
setLoginError(null);
|
setLoginError(null);
|
||||||
const { error } = await authClient.signIn.social({
|
|
||||||
provider,
|
let error;
|
||||||
callbackURL: "/boards",
|
if (provider === "oidc") {
|
||||||
});
|
// Use oauth2 signin for OIDC provider
|
||||||
|
const result = await authClient.signIn.oauth2({
|
||||||
|
providerId: "oidc",
|
||||||
|
callbackURL: "/boards",
|
||||||
|
});
|
||||||
|
error = result.error;
|
||||||
|
} else {
|
||||||
|
// Use social signin for traditional social providers
|
||||||
|
const result = await authClient.signIn.social({
|
||||||
|
provider,
|
||||||
|
callbackURL: "/boards",
|
||||||
|
});
|
||||||
|
error = result.error;
|
||||||
|
}
|
||||||
|
|
||||||
setIsLoginWithProviderPending(null);
|
setIsLoginWithProviderPending(null);
|
||||||
|
|
||||||
@@ -260,13 +288,16 @@ export function Auth({ setIsMagicLinkSent, isSignUp }: AuthProps) {
|
|||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
key={key}
|
key={key}
|
||||||
onClick={() => handleLoginWithProvider(key as SocialProvider)}
|
onClick={() => handleLoginWithProvider(key as AuthProvider)}
|
||||||
isLoading={isLoginWithProviderPending === key}
|
isLoading={isLoginWithProviderPending === key}
|
||||||
iconLeft={<provider.icon />}
|
iconLeft={<provider.icon />}
|
||||||
fullWidth
|
fullWidth
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
<Trans>Continue with {provider.name}</Trans>
|
<Trans>
|
||||||
|
Continue with{" "}
|
||||||
|
{key === "oidc" ? oidcProviderName : provider.name}
|
||||||
|
</Trans>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ export function NewWorkspaceForm() {
|
|||||||
description: values.description,
|
description: values.description,
|
||||||
slug: values.slug,
|
slug: values.slug,
|
||||||
plan: values.plan,
|
plan: values.plan,
|
||||||
|
role: "admin",
|
||||||
});
|
});
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ export const env = createEnv({
|
|||||||
VK_CLIENT_SECRET: z.string().optional(),
|
VK_CLIENT_SECRET: z.string().optional(),
|
||||||
LINKEDIN_CLIENT_ID: z.string().optional(),
|
LINKEDIN_CLIENT_ID: z.string().optional(),
|
||||||
LINKEDIN_CLIENT_SECRET: z.string().optional(),
|
LINKEDIN_CLIENT_SECRET: z.string().optional(),
|
||||||
|
// Generic OIDC Provider
|
||||||
|
OIDC_CLIENT_ID: z.string().optional(),
|
||||||
|
OIDC_CLIENT_SECRET: z.string().optional(),
|
||||||
|
OIDC_DISCOVERY_URL: z.string().optional(),
|
||||||
REDDIT_CLIENT_ID: z.string().optional(),
|
REDDIT_CLIENT_ID: z.string().optional(),
|
||||||
REDDIT_CLIENT_SECRET: z.string().optional(),
|
REDDIT_CLIENT_SECRET: z.string().optional(),
|
||||||
ROBLOX_CLIENT_ID: z.string().optional(),
|
ROBLOX_CLIENT_ID: z.string().optional(),
|
||||||
|
|||||||
@@ -46,6 +46,11 @@ services:
|
|||||||
- TRELLO_APP_API_KEY=${TRELLO_APP_API_KEY}
|
- TRELLO_APP_API_KEY=${TRELLO_APP_API_KEY}
|
||||||
- TRELLO_APP_SECRET=${TRELLO_APP_SECRET}
|
- TRELLO_APP_SECRET=${TRELLO_APP_SECRET}
|
||||||
|
|
||||||
|
# Generic OIDC provider (optional)
|
||||||
|
- OIDC_CLIENT_ID=${OIDC_CLIENT_ID}
|
||||||
|
- OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET}
|
||||||
|
- OIDC_DISCOVERY_URL=${OIDC_DISCOVERY_URL}
|
||||||
|
|
||||||
# OAuth providers (optional)
|
# OAuth providers (optional)
|
||||||
- BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS}
|
- BETTER_AUTH_TRUSTED_ORIGINS=${BETTER_AUTH_TRUSTED_ORIGINS}
|
||||||
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { PutObjectCommand, S3Client } from "@aws-sdk/client-s3";
|
|||||||
import { betterAuth } from "better-auth";
|
import { betterAuth } from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import { createAuthEndpoint, createAuthMiddleware } from "better-auth/api";
|
import { createAuthEndpoint, createAuthMiddleware } from "better-auth/api";
|
||||||
import { apiKey } from "better-auth/plugins";
|
import { apiKey, genericOAuth } from "better-auth/plugins";
|
||||||
import { magicLink } from "better-auth/plugins/magic-link";
|
import { magicLink } from "better-auth/plugins/magic-link";
|
||||||
import { socialProviderList } from "better-auth/social-providers";
|
import { socialProviderList } from "better-auth/social-providers";
|
||||||
import { env } from "next-runtime-env";
|
import { env } from "next-runtime-env";
|
||||||
@@ -83,8 +83,20 @@ export const socialProvidersPlugin = () => ({
|
|||||||
{
|
{
|
||||||
method: "GET",
|
method: "GET",
|
||||||
},
|
},
|
||||||
async (ctx) =>
|
async (ctx) => {
|
||||||
ctx.json(ctx.context.socialProviders.map((p) => p.id.toLowerCase())),
|
const providers = ctx.context.socialProviders.map((p) =>
|
||||||
|
p.id.toLowerCase(),
|
||||||
|
);
|
||||||
|
// Add OIDC provider if configured
|
||||||
|
if (
|
||||||
|
process.env.OIDC_CLIENT_ID &&
|
||||||
|
process.env.OIDC_CLIENT_SECRET &&
|
||||||
|
process.env.OIDC_DISCOVERY_URL
|
||||||
|
) {
|
||||||
|
providers.push("oidc");
|
||||||
|
}
|
||||||
|
return ctx.json(providers);
|
||||||
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -162,6 +174,25 @@ export const initAuth = (db: dbClient) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
// Generic OIDC provider
|
||||||
|
...(process.env.OIDC_CLIENT_ID &&
|
||||||
|
process.env.OIDC_CLIENT_SECRET &&
|
||||||
|
process.env.OIDC_DISCOVERY_URL
|
||||||
|
? [
|
||||||
|
genericOAuth({
|
||||||
|
config: [
|
||||||
|
{
|
||||||
|
providerId: "oidc",
|
||||||
|
clientId: process.env.OIDC_CLIENT_ID,
|
||||||
|
clientSecret: process.env.OIDC_CLIENT_SECRET,
|
||||||
|
discoveryUrl: process.env.OIDC_DISCOVERY_URL,
|
||||||
|
scopes: ["openid", "email", "profile"],
|
||||||
|
pkce: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
]
|
||||||
|
: []),
|
||||||
],
|
],
|
||||||
databaseHooks: {
|
databaseHooks: {
|
||||||
user: {
|
user: {
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
import { BetterAuthClientPlugin } from "better-auth";
|
import type { BetterAuthClientPlugin } from "better-auth";
|
||||||
import { apiKeyClient, magicLinkClient } from "better-auth/client/plugins";
|
import type { BetterFetchOption } from "better-auth/react";
|
||||||
import { BetterFetchOption, createAuthClient } from "better-auth/react";
|
import {
|
||||||
|
apiKeyClient,
|
||||||
|
genericOAuthClient,
|
||||||
|
magicLinkClient,
|
||||||
|
} from "better-auth/client/plugins";
|
||||||
|
import { createAuthClient } from "better-auth/react";
|
||||||
|
|
||||||
import { socialProvidersPlugin } from "./auth";
|
import type { socialProvidersPlugin } from "./auth";
|
||||||
|
|
||||||
const socialProvidersPluginClient = {
|
const socialProvidersPluginClient = {
|
||||||
id: "social-providers-plugin",
|
id: "social-providers-plugin",
|
||||||
@@ -21,5 +26,10 @@ const socialProvidersPluginClient = {
|
|||||||
} satisfies BetterAuthClientPlugin;
|
} satisfies BetterAuthClientPlugin;
|
||||||
|
|
||||||
export const authClient = createAuthClient({
|
export const authClient = createAuthClient({
|
||||||
plugins: [magicLinkClient(), apiKeyClient(), socialProvidersPluginClient],
|
plugins: [
|
||||||
|
magicLinkClient(),
|
||||||
|
apiKeyClient(),
|
||||||
|
genericOAuthClient(),
|
||||||
|
socialProvidersPluginClient,
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -72,6 +72,9 @@
|
|||||||
"VK_CLIENT_SECRET",
|
"VK_CLIENT_SECRET",
|
||||||
"LINKEDIN_CLIENT_ID",
|
"LINKEDIN_CLIENT_ID",
|
||||||
"LINKEDIN_CLIENT_SECRET",
|
"LINKEDIN_CLIENT_SECRET",
|
||||||
|
"OIDC_CLIENT_ID",
|
||||||
|
"OIDC_CLIENT_SECRET",
|
||||||
|
"OIDC_DISCOVERY_URL",
|
||||||
"REDDIT_CLIENT_ID",
|
"REDDIT_CLIENT_ID",
|
||||||
"REDDIT_CLIENT_SECRET",
|
"REDDIT_CLIENT_SECRET",
|
||||||
"ROBLOX_CLIENT_ID",
|
"ROBLOX_CLIENT_ID",
|
||||||
|
|||||||
Reference in New Issue
Block a user