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  }