Merge pull request #492 from pat-s/agent/sso-only-auth

feat(auth): add SSO-only login mode
This commit is contained in:
Abhinav Raut
2026-09-09 01:32:18 +05:30
committed by GitHub
6 changed files with 86 additions and 2 deletions
+20
View File
@@ -4,9 +4,12 @@ import (
"encoding/json"
"github.com/abhinavxd/libredesk/internal/envelope"
oidcmodels "github.com/abhinavxd/libredesk/internal/oidc/models"
"github.com/zerodha/fastglue"
)
const localLoginEnabledKey = "auth.local_login_enabled"
// handleGetConfig returns the public configuration needed for app initialization, this includes minimal app settings and enabled SSO providers (without secrets).
func handleGetConfig(r *fastglue.Request) error {
var app = r.Context.(*App)
@@ -58,6 +61,23 @@ func handleGetConfig(r *fastglue.Request) error {
// Add SSO providers to the response
publicSettings["app.sso_providers"] = enabledProviders
publicSettings["auth.local_login_enabled"] = isLocalLoginEnabled(oidcProviders)
return r.SendEnvelope(publicSettings)
}
func localLoginDisabledInConfig() bool {
return ko.Exists(localLoginEnabledKey) && !ko.Bool(localLoginEnabledKey)
}
func isLocalLoginEnabled(providers []oidcmodels.OIDC) bool {
if !localLoginDisabledInConfig() {
return true
}
for _, provider := range providers {
if provider.Enabled {
return false
}
}
return true
}
+41
View File
@@ -0,0 +1,41 @@
package main
import (
"testing"
oidcmodels "github.com/abhinavxd/libredesk/internal/oidc/models"
)
func TestIsLocalLoginEnabled(t *testing.T) {
tests := []struct {
name string
configured *bool
providers []oidcmodels.OIDC
want bool
}{
{name: "defaults to enabled", want: true},
{name: "remains enabled when configured", configured: boolPointer(true), want: true},
{name: "remains enabled without an enabled OIDC provider", configured: boolPointer(false), providers: []oidcmodels.OIDC{{Enabled: false}}, want: true},
{name: "is disabled with an enabled OIDC provider", configured: boolPointer(false), providers: []oidcmodels.OIDC{{Enabled: true}}, want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ko.Delete(localLoginEnabledKey)
if tt.configured != nil {
if err := ko.Set(localLoginEnabledKey, *tt.configured); err != nil {
t.Fatalf("set config: %v", err)
}
}
t.Cleanup(func() { ko.Delete(localLoginEnabledKey) })
if got := isLocalLoginEnabled(tt.providers); got != tt.want {
t.Fatalf("isLocalLoginEnabled() = %v, want %v", got, tt.want)
}
})
}
}
func boolPointer(value bool) *bool {
return &value
}
+11
View File
@@ -21,6 +21,17 @@ func handleLogin(r *fastglue.Request) error {
loginReq loginRequest
)
if localLoginDisabledInConfig() {
oidcProviders, err := app.oidc.GetAll()
if err != nil {
return sendErrorEnvelope(r, err)
}
if !isLocalLoginEnabled(oidcProviders) {
return r.SendErrorEnvelope(fasthttp.StatusForbidden, app.i18n.T("auth.localLoginDisabled"), nil, envelope.PermissionError)
}
app.lo.Warn("local login is disabled in config but no OIDC provider is enabled, allowing password login")
}
// Decode JSON request.
if err := r.Decode(&loginReq, "json"); err != nil {
return r.SendErrorEnvelope(fasthttp.StatusBadRequest, app.i18n.T("errors.parsingRequest"), nil, envelope.InputError)
+7
View File
@@ -34,6 +34,13 @@ read_buffer_size = 65536
# Keepalive settings.
keepalive_timeout = "10s"
[auth]
# Allow users to sign in with their libredesk email and password.
# Disable this only after configuring and enabling at least one OIDC provider.
# libredesk automatically keeps local login available when no OIDC provider is enabled to prevent lockout.
# If an enabled provider is unavailable or misconfigured, set auth.local_login_enabled = true to restore password login.
local_login_enabled = true
# File upload provider to use, either `fs` or `s3`.
[upload]
provider = "fs"
@@ -32,7 +32,7 @@
{{ oidcProvider.name }}
</Button>
<div class="relative">
<div v-if="localLoginEnabled" class="relative">
<div class="absolute inset-0 flex items-center">
<span class="w-full border-t border-border"></span>
</div>
@@ -42,7 +42,7 @@
</div>
</div>
<form @submit.prevent="loginAction" class="space-y-3">
<form v-if="localLoginEnabled" @submit.prevent="loginAction" class="space-y-3">
<div class="space-y-2">
<Label for="email" class="text-muted-foreground">{{ t('globals.terms.email') }}</Label>
<Input
@@ -259,6 +259,10 @@ const enabledOIDCProviders = computed(() => {
return oidcProviders.value.filter((provider) => !provider.disabled)
})
const localLoginEnabled = computed(
() => appSettingsStore.public_config?.['auth.local_login_enabled'] !== false
)
const emailHasError = computed(() => {
if (!submitted.value) return false
const email = loginForm.value.email
+1
View File
@@ -608,6 +608,7 @@
"auth.invalidOrExpiredSessionClearCookie": "Invalid or expired session. Please clear your cookies and try again.",
"auth.invalidResetLink": "Invalid reset link. Please request a new password reset link.",
"auth.loggingIn": "Logging in...",
"auth.localLoginDisabled": "Password login is disabled. Sign in with your SSO provider.",
"auth.newPassword": "New password",
"auth.oidcAccessDenied": "The sign-in was cancelled or denied at the identity provider. Try signing in again.",
"auth.oidcInvalidClient": "Sign-in is unavailable due to a configuration problem. Ask your administrator to check the SSO client ID and secret.",