feat: add keycloak login (#139)
Co-authored-by: Zdeněk Janeček <zdenek.janecek@firma.seznam.cz>
This commit is contained in:
parent
7887b42404
commit
3433371930
11 changed files with 1283 additions and 950 deletions
|
@ -20,6 +20,12 @@ REGISTRATION_PASSWORD=
|
||||||
GITHUB_CLIENT_ID=
|
GITHUB_CLIENT_ID=
|
||||||
GITHUB_CLIENT_SECRET=
|
GITHUB_CLIENT_SECRET=
|
||||||
|
|
||||||
|
# Optional: if you want Keycloak oauth. Currently incompatible with the registration password
|
||||||
|
KEYCLOAK_ID=
|
||||||
|
KEYCLOAK_SECRET=
|
||||||
|
KEYCLOAK_ISSUER= # keycloak path including realm
|
||||||
|
KEYCLOAK_NAME=
|
||||||
|
|
||||||
# Optional: if you want to support credential auth (username/password, supports registration password)
|
# Optional: if you want to support credential auth (username/password, supports registration password)
|
||||||
# Defaults to true
|
# Defaults to true
|
||||||
CREDENTIAL_AUTH=true
|
CREDENTIAL_AUTH=true
|
||||||
|
|
63
package.json
63
package.json
|
@ -14,68 +14,69 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@next-auth/prisma-adapter": "^1.0.5",
|
"@next-auth/prisma-adapter": "^1.0.5",
|
||||||
"@next/eslint-plugin-next": "13.2.5-canary.19",
|
"@next/eslint-plugin-next": "^13.2.4",
|
||||||
"@prisma/client": "^4.10.1",
|
"@prisma/client": "^4.11.0",
|
||||||
"@radix-ui/react-dialog": "^1.0.2",
|
"@radix-ui/react-dialog": "^1.0.3",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.3",
|
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
||||||
"@radix-ui/react-popover": "^1.0.4",
|
"@radix-ui/react-popover": "^1.0.5",
|
||||||
"@radix-ui/react-tabs": "^1.0.2",
|
"@radix-ui/react-tabs": "^1.0.3",
|
||||||
"@radix-ui/react-tooltip": "^1.0.4",
|
"@radix-ui/react-tooltip": "^1.0.5",
|
||||||
"@vercel/og": "^0.0.27",
|
"@vercel/og": "^0.4.0",
|
||||||
"client-only": "^0.0.1",
|
"client-only": "^0.0.1",
|
||||||
"client-zip": "2.3.0",
|
"client-zip": "2.3.0",
|
||||||
"cmdk": "^0.1.22",
|
"cmdk": "^0.2.0",
|
||||||
"jest": "^29.4.3",
|
"jest": "^29.5.0",
|
||||||
"lodash.debounce": "^4.0.8",
|
"lodash.debounce": "^4.0.8",
|
||||||
"next": "13.2.5-canary.19",
|
"next": "^13.2.4",
|
||||||
"next-auth": "^4.19.2",
|
"next-auth": "^4.20.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-cookie": "^4.1.1",
|
"react-cookie": "^4.1.1",
|
||||||
"react-datepicker": "4.8.0",
|
"react-datepicker": "4.10.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-dropzone": "14.2.3",
|
"react-dropzone": "14.2.3",
|
||||||
"react-error-boundary": "^3.1.4",
|
"react-error-boundary": "^3.1.4",
|
||||||
"react-feather": "^2.0.10",
|
"react-feather": "^2.0.10",
|
||||||
"react-hot-toast": "2.4.0",
|
"react-hot-toast": "2.4.0",
|
||||||
"server-only": "^0.0.1",
|
"server-only": "^0.0.1",
|
||||||
"swr": "^2.0.4",
|
"swr": "^2.1.0",
|
||||||
"textarea-markdown-editor": "1.0.4",
|
"textarea-markdown-editor": "1.0.4",
|
||||||
"ts-jest": "^29.0.5",
|
"ts-jest": "^29.0.5",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"zlib": "^1.0.5"
|
"zlib": "^1.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@next/bundle-analyzer": "13.1.7-canary.26",
|
"@next/bundle-analyzer": "13.2.4",
|
||||||
"@total-typescript/ts-reset": "^0.3.7",
|
"@total-typescript/ts-reset": "^0.4.2",
|
||||||
"@types/bcrypt": "^5.0.0",
|
"@types/bcrypt": "^5.0.0",
|
||||||
"@types/git-http-backend": "^1.0.1",
|
"@types/git-http-backend": "^1.0.1",
|
||||||
"@types/jest": "^29.4.0",
|
"@types/jest": "^29.4.1",
|
||||||
"@types/lodash.debounce": "^4.0.7",
|
"@types/lodash.debounce": "^4.0.7",
|
||||||
"@types/node": "18.11.18",
|
"@types/node": "18.15.3",
|
||||||
"@types/react": "18.0.27",
|
"@types/react": "18.0.28",
|
||||||
"@types/react-datepicker": "4.8.0",
|
"@types/react-datepicker": "4.10.0",
|
||||||
"@types/react-dom": "18.0.10",
|
"@types/react-dom": "18.0.11",
|
||||||
"@types/uuid": "^9.0.1",
|
"@types/uuid": "^9.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.53.0",
|
"@typescript-eslint/eslint-plugin": "^5.55.0",
|
||||||
"@typescript-eslint/parser": "^5.53.0",
|
"@typescript-eslint/parser": "^5.55.0",
|
||||||
"@wcj/markdown-to-html": "^2.2.1",
|
"@wcj/markdown-to-html": "^2.2.1",
|
||||||
"clsx": "^1.2.1",
|
"clsx": "^1.2.1",
|
||||||
"cross-env": "7.0.3",
|
"cross-env": "7.0.3",
|
||||||
"csstype": "^3.1.1",
|
"csstype": "^3.1.1",
|
||||||
"dotenv": "^16.0.3",
|
"dotenv": "^16.0.3",
|
||||||
"eslint": "8.33.0",
|
"eslint": "8.36.0",
|
||||||
"eslint-config-next": "13.1.7-canary.26",
|
"eslint-config-next": "13.2.4",
|
||||||
"jest-mock-extended": "^3.0.2",
|
"jest-mock-extended": "^3.0.3",
|
||||||
"next-unused": "0.0.6",
|
"next-unused": "0.0.6",
|
||||||
|
"postcss": "^8.4.21",
|
||||||
"postcss-flexbugs-fixes": "^5.0.2",
|
"postcss-flexbugs-fixes": "^5.0.2",
|
||||||
"postcss-hover-media-feature": "^1.0.2",
|
"postcss-hover-media-feature": "^1.0.2",
|
||||||
"postcss-nested": "^6.0.1",
|
"postcss-nested": "^6.0.1",
|
||||||
"postcss-preset-env": "^8.0.1",
|
"postcss-preset-env": "^8.0.1",
|
||||||
"prettier": "2.8.3",
|
"prettier": "2.8.4",
|
||||||
"prisma": "^4.10.1",
|
"prisma": "^4.11.0",
|
||||||
"typescript": "4.9.4",
|
"typescript": "4.9.5",
|
||||||
"typescript-plugin-css-modules": "4.1.1"
|
"typescript-plugin-css-modules": "4.2.3"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"sharp": "^0.31.3"
|
"sharp": "^0.31.3"
|
||||||
|
|
1868
pnpm-lock.yaml
1868
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -6,20 +6,23 @@ import Link from "../../../components/link"
|
||||||
import { signIn } from "next-auth/react"
|
import { signIn } from "next-auth/react"
|
||||||
import Input from "@components/input"
|
import Input from "@components/input"
|
||||||
import Button from "@components/button"
|
import Button from "@components/button"
|
||||||
import { GitHub } from "react-feather"
|
import { Key } from "react-feather"
|
||||||
import { useToasts } from "@components/toasts"
|
import { useToasts } from "@components/toasts"
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import Note from "@components/note"
|
import Note from "@components/note"
|
||||||
import { ErrorQueryParamsHandler } from "./query-handler"
|
import { ErrorQueryParamsHandler } from "./query-handler"
|
||||||
|
import { AuthProviders } from "@lib/server/auth-props"
|
||||||
|
|
||||||
function Auth({
|
function Auth({
|
||||||
page,
|
page,
|
||||||
|
credentialAuth,
|
||||||
requiresServerPassword,
|
requiresServerPassword,
|
||||||
isGithubEnabled
|
authProviders
|
||||||
}: {
|
}: {
|
||||||
page: "signup" | "signin"
|
page: "signup" | "signin"
|
||||||
|
credentialAuth?: boolean
|
||||||
requiresServerPassword?: boolean
|
requiresServerPassword?: boolean
|
||||||
isGithubEnabled?: boolean
|
authProviders?: AuthProviders
|
||||||
}) {
|
}) {
|
||||||
const [serverPassword, setServerPassword] = useState("")
|
const [serverPassword, setServerPassword] = useState("")
|
||||||
const { setToast } = useToasts()
|
const { setToast } = useToasts()
|
||||||
|
@ -102,53 +105,62 @@ function Auth({
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
<Input
|
{credentialAuth ? (
|
||||||
type="text"
|
|
||||||
id="username"
|
|
||||||
value={username}
|
|
||||||
onChange={handleChangeUsername}
|
|
||||||
placeholder="Username"
|
|
||||||
required={true}
|
|
||||||
minLength={3}
|
|
||||||
width="100%"
|
|
||||||
aria-label="Username"
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
id="password"
|
|
||||||
value={password}
|
|
||||||
onChange={handleChangePassword}
|
|
||||||
placeholder="Password"
|
|
||||||
required={true}
|
|
||||||
minLength={6}
|
|
||||||
width="100%"
|
|
||||||
aria-label="Password"
|
|
||||||
/>
|
|
||||||
<Button width={"100%"} type="submit" loading={submitting}>
|
|
||||||
Sign {signText}
|
|
||||||
</Button>
|
|
||||||
{isGithubEnabled ? (
|
|
||||||
<>
|
<>
|
||||||
<hr style={{ width: "100%" }} />
|
<Input
|
||||||
<Button
|
type="text"
|
||||||
type="submit"
|
id="username"
|
||||||
|
value={username}
|
||||||
|
onChange={handleChangeUsername}
|
||||||
|
placeholder="Username"
|
||||||
|
required={true}
|
||||||
|
minLength={3}
|
||||||
width="100%"
|
width="100%"
|
||||||
style={{
|
aria-label="Username"
|
||||||
color: "var(--fg)"
|
/>
|
||||||
}}
|
<Input
|
||||||
iconLeft={<GitHub />}
|
type="password"
|
||||||
onClick={(e) => {
|
id="password"
|
||||||
e.preventDefault()
|
value={password}
|
||||||
signIn("github", {
|
onChange={handleChangePassword}
|
||||||
callbackUrl: "/",
|
placeholder="Password"
|
||||||
registration_password: serverPassword
|
required={true}
|
||||||
})
|
minLength={6}
|
||||||
}}
|
width="100%"
|
||||||
>
|
aria-label="Password"
|
||||||
Sign {signText.toLowerCase()} with GitHub
|
/>
|
||||||
|
<Button width={"100%"} type="submit" loading={submitting}>
|
||||||
|
Sign {signText}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
|
{authProviders?.length ? (
|
||||||
|
<>
|
||||||
|
{authProviders?.map((provider) => {
|
||||||
|
return provider.enabled ? (
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
width="100%"
|
||||||
|
key={provider.id + "-button"}
|
||||||
|
style={{
|
||||||
|
color: "var(--fg)"
|
||||||
|
}}
|
||||||
|
iconLeft={<Key />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
signIn(provider.id, {
|
||||||
|
callbackUrl: "/",
|
||||||
|
registration_password: serverPassword
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Sign {signText.toLowerCase()} with {provider.public_name}
|
||||||
|
</Button>
|
||||||
|
) : null
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.formContentSpace}>
|
<div className={styles.formContentSpace}>
|
||||||
{signingIn ? (
|
{signingIn ? (
|
||||||
|
|
|
@ -1,15 +1,16 @@
|
||||||
import { getMetadata } from "src/app/lib/metadata"
|
import { getMetadata } from "src/app/lib/metadata"
|
||||||
import config from "@lib/config"
|
|
||||||
import Auth from "../components"
|
|
||||||
|
|
||||||
function isGithubEnabled() {
|
import Auth from "../components"
|
||||||
return config.github_client_id.length && config.github_client_secret.length
|
import { getAuthProviders, isCredentialEnabled } from "@lib/server/auth-props"
|
||||||
? true
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SignInPage() {
|
export default function SignInPage() {
|
||||||
return <Auth page="signin" isGithubEnabled={isGithubEnabled()} />
|
return (
|
||||||
|
<Auth
|
||||||
|
page="signin"
|
||||||
|
credentialAuth={isCredentialEnabled()}
|
||||||
|
authProviders={getAuthProviders()}
|
||||||
|
/>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const metadata = getMetadata({
|
export const metadata = getMetadata({
|
||||||
|
|
|
@ -1,25 +1,20 @@
|
||||||
import Auth from "../components"
|
import Auth from "../components"
|
||||||
import { getRequiresPasscode } from "src/pages/api/auth/requires-passcode"
|
import { getRequiresPasscode } from "src/pages/api/auth/requires-passcode"
|
||||||
import config from "@lib/config"
|
|
||||||
import { getMetadata } from "src/app/lib/metadata"
|
import { getMetadata } from "src/app/lib/metadata"
|
||||||
|
import { getAuthProviders, isCredentialEnabled } from "@lib/server/auth-props"
|
||||||
|
|
||||||
async function getPasscode() {
|
async function getPasscode() {
|
||||||
return await getRequiresPasscode()
|
return await getRequiresPasscode()
|
||||||
}
|
}
|
||||||
|
|
||||||
function isGithubEnabled() {
|
|
||||||
return config.github_client_id.length && config.github_client_secret.length
|
|
||||||
? true
|
|
||||||
: false
|
|
||||||
}
|
|
||||||
|
|
||||||
export default async function SignUpPage() {
|
export default async function SignUpPage() {
|
||||||
const requiresPasscode = await getPasscode()
|
const requiresPasscode = await getPasscode()
|
||||||
return (
|
return (
|
||||||
<Auth
|
<Auth
|
||||||
page="signup"
|
page="signup"
|
||||||
requiresServerPassword={requiresPasscode}
|
requiresServerPassword={requiresPasscode}
|
||||||
isGithubEnabled={isGithubEnabled()}
|
credentialAuth={isCredentialEnabled()}
|
||||||
|
authProviders={getAuthProviders()}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -45,14 +45,15 @@ export function HeaderButtons({
|
||||||
isAuthenticated: boolean
|
isAuthenticated: boolean
|
||||||
theme: string
|
theme: string
|
||||||
}) {
|
}) {
|
||||||
const { isAdmin } = useSessionSWR()
|
const { isAdmin, userId } = useSessionSWR()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{getButtons({
|
{getButtons({
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
theme,
|
theme,
|
||||||
isAdmin
|
isAdmin,
|
||||||
|
userId
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
@ -108,12 +109,14 @@ export function getButtons({
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
theme,
|
theme,
|
||||||
// mutate: mutateSession,
|
// mutate: mutateSession,
|
||||||
isAdmin
|
isAdmin,
|
||||||
|
userId
|
||||||
}: {
|
}: {
|
||||||
isAuthenticated: boolean
|
isAuthenticated: boolean
|
||||||
theme: string
|
theme: string
|
||||||
// mutate: KeyedMutator<Session>
|
// mutate: KeyedMutator<Session>
|
||||||
isAdmin?: boolean
|
isAdmin?: boolean,
|
||||||
|
userId?: string
|
||||||
}) {
|
}) {
|
||||||
return [
|
return [
|
||||||
<NavButton
|
<NavButton
|
||||||
|
@ -151,7 +154,11 @@ export function getButtons({
|
||||||
icon={<UserX />}
|
icon={<UserX />}
|
||||||
value="signout"
|
value="signout"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
signOut()
|
signOut({
|
||||||
|
callbackUrl: `/signedout${
|
||||||
|
userId ? "?userId=" + userId : ""
|
||||||
|
}`
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
width={SIGN_IN_WIDTH}
|
width={SIGN_IN_WIDTH}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -7,6 +7,10 @@ type Config = {
|
||||||
url: string
|
url: string
|
||||||
github_client_id: string
|
github_client_id: string
|
||||||
github_client_secret: string
|
github_client_secret: string
|
||||||
|
keycloak_client_id: string
|
||||||
|
keycloak_client_secret: string
|
||||||
|
keycloak_issuer: string
|
||||||
|
keycloak_name: string
|
||||||
nextauth_secret: string
|
nextauth_secret: string
|
||||||
credential_auth: boolean
|
credential_auth: boolean
|
||||||
}
|
}
|
||||||
|
@ -83,8 +87,12 @@ export const config = (env: Environment): Config => {
|
||||||
`https://${throwIfUndefined("VERCEL_URL")}`,
|
`https://${throwIfUndefined("VERCEL_URL")}`,
|
||||||
github_client_id: env.GITHUB_CLIENT_ID ?? "",
|
github_client_id: env.GITHUB_CLIENT_ID ?? "",
|
||||||
github_client_secret: env.GITHUB_CLIENT_SECRET ?? "",
|
github_client_secret: env.GITHUB_CLIENT_SECRET ?? "",
|
||||||
|
keycloak_client_id: env.KEYCLOAK_ID ?? "",
|
||||||
|
keycloak_client_secret: env.KEYCLOAK_SECRET ?? "",
|
||||||
|
keycloak_issuer: env.KEYCLOAK_ISSUER ?? "",
|
||||||
|
keycloak_name: env.KEYCLOAK_NAME ?? "",
|
||||||
nextauth_secret: throwIfUndefined("NEXTAUTH_SECRET"),
|
nextauth_secret: throwIfUndefined("NEXTAUTH_SECRET"),
|
||||||
credential_auth: stringToBoolean("CREDENTIAL_AUTH") ?? true
|
credential_auth: stringToBoolean(env.CREDENTIAL_AUTH) ?? true
|
||||||
}
|
}
|
||||||
return config
|
return config
|
||||||
}
|
}
|
||||||
|
|
38
src/lib/server/auth-props.ts
Normal file
38
src/lib/server/auth-props.ts
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
import config from "@lib/config"
|
||||||
|
|
||||||
|
export type AuthProviders = {
|
||||||
|
enabled: boolean
|
||||||
|
id: "keycloak" | "github"
|
||||||
|
public_name: string
|
||||||
|
}[]
|
||||||
|
|
||||||
|
export function isGithubEnabled(): boolean {
|
||||||
|
return !!(config.github_client_id && config.github_client_secret)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isKeycloakEnabled(): boolean {
|
||||||
|
return !!(
|
||||||
|
config.keycloak_client_id &&
|
||||||
|
config.keycloak_client_secret &&
|
||||||
|
config.keycloak_issuer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCredentialEnabled(): boolean {
|
||||||
|
return config.credential_auth
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAuthProviders(): AuthProviders {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
enabled: isGithubEnabled(),
|
||||||
|
id: "github",
|
||||||
|
public_name: "Github"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: isKeycloakEnabled(),
|
||||||
|
id: "keycloak",
|
||||||
|
public_name: config.keycloak_name || "Keycloak"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
|
@ -1,10 +1,16 @@
|
||||||
import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
import { PrismaAdapter } from "@next-auth/prisma-adapter"
|
||||||
import { NextAuthOptions } from "next-auth"
|
import { NextAuthOptions, User } from "next-auth"
|
||||||
import GitHubProvider from "next-auth/providers/github"
|
import GitHubProvider from "next-auth/providers/github"
|
||||||
|
import KeycloakProvider from "next-auth/providers/keycloak"
|
||||||
import CredentialsProvider from "next-auth/providers/credentials"
|
import CredentialsProvider from "next-auth/providers/credentials"
|
||||||
import { prisma } from "@lib/server/prisma"
|
import { prisma } from "@lib/server/prisma"
|
||||||
import config from "@lib/config"
|
import config from "@lib/config"
|
||||||
import * as crypto from "crypto"
|
import * as crypto from "crypto"
|
||||||
|
import {
|
||||||
|
isCredentialEnabled,
|
||||||
|
isGithubEnabled,
|
||||||
|
isKeycloakEnabled
|
||||||
|
} from "./auth-props"
|
||||||
|
|
||||||
const credentialsOptions = () => {
|
const credentialsOptions = () => {
|
||||||
const options: Record<string, unknown> = {
|
const options: Record<string, unknown> = {
|
||||||
|
@ -33,7 +39,7 @@ const credentialsOptions = () => {
|
||||||
const providers = () => {
|
const providers = () => {
|
||||||
const providers = []
|
const providers = []
|
||||||
|
|
||||||
if (config.github_client_id && config.github_client_secret) {
|
if (isGithubEnabled()) {
|
||||||
providers.push(
|
providers.push(
|
||||||
GitHubProvider({
|
GitHubProvider({
|
||||||
clientId: config.github_client_id,
|
clientId: config.github_client_id,
|
||||||
|
@ -42,7 +48,41 @@ const providers = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config.credential_auth) {
|
if (isKeycloakEnabled()) {
|
||||||
|
const keycloak = KeycloakProvider({
|
||||||
|
clientId: config.keycloak_client_id,
|
||||||
|
clientSecret: config.keycloak_client_secret,
|
||||||
|
issuer: config.keycloak_issuer,
|
||||||
|
token: {
|
||||||
|
async request(ctx) {
|
||||||
|
const { client, provider, params, checks } = ctx
|
||||||
|
const tokens = await client.callback(
|
||||||
|
provider.callbackUrl,
|
||||||
|
params,
|
||||||
|
checks
|
||||||
|
)
|
||||||
|
if ("refresh_expires_in" in tokens) {
|
||||||
|
tokens.refresh_token_expires_in = tokens.refresh_expires_in
|
||||||
|
delete tokens.refresh_expires_in
|
||||||
|
}
|
||||||
|
delete tokens["not-before-policy"]
|
||||||
|
return { tokens }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
const originalKeycloakProfile = keycloak.profile
|
||||||
|
;(keycloak.profile = async (profile, tokens) => {
|
||||||
|
const originalProfile = await originalKeycloakProfile(profile, tokens)
|
||||||
|
const newProfile: User & { displayName?: string | null } = {
|
||||||
|
...originalProfile,
|
||||||
|
displayName: originalProfile.name ?? null
|
||||||
|
}
|
||||||
|
return newProfile
|
||||||
|
}),
|
||||||
|
providers.push(keycloak)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isCredentialEnabled()) {
|
||||||
providers.push(
|
providers.push(
|
||||||
CredentialsProvider({
|
CredentialsProvider({
|
||||||
name: "Drift",
|
name: "Drift",
|
||||||
|
@ -207,6 +247,58 @@ export const authOptions: NextAuthOptions = {
|
||||||
username: dbUser.username,
|
username: dbUser.username,
|
||||||
sessionToken: token.sessionToken
|
sessionToken: token.sessionToken
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
async redirect({ url, baseUrl }) {
|
||||||
|
if (url.startsWith(baseUrl)) return url
|
||||||
|
if (url.startsWith("/signedout")) {
|
||||||
|
const userIdFound = url.match(/userId=([^?&]+)/)
|
||||||
|
let userId = null
|
||||||
|
if (userIdFound?.length === 2) {
|
||||||
|
userId = userIdFound[1]
|
||||||
|
}
|
||||||
|
if (!userId) {
|
||||||
|
return baseUrl
|
||||||
|
}
|
||||||
|
const account = await prisma.account.findFirst({
|
||||||
|
where: {
|
||||||
|
AND: [
|
||||||
|
{
|
||||||
|
userId
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let ssoLogoutUrl = null
|
||||||
|
let idToken = null
|
||||||
|
let clientId = null
|
||||||
|
|
||||||
|
// OpenID Connect Logout
|
||||||
|
if (account?.provider === "keycloak") {
|
||||||
|
ssoLogoutUrl = `${config.keycloak_issuer}/protocol/openid-connect/logout`
|
||||||
|
idToken = account.id_token
|
||||||
|
clientId = config.keycloak_client_id
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ssoLogoutUrl) {
|
||||||
|
return baseUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
let signoutWithRedirectUrl = `${ssoLogoutUrl}?post_logout_redirect_uri=${encodeURIComponent(
|
||||||
|
baseUrl
|
||||||
|
)}`
|
||||||
|
|
||||||
|
if (idToken) {
|
||||||
|
signoutWithRedirectUrl += `&id_token_hint=${idToken}`
|
||||||
|
} else if (clientId) {
|
||||||
|
signoutWithRedirectUrl += `&client_id=${clientId}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return signoutWithRedirectUrl
|
||||||
|
}
|
||||||
|
// Allows relative callback URLs
|
||||||
|
if (url.startsWith("/")) return new URL(url, baseUrl).toString()
|
||||||
|
return baseUrl
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} as const
|
} as const
|
||||||
|
|
|
@ -24,6 +24,7 @@ export function useSessionSWR(swrOpts: SWRConfiguration = {}) {
|
||||||
isAuthenticated: session?.user?.id ? true : isLoading ? undefined : false,
|
isAuthenticated: session?.user?.id ? true : isLoading ? undefined : false,
|
||||||
/** undefined while loading */
|
/** undefined while loading */
|
||||||
isAdmin:
|
isAdmin:
|
||||||
session?.user?.id === "admin" ? true : isLoading ? undefined : false
|
session?.user?.id === "admin" ? true : isLoading ? undefined : false,
|
||||||
|
userId: session?.user?.id
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue