Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,25 @@ NUXT_PUBLIC_SITE_URL=http://localhost:3000
# POSTHOG_PUBLIC_KEY=phc_...
# EU data center (default). Use https://us.i.posthog.com for US.
# POSTHOG_HOST=https://eu.i.posthog.com

# ─── Optional: OIDC SSO (Keycloak, Authentik, Authelia, Okta, etc.) ──────────
# Enable Single Sign-On via any OIDC-compliant identity provider.
# All three variables (CLIENT_ID, CLIENT_SECRET, DISCOVERY_URL) must be set to activate SSO.
# When configured, a "Sign in with SSO" button appears on the login page.

# OIDC client ID — from your identity provider's client/application settings
# OIDC_CLIENT_ID=reqcore

# OIDC client secret — from your identity provider's credentials tab
# OIDC_CLIENT_SECRET=your-client-secret-here

# OIDC discovery URL — the .well-known/openid-configuration endpoint
# Keycloak: https://keycloak.example.com/realms/YOUR_REALM/.well-known/openid-configuration
# Authentik: https://authentik.example.com/application/o/YOUR_APP/.well-known/openid-configuration
# Authelia: https://authelia.example.com/.well-known/openid-configuration
# Okta: https://YOUR_ORG.okta.com/.well-known/openid-configuration
# Azure AD: https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0/.well-known/openid-configuration
# OIDC_DISCOVERY_URL=https://keycloak.example.com/realms/master/.well-known/openid-configuration

# Display name for the SSO button (default: "SSO")
# OIDC_PROVIDER_NAME=Company SSO
62 changes: 62 additions & 0 deletions SELF-HOSTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -450,6 +450,68 @@ sudo dpkg-reconfigure -plow unattended-upgrades

---

## OIDC Single Sign-On (SSO)

Reqcore supports Single Sign-On via any OIDC-compliant identity provider — Keycloak, Authentik, Authelia, Okta, Azure AD, and more. When configured, a "Sign in with SSO" button appears on the login and registration pages.

### Why SSO?

- **Centralized identity** — users sign in once across all internal tools
- **Zero-friction onboarding** — new hires get instant access, leavers are cut off centrally
- **Enterprise security** — MFA, session policies, and brute-force protection managed in one place

### Setup

**1. Create an OIDC client in your identity provider:**

| Setting | Value |
|---|---|
| Client type | OpenID Connect (confidential) |
| Client ID | Any name (e.g., `reqcore`) |
| Client authentication | ON (confidential/secret) |
| Valid redirect URI | `https://your-reqcore-domain.com/api/auth/oauth2/callback/oidc` |
| Valid post-logout redirect URI | `https://your-reqcore-domain.com/*` |
| Scopes | `openid`, `email`, `profile` |

**2. Set environment variables:**

```bash
# All three are required to activate SSO
OIDC_CLIENT_ID=reqcore
OIDC_CLIENT_SECRET=your-client-secret-from-provider
OIDC_DISCOVERY_URL=https://keycloak.example.com/realms/master/.well-known/openid-configuration

# Optional: customize the button label (default: "SSO")
OIDC_PROVIDER_NAME=Company SSO
```

**3. Restart Reqcore:**

```bash
docker compose down && docker compose up -d
```

The SSO button appears automatically on the sign-in and sign-up pages.

### Provider-Specific Discovery URLs

| Provider | Discovery URL format |
|---|---|
| Keycloak | `https://keycloak.example.com/realms/YOUR_REALM/.well-known/openid-configuration` |
| Authentik | `https://authentik.example.com/application/o/YOUR_APP/.well-known/openid-configuration` |
| Authelia | `https://authelia.example.com/.well-known/openid-configuration` |
| Okta | `https://YOUR_ORG.okta.com/.well-known/openid-configuration` |
| Azure AD | `https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0/.well-known/openid-configuration` |

### Security

- **PKCE** (Proof Key for Code Exchange) is enabled by default for protection against authorization code interception
- **Issuer validation** (RFC 9207) is enforced to prevent OAuth mix-up attacks
- **OIDC discovery** automatically fetches and validates all provider endpoints
- SSO is **completely opt-in** — it has zero impact when the environment variables are not set

---

## Monitoring & Health Checks

### Built-in System Health Dashboard
Expand Down
8 changes: 7 additions & 1 deletion app/components/SettingsMobileNav.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import {
Building2, Users, UserCircle, ChevronLeft, Plug, Brain,
Building2, Users, UserCircle, ChevronLeft, Plug, Brain, ShieldCheck,
} from 'lucide-vue-next'

const route = useRoute()
Expand Down Expand Up @@ -31,6 +31,12 @@ const settingsNav = [
icon: Brain,
exact: true,
},
{
label: 'SSO',
to: '/dashboard/settings/sso',
icon: ShieldCheck,
exact: true,
},
{
label: 'Account',
to: '/dashboard/settings/account',
Expand Down
22 changes: 19 additions & 3 deletions app/components/SettingsSidebar.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import {
Building2, Users, UserCircle, ChevronLeft, Settings, Plug, Brain,
Building2, Users, UserCircle, ChevronLeft, Settings, Plug, Brain, ShieldCheck,
} from 'lucide-vue-next'

const route = useRoute()
Expand Down Expand Up @@ -35,6 +35,14 @@ const settingsNav = [
icon: Brain,
exact: true,
},
{
label: 'Single Sign-On',
description: 'Enterprise SSO',
to: '/dashboard/settings/sso',
icon: ShieldCheck,
exact: true,
badge: 'Beta',
},
{
label: 'Account',
description: 'Profile & security',
Expand Down Expand Up @@ -94,8 +102,16 @@ function isActive(to: string, exact: boolean) {
>
<component :is="item.icon" class="size-4" />
</div>
<div class="min-w-0">
<div class="truncate leading-tight">{{ item.label }}</div>
<div class="min-w-0 flex-1">
<div class="flex items-center gap-1.5 leading-tight">
<span class="truncate">{{ item.label }}</span>
<span
v-if="item.badge"
class="shrink-0 inline-flex items-center rounded-full bg-amber-50 dark:bg-amber-950/40 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-800"
>
{{ item.badge }}
</span>
</div>
<div
class="text-[11px] leading-tight mt-0.5 truncate"
:class="isActive(item.to, item.exact)
Expand Down
Loading
Loading