// Vikunja is a to-do list application to facilitate your life. // Copyright 2018-present Vikunja and contributors. All rights reserved. // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . package openid import ( "fmt" "strconv" "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/modules/keyvalue" "github.com/coreos/go-oidc/v3/oidc" "golang.org/x/oauth2" ) // GetAllProviders returns all configured providers func GetAllProviders() (providers []*Provider, err error) { if !config.AuthOpenIDEnabled.GetBool() { return nil, nil } providers = []*Provider{} exists, err := keyvalue.GetWithValue("openid_providers", &providers) if !exists { rawProviders := config.AuthOpenIDProviders.Get() if rawProviders == nil { return nil, nil } rawProvider, is := rawProviders.(map[string]interface{}) if !is { // Try to convert from map[interface{}]interface{} (YAML format) if rawProviderInterface, ok := rawProviders.(map[interface{}]interface{}); ok { rawProvider = make(map[string]interface{}, len(rawProviderInterface)) for k, v := range rawProviderInterface { if key, keyOK := k.(string); keyOK { rawProvider[key] = v } } } else { log.Criticalf("It looks like your openid configuration is in the wrong format. Please check the docs for the correct format.") return } } for key, p := range rawProvider { var pi map[string]interface{} var is bool pi, is = p.(map[string]interface{}) // JSON config is a map[string]interface{}, other providers are not. Under the hood they are all strings so // it is safe to cast. if !is { if pis, pisOK := p.(map[interface{}]interface{}); pisOK { pi = make(map[string]interface{}, len(pis)) for i, s := range pis { if key, keyOK := i.(string); keyOK { pi[key] = s } } } else { log.Errorf("Provider %s has invalid configuration format, skipping", key) continue } } provider, err := getProviderFromMap(pi, key) if err != nil { log.Errorf("Error while getting openid provider %s: %s", key, err) continue } if provider == nil { log.Errorf("Could not openid provider %s, please check your config", key) continue } providers = append(providers, provider) err = keyvalue.Put("openid_provider_"+key, provider) if err != nil { return nil, err } } err = keyvalue.Put("openid_providers", providers) } return } // GetProvider retrieves a provider from keyvalue func GetProvider(key string) (provider *Provider, err error) { provider = &Provider{} exists, err := keyvalue.GetWithValue("openid_provider_"+key, provider) if err != nil { return nil, err } if !exists { _, err = GetAllProviders() // This will put all providers in cache if err != nil { return nil, err } _, err = keyvalue.GetWithValue("openid_provider_"+key, provider) if err != nil { return nil, err } } err = provider.setOicdProvider() return } func getProviderFromMap(pi map[string]interface{}, key string) (provider *Provider, err error) { requiredKeys := []string{ "name", "authurl", "clientsecret", "clientid", } allKeys := append( []string{ "logouturl", "scope", "emailfallback", "usernamefallback", "forceuserinfo", "requireavailability", }, requiredKeys..., ) for _, configKey := range allKeys { valueFromFile := config.GetConfigValueFromFile("auth.openid.providers." + key + "." + configKey) if valueFromFile != "" { pi[configKey] = valueFromFile } } for _, key := range requiredKeys { if _, exists := pi[key]; !exists { return nil, fmt.Errorf("required key '%s' is missing in the provider configuration", key) } } name, is := pi["name"].(string) if !is { return nil, nil } var logoutURL string logoutValue, exists := pi["logouturl"] if exists { url, ok := logoutValue.(string) if ok { logoutURL = url } } var scope string if scopeValue, exists := pi["scope"]; exists { scope = scopeValue.(string) } if scope == "" { scope = "openid profile email" } var emailFallback = false emailFallbackValue, exists := pi["emailfallback"] if exists { emailFallbackTypedValue, ok := emailFallbackValue.(bool) if ok { emailFallback = emailFallbackTypedValue } } var usernameFallback = false usernameFallbackValue, exists := pi["usernamefallback"] if exists { usernameFallbackTypedValue, ok := usernameFallbackValue.(bool) if ok { usernameFallback = usernameFallbackTypedValue } } var forceUserInfo = false forceUserInfoValue, exists := pi["forceuserinfo"] if exists { forceUserInfoTypedValue, ok := forceUserInfoValue.(bool) if ok { forceUserInfo = forceUserInfoTypedValue } else { log.Errorf("forceuserinfo is not a boolean for provider %s, value: %v", key, forceUserInfoValue) } } 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{ Name: name, Key: key, AuthURL: pi["authurl"].(string), OriginalAuthURL: pi["authurl"].(string), ClientSecret: pi["clientsecret"].(string), LogoutURL: logoutURL, Scope: scope, EmailFallback: emailFallback, UsernameFallback: usernameFallback, ForceUserInfo: forceUserInfo, RequireAvailability: requireAvailability, } cl, is := pi["clientid"].(int) if is { provider.ClientID = strconv.Itoa(cl) } else { provider.ClientID = pi["clientid"].(string) } err = provider.setOicdProvider() if err != nil { return } provider.Oauth2Config = &oauth2.Config{ ClientID: provider.ClientID, ClientSecret: provider.ClientSecret, // Discovery returns the OAuth2 endpoints. Endpoint: provider.openIDProvider.Endpoint(), // "openid" is a required scope for OpenID Connect flows. Scopes: []string{oidc.ScopeOpenID, "profile", "email"}, } provider.AuthURL = provider.Oauth2Config.Endpoint.AuthURL return } func CleanupSavedOpenIDProviders() { _ = keyvalue.Del("openid_providers") }