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:
Tronax 2026-08-06 10:28:12 +02:00
parent b44bc8c3af
commit 3f187f1ede
Signed by: Tronax
SSH key fingerprint: SHA256:2pKKXDZucWvaF/GzXNz0FY53EAO1YDLN80bqS+TTz/o
10 changed files with 577 additions and 244 deletions

View file

@ -11,6 +11,10 @@ interface MitbringslApi {
// --- Auth ---------------------------------------------------------------
/** Public server config: which auth methods are available. No auth required. */
@GET("api/config")
suspend fun getServerConfig(): Response<ServerConfigDto>
@POST("auth/register")
suspend fun register(@Body body: RegisterRequestDto): Response<AuthResponseDto>

View file

@ -142,6 +142,35 @@ data class ServerOpDto(
@SerialName("created_at") val createdAt: String,
)
// ---------------------------------------------------------------------------
// Server config (public, used by the login screen to decide which auth
// methods to offer)
// ---------------------------------------------------------------------------
@Serializable
data class ServerConfigDto(
val auth: AuthConfigDto,
)
@Serializable
data class AuthConfigDto(
@SerialName("password_enabled") val passwordEnabled: Boolean,
val oidc: OidcProvidersDto,
)
@Serializable
data class OidcProvidersDto(
val google: OidcProviderDto,
val generic: OidcProviderDto,
)
@Serializable
data class OidcProviderDto(
val enabled: Boolean,
val issuer: String? = null,
@SerialName("display_name") val displayName: String? = null,
)
// ---------------------------------------------------------------------------
// Suggestions
// ---------------------------------------------------------------------------

View file

@ -41,8 +41,6 @@ fun AuthScreen(
val currentServerUrl by viewModel.sessionManager.serverUrl.collectAsStateWithLifecycle()
val userEmail = viewModel.sessionManager.getUserEmail()
var showServerUrlDialog by remember { mutableStateOf(false) }
val focusManager = LocalFocusManager.current
val gradientBrush = Brush.verticalGradient(
colors = listOf(
@ -88,7 +86,7 @@ fun AuthScreen(
)
Text(
text = "Verbinde dich mit deinem Server oder Authentik für geräteübergreifenden Sync.",
text = "Verbinde dich mit deinem Server für geräteübergreifenden Sync.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
@ -98,7 +96,10 @@ fun AuthScreen(
// Server URL Chip
AssistChip(
onClick = { showServerUrlDialog = true },
onClick = {
// Reset back to the connect screen to re-configure the server.
viewModel.onServerUrlChanged(currentServerUrl)
},
label = {
Text(
text = "Server: $currentServerUrl",
@ -132,6 +133,39 @@ fun AuthScreen(
horizontalAlignment = Alignment.CenterHorizontally
) {
if (isLoggedIn) {
LoggedInView(userEmail, viewModel)
} else {
val cfg = uiState.serverConfig
if (cfg == null) {
// Step 1: connect to the server first.
ConnectView(uiState, viewModel, focusManager)
} else {
// Step 2: login with the methods the server offers.
LoginView(uiState, viewModel, cfg, focusManager)
}
}
Spacer(modifier = Modifier.height(8.dp))
TextButton(onClick = onLoginSuccess) {
Text(
text = "Zurück zu meinen Listen",
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
Spacer(modifier = Modifier.height(32.dp))
}
}
}
@Composable
private fun LoggedInView(
userEmail: String?,
viewModel: AuthViewModel,
) {
Text(
text = "Angemeldet als",
style = MaterialTheme.typography.labelMedium,
@ -156,10 +190,94 @@ fun AuthScreen(
) {
Text("Abmelden")
}
}
@Composable
private fun ConnectView(
uiState: AuthUiState,
viewModel: AuthViewModel,
focusManager: androidx.compose.ui.platform.FocusManager,
) {
Text(
text = "Mit Server verbinden",
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Gib die URL deines Mitbringsl-Servers ein. Danach wird geprüft, welche Anmeldemethoden verfügbar sind.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(20.dp))
// Error Message
AnimatedVisibility(
visible = uiState.connectError != null,
enter = fadeIn(),
exit = fadeOut()
) {
uiState.connectError?.let { msg ->
ErrorBox(msg)
Spacer(modifier = Modifier.height(16.dp))
}
}
OutlinedTextField(
value = uiState.serverUrlInput,
onValueChange = viewModel::onServerUrlChanged,
label = { Text("Server URL") },
placeholder = { Text("https://mitbringsl.example.com") },
singleLine = true,
shape = RoundedCornerShape(16.dp),
modifier = Modifier.fillMaxWidth(),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Uri,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(onDone = {
focusManager.clearFocus()
viewModel.connect()
})
)
Spacer(modifier = Modifier.height(24.dp))
Button(
onClick = {
focusManager.clearFocus()
viewModel.connect()
},
enabled = !uiState.isConnecting,
shape = RoundedCornerShape(16.dp),
modifier = Modifier
.fillMaxWidth()
.height(52.dp)
) {
if (uiState.isConnecting) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.5.dp
)
} else {
Text("Verbinden", fontWeight = FontWeight.Bold, fontSize = 16.sp)
}
}
}
@Composable
private fun LoginView(
uiState: AuthUiState,
viewModel: AuthViewModel,
cfg: ServerAuthConfig,
focusManager: androidx.compose.ui.platform.FocusManager,
) {
Text(
text = when {
uiState.isOidcMode -> "Authentik / OIDC Login"
uiState.isOidcMode -> "Mit ${cfg.oidcDisplayName} anmelden"
uiState.isRegisteringMode -> "Konto erstellen"
else -> "Anmelden"
},
@ -177,26 +295,13 @@ fun AuthScreen(
exit = fadeOut()
) {
uiState.errorMessage?.let { msg ->
Surface(
color = MaterialTheme.colorScheme.errorContainer,
shape = RoundedCornerShape(12.dp),
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 16.dp)
) {
Text(
text = msg,
color = MaterialTheme.colorScheme.onErrorContainer,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(12.dp),
textAlign = TextAlign.Center
)
}
ErrorBox(msg)
Spacer(modifier = Modifier.height(16.dp))
}
}
if (uiState.isOidcMode) {
// OIDC / Authentik Token Field
// OIDC token input (App does PKCE itself; for now manual token paste).
OutlinedTextField(
value = uiState.oidcTokenInput,
onValueChange = viewModel::onOidcTokenInputChanged,
@ -288,7 +393,7 @@ fun AuthScreen(
} else {
Text(
text = when {
uiState.isOidcMode -> "Mit Authentik / OIDC verbinden"
uiState.isOidcMode -> "Mit ${cfg.oidcDisplayName} verbinden"
uiState.isRegisteringMode -> "Registrieren"
else -> "Anmelden"
},
@ -300,11 +405,13 @@ fun AuthScreen(
Spacer(modifier = Modifier.height(12.dp))
// Toggle Buttons (E-Mail vs OIDC / Authentik)
// Toggle Buttons only shown when the server offers multiple methods.
if (cfg.hasMultipleMethods) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
if (cfg.passwordEnabled) {
TextButton(onClick = viewModel::toggleMode) {
Text(
text = if (uiState.isRegisteringMode) "Zurück zu Login" else "Konto erstellen",
@ -312,6 +419,9 @@ fun AuthScreen(
style = MaterialTheme.typography.bodySmall
)
}
} else {
Spacer(modifier = Modifier.width(0.dp))
}
TextButton(onClick = viewModel::toggleOidcMode) {
Row(verticalAlignment = Alignment.CenterVertically) {
@ -322,74 +432,37 @@ fun AuthScreen(
)
Spacer(modifier = Modifier.width(4.dp))
Text(
text = if (uiState.isOidcMode) "Passwort Login" else "Authentik / OIDC",
text = if (uiState.isOidcMode) "Passwort Login" else cfg.oidcDisplayName,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
Spacer(modifier = Modifier.height(8.dp))
TextButton(onClick = onLoginSuccess) {
} else if (cfg.passwordEnabled) {
// Only password auth → just show register/login toggle.
TextButton(onClick = viewModel::toggleMode) {
Text(
text = "Zurück zu meinen Listen",
color = MaterialTheme.colorScheme.onSurfaceVariant
text = if (uiState.isRegisteringMode) "Zurück zu Login" else "Konto erstellen",
color = MaterialTheme.colorScheme.primary,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
Spacer(modifier = Modifier.height(32.dp))
}
}
// Server URL Dialog
if (showServerUrlDialog) {
var tempUrl by remember { mutableStateOf(currentServerUrl) }
AlertDialog(
onDismissRequest = { showServerUrlDialog = false },
title = { Text("Self-Hosted Server URL") },
text = {
Column {
Text(
text = "Gib die URL deines Mitbringsl Backend-Servers ein:",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(12.dp))
OutlinedTextField(
value = tempUrl,
onValueChange = { tempUrl = it },
label = { Text("Server URL") },
placeholder = { Text("https://mitbringsl.example.com") },
singleLine = true,
@Composable
private fun ErrorBox(msg: String) {
Surface(
color = MaterialTheme.colorScheme.errorContainer,
shape = RoundedCornerShape(12.dp),
modifier = Modifier.fillMaxWidth()
)
}
},
confirmButton = {
Button(
onClick = {
if (tempUrl.isNotBlank()) {
viewModel.saveServerUrl(tempUrl)
showServerUrlDialog = false
}
},
shape = RoundedCornerShape(12.dp)
) {
Text("Speichern")
}
},
dismissButton = {
TextButton(onClick = { showServerUrlDialog = false }) {
Text("Abbrechen")
}
},
shape = RoundedCornerShape(24.dp)
Text(
text = msg,
color = MaterialTheme.colorScheme.onErrorContainer,
style = MaterialTheme.typography.bodySmall,
modifier = Modifier.padding(12.dp),
textAlign = TextAlign.Center
)
}
}

View file

@ -5,6 +5,8 @@ import androidx.lifecycle.viewModelScope
import com.example.mitbringsl.data.auth.AuthRepository
import com.example.mitbringsl.data.auth.AuthResult
import com.example.mitbringsl.data.auth.SessionManager
import com.example.mitbringsl.data.remote.api.MitbringslApi
import com.example.mitbringsl.data.remote.dto.AuthConfigDto
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@ -13,7 +15,40 @@ import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
/**
* The auth methods the connected server offers. `null` until the user has
* successfully connected to a server (or when running in local-only mode).
*/
data class ServerAuthConfig(
val passwordEnabled: Boolean,
val googleEnabled: Boolean,
val googleDisplayName: String,
val genericEnabled: Boolean,
val genericDisplayName: String,
) {
/** At least one login method is available. */
val hasAnyMethod: Boolean get() = passwordEnabled || googleEnabled || genericEnabled
/** Both password and an OIDC provider are available → show a mode toggle. */
val hasMultipleMethods: Boolean get() {
val oidcCount = listOf(googleEnabled, genericEnabled).count { it }
return (if (passwordEnabled) 1 else 0) + oidcCount > 1
}
val oidcDisplayName: String
get() = when {
genericEnabled -> genericDisplayName.ifBlank { "OIDC" }
googleEnabled -> "Google"
else -> "OIDC"
}
}
data class AuthUiState(
val serverUrlInput: String = "",
val isConnecting: Boolean = false,
val serverConfig: ServerAuthConfig? = null,
val connectError: String? = null,
val emailInput: String = "",
val passwordInput: String = "",
val displayNameInput: String = "",
@ -22,12 +57,12 @@ data class AuthUiState(
val isOidcMode: Boolean = false,
val isLoading: Boolean = false,
val errorMessage: String? = null,
val serverUrlInput: String = "",
)
@HiltViewModel
class AuthViewModel @Inject constructor(
private val authRepository: AuthRepository,
private val api: MitbringslApi,
val sessionManager: SessionManager,
) : ViewModel() {
@ -36,13 +71,87 @@ class AuthViewModel @Inject constructor(
)
val uiState: StateFlow<AuthUiState> = _uiState.asStateFlow()
// --- server connection -------------------------------------------------
fun onServerUrlChanged(value: String) {
_uiState.update { it.copy(serverUrlInput = value) }
_uiState.update {
it.copy(
serverUrlInput = value,
connectError = null,
serverConfig = null,
)
}
}
/**
* Saves the server URL and fetches the server's auth config so the UI can
* show only the login methods the server actually offers.
*/
fun connect() {
val url = _uiState.value.serverUrlInput.trim()
if (url.isBlank()) {
_uiState.update { it.copy(connectError = "Bitte eine Server-URL eingeben.") }
return
}
sessionManager.saveServerUrl(url)
viewModelScope.launch {
_uiState.update { it.copy(isConnecting = true, connectError = null) }
try {
val resp = api.getServerConfig()
if (resp.isSuccessful && resp.body() != null) {
val cfg = resp.body()!!.auth.toServerAuthConfig()
if (!cfg.hasAnyMethod) {
_uiState.update {
it.copy(
isConnecting = false,
serverConfig = cfg,
connectError = "Der Server bietet keine Anmeldemethode an.",
)
}
} else {
// Pick a sensible default mode based on what's available.
val defaultOidc = !cfg.passwordEnabled && (cfg.googleEnabled || cfg.genericEnabled)
_uiState.update {
it.copy(
isConnecting = false,
serverConfig = cfg,
connectError = null,
isOidcMode = defaultOidc,
isRegisteringMode = false,
errorMessage = null,
)
}
}
} else {
_uiState.update {
it.copy(
isConnecting = false,
connectError = "Server nicht erreichbar (HTTP ${resp.code()}).",
)
}
}
} catch (e: Exception) {
_uiState.update {
it.copy(
isConnecting = false,
connectError = e.localizedMessage ?: "Verbindung fehlgeschlagen.",
)
}
}
}
}
// --- form input --------------------------------------------------------
fun saveServerUrl(url: String) {
sessionManager.saveServerUrl(url)
_uiState.update { it.copy(serverUrlInput = sessionManager.getServerUrl(), errorMessage = null) }
_uiState.update {
it.copy(
serverUrlInput = sessionManager.getServerUrl(),
serverConfig = null,
connectError = null,
)
}
}
fun onEmailChanged(value: String) {
@ -71,10 +180,15 @@ class AuthViewModel @Inject constructor(
}
}
/**
* Only callable when the server offers more than one method; toggles
* between password and OIDC mode.
*/
fun toggleOidcMode() {
_uiState.update {
it.copy(
isOidcMode = !it.isOidcMode,
isRegisteringMode = false,
errorMessage = null
)
}
@ -88,7 +202,8 @@ class AuthViewModel @Inject constructor(
_uiState.update { it.copy(errorMessage = "Bitte ID-Token eingeben.") }
return
}
onOidcTokenReceived(provider = "generic", idToken = state.oidcTokenInput.trim())
val provider = pickOidcProvider(state.serverConfig)
onOidcTokenReceived(provider = provider, idToken = state.oidcTokenInput.trim())
return
}
@ -141,4 +256,22 @@ class AuthViewModel @Inject constructor(
}
}
}
/**
* Picks the OIDC provider key the backend expects. Prefers "generic" when
* available (Authentik/Keycloak), falls back to "google".
*/
private fun pickOidcProvider(cfg: ServerAuthConfig?): String = when {
cfg?.genericEnabled == true -> "generic"
cfg?.googleEnabled == true -> "google"
else -> "generic"
}
private fun AuthConfigDto.toServerAuthConfig(): ServerAuthConfig = ServerAuthConfig(
passwordEnabled = passwordEnabled,
googleEnabled = oidc.google.enabled,
googleDisplayName = oidc.google.displayName?.ifBlank { "Google" } ?: "Google",
genericEnabled = oidc.generic.enabled,
genericDisplayName = oidc.generic.displayName?.ifBlank { "OIDC" } ?: "OIDC",
)
}

View file

@ -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
@ -55,6 +59,9 @@ type GenericOIDCConfig struct {
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.

View file

@ -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)

View file

@ -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

View 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,
},
},
},
})
}

View file

@ -26,6 +26,12 @@ POSTGRES_DB=appdb
# TTL of the opaque session token issued after login.
SESSION_TOKEN_TTL=720h # 30 days
# --- Auth methods ---
# Set to false to disable email/password registration and login and enforce
# OIDC-only login. The Android app reads /api/config and only shows the login
# methods the server actually offers.
AUTH_PASSWORD_ENABLED=true
# --- OIDC: Google (optional) ---
OIDC_GOOGLE_ENABLED=false
# The OAuth client ID you created in Google Cloud Console (Audience the
@ -38,6 +44,8 @@ OIDC_GOOGLE_ISSUER=https://accounts.google.com
OIDC_GENERIC_ENABLED=false
OIDC_GENERIC_ISSUER= # e.g. https://idp.example.com/realms/main
OIDC_GENERIC_CLIENT_ID= # audience the backend accepts
# Human-readable name shown in the app, e.g. "Authentik", "Keycloak".
OIDC_GENERIC_DISPLAY_NAME=OIDC
# --- CORS (only relevant for browser clients; Android doesn't need it) ---
# Comma-separated list of allowed origins, e.g. https://app.example.com

View file

@ -62,6 +62,7 @@ services:
HTTP_ADDR: ":8080"
PUBLIC_BASE_URL: ${PUBLIC_BASE_URL:-http://localhost:8080}
SESSION_TOKEN_TTL: ${SESSION_TOKEN_TTL:-720h}
AUTH_PASSWORD_ENABLED: ${AUTH_PASSWORD_ENABLED:-true}
# OIDC (all optional)
OIDC_GOOGLE_ENABLED: ${OIDC_GOOGLE_ENABLED:-false}
OIDC_GOOGLE_CLIENT_ID: ${OIDC_GOOGLE_CLIENT_ID:-}
@ -69,6 +70,7 @@ services:
OIDC_GENERIC_ENABLED: ${OIDC_GENERIC_ENABLED:-false}
OIDC_GENERIC_ISSUER: ${OIDC_GENERIC_ISSUER:-}
OIDC_GENERIC_CLIENT_ID: ${OIDC_GENERIC_CLIENT_ID:-}
OIDC_GENERIC_DISPLAY_NAME: ${OIDC_GENERIC_DISPLAY_NAME:-OIDC}
CORS_ALLOWED_ORIGINS: ${CORS_ALLOWED_ORIGINS:-}
expose: ["8080"]
depends_on: