diff --git a/README.md b/README.md index 4d2497a0f..257ad8924 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,10 @@ For configuration options, examples and more, [please check out the homepage doc ## Security Notice 🔒 -Please note that when using features such as widgets, Homepage can access personal information (for example from your home automation system) and Homepage currently does not (and is not planned to) include any authentication layer itself. If Homepage is reachable from any untrusted network, it **must** sit behind a reverse proxy (and/or VPN) that enforces authentication, TLS, and strictly validates Host headers. The built-in host check in Homepage is a best-effort guard and should not be treated as security when exposed publicly. +Please note that when using features such as widgets, Homepage can access personal information (for example from your home automation system). To keep your information private, if Homepage is reachable from any untrusted network, it: + +1. **must** sit behind a reverse proxy (and/or VPN) that enforces authentication, TLS, and strictly validates Host headers. OR +2. An optional built-in OIDC login flow is available (opt-in) to gate the app without a reverse proxy; it is a simple “authenticated or not” guard (no per-user roles or personalization). ## With Docker diff --git a/docs/installation/index.md b/docs/installation/index.md index b6bfb23b5..02245faeb 100644 --- a/docs/installation/index.md +++ b/docs/installation/index.md @@ -37,4 +37,18 @@ The value is a comma-separated (no spaces) list of allowed hosts (sometimes with If you are seeing errors about host validation, check the homepage logs and ensure that the host exactly as output in the logs is in the `HOMEPAGE_ALLOWED_HOSTS` list. -This can be disabled by setting `HOMEPAGE_ALLOWED_HOSTS` to `*` but this is not recommended. Public deployments must rely on a reverse proxy (and/or VPN) that enforces authentication, TLS, and unexpected Host headers; the built-in host check is a best-effort guard for local setups and is not a substitute for edge protections. +This can be disabled by setting `HOMEPAGE_ALLOWED_HOSTS` to `*` but this is not recommended. Public deployments must rely on a reverse proxy (and/or VPN) that enforces authentication, TLS, and blocks direct-IP access and unexpected Host headers; the built-in host check is a best-effort guard for local setups and is not a substitute for edge protections. + +### Built-in OIDC authentication (optional, opt-in) + +Homepage now supports a minimal OIDC login flow (no per-user roles or personalization) so you can deploy without a reverse proxy handling auth. Enable it with: + +- `HOMEPAGE_AUTH_ENABLED=true` +- `HOMEPAGE_OIDC_ISSUER` (OIDC issuer URL, e.g., `https://auth.example.com/realms/homepage`) +- `HOMEPAGE_OIDC_CLIENT_ID` +- `HOMEPAGE_OIDC_CLIENT_SECRET` +- `HOMEPAGE_AUTH_SECRET` (random string for signing/encrypting cookies) +- `HOMEPAGE_EXTERNAL_URL` (external URL to your Homepage instance; used for callbacks) +- Optional: `HOMEPAGE_OIDC_NAME` (display name), `HOMEPAGE_OIDC_SCOPE` (defaults to `openid email profile`) + +All app pages and `/api` routes will require a signed-in session. Static assets remain public. Homepage still does not implement per-user dashboards or roles; authentication is a simple gate only. diff --git a/package.json b/package.json index 9e768b0f3..738fe66eb 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "memory-cache": "^0.2.0", "minecraftstatuspinger": "^1.2.2", "next": "^15.5.11", + "next-auth": "^4.24.10", "next-i18next": "^12.1.0", "ping": "^0.4.4", "pretty-bytes": "^7.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 84e4dabaf..60fb75d24 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: next: specifier: ^15.5.11 version: 15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + next-auth: + specifier: ^4.24.10 + version: 4.24.13(next@15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-i18next: specifier: ^12.1.0 version: 12.1.0(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -803,6 +806,9 @@ packages: resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} engines: {node: '>=12.4.0'} + '@panva/hkdf@1.2.1': + resolution: {integrity: sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==} + '@pkgjs/parseargs@0.11.0': resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} engines: {node: '>=14'} @@ -1683,6 +1689,10 @@ packages: concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + cookie@0.7.2: + resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==} + engines: {node: '>= 0.6'} + core-js@3.40.0: resolution: {integrity: sha512-7vsMc/Lty6AGnn7uFpYT56QesI5D2Y/UkgKounk87OP9Z2H9Z8kj6jzcSGAxFmUtDOS0ntK6lbQz+Nsa0Jj6mQ==} @@ -2551,6 +2561,9 @@ packages: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true + jose@4.15.9: + resolution: {integrity: sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==} + jose@5.10.0: resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} @@ -2723,6 +2736,10 @@ packages: lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + luxon@3.6.1: resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==} engines: {node: '>=12'} @@ -2831,6 +2848,20 @@ packages: net@1.0.2: resolution: {integrity: sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==} + next-auth@4.24.13: + resolution: {integrity: sha512-sgObCfcfL7BzIK76SS5TnQtc3yo2Oifp/yIpfv6fMfeBOiBJkDWF3A2y9+yqnmJ4JKc2C+nMjSjmgDeTwgN1rQ==} + peerDependencies: + '@auth/core': 0.34.3 + next: ^12.2.5 || ^13 || ^14 || ^15 || ^16 + nodemailer: ^7.0.7 + react: ^17.0.2 || ^18 || ^19 + react-dom: ^17.0.2 || ^18 || ^19 + peerDependenciesMeta: + '@auth/core': + optional: true + nodemailer: + optional: true + next-i18next@12.1.0: resolution: {integrity: sha512-rhos/PVULmZPdC0jpec2MDBQMXdGZ3+Mbh/tZfrDtjgnVN3ucdq7k8BlwsJNww6FnqC8AC31n6dSYuqVzYsGsw==} engines: {node: '>=12'} @@ -2878,10 +2909,17 @@ packages: oauth4webapi@3.3.0: resolution: {integrity: sha512-ZlozhPlFfobzh3hB72gnBFLjXpugl/dljz1fJSRdqaV2r3D5dmi5lg2QWI0LmUYuazmE+b5exsloEv6toUtw9g==} + oauth@0.9.15: + resolution: {integrity: sha512-a5ERWK1kh38ExDEfoO6qUHJb32rd7aYmPHuyCu3Fta/cnICvYmgd2uhuKXvPD+PXB+gCEYYEaQdIRAjCOwAKNA==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} + object-hash@2.2.0: + resolution: {integrity: sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw==} + engines: {node: '>= 6'} + object-inspect@1.13.4: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} @@ -2910,12 +2948,19 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + oidc-token-hash@5.2.0: + resolution: {integrity: sha512-6gj2m8cJZ+iSW8bm0FXdGF0YhIQbKrfP4yWTNzxc31U6MOjfEmB1rHvlYvxI1B7t7BCi1F2vYTT6YhtQRG4hxw==} + engines: {node: ^10.13.0 || >=12.0.0} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} one-time@1.0.0: resolution: {integrity: sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==} + openid-client@5.7.1: + resolution: {integrity: sha512-jDBPgSVfTnkIh71Hg9pRvtJc6wTwqjRkN88+gCFtYWrlP4Yx2Dsrow8uPi3qLr/aeymPF3o2+dS+wOpglK04ew==} + openid-client@6.3.0: resolution: {integrity: sha512-68LMqb/Whtq214B9c9kCtuniCKQrEqWJRTEoOEZlv2QV5VgqhjySCpBe4RXeU+pj/VNOi7erP/ixxfHtqR7FOw==} @@ -3007,6 +3052,14 @@ packages: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} + preact-render-to-string@5.2.6: + resolution: {integrity: sha512-JyhErpYOvBV1hEPwIxc/fHWXPfnEGdRKxc8gFdAZ7XV4tlzyzG847XAyEZqoDnynP88akM4eaHcSOzNcLWFguw==} + peerDependencies: + preact: '>=10' + + preact@10.28.2: + resolution: {integrity: sha512-lbteaWGzGHdlIuiJ0l2Jq454m6kcpI1zNje6d8MlGAFlYvP2GO4ibnat7P74Esfz4sPTdM6UxtTwh/d3pwM9JA==} + prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} @@ -3670,6 +3723,10 @@ packages: resolution: {integrity: sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==} hasBin: true + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + varint@6.0.0: resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} @@ -3868,6 +3925,9 @@ packages: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} @@ -4389,6 +4449,8 @@ snapshots: '@nolyfill/is-core-module@1.0.39': {} + '@panva/hkdf@1.2.1': {} + '@pkgjs/parseargs@0.11.0': optional: true @@ -5255,6 +5317,8 @@ snapshots: concat-map@0.0.1: {} + cookie@0.7.2: {} + core-js@3.40.0: {} core-util-is@1.0.3: {} @@ -6347,6 +6411,8 @@ snapshots: jiti@2.6.1: {} + jose@4.15.9: {} + jose@5.10.0: {} js-tokens@10.0.0: {} @@ -6508,6 +6574,10 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + luxon@3.6.1: {} lz-string@1.5.0: {} @@ -6587,6 +6657,21 @@ snapshots: net@1.0.2: {} + next-auth@4.24.13(next@15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.28.4 + '@panva/hkdf': 1.2.1 + cookie: 0.7.2 + jose: 4.15.9 + next: 15.5.9(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + oauth: 0.9.15 + openid-client: 5.7.1 + preact: 10.28.2 + preact-render-to-string: 5.2.6(preact@10.28.2) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + uuid: 8.3.2 + next-i18next@12.1.0(next@15.5.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@babel/runtime': 7.26.9 @@ -6635,8 +6720,12 @@ snapshots: oauth4webapi@3.3.0: {} + oauth@0.9.15: {} + object-assign@4.1.1: {} + object-hash@2.2.0: {} + object-inspect@1.13.4: {} object-keys@1.1.1: {} @@ -6676,6 +6765,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + oidc-token-hash@5.2.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -6684,6 +6775,13 @@ snapshots: dependencies: fn.name: 1.1.0 + openid-client@5.7.1: + dependencies: + jose: 4.15.9 + lru-cache: 6.0.0 + object-hash: 2.2.0 + oidc-token-hash: 5.2.0 + openid-client@6.3.0: dependencies: jose: 5.10.0 @@ -6766,6 +6864,13 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + preact-render-to-string@5.2.6(preact@10.28.2): + dependencies: + preact: 10.28.2 + pretty-format: 3.8.0 + + preact@10.28.2: {} + prelude-ls@1.2.1: {} prettier-linter-helpers@1.0.0: @@ -6781,6 +6886,8 @@ snapshots: pretty-bytes@7.1.0: {} + pretty-format@3.8.0: {} + pretty-format@27.5.1: dependencies: ansi-regex: 5.0.1 @@ -7544,6 +7651,8 @@ snapshots: uuid@10.0.0: {} + uuid@8.3.2: {} + varint@6.0.0: {} victory-vendor@37.3.6: @@ -7780,6 +7889,8 @@ snapshots: y18n@5.0.8: {} + yallist@4.0.0: {} + yallist@5.0.0: {} yargs-parser@21.1.1: {} diff --git a/src/middleware.js b/src/middleware.js index 39452f6cc..704af9c25 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -1,7 +1,11 @@ +import { getToken } from "next-auth/jwt"; import { NextResponse } from "next/server"; -export function middleware(req) { - // Check the Host header, if HOMEPAGE_ALLOWED_HOSTS is set +const authEnabled = process.env.HOMEPAGE_AUTH_ENABLED === "true"; +const authSecret = process.env.NEXTAUTH_SECRET || process.env.HOMEPAGE_AUTH_SECRET; + +export async function middleware(req) { + // Host validation (status quo) const host = req.headers.get("host"); const port = process.env.PORT || 3000; let allowedHosts = [`localhost:${port}`, `127.0.0.1:${port}`, `[::1]:${port}`]; @@ -15,9 +19,23 @@ export function middleware(req) { ); return NextResponse.json({ error: "Host validation failed. See logs for more details." }, { status: 400 }); } + + if (authEnabled) { + const token = await getToken({ req, secret: authSecret }); + if (!token) { + const signInUrl = new URL("/auth/signin", req.url); + signInUrl.searchParams.set("callbackUrl", req.url); + return NextResponse.redirect(signInUrl); + } + } + return NextResponse.next(); } export const config = { - matcher: "/api/:path*", + // Protect all app and API routes; allow Next.js internals, public assets, auth pages, and NextAuth endpoints. + matcher: [ + "/", + "/((?!_next/static|_next/image|favicon.ico|robots.txt|manifest.json|sitemap.xml|icons/|api/auth|auth/).*)", + ], }; diff --git a/src/pages/_app.jsx b/src/pages/_app.jsx index 8e88f6b28..405b113a6 100644 --- a/src/pages/_app.jsx +++ b/src/pages/_app.jsx @@ -1,4 +1,5 @@ /* eslint-disable react/jsx-props-no-spreading */ +import { SessionProvider } from "next-auth/react"; import { appWithTranslation } from "next-i18next"; import Head from "next/head"; import "styles/globals.css"; @@ -69,28 +70,30 @@ const tailwindSafelist = [ function MyApp({ Component, pageProps }) { return ( - fetch(resource, init).then((res) => res.json()), - }} - > - - {/* https://nextjs.org/docs/messages/no-document-viewport-meta */} - - - - - - - - - - - - + + fetch(resource, init).then((res) => res.json()), + }} + > + + {/* https://nextjs.org/docs/messages/no-document-viewport-meta */} + + + + + + + + + + + + + ); } diff --git a/src/pages/api/auth/[...nextauth].js b/src/pages/api/auth/[...nextauth].js new file mode 100644 index 000000000..bfb6be510 --- /dev/null +++ b/src/pages/api/auth/[...nextauth].js @@ -0,0 +1,79 @@ +import NextAuth from "next-auth"; + +const authEnabled = process.env.HOMEPAGE_AUTH_ENABLED === "true"; +const issuer = process.env.HOMEPAGE_OIDC_ISSUER; +const clientId = process.env.HOMEPAGE_OIDC_CLIENT_ID; +const clientSecret = process.env.HOMEPAGE_OIDC_CLIENT_SECRET; +const homepageAuthSecret = process.env.HOMEPAGE_AUTH_SECRET; +const homepageExternalUrl = process.env.HOMEPAGE_EXTERNAL_URL; + +// Map HOMEPAGE_* envs to what NextAuth expects so users don’t need NEXTAUTH_* explicitly. +if (!process.env.NEXTAUTH_SECRET && homepageAuthSecret) { + process.env.NEXTAUTH_SECRET = homepageAuthSecret; +} +if (!process.env.NEXTAUTH_URL && homepageExternalUrl) { + process.env.NEXTAUTH_URL = homepageExternalUrl; +} + +const defaultScope = process.env.HOMEPAGE_OIDC_SCOPE || "openid email profile"; +const cleanedIssuer = issuer ? issuer.replace(/\/+$/, "") : issuer; + +if ( + authEnabled && + (!issuer || !clientId || !clientSecret || !process.env.NEXTAUTH_SECRET || !process.env.NEXTAUTH_URL) +) { + throw new Error( + "OIDC auth is enabled but required settings are missing. Please set HOMEPAGE_OIDC_ISSUER, HOMEPAGE_OIDC_CLIENT_ID, HOMEPAGE_OIDC_CLIENT_SECRET, HOMEPAGE_AUTH_SECRET, and HOMEPAGE_EXTERNAL_URL.", + ); +} + +let providers = []; +if (authEnabled) { + providers = [ + { + id: "homepage-oidc", + name: process.env.HOMEPAGE_OIDC_NAME || "Homepage OIDC", + type: "oauth", + idToken: true, + issuer: cleanedIssuer, + wellKnown: `${cleanedIssuer}/.well-known/openid-configuration`, + clientId, + clientSecret, + authorization: { + params: { + scope: defaultScope, + }, + }, + profile(profile) { + return { + id: profile.sub ?? profile.id ?? profile.user_id ?? profile.uid ?? profile.email, + name: profile.name ?? profile.preferred_username ?? profile.nickname ?? profile.email, + email: profile.email ?? null, + image: profile.picture ?? null, + }; + }, + }, + ]; +} + +export default NextAuth({ + providers, + session: { + strategy: "jwt", + }, + secret: process.env.NEXTAUTH_SECRET, + pages: { + signIn: "/auth/signin", + }, + debug: true, + logger: { + error: (...args) => console.error("[nextauth][error]", ...args), + warn: (...args) => console.warn("[nextauth][warn]", ...args), + debug: (...args) => console.debug("[nextauth][debug]", ...args), + }, + events: { + signIn: async (message) => console.debug("[nextauth][event][signIn]", message), + signOut: async (message) => console.debug("[nextauth][event][signOut]", message), + error: async (message) => console.error("[nextauth][event][error]", message), + }, +}); diff --git a/src/pages/auth/signin.jsx b/src/pages/auth/signin.jsx new file mode 100644 index 000000000..57331c1d6 --- /dev/null +++ b/src/pages/auth/signin.jsx @@ -0,0 +1,43 @@ +import { getProviders, signIn } from "next-auth/react"; + +export default function SignIn({ providers, callbackUrl }) { + if (!providers || Object.keys(providers).length === 0) { + return ( +
+
+

Authentication not configured

+

OIDC is disabled or missing required environment variables.

+
+
+ ); + } + + return ( +
+
+

Sign in

+

Continue with your identity provider.

+
+ {Object.values(providers).map((provider) => ( + + ))} +
+
+
+ ); +} + +export async function getServerSideProps(context) { + const providers = await getProviders(); + const callbackUrl = context?.query?.callbackUrl ?? "/"; + return { + props: { providers, callbackUrl }, + }; +}