Feature: Server-driven auth method discovery + OIDC-only enforcement
Backend:
- New AUTH_PASSWORD_ENABLED flag (default true). When false, email/password
registration and login return 403; the server enforces OIDC-only login.
- New OIDC_GENERIC_DISPLAY_NAME so the app can show 'Authentik'/'Keycloak'
instead of a generic 'OIDC' label.
- New public endpoint GET /api/config returns which auth methods the
server offers (password_enabled + per-provider OIDC capabilities).
No auth required, so the login screen can query it before logging in.
- .env.example and docker-compose.yml expose the new env vars.
App:
- DTOs + MitbringslApi.getServerConfig() for /api/config.
- AuthViewModel: new 'connect' flow. The user enters the server URL,
taps 'Verbinden', and the app fetches /api/config. The returned
ServerAuthConfig drives which login options are shown:
* password-only -> email/password form
* OIDC-only -> OIDC token form
* both -> toggle between the two
If the server offers no method, a clear error is shown.
- AuthScreen: split into ConnectView (server URL) and LoginView (the
login form matching the server's capabilities). The mode toggle only
appears when the server offers more than one method.
This commit is contained in:
parent
b44bc8c3af
commit
3f187f1ede
10 changed files with 577 additions and 244 deletions
|
|
@ -35,6 +35,10 @@ type Config struct {
|
|||
SessionTokenTTL time.Duration `env:"SESSION_TOKEN_TTL" envDefault:"720h"` // 30 days
|
||||
SessionCookieName string `env:"SESSION_COOKIE_NAME" envDefault:"mitbringsl_session"`
|
||||
|
||||
// AuthPasswordEnabled controls whether email/password registration and
|
||||
// login are offered. Set to false to enforce OIDC-only login.
|
||||
AuthPasswordEnabled bool `env:"AUTH_PASSWORD_ENABLED" envDefault:"true"`
|
||||
|
||||
// OIDC providers. Both optional; enable per provider.
|
||||
GoogleOIDC GoogleOIDCConfig
|
||||
GenericOIDC GenericOIDCConfig
|
||||
|
|
@ -52,9 +56,12 @@ type GoogleOIDCConfig struct {
|
|||
|
||||
// GenericOIDCConfig for any standards-compliant OIDC IdP (Keycloak, Authentik, ...).
|
||||
type GenericOIDCConfig struct {
|
||||
Enabled bool `env:"OIDC_GENERIC_ENABLED" envDefault:"false"`
|
||||
Issuer string `env:"OIDC_GENERIC_ISSUER"`
|
||||
ClientID string `env:"OIDC_GENERIC_CLIENT_ID"`
|
||||
Enabled bool `env:"OIDC_GENERIC_ENABLED" envDefault:"false"`
|
||||
Issuer string `env:"OIDC_GENERIC_ISSUER"`
|
||||
ClientID string `env:"OIDC_GENERIC_CLIENT_ID"`
|
||||
// DisplayName is shown to users in the app, e.g. "Authentik" or "Keycloak".
|
||||
// Defaults to "OIDC" when empty.
|
||||
DisplayName string `env:"OIDC_GENERIC_DISPLAY_NAME" envDefault:"OIDC"`
|
||||
}
|
||||
|
||||
// Load reads configuration from environment variables and validates basic invariants.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ type API struct {
|
|||
lists *ListHandler
|
||||
ops *OpsHandler
|
||||
suggest *SuggestHandler
|
||||
config *ConfigHandler
|
||||
}
|
||||
|
||||
// NewAPI constructs the API with all handler groups.
|
||||
|
|
@ -49,6 +50,7 @@ func NewAPI(cfg *config.Config, pool *pgxpool.Pool) *API {
|
|||
lists: NewListHandler(listStore, itemStore),
|
||||
ops: NewOpsHandler(opStore, listStore),
|
||||
suggest: NewSuggestHandler(suggestStore),
|
||||
config: NewConfigHandler(cfg),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -73,6 +75,9 @@ func (a *API) Handler() http.Handler {
|
|||
mux.HandleFunc("GET /healthz", a.health.Healthz)
|
||||
mux.HandleFunc("GET /readyz", a.health.Readyz)
|
||||
|
||||
// --- public server config (auth capabilities, no auth required) ---
|
||||
mux.HandleFunc("GET /api/config", a.config.Config)
|
||||
|
||||
// --- auth endpoints ---
|
||||
mux.HandleFunc("POST /auth/register", a.auth.Register)
|
||||
mux.HandleFunc("POST /auth/login", a.auth.Login)
|
||||
|
|
|
|||
|
|
@ -65,6 +65,11 @@ type userDTO struct {
|
|||
|
||||
// Register creates a new email/password account and immediately issues a session.
|
||||
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.cfg.AuthPasswordEnabled {
|
||||
renderError(w, http.StatusForbidden, "Password auth disabled",
|
||||
"Email/password registration is disabled on this server. Use OIDC.")
|
||||
return
|
||||
}
|
||||
var req registerRequest
|
||||
if !decodeJSON(w, r, &req) {
|
||||
return
|
||||
|
|
@ -104,6 +109,11 @@ func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
|||
// Login verifies credentials and issues a session. Uses a constant-shape error
|
||||
// path so a wrong password and an unknown email yield the same response.
|
||||
func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
if !h.cfg.AuthPasswordEnabled {
|
||||
renderError(w, http.StatusForbidden, "Password auth disabled",
|
||||
"Email/password login is disabled on this server. Use OIDC.")
|
||||
return
|
||||
}
|
||||
var req loginRequest
|
||||
if !decodeJSON(w, r, &req) {
|
||||
return
|
||||
|
|
|
|||
62
backend/internal/httpapi/config.go
Normal file
62
backend/internal/httpapi/config.go
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
package httpapi
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/mitbringsl/backend/internal/config"
|
||||
)
|
||||
|
||||
// ConfigHandler exposes the public server configuration that the Android app
|
||||
// needs before logging in (e.g. which auth methods are available).
|
||||
type ConfigHandler struct {
|
||||
cfg *config.Config
|
||||
}
|
||||
|
||||
// NewConfigHandler creates a ConfigHandler.
|
||||
func NewConfigHandler(cfg *config.Config) *ConfigHandler {
|
||||
return &ConfigHandler{cfg: cfg}
|
||||
}
|
||||
|
||||
// serverConfigResponse is the public config payload returned to the app.
|
||||
type serverConfigResponse struct {
|
||||
Auth authConfig `json:"auth"`
|
||||
}
|
||||
|
||||
// authConfig describes which login methods the server offers.
|
||||
type authConfig struct {
|
||||
PasswordEnabled bool `json:"password_enabled"`
|
||||
OIDC oidcProvidersDTO `json:"oidc"`
|
||||
}
|
||||
|
||||
type oidcProvidersDTO struct {
|
||||
Google oidcProviderDTO `json:"google"`
|
||||
Generic oidcProviderDTO `json:"generic"`
|
||||
}
|
||||
|
||||
type oidcProviderDTO struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Issuer string `json:"issuer,omitempty"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
}
|
||||
|
||||
// Config returns the public server configuration.
|
||||
// GET /api/config (public, no auth required)
|
||||
func (h *ConfigHandler) Config(w http.ResponseWriter, r *http.Request) {
|
||||
renderJSON(w, http.StatusOK, serverConfigResponse{
|
||||
Auth: authConfig{
|
||||
PasswordEnabled: h.cfg.AuthPasswordEnabled,
|
||||
OIDC: oidcProvidersDTO{
|
||||
Google: oidcProviderDTO{
|
||||
Enabled: h.cfg.GoogleOIDC.Enabled,
|
||||
Issuer: h.cfg.GoogleOIDC.Issuer,
|
||||
DisplayName: "Google",
|
||||
},
|
||||
Generic: oidcProviderDTO{
|
||||
Enabled: h.cfg.GenericOIDC.Enabled,
|
||||
Issuer: h.cfg.GenericOIDC.Issuer,
|
||||
DisplayName: h.cfg.GenericOIDC.DisplayName,
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue