github.com/greenpau/go-authcrunch@v1.1.4/pkg/idp/oauth/config.go (about) 1 // Copyright 2022 Paul Greenberg greenpau@outlook.com 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package oauth 16 17 import ( 18 "fmt" 19 "github.com/greenpau/go-authcrunch/pkg/authn/icons" 20 "github.com/greenpau/go-authcrunch/pkg/errors" 21 "net/url" 22 "regexp" 23 "strings" 24 ) 25 26 const defaultIdentityTokenCookieName string = "AUTHP_ID_TOKEN" 27 28 // Config holds the configuration for the IdentityProvider. 29 type Config struct { 30 Name string `json:"name,omitempty" xml:"name,omitempty" yaml:"name,omitempty"` 31 Realm string `json:"realm,omitempty" xml:"realm,omitempty" yaml:"realm,omitempty"` 32 Driver string `json:"driver,omitempty" xml:"driver,omitempty" yaml:"driver,omitempty"` 33 DomainName string `json:"domain_name,omitempty" xml:"domain_name,omitempty" yaml:"domain_name,omitempty"` 34 ClientID string `json:"client_id,omitempty" xml:"client_id,omitempty" yaml:"client_id,omitempty"` 35 ClientSecret string `json:"client_secret,omitempty" xml:"client_secret,omitempty" yaml:"client_secret,omitempty"` 36 ServerID string `json:"server_id,omitempty" xml:"server_id,omitempty" yaml:"server_id,omitempty"` 37 ServerName string `json:"server_name,omitempty" xml:"server_name,omitempty" yaml:"server_name,omitempty"` 38 AppSecret string `json:"app_secret,omitempty" xml:"app_secret,omitempty" yaml:"app_secret,omitempty"` 39 TenantID string `json:"tenant_id,omitempty" xml:"tenant_id,omitempty" yaml:"tenant_id,omitempty"` 40 IdentityTokenName string `json:"identity_token_name,omitempty" xml:"identity_token_name,omitempty" yaml:"identity_token_name,omitempty"` 41 42 // AWS Cognito User Pool ID 43 UserPoolID string `json:"user_pool_id,omitempty" xml:"user_pool_id,omitempty" yaml:"user_pool_id,omitempty"` 44 // AWS Region 45 Region string `json:"region,omitempty" xml:"region,omitempty" yaml:"region,omitempty"` 46 47 Scopes []string `json:"scopes,omitempty" xml:"scopes,omitempty" yaml:"scopes,omitempty"` 48 49 // The number if seconds to wait before getting key material 50 // from an OAuth 2.0 identity provider. 51 DelayStart int `json:"delay_start,omitempty" xml:"delay_start,omitempty" yaml:"delay_start,omitempty"` 52 // The number of the retry attempts getting key material 53 // from an OAuth 2.0 identity provider. 54 RetryAttempts int `json:"retry_attempts,omitempty" xml:"retry_attempts,omitempty" yaml:"retry_attempts,omitempty"` 55 // The number of seconds to wait until the retrying. 56 RetryInterval int `json:"retry_interval,omitempty" xml:"retry_interval,omitempty" yaml:"retry_interval,omitempty"` 57 58 UserRoleMapList []map[string]interface{} `json:"user_roles,omitempty" xml:"user_roles,omitempty" yaml:"user_roles,omitempty"` 59 60 // The URL to OAuth 2.0 Custom Authorization Server. 61 BaseAuthURL string `json:"base_auth_url,omitempty" xml:"base_auth_url,omitempty" yaml:"base_auth_url,omitempty"` 62 63 // The URL to OAuth 2.0 metadata related to your Custom Authorization Server. 64 MetadataURL string `json:"metadata_url,omitempty" xml:"metadata_url,omitempty" yaml:"metadata_url,omitempty"` 65 66 // The regex filters for user groups extracted via IdP API. 67 UserGroupFilters []string `json:"user_group_filters,omitempty" xml:"user_group_filters,omitempty" yaml:"user_group_filters,omitempty"` 68 // The regex filters for user orgs extracted via IdP API. 69 UserOrgFilters []string `json:"user_org_filters,omitempty" xml:"user_org_filters,omitempty" yaml:"user_org_filters,omitempty"` 70 71 // Disables metadata discovery via public metadata URL. 72 MetadataDiscoveryDisabled bool `json:"metadata_discovery_disabled,omitempty" xml:"metadata_discovery_disabled,omitempty" yaml:"metadata_discovery_disabled,omitempty"` 73 74 KeyVerificationDisabled bool `json:"key_verification_disabled,omitempty" xml:"key_verification_disabled,omitempty" yaml:"key_verification_disabled,omitempty"` 75 PassGrantTypeDisabled bool `json:"pass_grant_type_disabled,omitempty" xml:"pass_grant_type_disabled,omitempty" yaml:"pass_grant_type_disabled,omitempty"` 76 ResponseTypeDisabled bool `json:"response_type_disabled,omitempty" xml:"response_type_disabled,omitempty" yaml:"response_type_disabled,omitempty"` 77 NonceDisabled bool `json:"nonce_disabled,omitempty" xml:"nonce_disabled,omitempty" yaml:"nonce_disabled,omitempty"` 78 ScopeDisabled bool `json:"scope_disabled,omitempty" xml:"scope_disabled,omitempty" yaml:"scope_disabled,omitempty"` 79 80 AcceptHeaderEnabled bool `json:"accept_header_enabled,omitempty" xml:"accept_header_enabled,omitempty" yaml:"accept_header_enabled,omitempty"` 81 82 JsCallbackEnabled bool `json:"js_callback_enabled,omitempty" xml:"js_callback_enabled,omitempty" yaml:"js_callback_enabled,omitempty"` 83 84 // If enabled, portal redirects to identity provider logout URL. This would end the session with the provider. 85 LogoutEnabled bool `json:"logout_enabled,omitempty" xml:"logout_enabled,omitempty" yaml:"logout_enabled,omitempty"` 86 87 ResponseType []string `json:"response_type,omitempty" xml:"response_type,omitempty" yaml:"response_type,omitempty"` 88 89 AuthorizationURL string `json:"authorization_url,omitempty" xml:"authorization_url,omitempty" yaml:"authorization_url,omitempty"` 90 TokenURL string `json:"token_url,omitempty" xml:"token_url,omitempty" yaml:"token_url,omitempty"` 91 92 RequiredTokenFields []string `json:"required_token_fields,omitempty" xml:"required_token_fields,omitempty" yaml:"required_token_fields,omitempty"` 93 94 TLSInsecureSkipVerify bool `json:"tls_insecure_skip_verify,omitempty" xml:"tls_insecure_skip_verify,omitempty" yaml:"tls_insecure_skip_verify,omitempty"` 95 96 // The predefined public RSA based JWKS keys. 97 JwksKeys map[string]string `json:"jwks_keys,omitempty" xml:"jwks_keys,omitempty" yaml:"jwks_keys,omitempty"` 98 99 // Disables the check for the presence of email field in a token. 100 EmailClaimCheckDisabled bool `json:"email_claim_check_disabled,omitempty" xml:"email_claim_check_disabled,omitempty" yaml:"email_claim_check_disabled,omitempty"` 101 102 // LoginIcon is the UI login icon attributes. 103 LoginIcon *icons.LoginIcon `json:"login_icon,omitempty" xml:"login_icon,omitempty" yaml:"login_icon,omitempty"` 104 105 UserInfoFields []string `json:"user_info_fields,omitempty" xml:"user_info_fields,omitempty" yaml:"user_info_fields,omitempty"` 106 UserInfoRolesFieldName string `json:"user_info_roles_field_name,omitempty" xml:"user_info_roles_field_name,omitempty" yaml:"user_info_roles_field_name,omitempty"` 107 108 // The name of the cookie storing id_token from OAuth provider. 109 IdentityTokenCookieName string `json:"identity_token_cookie_name,omitempty" xml:"identity_token_cookie_name,omitempty" yaml:"identity_token_cookie_name,omitempty"` 110 // Enables the storing of id_token from OAuth provider in a HTTP cookie. 111 IdentityTokenCookieEnabled bool `json:"identity_token_cookie_enabled,omitempty" xml:"identity_token_cookie_enabled,omitempty" yaml:"identity_token_cookie_enabled,omitempty"` 112 } 113 114 // Validate validates identity store configuration. 115 func (cfg *Config) Validate() error { 116 if cfg.Name == "" { 117 return errors.ErrIdentityProviderConfigureNameEmpty 118 } 119 120 if cfg.Realm == "" { 121 return errors.ErrIdentityProviderConfigureRealmEmpty 122 } 123 124 if cfg.ClientID == "" { 125 return errors.ErrIdentityProviderConfig.WithArgs("client id not found") 126 } 127 128 if cfg.ClientSecret == "" { 129 return errors.ErrIdentityProviderConfig.WithArgs("client secret not found") 130 } 131 132 if cfg.DelayStart > 0 { 133 if cfg.RetryAttempts < 1 { 134 cfg.RetryAttempts = 2 135 } 136 if cfg.RetryInterval == 0 { 137 cfg.RetryInterval = cfg.DelayStart 138 } 139 } 140 141 if cfg.RetryAttempts > 0 && cfg.DelayStart == 0 { 142 if cfg.RetryInterval == 0 { 143 cfg.RetryInterval = 5 144 } 145 } 146 147 if len(cfg.Scopes) < 1 { 148 switch cfg.Driver { 149 case "facebook": 150 cfg.Scopes = []string{ 151 // "public_profile", 152 "email", 153 } 154 case "github": 155 cfg.Scopes = []string{"read:user"} 156 case "nextcloud": 157 cfg.Scopes = []string{"email"} 158 case "google": 159 cfg.Scopes = []string{"openid", "email", "profile"} 160 case "cognito": 161 cfg.Scopes = []string{"openid", "email", "profile"} 162 case "discord": 163 cfg.Scopes = []string{"identify"} 164 case "linkedin": 165 cfg.Scopes = []string{"openid", "email", "profile"} 166 default: 167 cfg.Scopes = []string{"openid", "email", "profile"} 168 } 169 } 170 171 switch cfg.IdentityTokenName { 172 case "": 173 cfg.IdentityTokenName = "id_token" 174 case "id_token", "access_token": 175 default: 176 return errors.ErrIdentityProviderConfig.WithArgs( 177 fmt.Errorf("identity token name %q is unsupported", cfg.IdentityTokenName), 178 ) 179 } 180 181 switch cfg.Driver { 182 case "okta": 183 if cfg.ServerID == "" { 184 return errors.ErrIdentityProviderConfig.WithArgs("server id not found") 185 } 186 if cfg.DomainName == "" { 187 return errors.ErrIdentityProviderConfig.WithArgs("domain name not found") 188 } 189 if cfg.BaseAuthURL == "" { 190 cfg.BaseAuthURL = fmt.Sprintf( 191 "https://%s/oauth2/%s/", 192 cfg.DomainName, cfg.ServerID, 193 ) 194 cfg.MetadataURL = cfg.BaseAuthURL + ".well-known/openid-configuration?client_id=" + cfg.ClientID 195 } 196 case "cognito": 197 if cfg.Region == "" { 198 return errors.ErrIdentityProviderConfig.WithArgs("region not found") 199 } 200 if cfg.UserPoolID == "" { 201 return errors.ErrIdentityProviderConfig.WithArgs("user_pool_id not found") 202 } 203 cfg.BaseAuthURL = fmt.Sprintf( 204 "https://cognito-idp.%s.amazonaws.com/%s/", cfg.Region, cfg.UserPoolID, 205 ) 206 cfg.MetadataURL = cfg.BaseAuthURL + ".well-known/openid-configuration" 207 case "google": 208 if cfg.BaseAuthURL == "" { 209 cfg.BaseAuthURL = "https://accounts.google.com/o/oauth2/v2/" 210 cfg.MetadataURL = "https://accounts.google.com/.well-known/openid-configuration" 211 } 212 // If Google client_id does not contains domain name, append with 213 // the default of .apps.googleusercontent.com. 214 if !strings.Contains(cfg.ClientID, ".") { 215 cfg.ClientID = cfg.ClientID + ".apps.googleusercontent.com" 216 } 217 case "github": 218 if cfg.BaseAuthURL == "" { 219 cfg.BaseAuthURL = "https://github.com/login/oauth/" 220 } 221 cfg.RequiredTokenFields = []string{"access_token"} 222 cfg.AuthorizationURL = "https://github.com/login/oauth/authorize" 223 cfg.TokenURL = "https://github.com/login/oauth/access_token" 224 case "gitlab": 225 if cfg.DomainName == "" { 226 cfg.DomainName = "gitlab.com" 227 } 228 if cfg.BaseAuthURL == "" { 229 cfg.BaseAuthURL = fmt.Sprintf("https://%s/", cfg.DomainName) 230 cfg.MetadataURL = cfg.BaseAuthURL + ".well-known/openid-configuration" 231 } 232 case "azure": 233 if cfg.TenantID == "" { 234 cfg.TenantID = "common" 235 } 236 if cfg.BaseAuthURL == "" { 237 cfg.BaseAuthURL = "https://login.microsoftonline.com/" + cfg.TenantID + "/oauth2/v2.0/" 238 cfg.MetadataURL = "https://login.microsoftonline.com/" + cfg.TenantID + "/v2.0/.well-known/openid-configuration" 239 } 240 case "facebook": 241 if cfg.BaseAuthURL == "" { 242 cfg.BaseAuthURL = "https://www.facebook.com/v12.0/dialog/" 243 } 244 cfg.RequiredTokenFields = []string{"access_token"} 245 cfg.AuthorizationURL = "https://www.facebook.com/v12.0/dialog/oauth" 246 cfg.TokenURL = "https://graph.facebook.com/v12.0/oauth/access_token" 247 case "nextcloud": 248 cfg.AuthorizationURL = fmt.Sprintf("%s/apps/oauth2/authorize", cfg.BaseAuthURL) 249 cfg.TokenURL = fmt.Sprintf("%s/apps/oauth2/api/v1/token", cfg.BaseAuthURL) 250 case "discord": 251 cfg.BaseAuthURL = "https://discord.com/oauth2" 252 cfg.AuthorizationURL = "https://discord.com/oauth2/authorize" 253 cfg.TokenURL = "https://discord.com/api/oauth2/token" 254 cfg.RequiredTokenFields = []string{"access_token"} 255 case "linkedin": 256 if cfg.BaseAuthURL == "" { 257 cfg.BaseAuthURL = "https://www.linkedin.com/oauth/" 258 cfg.MetadataURL = cfg.BaseAuthURL + ".well-known/openid-configuration" 259 } 260 case "generic": 261 case "": 262 return errors.ErrIdentityProviderConfig.WithArgs("driver name not found") 263 default: 264 return errors.ErrIdentityProviderConfig.WithArgs( 265 fmt.Errorf("driver %q is unsupported", cfg.Driver), 266 ) 267 } 268 269 if len(cfg.RequiredTokenFields) < 1 { 270 cfg.RequiredTokenFields = []string{"access_token", "id_token"} 271 } 272 273 if cfg.BaseAuthURL == "" { 274 if cfg.MetadataURL == "" { 275 return errors.ErrIdentityProviderConfig.WithArgs("base authentication url not found") 276 } 277 } 278 279 // Validate metadata URL, i.e. endpoint discovery. 280 switch cfg.Driver { 281 case "github": 282 case "facebook": 283 case "nextcloud": 284 case "discord": 285 default: 286 if len(cfg.JwksKeys) > 0 && cfg.AuthorizationURL != "" && cfg.TokenURL != "" { 287 for kid, fp := range cfg.JwksKeys { 288 if _, err := NewJwksKeyFromRSAPublicKeyPEM(kid, fp); err != nil { 289 return errors.ErrIdentityProviderConfig.WithArgs( 290 fmt.Errorf("failed loading kid %q: %v", kid, err), 291 ) 292 } 293 } 294 } else { 295 if cfg.MetadataURL == "" { 296 return errors.ErrIdentityProviderConfig.WithArgs("metadata url not found") 297 } 298 } 299 } 300 301 parsedBaseAuthURL, err := url.Parse(cfg.BaseAuthURL) 302 if err != nil { 303 return errors.ErrIdentityProviderConfig.WithArgs( 304 fmt.Errorf("failed to parse base auth url %q: %v", cfg.BaseAuthURL, err), 305 ) 306 } 307 cfg.ServerName = parsedBaseAuthURL.Host 308 309 if len(cfg.ResponseType) < 1 { 310 cfg.ResponseType = []string{"code"} 311 } 312 313 // Configure user group filters, if any. 314 for _, pattern := range cfg.UserGroupFilters { 315 if _, err := regexp.Compile(pattern); err != nil { 316 return errors.ErrIdentityProviderConfig.WithArgs( 317 fmt.Errorf("invalid user group pattern %q: %v", pattern, err), 318 ) 319 } 320 } 321 322 // Configure user org filters, if any. 323 for _, pattern := range cfg.UserOrgFilters { 324 if _, err := regexp.Compile(pattern); err != nil { 325 return errors.ErrIdentityProviderConfig.WithArgs( 326 fmt.Errorf("invalid user org pattern %q: %v", pattern, err), 327 ) 328 } 329 } 330 331 // Configure UI login icon. 332 if cfg.LoginIcon == nil { 333 cfg.LoginIcon = icons.NewLoginIcon(cfg.Driver) 334 } else { 335 cfg.LoginIcon.Configure(cfg.Driver) 336 } 337 338 // Configure default identity token name. 339 if cfg.IdentityTokenCookieEnabled && cfg.IdentityTokenCookieName == "" { 340 cfg.IdentityTokenCookieName = defaultIdentityTokenCookieName 341 } 342 343 return nil 344 }