feat(auth): add oauth require availability configuration on startup (#1358)

This commit is contained in:
Copilot 2025-08-30 22:15:20 +00:00 committed by GitHub
parent 523dad5134
commit 5ca637a7e6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 51 additions and 23 deletions

View File

@ -717,6 +717,11 @@
"key": "forceuserinfo", "key": "forceuserinfo",
"default_value": "false", "default_value": "false",
"comment": "This option forces the use of the OpenID Connect UserInfo endpoint to retrieve user information instead of relying on claims from the ID token. When set to `true`, user data (email, name, username) will always be obtained from the UserInfo endpoint even if the information is available in the token claims. This is useful for providers that don't include complete user information in their tokens or when you need the most up-to-date user data. Allowed value is either `true` or `false`." "comment": "This option forces the use of the OpenID Connect UserInfo endpoint to retrieve user information instead of relying on claims from the ID token. When set to `true`, user data (email, name, username) will always be obtained from the UserInfo endpoint even if the information is available in the token claims. This is useful for providers that don't include complete user information in their tokens or when you need the most up-to-date user data. Allowed value is either `true` or `false`."
},
{
"key": "requireavailability",
"default_value": "false",
"comment": "This option requires the OpenID Connect provider to be available during Vikunja startup. When set to `true`, Vikunja will crash if it cannot connect to the provider during initialization, allowing container orchestrators like Kubernetes to handle the failure by restarting the application. This is useful in environments where you want to ensure all authentication providers are available before the application starts serving requests. Allowed value is either `true` or `false`."
} }
] ]
} }

View File

@ -97,6 +97,12 @@ func FullInitWithoutAsync() {
// Connect to ldap if enabled // Connect to ldap if enabled
ldap.InitializeLDAPConnection() ldap.InitializeLDAPConnection()
// Check all OpenID Connect providers at startup
_, err := openid.GetAllProviders()
if err != nil {
log.Errorf("Error initializing OpenID Connect providers: %s", err)
}
// Load translations // Load translations
i18n.Init() i18n.Init()

View File

@ -52,19 +52,20 @@ type Callback struct {
// Provider is the structure of an OpenID Connect provider // Provider is the structure of an OpenID Connect provider
type Provider struct { type Provider struct {
Name string `json:"name"` Name string `json:"name"`
Key string `json:"key"` Key string `json:"key"`
OriginalAuthURL string `json:"-"` OriginalAuthURL string `json:"-"`
AuthURL string `json:"auth_url"` AuthURL string `json:"auth_url"`
LogoutURL string `json:"logout_url"` LogoutURL string `json:"logout_url"`
ClientID string `json:"client_id"` ClientID string `json:"client_id"`
Scope string `json:"scope"` Scope string `json:"scope"`
EmailFallback bool `json:"email_fallback"` EmailFallback bool `json:"email_fallback"`
UsernameFallback bool `json:"username_fallback"` UsernameFallback bool `json:"username_fallback"`
ForceUserInfo bool `json:"force_user_info"` ForceUserInfo bool `json:"force_user_info"`
ClientSecret string `json:"-"` RequireAvailability bool `json:"-"`
openIDProvider *oidc.Provider ClientSecret string `json:"-"`
Oauth2Config *oauth2.Config `json:"-"` openIDProvider *oidc.Provider
Oauth2Config *oauth2.Config `json:"-"`
} }
type claims struct { type claims struct {
@ -83,6 +84,9 @@ func init() {
func (p *Provider) setOicdProvider() (err error) { func (p *Provider) setOicdProvider() (err error) {
p.openIDProvider, err = oidc.NewProvider(context.Background(), p.OriginalAuthURL) p.openIDProvider, err = oidc.NewProvider(context.Background(), p.OriginalAuthURL)
if err != nil && p.RequireAvailability {
log.Fatalf("OpenID Connect provider '%s' is not available and require_availability is enabled: %s", p.Name, err)
}
return err return err
} }

View File

@ -126,6 +126,7 @@ func getProviderFromMap(pi map[string]interface{}, key string) (provider *Provid
"emailfallback", "emailfallback",
"usernamefallback", "usernamefallback",
"forceuserinfo", "forceuserinfo",
"requireavailability",
}, },
requiredKeys..., requiredKeys...,
) )
@ -193,17 +194,29 @@ func getProviderFromMap(pi map[string]interface{}, key string) (provider *Provid
} }
} }
var requireAvailability = false
requireAvailabilityValue, exists := pi["requireavailability"]
if exists {
requireAvailabilityTypedValue, ok := requireAvailabilityValue.(bool)
if ok {
requireAvailability = requireAvailabilityTypedValue
} else {
log.Errorf("requireavailability is not a boolean for provider %s, value: %v", key, requireAvailabilityValue)
}
}
provider = &Provider{ provider = &Provider{
Name: name, Name: name,
Key: key, Key: key,
AuthURL: pi["authurl"].(string), AuthURL: pi["authurl"].(string),
OriginalAuthURL: pi["authurl"].(string), OriginalAuthURL: pi["authurl"].(string),
ClientSecret: pi["clientsecret"].(string), ClientSecret: pi["clientsecret"].(string),
LogoutURL: logoutURL, LogoutURL: logoutURL,
Scope: scope, Scope: scope,
EmailFallback: emailFallback, EmailFallback: emailFallback,
UsernameFallback: usernameFallback, UsernameFallback: usernameFallback,
ForceUserInfo: forceUserInfo, ForceUserInfo: forceUserInfo,
RequireAvailability: requireAvailability,
} }
cl, is := pi["clientid"].(int) cl, is := pi["clientid"].(int)