Skip to content

Commit 698729d

Browse files
authored
Merge pull request #136 from reqcore-inc/feat/sso
Add OIDC SSO support
2 parents 596e6c8 + 76c54b4 commit 698729d

28 files changed

Lines changed: 10667 additions & 3517 deletions

.env.example

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,25 @@ NUXT_PUBLIC_SITE_URL=http://localhost:3000
7474
# POSTHOG_PUBLIC_KEY=phc_...
7575
# EU data center (default). Use https://us.i.posthog.com for US.
7676
# POSTHOG_HOST=https://eu.i.posthog.com
77+
78+
# ─── Optional: OIDC SSO (Keycloak, Authentik, Authelia, Okta, etc.) ──────────
79+
# Enable Single Sign-On via any OIDC-compliant identity provider.
80+
# All three variables (CLIENT_ID, CLIENT_SECRET, DISCOVERY_URL) must be set to activate SSO.
81+
# When configured, a "Sign in with SSO" button appears on the login page.
82+
83+
# OIDC client ID — from your identity provider's client/application settings
84+
# OIDC_CLIENT_ID=reqcore
85+
86+
# OIDC client secret — from your identity provider's credentials tab
87+
# OIDC_CLIENT_SECRET=your-client-secret-here
88+
89+
# OIDC discovery URL — the .well-known/openid-configuration endpoint
90+
# Keycloak: https://keycloak.example.com/realms/YOUR_REALM/.well-known/openid-configuration
91+
# Authentik: https://authentik.example.com/application/o/YOUR_APP/.well-known/openid-configuration
92+
# Authelia: https://authelia.example.com/.well-known/openid-configuration
93+
# Okta: https://YOUR_ORG.okta.com/.well-known/openid-configuration
94+
# Azure AD: https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0/.well-known/openid-configuration
95+
# OIDC_DISCOVERY_URL=https://keycloak.example.com/realms/master/.well-known/openid-configuration
96+
97+
# Display name for the SSO button (default: "SSO")
98+
# OIDC_PROVIDER_NAME=Company SSO

SELF-HOSTING.md

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,6 +450,68 @@ sudo dpkg-reconfigure -plow unattended-upgrades
450450

451451
---
452452

453+
## OIDC Single Sign-On (SSO)
454+
455+
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.
456+
457+
### Why SSO?
458+
459+
- **Centralized identity** — users sign in once across all internal tools
460+
- **Zero-friction onboarding** — new hires get instant access, leavers are cut off centrally
461+
- **Enterprise security** — MFA, session policies, and brute-force protection managed in one place
462+
463+
### Setup
464+
465+
**1. Create an OIDC client in your identity provider:**
466+
467+
| Setting | Value |
468+
|---|---|
469+
| Client type | OpenID Connect (confidential) |
470+
| Client ID | Any name (e.g., `reqcore`) |
471+
| Client authentication | ON (confidential/secret) |
472+
| Valid redirect URI | `https://your-reqcore-domain.com/api/auth/oauth2/callback/oidc` |
473+
| Valid post-logout redirect URI | `https://your-reqcore-domain.com/*` |
474+
| Scopes | `openid`, `email`, `profile` |
475+
476+
**2. Set environment variables:**
477+
478+
```bash
479+
# All three are required to activate SSO
480+
OIDC_CLIENT_ID=reqcore
481+
OIDC_CLIENT_SECRET=your-client-secret-from-provider
482+
OIDC_DISCOVERY_URL=https://keycloak.example.com/realms/master/.well-known/openid-configuration
483+
484+
# Optional: customize the button label (default: "SSO")
485+
OIDC_PROVIDER_NAME=Company SSO
486+
```
487+
488+
**3. Restart Reqcore:**
489+
490+
```bash
491+
docker compose down && docker compose up -d
492+
```
493+
494+
The SSO button appears automatically on the sign-in and sign-up pages.
495+
496+
### Provider-Specific Discovery URLs
497+
498+
| Provider | Discovery URL format |
499+
|---|---|
500+
| Keycloak | `https://keycloak.example.com/realms/YOUR_REALM/.well-known/openid-configuration` |
501+
| Authentik | `https://authentik.example.com/application/o/YOUR_APP/.well-known/openid-configuration` |
502+
| Authelia | `https://authelia.example.com/.well-known/openid-configuration` |
503+
| Okta | `https://YOUR_ORG.okta.com/.well-known/openid-configuration` |
504+
| Azure AD | `https://login.microsoftonline.com/YOUR_TENANT_ID/v2.0/.well-known/openid-configuration` |
505+
506+
### Security
507+
508+
- **PKCE** (Proof Key for Code Exchange) is enabled by default for protection against authorization code interception
509+
- **Issuer validation** (RFC 9207) is enforced to prevent OAuth mix-up attacks
510+
- **OIDC discovery** automatically fetches and validates all provider endpoints
511+
- SSO is **completely opt-in** — it has zero impact when the environment variables are not set
512+
513+
---
514+
453515
## Monitoring & Health Checks
454516

455517
### Built-in System Health Dashboard

app/components/SettingsMobileNav.vue

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import {
3-
Building2, Users, UserCircle, ChevronLeft, Plug, Brain,
3+
Building2, Users, UserCircle, ChevronLeft, Plug, Brain, ShieldCheck,
44
} from 'lucide-vue-next'
55
66
const route = useRoute()
@@ -31,6 +31,12 @@ const settingsNav = [
3131
icon: Brain,
3232
exact: true,
3333
},
34+
{
35+
label: 'SSO',
36+
to: '/dashboard/settings/sso',
37+
icon: ShieldCheck,
38+
exact: true,
39+
},
3440
{
3541
label: 'Account',
3642
to: '/dashboard/settings/account',

app/components/SettingsSidebar.vue

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup lang="ts">
22
import {
3-
Building2, Users, UserCircle, ChevronLeft, Settings, Plug, Brain,
3+
Building2, Users, UserCircle, ChevronLeft, Settings, Plug, Brain, ShieldCheck,
44
} from 'lucide-vue-next'
55
66
const route = useRoute()
@@ -35,6 +35,14 @@ const settingsNav = [
3535
icon: Brain,
3636
exact: true,
3737
},
38+
{
39+
label: 'Single Sign-On',
40+
description: 'Enterprise SSO',
41+
to: '/dashboard/settings/sso',
42+
icon: ShieldCheck,
43+
exact: true,
44+
badge: 'Beta',
45+
},
3846
{
3947
label: 'Account',
4048
description: 'Profile & security',
@@ -94,8 +102,16 @@ function isActive(to: string, exact: boolean) {
94102
>
95103
<component :is="item.icon" class="size-4" />
96104
</div>
97-
<div class="min-w-0">
98-
<div class="truncate leading-tight">{{ item.label }}</div>
105+
<div class="min-w-0 flex-1">
106+
<div class="flex items-center gap-1.5 leading-tight">
107+
<span class="truncate">{{ item.label }}</span>
108+
<span
109+
v-if="item.badge"
110+
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"
111+
>
112+
{{ item.badge }}
113+
</span>
114+
</div>
99115
<div
100116
class="text-[11px] leading-tight mt-0.5 truncate"
101117
:class="isActive(item.to, item.exact)

0 commit comments

Comments
 (0)