diff --git a/account/email/verification_service.go b/account/email/verification_service.go index 5f2a377..87870bc 100644 --- a/account/email/verification_service.go +++ b/account/email/verification_service.go @@ -68,7 +68,7 @@ func (c *EmailVerificationClient) SendVerificationCode(ctx context.Context, req } func (c *EmailVerificationClient) generateVerificationURL(ctx context.Context, req *goa.RequestData, code string) string { - return rest.AbsoluteURL(req, authclient.VerifyEmailUsersPath()) + "?code=" + code + return rest.AbsoluteURL(req, authclient.VerifyEmailUsersPath(), nil) + "?code=" + code } // VerifyCode validates whether the code is present in our database and returns a non-nil if yes. diff --git a/auth/authz_blackbox_test.go b/auth/authz_blackbox_test.go index 29c75ca..695c86b 100644 --- a/auth/authz_blackbox_test.go +++ b/auth/authz_blackbox_test.go @@ -205,7 +205,7 @@ func (s *TestAuthSuite) TestGetEntitlement() { require.Nil(s.T(), err) tokenEndpoint, err := configuration.GetKeycloakEndpointToken(r) require.Nil(s.T(), err) - testUserToken, err := controller.GenerateUserToken(ctx, tokenEndpoint, configuration, configuration.GetKeycloakTestUserName(), configuration.GetKeycloakTestUserSecret()) + testUserToken, err := controller.ObtainKeycloakUserToken(ctx, tokenEndpoint, configuration, configuration.GetKeycloakTestUserName(), configuration.GetKeycloakTestUserSecret()) // {"permissions" : [{"resource_set_name" : ""}]} entitlementResource := auth.EntitlementResource{ Permissions: []auth.ResourceSet{{Name: resourceName}}, @@ -426,7 +426,7 @@ func getUserID(t *testing.T, username string, usersecret string) string { require.Nil(t, err) ctx := context.Background() - testToken, err := controller.GenerateUserToken(ctx, tokenEndpoint, configuration, username, usersecret) + testToken, err := controller.ObtainKeycloakUserToken(ctx, tokenEndpoint, configuration, username, usersecret) require.Nil(t, err) accessToken := testToken.Token.AccessToken require.NotNil(t, accessToken) diff --git a/configuration/configuration.go b/configuration/configuration.go index 76ed54f..be629c6 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -36,69 +36,94 @@ const ( // Constants for viper variable names. Will be used to set // default values as well as to get each value - varPostgresHost = "postgres.host" - varPostgresPort = "postgres.port" - varPostgresUser = "postgres.user" - varPostgresDatabase = "postgres.database" - varPostgresPassword = "postgres.password" - varPostgresSSLMode = "postgres.sslmode" - varPostgresConnectionTimeout = "postgres.connection.timeout" - varPostgresTransactionTimeout = "postgres.transaction.timeout" - varPostgresConnectionRetrySleep = "postgres.connection.retrysleep" - varPostgresConnectionMaxIdle = "postgres.connection.maxidle" - varPostgresConnectionMaxOpen = "postgres.connection.maxopen" - varHTTPAddress = "http.address" - varMetricsHTTPAddress = "metrics.http.address" - varDeveloperModeEnabled = "developer.mode.enabled" - varKeycloakSecret = "keycloak.secret" - varKeycloakClientID = "keycloak.client.id" - varPublicOauthClientID = "public.oauth.client.id" - varKeycloakDomainPrefix = "keycloak.domain.prefix" - varKeycloakRealm = "keycloak.realm" - varKeycloakTesUserName = "keycloak.testuser.name" - varKeycloakTesUserSecret = "keycloak.testuser.secret" - varKeycloakTesUser2Name = "keycloak.testuser2.name" - varKeycloakTesUser2Secret = "keycloak.testuser2.secret" - varKeycloakURL = "keycloak.url" - varKeycloakEndpointAdmin = "keycloak.endpoint.admin" - varKeycloakEndpointAuth = "keycloak.endpoint.auth" - varKeycloakEndpointToken = "keycloak.endpoint.token" - varKeycloakEndpointUserinfo = "keycloak.endpoint.userinfo" - varKeycloakEndpointAuthzResourceset = "keycloak.endpoint.authz.resourceset" - varKeycloakEndpointClients = "keycloak.endpoint.clients" - varKeycloakEndpointEntitlement = "keycloak.endpoint.entitlement" - varKeycloakEndpointBroker = "keycloak.endpoint.broker" - varKeycloakEndpointAccount = "keycloak.endpoint.account" - varKeycloakEndpointLogout = "keycloak.endpoint.logout" + // General + varHTTPAddress = "http.address" + varMetricsHTTPAddress = "metrics.http.address" + varDeveloperModeEnabled = "developer.mode.enabled" + varTLSInsecureSkipVerify = "tls.insecureskipverify" + varNotApprovedRedirect = "notapproved.redirect" + varHeaderMaxLength = "header.maxlength" + varUsersListLimit = "users.listlimit" + defaultConfigFile = "config.yaml" + varValidRedirectURLs = "redirect.valid" + varLogLevel = "log.level" + varLogJSON = "log.json" + varEmailVerifiedRedirectURL = "email.verify.url" + varInternalUsersEmailAddressSuffix = "internal.users.email.address.domain" + varKeycloakTestsDisabled = "keycloak.tests.disabled" + varIgnoreEmailInProd = "ignore.email.prod" + + // Postgres + varPostgresHost = "postgres.host" + varPostgresPort = "postgres.port" + varPostgresUser = "postgres.user" + varPostgresDatabase = "postgres.database" + varPostgresPassword = "postgres.password" + varPostgresSSLMode = "postgres.sslmode" + varPostgresConnectionTimeout = "postgres.connection.timeout" + varPostgresTransactionTimeout = "postgres.transaction.timeout" + varPostgresConnectionRetrySleep = "postgres.connection.retrysleep" + varPostgresConnectionMaxIdle = "postgres.connection.maxidle" + varPostgresConnectionMaxOpen = "postgres.connection.maxopen" + + // Public Client ID for logging into Auth service via OAuth2 + varPublicOauthClientID = "public.oauth.client.id" + + // Keycloak + varKeycloakSecret = "keycloak.secret" + varKeycloakClientID = "keycloak.client.id" + varKeycloakDomainPrefix = "keycloak.domain.prefix" + varKeycloakRealm = "keycloak.realm" + varKeycloakTesUserName = "keycloak.testuser.name" + varKeycloakTesUserSecret = "keycloak.testuser.secret" + varKeycloakTesUser2Name = "keycloak.testuser2.name" + varKeycloakTesUser2Secret = "keycloak.testuser2.secret" + varKeycloakURL = "keycloak.url" + varKeycloakEndpointAdmin = "keycloak.endpoint.admin" + varKeycloakEndpointAuth = "keycloak.endpoint.auth" + varKeycloakEndpointToken = "keycloak.endpoint.token" + varKeycloakEndpointUserinfo = "keycloak.endpoint.userinfo" + varKeycloakEndpointAuthzResourceset = "keycloak.endpoint.authz.resourceset" + varKeycloakEndpointClients = "keycloak.endpoint.clients" + varKeycloakEndpointEntitlement = "keycloak.endpoint.entitlement" + varKeycloakEndpointBroker = "keycloak.endpoint.broker" + varKeycloakEndpointAccount = "keycloak.endpoint.account" + varKeycloakEndpointLogout = "keycloak.endpoint.logout" + + // Private keys for signing OSIO Serivice Account tokens varServiceAccountPrivateKeyDeprecated = "serviceaccount.privatekey.deprecated" varServiceAccountPrivateKeyIDDeprecated = "serviceaccount.privatekeyid.deprecated" varServiceAccountPrivateKey = "serviceaccount.privatekey" varServiceAccountPrivateKeyID = "serviceaccount.privatekeyid" - varGitHubClientID = "github.client.id" - varGitHubClientSecret = "github.client.secret" - varGitHubClientDefaultScopes = "github.client.defaultscopes" - varOSOClientApiUrl = "oso.client.apiurl" - varTLSInsecureSkipVerify = "tls.insecureskipverify" - varNotApprovedRedirect = "notapproved.redirect" - varHeaderMaxLength = "header.maxlength" - varCacheControlUsers = "cachecontrol.users" - varCacheControlCollaborators = "cachecontrol.collaborators" - varCacheControlUser = "cachecontrol.user" - varUsersListLimit = "users.listlimit" - defaultConfigFile = "config.yaml" - varValidRedirectURLs = "redirect.valid" - varLogLevel = "log.level" - varLogJSON = "log.json" - varWITDomainPrefix = "wit.domain.prefix" - varWITURL = "wit.url" - varNotificationServiceURL = "notification.serviceurl" - varEmailVerifiedRedirectURL = "email.verify.url" - varInternalUsersEmailAddressSuffix = "internal.users.email.address.domain" - varIgnoreEmailInProd = "ignore.email.prod" - - varTenantServiceURL = "tenant.serviceurl" - - varKeycloakTestsDisabled = "keycloak.tests.disabled" + + // Private keys for signing OSIO Access and Refresh tokens + varUserAccountPrivateKeyDeprecated = "useraccount.privatekey.deprecated" + varUserAccountPrivateKeyIDDeprecated = "useraccount.privatekeyid.deprecated" + varUserAccountPrivateKey = "useraccount.privatekey" + varUserAccountPrivateKeyID = "useraccount.privatekeyid" + + // Token configuration + varAccessTokenExpiresIn = "useraccount.token.access.expiresin" // In seconds + varRefreshTokenExpiresIn = "useraccount.token.refresh.expiresin" // In seconds + + // GitHub linking + varGitHubClientID = "github.client.id" + varGitHubClientSecret = "github.client.secret" + varGitHubClientDefaultScopes = "github.client.defaultscopes" + + // Default OSO cluster API URL + varOSOClientApiUrl = "oso.client.apiurl" + + // Cache control + varCacheControlUsers = "cachecontrol.users" + varCacheControlCollaborators = "cachecontrol.collaborators" + varCacheControlUser = "cachecontrol.user" + + // Other services URLs + varWITDomainPrefix = "wit.domain.prefix" + varTenantServiceURL = "tenant.serviceurl" + varWITURL = "wit.url" + varNotificationServiceURL = "notification.serviceurl" ) type serviceAccountConfig struct { @@ -173,7 +198,9 @@ func NewConfigurationData(mainConfigFile string, serviceAccountConfigFile string if err != nil { return nil, err } - c.appendDefaultConfigErrorMessage(defaultConfigErrorMsg) + if defaultConfigErrorMsg != nil { + c.appendDefaultConfigErrorMessage(*defaultConfigErrorMsg) + } var saConf serviceAccountConfig err = saViper.UnmarshalExact(&saConf) @@ -191,7 +218,9 @@ func NewConfigurationData(mainConfigFile string, serviceAccountConfigFile string if err != nil { return nil, err } - c.appendDefaultConfigErrorMessage(defaultConfigErrorMsg) + if defaultConfigErrorMsg != nil { + c.appendDefaultConfigErrorMessage(*defaultConfigErrorMsg) + } var clusterConf osoClusterConfig err = clusterViper.Unmarshal(&clusterConf) @@ -224,41 +253,45 @@ func NewConfigurationData(mainConfigFile string, serviceAccountConfigFile string // Check sensitive default configuration if c.IsPostgresDeveloperModeEnabled() { - msg := "developer Mode is enabled" - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage("developer Mode is enabled") } key, kid := c.GetServiceAccountPrivateKey() if string(key) == DefaultServiceAccountPrivateKey { - msg := "default service account private key is used" - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage("default service account private key is used") } if kid == defaultServiceAccountPrivateKeyID { - msg := "default service account private key ID is used" - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage("default service account private key ID is used") + } + key, kid = c.GetUserAccountPrivateKey() + if string(key) == DefaultUserAccountPrivateKey { + c.appendDefaultConfigErrorMessage("default user account private key is used") + } + if kid == defaultUserAccountPrivateKeyID { + c.appendDefaultConfigErrorMessage("default user account private key ID is used") } if c.GetPostgresPassword() == defaultDBPassword { - msg := "default DB password is used" - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage("default DB password is used") } if c.GetKeycloakSecret() == defaultKeycloakSecret { - msg := "default Keycloak client secret is used" - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage("default Keycloak client secret is used") } if c.GetGitHubClientSecret() == defaultGitHubClientSecret { - msg := "default GitHub client secret is used" - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage("default GitHub client secret is used") } if c.IsTLSInsecureSkipVerify() { - msg := "TLS verification disabled" - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage("TLS verification disabled") } if c.GetValidRedirectURLs() == ".*" { - msg := "no restrictions for valid redirect URLs" - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage("no restrictions for valid redirect URLs") } if c.GetNotificationServiceURL() == "" { - msg := "notification service url is empty" - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage("notification service url is empty") + } + if c.GetAccessTokenExpiresIn() < 3*60 { + c.appendDefaultConfigErrorMessage("too short lifespan of access tokens") + } + if c.GetRefreshTokenExpiresIn() < 3*60 { + c.appendDefaultConfigErrorMessage("too short lifespan of refresh tokens") } c.checkClusterConfig() if c.defaultConfigurationError != nil { @@ -283,23 +316,19 @@ func (c *ConfigurationData) checkServiceAccountConfig() { } for _, sa := range c.sa { if sa.Name == "" { - msg := "service account name is empty in service account config" - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage("service account name is empty in service account config") } else { delete(notFoundServiceAccountNames, sa.Name) } if sa.ID == "" { - msg := fmt.Sprintf("%s service account ID is empty in service account config", sa.Name) - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage(fmt.Sprintf("%s service account ID is empty in service account config", sa.Name)) } if len(sa.Secrets) == 0 { - msg := fmt.Sprintf("%s service account secret array is empty in service account config", sa.Name) - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage(fmt.Sprintf("%s service account secret array is empty in service account config", sa.Name)) } } if len(notFoundServiceAccountNames) != 0 { - msg := "some expected service accounts are missing in service account config" - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage("some expected service accounts are missing in service account config") } } @@ -314,12 +343,10 @@ func (c *ConfigurationData) checkClusterConfig() { switch f.Interface().(type) { case string: if f.String() == "" { - msg := fmt.Sprintf("key %v is missing in cluster config", tag) - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage(fmt.Sprintf("key %v is missing in cluster config", tag)) } default: - msg := fmt.Sprintf("wront type of key %v", tag) - c.appendDefaultConfigErrorMessage(&msg) + c.appendDefaultConfigErrorMessage(fmt.Sprintf("wront type of key %v", tag)) } } } @@ -385,14 +412,11 @@ func readFromJSONFile(configFilePath string, defaultConfigFilePath string, confi return jsonViper, defaultConfigErrorMsg, nil } -func (c *ConfigurationData) appendDefaultConfigErrorMessage(message *string) { - if message == nil { - return - } +func (c *ConfigurationData) appendDefaultConfigErrorMessage(message string) { if c.defaultConfigurationError == nil { - c.defaultConfigurationError = errors.New(*message) + c.defaultConfigurationError = errors.New(message) } else { - c.defaultConfigurationError = errors.Errorf("%s; %s", c.defaultConfigurationError.Error(), *message) + c.defaultConfigurationError = errors.Errorf("%s; %s", c.defaultConfigurationError.Error(), message) } } @@ -522,6 +546,12 @@ func (c *ConfigurationData) setConfigDefaults() { c.v.SetDefault(varKeycloakURL, devModeKeycloakURL) c.v.SetDefault(varServiceAccountPrivateKey, DefaultServiceAccountPrivateKey) c.v.SetDefault(varServiceAccountPrivateKeyID, defaultServiceAccountPrivateKeyID) + c.v.SetDefault(varUserAccountPrivateKey, DefaultUserAccountPrivateKey) + c.v.SetDefault(varUserAccountPrivateKeyID, defaultUserAccountPrivateKeyID) + var in30Days int64 + in30Days = 30 * 24 * 60 * 60 + c.v.SetDefault(varAccessTokenExpiresIn, in30Days) + c.v.SetDefault(varRefreshTokenExpiresIn, in30Days) c.v.SetDefault(varKeycloakClientID, defaultKeycloakClientID) c.v.SetDefault(varKeycloakSecret, defaultKeycloakSecret) c.v.SetDefault(varPublicOauthClientID, defaultPublicOauthClientID) @@ -685,17 +715,49 @@ func (c *ConfigurationData) GetCacheControlUser() string { } // GetDeprecatedServiceAccountPrivateKey returns the deprecated service account private key (if any) and its ID -// that is used to verify the service account authentication tokens during key rotation. +// that is used to verify service account authentication tokens during key rotation. func (c *ConfigurationData) GetDeprecatedServiceAccountPrivateKey() ([]byte, string) { return []byte(c.v.GetString(varServiceAccountPrivateKeyDeprecated)), c.v.GetString(varServiceAccountPrivateKeyIDDeprecated) } // GetServiceAccountPrivateKey returns the service account private key and its ID -// that is used to sign the service account authentication tokens. +// that is used to sign service account authentication tokens. func (c *ConfigurationData) GetServiceAccountPrivateKey() ([]byte, string) { return []byte(c.v.GetString(varServiceAccountPrivateKey)), c.v.GetString(varServiceAccountPrivateKeyID) } +// GetDeprecatedUserAccountPrivateKey returns the deprecated user account private key (if any) and its ID +// that is used to verify user access and refresh tokens during key rotation. +func (c *ConfigurationData) GetDeprecatedUserAccountPrivateKey() ([]byte, string) { + return []byte(c.v.GetString(varUserAccountPrivateKeyDeprecated)), c.v.GetString(varUserAccountPrivateKeyIDDeprecated) +} + +// GetUserAccountPrivateKey returns the service account private key and its ID +// that is used to sign user access and refresh tokens. +func (c *ConfigurationData) GetUserAccountPrivateKey() ([]byte, string) { + return []byte(c.v.GetString(varUserAccountPrivateKey)), c.v.GetString(varUserAccountPrivateKeyID) +} + +// GetAccessTokenExpiresIn returns lifespan of user access tokens generated by Auth in seconds +func (c *ConfigurationData) GetAccessTokenExpiresIn() int64 { + return c.v.GetInt64(varAccessTokenExpiresIn) +} + +// GetRefreshTokenExpiresIn returns lifespan of user refresh tokens generated by Auth in seconds +func (c *ConfigurationData) GetRefreshTokenExpiresIn() int64 { + return c.v.GetInt64(varRefreshTokenExpiresIn) +} + +// GetDevModePublicKey returns additional public key and its ID which should be used by the Auth service in Dev Mode +// For example a public key from Keycloak +// Returns false if in in Dev Mode +func (c *ConfigurationData) GetDevModePublicKey() (bool, []byte, string) { + if c.IsPostgresDeveloperModeEnabled() { + return true, []byte(devModePublicKey), devModePublicKeyID + } + return false, nil, "" +} + // GetGitHubClientID return GitHub client ID used to link GitHub accounts func (c *ConfigurationData) GetGitHubClientID() string { return c.v.GetString(varGitHubClientID) @@ -786,12 +848,8 @@ func (c *ConfigurationData) GetKeycloakTestUser2Secret() string { return c.v.GetString(varKeycloakTesUser2Secret) } -func (c *ConfigurationData) GetKeycloakEndpointCerts() string { - return fmt.Sprintf("%s/auth/realms/%s/protocol/openid-connect/certs", c.v.GetString(varKeycloakURL), c.GetKeycloakRealm()) -} - // GetKeycloakEndpointAuth returns the keycloak auth endpoint set via config file or environment variable. -// If nothing set then in Dev environment the defualt endopoint will be returned. +// If nothing set then in Dev environment the default endopoint will be returned. // In producion the endpoint will be calculated from the request by replacing the last domain/host name in the full host name. // Example: api.service.domain.org -> sso.service.domain.org // or api.domain.org -> sso.domain.org @@ -800,7 +858,7 @@ func (c *ConfigurationData) GetKeycloakEndpointAuth(req *goa.RequestData) (strin } // GetKeycloakEndpointToken returns the keycloak token endpoint set via config file or environment variable. -// If nothing set then in Dev environment the defualt endopoint will be returned. +// If nothing set then in Dev environment the default endopoint will be returned. // In producion the endpoint will be calculated from the request by replacing the last domain/host name in the full host name. // Example: api.service.domain.org -> sso.service.domain.org // or api.domain.org -> sso.domain.org @@ -809,7 +867,7 @@ func (c *ConfigurationData) GetKeycloakEndpointToken(req *goa.RequestData) (stri } // GetKeycloakEndpointUserInfo returns the keycloak userinfo endpoint set via config file or environment variable. -// If nothing set then in Dev environment the defualt endopoint will be returned. +// If nothing set then in Dev environment the default endopoint will be returned. // In producion the endpoint will be calculated from the request by replacing the last domain/host name in the full host name. // Example: api.service.domain.org -> sso.service.domain.org // or api.domain.org -> sso.domain.org @@ -824,7 +882,7 @@ func (c *ConfigurationData) GetNotificationServiceURL() string { // GetKeycloakEndpointAdmin returns the /realms/admin/ endpoint // set via config file or environment variable. -// If nothing set then in Dev environment the defualt endopoint will be returned. +// If nothing set then in Dev environment the default endopoint will be returned. // In producion the endpoint will be calculated from the request by replacing the last domain/host name in the full host name. // Example: api.service.domain.org -> sso.service.domain.org // or api.domain.org -> sso.domain.org @@ -834,7 +892,7 @@ func (c *ConfigurationData) GetKeycloakEndpointAdmin(req *goa.RequestData) (stri // GetKeycloakEndpointUsers returns the /realms/admin//users endpoint // set via config file or environment variable. -// If nothing set then in Dev environment the defualt endopoint will be returned. +// If nothing set then in Dev environment the default endopoint will be returned. // In producion the endpoint will be calculated from the request by replacing the last domain/host name in the full host name. // Example: api.service.domain.org -> sso.service.domain.org // or api.domain.org -> sso.domain.org @@ -845,7 +903,7 @@ func (c *ConfigurationData) GetKeycloakEndpointUsers(req *goa.RequestData) (stri // GetKeycloakEndpointIDP returns the /realms/admin//users/USER_ID/federated-identity/rhd endpoint // set via config file or environment variable. -// If nothing set then in Dev environment the defualt endopoint will be returned. +// If nothing set then in Dev environment the default endopoint will be returned. // In producion the endpoint will be calculated from the request by replacing the last domain/host name in the full host name. // Example: api.service.domain.org -> sso.service.domain.org // or api.domain.org -> sso.domain.org @@ -855,7 +913,7 @@ func (c *ConfigurationData) GetKeycloakEndpointLinkIDP(req *goa.RequestData, id // GetKeycloakEndpointAuthzResourceset returns the /realms//authz/protection/resource_set endpoint // set via config file or environment variable. -// If nothing set then in Dev environment the defualt endopoint will be returned. +// If nothing set then in Dev environment the default endopoint will be returned. // In producion the endpoint will be calculated from the request by replacing the last domain/host name in the full host name. // Example: api.service.domain.org -> sso.service.domain.org // or api.domain.org -> sso.domain.org @@ -865,7 +923,7 @@ func (c *ConfigurationData) GetKeycloakEndpointAuthzResourceset(req *goa.Request // GetKeycloakEndpointClients returns the /admin/realms//clients endpoint // set via config file or environment variable. -// If nothing set then in Dev environment the defualt endopoint will be returned. +// If nothing set then in Dev environment the default endopoint will be returned. // In producion the endpoint will be calculated from the request by replacing the last domain/host name in the full host name. // Example: api.service.domain.org -> sso.service.domain.org // or api.domain.org -> sso.domain.org @@ -875,7 +933,7 @@ func (c *ConfigurationData) GetKeycloakEndpointClients(req *goa.RequestData) (st // GetKeycloakEndpointEntitlement returns the /realms//authz/entitlement/ endpoint // set via config file or environment variable. -// If nothing set then in Dev environment the defualt endopoint will be returned. +// If nothing set then in Dev environment the default endopoint will be returned. // In producion the endpoint will be calculated from the request by replacing the last domain/host name in the full host name. // Example: api.service.domain.org -> sso.service.domain.org // or api.domain.org -> sso.domain.org @@ -885,7 +943,7 @@ func (c *ConfigurationData) GetKeycloakEndpointEntitlement(req *goa.RequestData) // GetKeycloakEndpointBroker returns the /realms//authz/entitlement/ endpoint // set via config file or environment variable. -// If nothing set then in Dev environment the defualt endopoint will be returned. +// If nothing set then in Dev environment the default endopoint will be returned. // In producion the endpoint will be calculated from the request by replacing the last domain/host name in the full host name. // Example: api.service.domain.org -> sso.service.domain.org // or api.domain.org -> sso.domain.org @@ -899,7 +957,7 @@ func (c *ConfigurationData) GetKeycloakAccountEndpoint(req *goa.RequestData) (st } // GetKeycloakEndpointLogout returns the keycloak logout endpoint set via config file or environment variable. -// If nothing set then in Dev environment the defualt endopoint will be returned. +// If nothing set then in Dev environment the default endopoint will be returned. // In producion the endpoint will be calculated from the request by replacing the last domain/host name in the full host name. // Example: api.service.domain.org -> sso.service.domain.org // or api.domain.org -> sso.domain.org @@ -1057,7 +1115,7 @@ const ( // Auth-related defaults // RSAPrivateKey for signing JWT Tokens for service accounts - // ssh-keygen -f alm_rsa + // ssh-keygen -f auth_rsa DefaultServiceAccountPrivateKey = `-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEAnwrjH5iTSErw9xUptp6QSFoUfpHUXZ+PaslYSUrpLjw1q27O DSFwmhV4+dAaTMO5chFv/kM36H3ZOyA146nwxBobS723okFaIkshRrf6qgtD6coT @@ -1088,6 +1146,48 @@ OCCAgsB8g8yTB4qntAYyfofEoDiseKrngQT5DSdxd51A/jw7B8WyBK8= defaultServiceAccountPrivateKeyID = "9MLnViaRkhVj1GT9kpWUkwHIwUD-wZfUxR-3CpkE-Xs" + DefaultUserAccountPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA40yB6SNoU4SpWxTfG5ilu+BlLYikRyyEcJIGg//w/GyqtjvT +/CVo92DRTh/DlrgwjSitmZrhauBnrCOoUBMin0/TXeSo3w2M5tEiiIFPbTDRf2jM +fbSGEOke9O0USCCR+bM2TncrgZR74qlSwq38VCND4zHc89rAzqJ2LVM2aXkuBbO7 +TcgLNyooBrpOK9khVHAD64cyODAdJY4esUjcLdlcB7TMDGOgxGGn2RARU7+TUf32 +gZZbTMikbuPM5gXuzGlo/22ECbQSKuZpbGwgPIAZ5NN9QA4D1NRz9+KDoiXZ6deZ +TTVCrZykJJ6RyLNfRh+XS+6G5nvcqAmfBpyOWwIDAQABAoIBAE5pBie23zZwfTu+ +Z3jNn96/+idLC+DBqq5qsXS3xhpOIlXbLbW98gfkjk+1BXPo9la7wadLlpeX8iuf +4WA+OaNblj69ssO/mOvHGXKdqRixzpN1Q5XZwKX0xYkYf/ahxbmt6P4IfimlX1dB +shsWigU8ZR7rBJ3ayMh/ouTf39ViIbXsHYpEubmACcLaOlXbEuZNr7ofkFQKl/mh +XLWUeOoM97xY6Agw/gv60GIcxIC5OAg7iNqS+XNzhba7f2nf2YqodbN9H1BmEJsf +RRaTTWlZAiQXC8lpZOKwP7DiMLOT78lfmlYtquEBhwRbXazfzsdf67Mr4Kdl2Cej +Jy0EGwECgYEA/DZWB0Lb0tPdT1FmORNrBfGg3PjhX9FOilhbtUgX3nNKp8Zsi3yO +yN6hf0/98qIGlmAQi5C92cXpdhqTiVAGktWD+q0a1W99udIjinS1tFrKgNtOyBWN +uwDBZyhw8RrwpQinMe7B966SVDaphvvOWlB1TadMDh5kReJCYpvRCrMCgYEA5rZj +djCU2UqMw6jIP07nCFjWgxPPjg7jP8aRo07oW2mv1sEA0doCyoZaMrdNeGd3fB0B +sm+IvlQtWD7r0tWZI1GkYpdRkDFurdkIzVPV5pMwH4ByOq/Jf5ZqtjIpoMaRBirA +whJyjmiGU3yDyPDLtEFpNgqM3mIyxS6M6UGKYbkCgYEAg6w+d6YBK+1uQiXGD5BC +tKS0jgjlaOfWcEW3A0qzI3Dfjf3610vdI6OPfu8dLppGhCV9HdAgPdykiQNQ+UQt +WmVcdPgA5WNCqUu7QGK0Joer52AXnkAacYHwdtHXPRkKf66n01rKK2wZexvan91A +m0gcJcFs5IYbZZy9ecvNdB8CgYEAo4JZ5Vay93j1YGnLWcrixDCp/wXYUJbOidGC +QBpZZQf3Hh11JkT7O2uSm2T727yAmw63uC2B3VotNOCLI8ZMHRLsjQ8vOCFAjqdF +rLeg3iQss/bFfkA9b1Y8VNoiVJbGC3fbWu/WDoWXxa12fL/jruG43hsGEUnJL6Q5 +K8tOdskCgYABpoHFRxsvJ5Sp9CUS3BBTicVSkpAjoX2O3+cS9XL8IsIqZEMW7VKb +16/H2BRvI0uUq12t+UCc0P0SyrWRGxwGR5zSYHVDOot5EDHqE8aYSbX4jiXtAAiu +qCn3Rug8QWyBjjxnU3CxPRiLSmEllQAAVlzfRWn6kL4RKSyruUhZaA== +-----END RSA PRIVATE KEY-----` + + defaultUserAccountPrivateKeyID = "aUGv8mQA85jg4V1DU8Uk1W0uKsxn187KQONAGl6AMtc" + + devModePublicKey = `-----BEGIN RSA PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvQ8p+HsTMrgcsuIMoOR1 +LXRhynL9YAU0qoDON6PLKCpdBv0Xy/jnsPjo5DrtUOijuJcID8CR7E0hYpY9MgK5 +H5pDFwC4lbUVENquHEVS/E0pQSKCIzSmORcIhjYW2+wKfDOVjeudZwdFBIxJ6KpI +ty/aF78hlUJZuvghFVqoHQYTq/DZOmKjS+PAVLw8FKE3wa/3WU0EkpP+iovRMCkl +lzxqrcLPIvx+T2gkwe0bn0kTvdMOhTLTN2tuvKrFpVUxVi8RM/V8PtgdKroxnES7 +SyUqK8rLO830jKJzAYrByQL+sdGuSqInIY/geahQHEGTwMI0CLj6zfhpjSgCflst +vwIDAQAB +-----END RSA PUBLIC KEY-----` + + devModePublicKeyID = "bNq-BCOR3ev-E6buGSaPrU-0SXX8whhDlmZ6geenkTE" + defaultDBPassword = "mysecretpassword" defaultGitHubClientSecret = "48d1498c849616dfecf83cf74f22dfb361ee2511" diff --git a/configuration/configuration_blackbox_test.go b/configuration/configuration_blackbox_test.go index d27cd04..f4d8a2e 100644 --- a/configuration/configuration_blackbox_test.go +++ b/configuration/configuration_blackbox_test.go @@ -48,7 +48,7 @@ func resetConfiguration() { } } -func TestGetKeycloakEndpointSetByUrlEnvVaribaleOK(t *testing.T) { +func TestGetKeycloakEndpointSetByUrlEnvVariableOK(t *testing.T) { resource.Require(t, resource.UnitTest) env := os.Getenv("AUTH_KEYCLOAK_URL") defer func() { @@ -94,9 +94,9 @@ func TestGetKeycloakEndpointAdminDevModeOK(t *testing.T) { checkGetKeycloakEndpointOK(t, config.GetKeycloakDevModeURL()+"/auth/admin/realms/"+config.GetKeycloakRealm(), config.GetKeycloakEndpointAdmin) } -func TestGetKeycloakEndpointAdminSetByEnvVaribaleOK(t *testing.T) { +func TestGetKeycloakEndpointAdminSetByEnvVariableOK(t *testing.T) { resource.Require(t, resource.UnitTest) - checkGetKeycloakEndpointSetByEnvVaribaleOK(t, "AUTH_KEYCLOAK_ENDPOINT_ADMIN", config.GetKeycloakEndpointAdmin) + checkGetKeycloakEndpointSetByEnvVariableOK(t, "AUTH_KEYCLOAK_ENDPOINT_ADMIN", config.GetKeycloakEndpointAdmin) } func TestGetKeycloakEndpointAuthzResourcesetDevModeOK(t *testing.T) { @@ -105,9 +105,9 @@ func TestGetKeycloakEndpointAuthzResourcesetDevModeOK(t *testing.T) { checkGetKeycloakEndpointOK(t, config.GetKeycloakDevModeURL()+"/auth/realms/"+config.GetKeycloakRealm()+"/authz/protection/resource_set", config.GetKeycloakEndpointAuthzResourceset) } -func TestGetKeycloakEndpointAuthzResourcesetSetByEnvVaribaleOK(t *testing.T) { +func TestGetKeycloakEndpointAuthzResourcesetSetByEnvVariableOK(t *testing.T) { resource.Require(t, resource.UnitTest) - checkGetKeycloakEndpointSetByEnvVaribaleOK(t, "AUTH_KEYCLOAK_ENDPOINT_AUTHZ_RESOURCESET", config.GetKeycloakEndpointAuthzResourceset) + checkGetKeycloakEndpointSetByEnvVariableOK(t, "AUTH_KEYCLOAK_ENDPOINT_AUTHZ_RESOURCESET", config.GetKeycloakEndpointAuthzResourceset) } func TestGetKeycloakEndpointClientsDevModeOK(t *testing.T) { @@ -116,9 +116,9 @@ func TestGetKeycloakEndpointClientsDevModeOK(t *testing.T) { checkGetKeycloakEndpointOK(t, config.GetKeycloakDevModeURL()+"/auth/admin/realms/"+config.GetKeycloakRealm()+"/clients", config.GetKeycloakEndpointClients) } -func TestGetKeycloakEndpoinClientsSetByEnvVaribaleOK(t *testing.T) { +func TestGetKeycloakEndpoinClientsSetByEnvVariableOK(t *testing.T) { resource.Require(t, resource.UnitTest) - checkGetKeycloakEndpointSetByEnvVaribaleOK(t, "AUTH_KEYCLOAK_ENDPOINT_CLIENTS", config.GetKeycloakEndpointClients) + checkGetKeycloakEndpointSetByEnvVariableOK(t, "AUTH_KEYCLOAK_ENDPOINT_CLIENTS", config.GetKeycloakEndpointClients) } func TestGetKeycloakEndpointAuthDevModeOK(t *testing.T) { @@ -127,9 +127,9 @@ func TestGetKeycloakEndpointAuthDevModeOK(t *testing.T) { checkGetKeycloakEndpointOK(t, config.GetKeycloakDevModeURL()+"/auth/realms/"+config.GetKeycloakRealm()+"/protocol/openid-connect/auth", config.GetKeycloakEndpointAuth) } -func TestGetKeycloakEndpointAuthSetByEnvVaribaleOK(t *testing.T) { +func TestGetKeycloakEndpointAuthSetByEnvVariableOK(t *testing.T) { resource.Require(t, resource.UnitTest) - checkGetKeycloakEndpointSetByEnvVaribaleOK(t, "AUTH_KEYCLOAK_ENDPOINT_AUTH", config.GetKeycloakEndpointAuth) + checkGetKeycloakEndpointSetByEnvVariableOK(t, "AUTH_KEYCLOAK_ENDPOINT_AUTH", config.GetKeycloakEndpointAuth) } func TestGetKeycloakEndpointLogoutDevModeOK(t *testing.T) { @@ -138,9 +138,9 @@ func TestGetKeycloakEndpointLogoutDevModeOK(t *testing.T) { checkGetKeycloakEndpointOK(t, config.GetKeycloakDevModeURL()+"/auth/realms/"+config.GetKeycloakRealm()+"/protocol/openid-connect/logout", config.GetKeycloakEndpointLogout) } -func TestGetKeycloakEndpointLogoutSetByEnvVaribaleOK(t *testing.T) { +func TestGetKeycloakEndpointLogoutSetByEnvVariableOK(t *testing.T) { resource.Require(t, resource.UnitTest) - checkGetKeycloakEndpointSetByEnvVaribaleOK(t, "AUTH_KEYCLOAK_ENDPOINT_LOGOUT", config.GetKeycloakEndpointLogout) + checkGetKeycloakEndpointSetByEnvVariableOK(t, "AUTH_KEYCLOAK_ENDPOINT_LOGOUT", config.GetKeycloakEndpointLogout) } func TestGetKeycloakEndpointTokenOK(t *testing.T) { @@ -149,9 +149,9 @@ func TestGetKeycloakEndpointTokenOK(t *testing.T) { checkGetKeycloakEndpointOK(t, config.GetKeycloakDevModeURL()+"/auth/realms/"+config.GetKeycloakRealm()+"/protocol/openid-connect/token", config.GetKeycloakEndpointToken) } -func TestGetKeycloakEndpointTokenSetByEnvVaribaleOK(t *testing.T) { +func TestGetKeycloakEndpointTokenSetByEnvVariableOK(t *testing.T) { resource.Require(t, resource.UnitTest) - checkGetKeycloakEndpointSetByEnvVaribaleOK(t, "AUTH_KEYCLOAK_ENDPOINT_TOKEN", config.GetKeycloakEndpointToken) + checkGetKeycloakEndpointSetByEnvVariableOK(t, "AUTH_KEYCLOAK_ENDPOINT_TOKEN", config.GetKeycloakEndpointToken) } func TestGetKeycloakEndpointUserInfoOK(t *testing.T) { @@ -168,12 +168,12 @@ func TestGetKeycloakEndpointLinkIDPOK(t *testing.T) { expectedEndpoint := config.GetKeycloakDevModeURL() + "/auth/admin/realms/" + config.GetKeycloakRealm() + "/users/" + sampleID + "/federated-identity/" + idp url, err := config.GetKeycloakEndpointLinkIDP(reqLong, sampleID, idp) assert.Nil(t, err) - // In dev mode it's always the defualt value regardless of the request + // In dev mode it's always the default value regardless of the request assert.Equal(t, expectedEndpoint, url) url, err = config.GetKeycloakEndpointLinkIDP(reqShort, sampleID, idp) assert.Nil(t, err) - // In dev mode it's always the defualt value regardless of the request + // In dev mode it's always the default value regardless of the request assert.Equal(t, expectedEndpoint, url) } @@ -183,9 +183,9 @@ func TestGetKeycloakEndpointUsersOK(t *testing.T) { checkGetKeycloakEndpointOK(t, config.GetKeycloakDevModeURL()+"/auth/admin/realms/"+config.GetKeycloakRealm()+"/users", config.GetKeycloakEndpointUsers) } -func TestGetKeycloakEndpointUserInfoSetByEnvVaribaleOK(t *testing.T) { +func TestGetKeycloakEndpointUserInfoSetByEnvVariableOK(t *testing.T) { resource.Require(t, resource.UnitTest) - checkGetKeycloakEndpointSetByEnvVaribaleOK(t, "AUTH_KEYCLOAK_ENDPOINT_USERINFO", config.GetKeycloakEndpointUserInfo) + checkGetKeycloakEndpointSetByEnvVariableOK(t, "AUTH_KEYCLOAK_ENDPOINT_USERINFO", config.GetKeycloakEndpointUserInfo) } func TestGetKeycloakEndpointEntitlementOK(t *testing.T) { @@ -194,9 +194,9 @@ func TestGetKeycloakEndpointEntitlementOK(t *testing.T) { checkGetKeycloakEndpointOK(t, config.GetKeycloakDevModeURL()+"/auth/realms/"+config.GetKeycloakRealm()+"/authz/entitlement/fabric8-online-platform", config.GetKeycloakEndpointEntitlement) } -func TestGetKeycloakEndpointEntitlementSetByEnvVaribaleOK(t *testing.T) { +func TestGetKeycloakEndpointEntitlementSetByEnvVariableOK(t *testing.T) { resource.Require(t, resource.UnitTest) - checkGetKeycloakEndpointSetByEnvVaribaleOK(t, "AUTH_KEYCLOAK_ENDPOINT_ENTITLEMENT", config.GetKeycloakEndpointEntitlement) + checkGetKeycloakEndpointSetByEnvVariableOK(t, "AUTH_KEYCLOAK_ENDPOINT_ENTITLEMENT", config.GetKeycloakEndpointEntitlement) } func TestGetKeycloakEndpointBrokerOK(t *testing.T) { @@ -205,9 +205,9 @@ func TestGetKeycloakEndpointBrokerOK(t *testing.T) { checkGetKeycloakEndpointOK(t, config.GetKeycloakDevModeURL()+"/auth/realms/"+config.GetKeycloakRealm()+"/broker", config.GetKeycloakEndpointBroker) } -func TestGetKeycloakEndpointBrokerSetByEnvVaribaleOK(t *testing.T) { +func TestGetKeycloakEndpointBrokerSetByEnvVariableOK(t *testing.T) { resource.Require(t, resource.UnitTest) - checkGetKeycloakEndpointSetByEnvVaribaleOK(t, "AUTH_KEYCLOAK_ENDPOINT_BROKER", config.GetKeycloakEndpointBroker) + checkGetKeycloakEndpointSetByEnvVariableOK(t, "AUTH_KEYCLOAK_ENDPOINT_BROKER", config.GetKeycloakEndpointBroker) } func TestGetKeycloakUserInfoEndpointOK(t *testing.T) { @@ -216,9 +216,9 @@ func TestGetKeycloakUserInfoEndpointOK(t *testing.T) { checkGetKeycloakEndpointOK(t, config.GetKeycloakDevModeURL()+"/auth/realms/"+config.GetKeycloakRealm()+"/account", config.GetKeycloakAccountEndpoint) } -func TestGetKeycloakUserInfoEndpointOKrSetByEnvVaribaleOK(t *testing.T) { +func TestGetKeycloakUserInfoEndpointOKrSetByEnvVariableOK(t *testing.T) { resource.Require(t, resource.UnitTest) - checkGetKeycloakEndpointSetByEnvVaribaleOK(t, "AUTH_KEYCLOAK_ENDPOINT_ACCOUNT", config.GetKeycloakAccountEndpoint) + checkGetKeycloakEndpointSetByEnvVariableOK(t, "AUTH_KEYCLOAK_ENDPOINT_ACCOUNT", config.GetKeycloakAccountEndpoint) } func TestGetWITURLNotDevModeOK(t *testing.T) { @@ -285,12 +285,12 @@ func TestGetWITURLSetViaEnvVarOK(t *testing.T) { func checkGetKeycloakEndpointOK(t *testing.T, expectedEndpoint string, getEndpoint func(req *goa.RequestData) (string, error)) { url, err := getEndpoint(reqLong) assert.Nil(t, err) - // In dev mode it's always the defualt value regardless of the request + // In dev mode it's always the default value regardless of the request assert.Equal(t, expectedEndpoint, url) url, err = getEndpoint(reqShort) assert.Nil(t, err) - // In dev mode it's always the defualt value regardless of the request + // In dev mode it's always the default value regardless of the request assert.Equal(t, expectedEndpoint, url) } @@ -324,7 +324,7 @@ func TestGetMaxHeaderSizeUsingDefaults(t *testing.T) { assert.Equal(t, int64(5000), viperValue) } -func TestGetMaxHeaderSizeSetByEnvVaribaleOK(t *testing.T) { +func TestGetMaxHeaderSizeSetByEnvVariableOK(t *testing.T) { resource.Require(t, resource.UnitTest) envName := "AUTH_HEADER_MAXLENGTH" envValue := time.Now().Unix() @@ -488,6 +488,33 @@ func checkCluster(t *testing.T, clusters map[string]configuration.OSOCluster, ex require.Nil(t, err) } +func TestExpiresIn(t *testing.T) { + checkExpiresIn(t, "AUTH_USERACCOUNT_TOKEN_ACCESS_EXPIRESIN", "too short lifespan of access tokens") + checkExpiresIn(t, "AUTH_USERACCOUNT_TOKEN_REFRESH_EXPIRESIN", "too short lifespan of refresh tokens") +} + +func checkExpiresIn(t *testing.T, envVarName, expectedErrorMessage string) { + resource.Require(t, resource.UnitTest) + + tokenExpiresIn := os.Getenv(envVarName) + defer func() { + os.Setenv(envVarName, tokenExpiresIn) + resetConfiguration() + }() + + // There should be an error message if expiresIn is less than 3 minutes + os.Setenv(envVarName, "179") + resetConfiguration() + + assert.Contains(t, config.DefaultConfigurationError().Error(), expectedErrorMessage) + + // No error message if expiresIn is >= 3 minutes + os.Setenv(envVarName, "180") + resetConfiguration() + + assert.NotContains(t, config.DefaultConfigurationError().Error(), expectedErrorMessage) +} + func TestIsTLSInsecureSkipVerifySetToFalse(t *testing.T) { resource.Require(t, resource.UnitTest) require.False(t, config.IsTLSInsecureSkipVerify()) @@ -497,7 +524,7 @@ func generateEnvKey(yamlKey string) string { return "AUTH_" + strings.ToUpper(strings.Replace(yamlKey, ".", "_", -1)) } -func checkGetKeycloakEndpointSetByEnvVaribaleOK(t *testing.T, envName string, getEndpoint func(req *goa.RequestData) (string, error)) { +func checkGetKeycloakEndpointSetByEnvVariableOK(t *testing.T, envName string, getEndpoint func(req *goa.RequestData) (string, error)) { envValue := uuid.NewV4().String() env := os.Getenv(envName) defer func() { diff --git a/controller/authorize.go b/controller/authorize.go index 29a3723..45abb96 100644 --- a/controller/authorize.go +++ b/controller/authorize.go @@ -68,7 +68,7 @@ func (c *AuthorizeController) Authorize(ctx *app.AuthorizeAuthorizeContext) erro ClientSecret: c.Configuration.GetKeycloakSecret(), Scopes: scope, Endpoint: oauth2.Endpoint{AuthURL: authEndpoint, TokenURL: tokenEndpoint}, - RedirectURL: rest.AbsoluteURL(ctx.RequestData, client.CallbackAuthorizePath()), + RedirectURL: rest.AbsoluteURL(ctx.RequestData, client.CallbackAuthorizePath(), nil), } redirectTo, err := c.Auth.AuthCodeURL(ctx, &ctx.RedirectURI, ctx.APIClient, &ctx.State, ctx.ResponseMode, ctx.RequestData, oauth, c.Configuration) diff --git a/controller/login.go b/controller/login.go index 12a6ea6..77c53b0 100644 --- a/controller/login.go +++ b/controller/login.go @@ -80,7 +80,7 @@ func (c *LoginController) Login(ctx *app.LoginLoginContext) error { ClientSecret: c.Configuration.GetKeycloakSecret(), Scopes: []string{"user:email"}, Endpoint: oauth2.Endpoint{AuthURL: authEndpoint, TokenURL: tokenEndpoint}, - RedirectURL: rest.AbsoluteURL(ctx.RequestData, "/api/login"), + RedirectURL: rest.AbsoluteURL(ctx.RequestData, "/api/login", nil), } ctx.ResponseData.Header().Set("Cache-Control", "no-cache") diff --git a/controller/openid_configuration.go b/controller/openid_configuration.go index 036f262..dafd4ae 100644 --- a/controller/openid_configuration.go +++ b/controller/openid_configuration.go @@ -20,12 +20,12 @@ func NewOpenidConfigurationController(service *goa.Service) *OpenidConfiguration // Show runs the show action. func (c *OpenidConfigurationController) Show(ctx *app.ShowOpenidConfigurationContext) error { - issuer := rest.AbsoluteURL(ctx.RequestData, "") - authorizationEndpoint := rest.AbsoluteURL(ctx.RequestData, client.AuthorizeAuthorizePath()) - tokenEndpoint := rest.AbsoluteURL(ctx.RequestData, client.ExchangeTokenPath()) - userinfoEndpoint := rest.AbsoluteURL(ctx.RequestData, client.ShowUserinfoPath()) - logoutEndpoint := rest.AbsoluteURL(ctx.RequestData, client.LogoutLogoutPath()) - jwksURI := rest.AbsoluteURL(ctx.RequestData, client.KeysTokenPath()) + issuer := rest.AbsoluteURL(ctx.RequestData, "", nil) + authorizationEndpoint := rest.AbsoluteURL(ctx.RequestData, client.AuthorizeAuthorizePath(), nil) + tokenEndpoint := rest.AbsoluteURL(ctx.RequestData, client.ExchangeTokenPath(), nil) + userinfoEndpoint := rest.AbsoluteURL(ctx.RequestData, client.ShowUserinfoPath(), nil) + logoutEndpoint := rest.AbsoluteURL(ctx.RequestData, client.LogoutLogoutPath(), nil) + jwksURI := rest.AbsoluteURL(ctx.RequestData, client.KeysTokenPath(), nil) authOpenIDConfiguration := &app.OpenIDConfiguration{ // REQUIRED properties diff --git a/controller/paging.go b/controller/paging.go index eea09e2..e4b2ab4 100644 --- a/controller/paging.go +++ b/controller/paging.go @@ -115,7 +115,7 @@ func setPagingLinks(links *app.PagingLinks, path string, resultLen, offset, limi } func buildAbsoluteURL(req *goa.RequestData) string { - return rest.AbsoluteURL(req, req.URL.Path) + return rest.AbsoluteURL(req, req.URL.Path, nil) } func parseInts(s *string) ([]int, error) { diff --git a/controller/status_test.go b/controller/status_test.go index f64ede5..74eb08f 100644 --- a/controller/status_test.go +++ b/controller/status_test.go @@ -19,8 +19,8 @@ import ( ) const ( - expectedDefaultConfDevModeErrorMessage = "Error: /etc/fabric8/service-account-secrets.conf is not used; /etc/fabric8/oso-clusters.conf is not used; developer Mode is enabled; default service account private key is used; default service account private key ID is used; default DB password is used; default Keycloak client secret is used; default GitHub client secret is used; no restrictions for valid redirect URLs; notification service url is empty" - expectedDefaultConfProdModeErrorMessage = "Error: /etc/fabric8/service-account-secrets.conf is not used; /etc/fabric8/oso-clusters.conf is not used; default service account private key is used; default service account private key ID is used; default DB password is used; default Keycloak client secret is used; default GitHub client secret is used; notification service url is empty" + expectedDefaultConfDevModeErrorMessage = "Error: /etc/fabric8/service-account-secrets.conf is not used; /etc/fabric8/oso-clusters.conf is not used; developer Mode is enabled; default service account private key is used; default service account private key ID is used; default user account private key is used; default user account private key ID is used; default DB password is used; default Keycloak client secret is used; default GitHub client secret is used; no restrictions for valid redirect URLs; notification service url is empty" + expectedDefaultConfProdModeErrorMessage = "Error: /etc/fabric8/service-account-secrets.conf is not used; /etc/fabric8/oso-clusters.conf is not used; default service account private key is used; default service account private key ID is used; default user account private key is used; default user account private key ID is used; default DB password is used; default Keycloak client secret is used; default GitHub client secret is used; notification service url is empty" ) type TestStatusREST struct { diff --git a/controller/test-files/token/keys/ok_jwk.golden.json b/controller/test-files/token/keys/ok_jwk.golden.json index 44f161f..033db65 100644 --- a/controller/test-files/token/keys/ok_jwk.golden.json +++ b/controller/test-files/token/keys/ok_jwk.golden.json @@ -3,9 +3,9 @@ { "alg": "RS256", "e": "AQAB", - "kid": "bNq-BCOR3ev-E6buGSaPrU-0SXX8whhDlmZ6geenkTE", + "kid": "aUGv8mQA85jg4V1DU8Uk1W0uKsxn187KQONAGl6AMtc", "kty": "RSA", - "n": "vQ8p-HsTMrgcsuIMoOR1LXRhynL9YAU0qoDON6PLKCpdBv0Xy_jnsPjo5DrtUOijuJcID8CR7E0hYpY9MgK5H5pDFwC4lbUVENquHEVS_E0pQSKCIzSmORcIhjYW2-wKfDOVjeudZwdFBIxJ6KpIty_aF78hlUJZuvghFVqoHQYTq_DZOmKjS-PAVLw8FKE3wa_3WU0EkpP-iovRMCkllzxqrcLPIvx-T2gkwe0bn0kTvdMOhTLTN2tuvKrFpVUxVi8RM_V8PtgdKroxnES7SyUqK8rLO830jKJzAYrByQL-sdGuSqInIY_geahQHEGTwMI0CLj6zfhpjSgCflstvw", + "n": "40yB6SNoU4SpWxTfG5ilu-BlLYikRyyEcJIGg__w_GyqtjvT_CVo92DRTh_DlrgwjSitmZrhauBnrCOoUBMin0_TXeSo3w2M5tEiiIFPbTDRf2jMfbSGEOke9O0USCCR-bM2TncrgZR74qlSwq38VCND4zHc89rAzqJ2LVM2aXkuBbO7TcgLNyooBrpOK9khVHAD64cyODAdJY4esUjcLdlcB7TMDGOgxGGn2RARU7-TUf32gZZbTMikbuPM5gXuzGlo_22ECbQSKuZpbGwgPIAZ5NN9QA4D1NRz9-KDoiXZ6deZTTVCrZykJJ6RyLNfRh-XS-6G5nvcqAmfBpyOWw", "use": "sig" }, { @@ -15,6 +15,14 @@ "kty": "RSA", "n": "nwrjH5iTSErw9xUptp6QSFoUfpHUXZ-PaslYSUrpLjw1q27ODSFwmhV4-dAaTMO5chFv_kM36H3ZOyA146nwxBobS723okFaIkshRrf6qgtD6coTHlVUSBTAcwKEjNn4C9jtEpyOl-eSgxhMzRH3bwTIFlLlVMiZf7XVE7P3yuOCpqkk2rdYVSpQWQWKU-ZRywJkYcLwjEYjc70AoNpjO5QnY-Exx98E30iEdPHZpsfNhsjh9Z7IX5TrMYgz7zBTw8-niO_uq3RBaHyIhDbvenbR9Q59d88lbnEeHKgSMe2RQpFR3rxFRkc_64Rn_bMuL_ptNowPqh1P-9GjYzWmPw", "use": "sig" + }, + { + "alg": "RS256", + "e": "AQAB", + "kid": "bNq-BCOR3ev-E6buGSaPrU-0SXX8whhDlmZ6geenkTE", + "kty": "RSA", + "n": "vQ8p-HsTMrgcsuIMoOR1LXRhynL9YAU0qoDON6PLKCpdBv0Xy_jnsPjo5DrtUOijuJcID8CR7E0hYpY9MgK5H5pDFwC4lbUVENquHEVS_E0pQSKCIzSmORcIhjYW2-wKfDOVjeudZwdFBIxJ6KpIty_aF78hlUJZuvghFVqoHQYTq_DZOmKjS-PAVLw8FKE3wa_3WU0EkpP-iovRMCkllzxqrcLPIvx-T2gkwe0bn0kTvdMOhTLTN2tuvKrFpVUxVi8RM_V8PtgdKroxnES7SyUqK8rLO830jKJzAYrByQL-sdGuSqInIY_geahQHEGTwMI0CLj6zfhpjSgCflstvw", + "use": "sig" } ] } \ No newline at end of file diff --git a/controller/test-files/token/keys/ok_pem.golden.json b/controller/test-files/token/keys/ok_pem.golden.json index 3b51508..4274d76 100644 --- a/controller/test-files/token/keys/ok_pem.golden.json +++ b/controller/test-files/token/keys/ok_pem.golden.json @@ -1,12 +1,16 @@ { "keys": [ { - "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvQ8p+HsTMrgcsuIMoOR1LXRhynL9YAU0qoDON6PLKCpdBv0Xy/jnsPjo5DrtUOijuJcID8CR7E0hYpY9MgK5H5pDFwC4lbUVENquHEVS/E0pQSKCIzSmORcIhjYW2+wKfDOVjeudZwdFBIxJ6KpIty/aF78hlUJZuvghFVqoHQYTq/DZOmKjS+PAVLw8FKE3wa/3WU0EkpP+iovRMCkllzxqrcLPIvx+T2gkwe0bn0kTvdMOhTLTN2tuvKrFpVUxVi8RM/V8PtgdKroxnES7SyUqK8rLO830jKJzAYrByQL+sdGuSqInIY/geahQHEGTwMI0CLj6zfhpjSgCflstvwIDAQAB", - "kid": "bNq-BCOR3ev-E6buGSaPrU-0SXX8whhDlmZ6geenkTE" + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA40yB6SNoU4SpWxTfG5ilu+BlLYikRyyEcJIGg//w/GyqtjvT/CVo92DRTh/DlrgwjSitmZrhauBnrCOoUBMin0/TXeSo3w2M5tEiiIFPbTDRf2jMfbSGEOke9O0USCCR+bM2TncrgZR74qlSwq38VCND4zHc89rAzqJ2LVM2aXkuBbO7TcgLNyooBrpOK9khVHAD64cyODAdJY4esUjcLdlcB7TMDGOgxGGn2RARU7+TUf32gZZbTMikbuPM5gXuzGlo/22ECbQSKuZpbGwgPIAZ5NN9QA4D1NRz9+KDoiXZ6deZTTVCrZykJJ6RyLNfRh+XS+6G5nvcqAmfBpyOWwIDAQAB", + "kid": "aUGv8mQA85jg4V1DU8Uk1W0uKsxn187KQONAGl6AMtc" }, { "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnwrjH5iTSErw9xUptp6QSFoUfpHUXZ+PaslYSUrpLjw1q27ODSFwmhV4+dAaTMO5chFv/kM36H3ZOyA146nwxBobS723okFaIkshRrf6qgtD6coTHlVUSBTAcwKEjNn4C9jtEpyOl+eSgxhMzRH3bwTIFlLlVMiZf7XVE7P3yuOCpqkk2rdYVSpQWQWKU+ZRywJkYcLwjEYjc70AoNpjO5QnY+Exx98E30iEdPHZpsfNhsjh9Z7IX5TrMYgz7zBTw8+niO/uq3RBaHyIhDbvenbR9Q59d88lbnEeHKgSMe2RQpFR3rxFRkc/64Rn/bMuL/ptNowPqh1P+9GjYzWmPwIDAQAB", "kid": "9MLnViaRkhVj1GT9kpWUkwHIwUD-wZfUxR-3CpkE-Xs" + }, + { + "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvQ8p+HsTMrgcsuIMoOR1LXRhynL9YAU0qoDON6PLKCpdBv0Xy/jnsPjo5DrtUOijuJcID8CR7E0hYpY9MgK5H5pDFwC4lbUVENquHEVS/E0pQSKCIzSmORcIhjYW2+wKfDOVjeudZwdFBIxJ6KpIty/aF78hlUJZuvghFVqoHQYTq/DZOmKjS+PAVLw8FKE3wa/3WU0EkpP+iovRMCkllzxqrcLPIvx+T2gkwe0bn0kTvdMOhTLTN2tuvKrFpVUxVi8RM/V8PtgdKroxnES7SyUqK8rLO830jKJzAYrByQL+sdGuSqInIY/geahQHEGTwMI0CLj6zfhpjSgCflstvwIDAQAB", + "kid": "bNq-BCOR3ev-E6buGSaPrU-0SXX8whhDlmZ6geenkTE" } ] } \ No newline at end of file diff --git a/controller/token.go b/controller/token.go index 4228f5a..689b74e 100644 --- a/controller/token.go +++ b/controller/token.go @@ -22,7 +22,6 @@ import ( "github.com/fabric8-services/fabric8-auth/token/keycloak" "github.com/fabric8-services/fabric8-auth/token/link" "github.com/fabric8-services/fabric8-auth/token/provider" - "github.com/fabric8-services/fabric8-auth/wit" "github.com/goadesign/goa" goajwt "github.com/goadesign/goa/middleware/security/jwt" @@ -106,78 +105,54 @@ func convertToken(t token.TokenSet) *app.AuthToken { // Generate obtain the access token from Keycloak for the test user func (c *TokenController) Generate(ctx *app.GenerateTokenContext) error { - var tokens app.AuthTokenCollection - - tokenEndpoint, err := c.Configuration.GetKeycloakEndpointToken(ctx.RequestData) - if err != nil { - log.Error(ctx, map[string]interface{}{ - "err": err, - }, "unable to get Keycloak token endpoint URL") - return jsonapi.JSONErrorResponse(ctx, errors.NewInternalError(ctx, errs.Wrap(err, "unable to get Keycloak token endpoint URL"))) - } - - testuser, err := GenerateUserToken(ctx, tokenEndpoint, c.Configuration, c.Configuration.GetKeycloakTestUserName(), c.Configuration.GetKeycloakTestUserSecret()) - if err != nil { - log.Error(ctx, map[string]interface{}{ - "err": err, - }, "unable to get Generate User token") - return jsonapi.JSONErrorResponse(ctx, errors.NewInternalError(ctx, errs.Wrap(err, "unable to generate test token "))) + if !c.Configuration.IsPostgresDeveloperModeEnabled() { + log.Error(ctx, map[string]interface{}{}, "developer mode not enabled") + return jsonapi.JSONErrorResponse(ctx, errors.NewInternalError(ctx, errs.New("postgres developer mode is not enabled"))) } - identity, _, err := c.Auth.CreateOrUpdateIdentityInDB(ctx, *testuser.Token.AccessToken, c.Configuration) - if err != nil { - log.Error(ctx, map[string]interface{}{ - "err": err, - }, "unable to persist user properly") - } - tokens = append(tokens, testuser) - - var remoteWITService wit.RemoteWITServiceCaller - witURL, err := c.Configuration.GetWITURL(ctx.RequestData) + devUsername := "developer" + var identities []account.Identity + err := application.Transactional(c.db, func(appl application.Application) error { + var err error + identities, err = appl.Identities().Query(account.IdentityWithUser(), account.IdentityFilterByUsername(devUsername), account.IdentityFilterByProviderType(account.KeycloakIDP)) + return err + }) if err != nil { return jsonapi.JSONErrorResponse(ctx, err) } - if identity != nil { - err = remoteWITService.CreateWITUser(ctx, ctx.RequestData, identity, witURL, identity.ID.String()) - if err != nil { - log.Warn(ctx, map[string]interface{}{ - "err": err, - "identity_id": identity.ID, - "username": identity.Username, - "wit_url": witURL, - }, "unable to create user in WIT ") + var devIdentity account.Identity + if len(identities) == 0 { + // Dev user doesn't exist yet. Let's create it. + devUser := account.User{ + EmailVerified: true, + FullName: "OSIO Developer", + Email: "osio-developer@email.com", } + devIdentity = account.Identity{ + User: devUser, + Username: devUsername, + ProviderType: account.KeycloakIDP, + RegistrationCompleted: true, + } + } else { + devIdentity = identities[0] } - testuser, err = GenerateUserToken(ctx, tokenEndpoint, c.Configuration, c.Configuration.GetKeycloakTestUser2Name(), c.Configuration.GetKeycloakTestUser2Secret()) + generatedToken, err := c.TokenManager.GenerateUserTokenForIdentity(ctx, devIdentity) if err != nil { - log.Error(ctx, map[string]interface{}{ - "err": err, - }, "unable to generate test token") - return jsonapi.JSONErrorResponse(ctx, errors.NewInternalError(ctx, errs.Wrap(err, "unable to generate test token"))) + return jsonapi.JSONErrorResponse(ctx, err) } - // Creates the testuser2 user and identity if they don't yet exist - identity, _, err = c.Auth.CreateOrUpdateIdentityInDB(ctx, *testuser.Token.AccessToken, c.Configuration) + _, token, err := c.Auth.CreateOrUpdateIdentityAndUser(ctx, ctx.RequestData.URL, generatedToken, ctx.RequestData, c.Configuration) if err != nil { - log.Error(ctx, map[string]interface{}{ - "err": err, - }, "unable to persist user properly") + return jsonapi.JSONErrorResponse(ctx, err) } - tokens = append(tokens, testuser) - - if identity != nil { - err = remoteWITService.CreateWITUser(ctx, ctx.RequestData, identity, witURL, identity.ID.String()) - if err != nil { - log.Warn(ctx, map[string]interface{}{ - "err": err, - "identity_id": identity.ID, - "username": identity.Username, - "wit_url": witURL, - }, "unable to create user in WIT ") - } + tokenSet, err := c.TokenManager.ConvertToken(*token) + if err != nil { + return jsonapi.JSONErrorResponse(ctx, err) } + tokens := app.AuthTokenCollection{convertToken(*tokenSet)} ctx.ResponseData.Header().Set("Cache-Control", "no-cache") return ctx.OK(tokens) @@ -274,7 +249,7 @@ func (c *TokenController) retrieveToken(ctx context.Context, forResource string, "provider_name": providerName, }, "Unable to obtain external token from Keycloak. Account linking may be required.") - linkURL := rest.AbsoluteURL(req, fmt.Sprintf("%s?for=%s", client.LinkTokenPath(), forResource)) + linkURL := rest.AbsoluteURL(req, fmt.Sprintf("%s?for=%s", client.LinkTokenPath(), forResource), nil) errorResponse := fmt.Sprintf("LINK url=%s, description=\"%s token is missing. Link %s account\"", linkURL, providerName, providerName) return nil, &errorResponse, errors.NewUnauthorizedError("token is missing") } @@ -497,7 +472,7 @@ func (c *TokenController) exchangeWithGrantTypeAuthorizationCode(ctx *app.Exchan ClientID: c.Configuration.GetKeycloakClientID(), ClientSecret: c.Configuration.GetKeycloakSecret(), Endpoint: oauth2.Endpoint{AuthURL: authEndpoint, TokenURL: tokenEndpoint}, - RedirectURL: rest.AbsoluteURL(ctx.RequestData, client.CallbackAuthorizePath()), + RedirectURL: rest.AbsoluteURL(ctx.RequestData, client.CallbackAuthorizePath(), nil), } ctx.ResponseData.Header().Set("Cache-Control", "no-cache") @@ -517,14 +492,14 @@ func (c *TokenController) exchangeWithGrantTypeAuthorizationCode(ctx *app.Exchan return nil, errors.NewInternalError(ctx, err) } - _, err = c.Auth.CreateOrUpdateIdentityAndUser(ctx, redirectURL, keycloakToken, ctx.RequestData, c.Configuration) + _, userToken, err := c.Auth.CreateOrUpdateIdentityAndUser(ctx, redirectURL, keycloakToken, ctx.RequestData, c.Configuration) if err != nil { return nil, err } // Convert expiry to expire_in - expiry := keycloakToken.Expiry + expiry := userToken.Expiry var expireIn *string if expiry != *new(time.Time) { exp := expiry.Sub(time.Now()) @@ -535,10 +510,10 @@ func (c *TokenController) exchangeWithGrantTypeAuthorizationCode(ctx *app.Exchan } token := &app.OauthToken{ - AccessToken: &keycloakToken.AccessToken, + AccessToken: &userToken.AccessToken, ExpiresIn: expireIn, - RefreshToken: &keycloakToken.RefreshToken, - TokenType: &keycloakToken.TokenType, + RefreshToken: &userToken.RefreshToken, + TokenType: &userToken.TokenType, } return token, nil @@ -614,7 +589,7 @@ func (c *TokenController) updateProfileIfEmpty(ctx context.Context, forResource "for": forResource, "provider_name": providerConfig.TypeName(), }, "Unable to fetch user profile for external token. Account relinking may be required.") - linkURL := rest.AbsoluteURL(req, fmt.Sprintf("%s?for=%s", client.LinkTokenPath(), forResource)) + linkURL := rest.AbsoluteURL(req, fmt.Sprintf("%s?for=%s", client.LinkTokenPath(), forResource), nil) errorResponse := fmt.Sprintf("LINK url=%s, description=\"%s token is not valid or expired. Relink %s account\"", linkURL, providerConfig.TypeName(), providerConfig.TypeName()) return externalToken, &errorResponse, errors.NewUnauthorizedError(err.Error()) } @@ -656,8 +631,8 @@ func modelToAppExternalToken(externalToken provider.ExternalToken, providerAPIUR } } -// GenerateUserToken obtains the access token from Keycloak for the user -func GenerateUserToken(ctx context.Context, tokenEndpoint string, configuration LoginConfiguration, username string, userSecret string) (*app.AuthToken, error) { +// ObtainKeycloakUserToken obtains the access token from Keycloak for the user +func ObtainKeycloakUserToken(ctx context.Context, tokenEndpoint string, configuration LoginConfiguration, username string, userSecret string) (*app.AuthToken, error) { if !configuration.IsPostgresDeveloperModeEnabled() { log.Error(ctx, map[string]interface{}{ "method": "Generate", diff --git a/controller/token_blackbox_test.go b/controller/token_blackbox_test.go index 5153c25..094dd14 100644 --- a/controller/token_blackbox_test.go +++ b/controller/token_blackbox_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "golang.org/x/oauth2" + "path/filepath" ) type TestTokenREST struct { @@ -34,6 +35,7 @@ type TestTokenREST struct { sampleAccessToken string sampleRefreshToken string exchangeStrategy string + testDir string } func TestRunTokenREST(t *testing.T) { @@ -48,10 +50,13 @@ func (rest *TestTokenREST) SetupSuite() { rest.DBTestSuite.SetupSuite() claims := make(map[string]interface{}) - act, err := testtoken.GenerateTokenWithClaims(claims) + act, err := testtoken.GenerateAccessTokenWithClaims(claims) require.Nil(rest.T(), err) rest.sampleAccessToken = act - rest.sampleRefreshToken = uuid.NewV4().String() + act, err = testtoken.GenerateRefreshTokenWithClaims(claims) + require.Nil(rest.T(), err) + rest.sampleRefreshToken = act + rest.testDir = filepath.Join("test-files", "token") } func (rest *TestTokenREST) SetupTest() { @@ -59,6 +64,23 @@ func (rest *TestTokenREST) SetupTest() { rest.exchangeStrategy = "" } +func (rest *TestTokenREST) UnSecuredController() (*goa.Service, *TokenController) { + svc := goa.New("Token-Service") + manager, err := token.NewManager(rest.Configuration) + require.Nil(rest.T(), err) + + loginService := &DummyKeycloakOAuthService{} + profileService := login.NewKeycloakUserProfileClient() + loginService.KeycloakOAuthProvider = *login.NewKeycloakOAuthProvider(rest.Application.Identities(), rest.Application.Users(), testtoken.TokenManager, rest.Application, profileService, nil) + loginService.Identities = rest.Application.Identities() + loginService.Users = rest.Application.Users() + loginService.TokenManager = manager + loginService.DB = rest.Application + loginService.RemoteWITService = &wit.RemoteWITServiceCaller{} + + return svc, NewTokenController(svc, rest.Application, loginService, nil, nil, manager, nil, rest.Configuration) +} + func (rest *TestTokenREST) SecuredControllerWithNonExistentIdentity() (*goa.Service, *TokenController) { return rest.SecuredControllerWithIdentity(testsupport.TestIdentity) } @@ -80,6 +102,12 @@ func (rest *TestTokenREST) SecuredControllerWithIdentity(identity account.Identi loginService.DB = rest.Application loginService.RemoteWITService = &wit.RemoteWITServiceCaller{} loginService.exchangeStrategy = rest.exchangeStrategy + + tokenSet, err := testtoken.GenerateUserTokenForIdentity(context.Background(), identity) + require.Nil(rest.T(), err) + rest.sampleAccessToken = tokenSet.AccessToken + rest.sampleRefreshToken = tokenSet.RefreshToken + loginService.accessToken = rest.sampleAccessToken loginService.refreshToken = rest.sampleRefreshToken @@ -89,6 +117,33 @@ func (rest *TestTokenREST) SecuredControllerWithIdentity(identity account.Identi return svc, NewTokenController(svc, rest.Application, loginService, linkService, nil, loginService.TokenManager, newMockKeycloakExternalTokenServiceClient(), rest.Configuration) } +func (rest *TestTokenREST) TestPublicKeys() { + svc, ctrl := rest.UnSecuredController() + + rest.T().Run("file not found", func(t *testing.T) { + _, keys := test.KeysTokenOK(rest.T(), svc.Context, svc, ctrl, nil) + rest.checkJWK(keys) + }) + rest.T().Run("file not found", func(t *testing.T) { + jwk := "jwk" + _, keys := test.KeysTokenOK(rest.T(), svc.Context, svc, ctrl, &jwk) + rest.checkJWK(keys) + }) + rest.T().Run("file not found", func(t *testing.T) { + pem := "pem" + _, keys := test.KeysTokenOK(rest.T(), svc.Context, svc, ctrl, &pem) + rest.checkPEM(keys) + }) +} + +func (rest *TestTokenREST) checkPEM(keys *app.PublicKeys) { + compareWithGolden(rest.T(), filepath.Join(rest.testDir, "keys", "ok_pem.golden.json"), keys) +} + +func (rest *TestTokenREST) checkJWK(keys *app.PublicKeys) { + compareWithGolden(rest.T(), filepath.Join(rest.testDir, "keys", "ok_jwk.golden.json"), keys) +} + func (rest *TestTokenREST) TestRefreshTokenUsingNilTokenFails() { t := rest.T() service, controller := rest.SecuredController() @@ -121,7 +176,7 @@ func (rest *TestTokenREST) TestRefreshTokenUsingCorrectRefreshTokenOK() { _, authToken := test.RefreshTokenOK(t, service.Context, service, controller, payload) token := authToken.Token require.NotNil(rest.T(), token.TokenType) - require.Equal(rest.T(), "bearer", *token.TokenType) + require.Equal(rest.T(), "Bearer", *token.TokenType) require.NotNil(rest.T(), token.AccessToken) require.Equal(rest.T(), rest.sampleAccessToken, *token.AccessToken) require.NotNil(rest.T(), token.RefreshToken) @@ -130,6 +185,7 @@ func (rest *TestTokenREST) TestRefreshTokenUsingCorrectRefreshTokenOK() { require.True(rest.T(), ok) require.True(rest.T(), *expiresIn > 60*59*24*30 && *expiresIn < 60*61*24*30) // The expires_in should be withing a minute range of 30 days. } + func (rest *TestTokenREST) TestLinkForNonExistentUserFails() { service, controller := rest.SecuredControllerWithNonExistentIdentity() @@ -234,6 +290,23 @@ func (rest *TestTokenREST) TestExchangeWithCorrectRefreshTokenOK() { rest.checkExchangeWithRefreshToken(service, controller, controller.Configuration.GetPublicOauthClientID(), "SOME_REFRESH_TOKEN") } +func (rest *TestTokenREST) TestGenerateOK() { + svc, ctrl := rest.UnSecuredController() + _, result := test.GenerateTokenOK(rest.T(), svc.Context, svc, ctrl) + require.Len(rest.T(), result, 1) + validateToken(rest.T(), result[0]) +} + +func validateToken(t *testing.T, token *app.AuthToken) { + assert.NotNil(t, token, "Token data is nil") + assert.NotEmpty(t, token.Token.AccessToken, "Access token is empty") + assert.NotEmpty(t, token.Token.RefreshToken, "Refresh token is empty") + assert.NotEmpty(t, token.Token.TokenType, "Token type is empty") + assert.NotNil(t, token.Token.ExpiresIn, "Expires-in is nil") + assert.NotNil(t, token.Token.RefreshExpiresIn, "Refresh-expires-in is nil") + assert.NotNil(t, token.Token.NotBeforePolicy, "Not-before-policy is nil") +} + func (rest *TestTokenREST) checkServiceAccountCredentials(name string, id string, secret string) { service, controller := rest.SecuredController() @@ -256,9 +329,9 @@ func (rest *TestTokenREST) checkAuthorizationCode(service *goa.Service, controll require.NotNil(rest.T(), token.TokenType) require.Equal(rest.T(), "bearer", *token.TokenType) require.NotNil(rest.T(), token.AccessToken) - require.Equal(rest.T(), rest.sampleAccessToken, *token.AccessToken) + assert.NoError(rest.T(), testtoken.EqualAccessTokens(context.Background(), rest.sampleAccessToken, *token.AccessToken)) require.NotNil(rest.T(), token.RefreshToken) - require.Equal(rest.T(), rest.sampleRefreshToken, *token.RefreshToken) + assert.NoError(rest.T(), testtoken.EqualRefreshTokens(context.Background(), rest.sampleRefreshToken, *token.RefreshToken)) expiresIn, err := strconv.Atoi(*token.ExpiresIn) require.Nil(rest.T(), err) require.True(rest.T(), expiresIn > 60*59*24*30 && expiresIn < 60*61*24*30) // The expires_in should be withing a minute range of 30 days. @@ -268,7 +341,7 @@ func (rest *TestTokenREST) checkExchangeWithRefreshToken(service *goa.Service, c _, token := test.ExchangeTokenOK(rest.T(), service.Context, service, controller, &app.TokenExchange{GrantType: "refresh_token", ClientID: rest.Configuration.GetPublicOauthClientID(), RefreshToken: &refreshToken}) require.NotNil(rest.T(), token.TokenType) - require.Equal(rest.T(), "bearer", *token.TokenType) + require.Equal(rest.T(), "Bearer", *token.TokenType) require.NotNil(rest.T(), token.AccessToken) require.Equal(rest.T(), rest.sampleAccessToken, *token.AccessToken) require.NotNil(rest.T(), token.RefreshToken) @@ -300,10 +373,10 @@ func (s *DummyKeycloakOAuthService) Exchange(ctx context.Context, code string, c if s.exchangeStrategy == "401" { return nil, errors.NewUnauthorizedError("failed") } - var thirtyDays int64 + var thirtyDays, nbf int64 thirtyDays = 60 * 60 * 24 * 30 token := &oauth2.Token{ - TokenType: "bearer", + TokenType: "Bearer", AccessToken: s.accessToken, RefreshToken: s.refreshToken, Expiry: time.Unix(time.Now().Unix()+thirtyDays, 0), @@ -311,6 +384,7 @@ func (s *DummyKeycloakOAuthService) Exchange(ctx context.Context, code string, c extra := make(map[string]interface{}) extra["expires_in"] = thirtyDays extra["refresh_expires_in"] = thirtyDays + extra["not_before_policy"] = nbf token = token.WithExtra(extra) return token, nil } @@ -322,7 +396,7 @@ func (s *DummyKeycloakOAuthService) ExchangeRefreshToken(ctx context.Context, re var thirtyDays int64 thirtyDays = 60 * 60 * 24 * 30 - bearer := "bearer" + bearer := "Bearer" token := &token.TokenSet{ TokenType: &bearer, AccessToken: &s.accessToken, diff --git a/controller/token_remote_test.go b/controller/token_remote_test.go deleted file mode 100644 index ca61462..0000000 --- a/controller/token_remote_test.go +++ /dev/null @@ -1,190 +0,0 @@ -package controller_test - -import ( - "context" - "path/filepath" - "testing" - - "github.com/fabric8-services/fabric8-auth/account" - "github.com/fabric8-services/fabric8-auth/app" - "github.com/fabric8-services/fabric8-auth/app/test" - "github.com/fabric8-services/fabric8-auth/application" - "github.com/fabric8-services/fabric8-auth/auth" - resource "github.com/fabric8-services/fabric8-auth/authorization/resource/repository" - resourcetype "github.com/fabric8-services/fabric8-auth/authorization/resourcetype/repository" - scope "github.com/fabric8-services/fabric8-auth/authorization/resourcetype/scope/repository" - identityrole "github.com/fabric8-services/fabric8-auth/authorization/role/identityrole/repository" - role "github.com/fabric8-services/fabric8-auth/authorization/role/repository" - . "github.com/fabric8-services/fabric8-auth/controller" - "github.com/fabric8-services/fabric8-auth/errors" - "github.com/fabric8-services/fabric8-auth/space" - testsupport "github.com/fabric8-services/fabric8-auth/test" - testsuite "github.com/fabric8-services/fabric8-auth/test/suite" - "github.com/fabric8-services/fabric8-auth/token" - "github.com/fabric8-services/fabric8-auth/token/provider" - - "github.com/goadesign/goa" - "github.com/jinzhu/gorm" - "github.com/satori/go.uuid" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -type TestTokenRemoteREST struct { - testsuite.RemoteTestSuite - testDir string -} - -func TestRunTokenRemoteREST(t *testing.T) { - suite.Run(t, &TestTokenRemoteREST{RemoteTestSuite: testsuite.NewRemoteTestSuite()}) -} - -func (rest *TestTokenRemoteREST) SetupTest() { - rest.testDir = filepath.Join("test-files", "token") -} - -func (rest *TestTokenRemoteREST) TearDownTest() { -} - -func (rest *TestTokenRemoteREST) UnSecuredController() (*goa.Service, *TokenController) { - svc := goa.New("Token-Service") - manager, err := token.NewManager(rest.Config) - require.Nil(rest.T(), err) - return svc, NewTokenController(svc, &MockDBApp{}, nil, nil, nil, manager, nil, rest.Config) -} - -func (rest *TestTokenRemoteREST) UnSecuredControllerWithDummyDB() (*goa.Service, *TokenController) { - loginService := newTestKeycloakOAuthProvider(&MockDBApp{}) - - svc := testsupport.ServiceAsUser("Token-Service", testsupport.TestIdentity) - return svc, NewTokenController(svc, nil, loginService, nil, nil, loginService.TokenManager, newMockKeycloakExternalTokenServiceClient(), rest.Config) -} - -func (rest *TestTokenRemoteREST) TestPublicKeys() { - svc, ctrl := rest.UnSecuredController() - - rest.T().Run("file not found", func(t *testing.T) { - _, keys := test.KeysTokenOK(rest.T(), svc.Context, svc, ctrl, nil) - rest.checkJWK(keys) - }) - rest.T().Run("file not found", func(t *testing.T) { - jwk := "jwk" - _, keys := test.KeysTokenOK(rest.T(), svc.Context, svc, ctrl, &jwk) - rest.checkJWK(keys) - }) - rest.T().Run("file not found", func(t *testing.T) { - pem := "pem" - _, keys := test.KeysTokenOK(rest.T(), svc.Context, svc, ctrl, &pem) - rest.checkPEM(keys) - }) -} - -func (rest *TestTokenRemoteREST) TestTestUserTokenObtainedFromKeycloakOK() { - t := rest.T() - service, controller := rest.UnSecuredControllerWithDummyDB() - resp, result := test.GenerateTokenOK(t, service.Context, service, controller) - - require.Equal(t, resp.Header().Get("Cache-Control"), "no-cache") - require.Len(t, result, 2, "The size of token array is not 2") - for _, data := range result { - validateToken(t, data) - } -} - -func (rest *TestTokenRemoteREST) TestRefreshTokenUsingValidRefreshTokenOK() { - t := rest.T() - service, controller := rest.UnSecuredControllerWithDummyDB() - _, result := test.GenerateTokenOK(t, service.Context, service, controller) - if len(result) != 2 || result[0].Token.RefreshToken == nil { - t.Fatal("Can't get the test user token") - } - refreshToken := result[0].Token.RefreshToken - - payload := &app.RefreshToken{RefreshToken: refreshToken} - resp, newToken := test.RefreshTokenOK(t, service.Context, service, controller, payload) - - require.Equal(t, resp.Header().Get("Cache-Control"), "no-cache") - validateToken(t, newToken) -} - -func (rest *TestTokenRemoteREST) TestRefreshTokenUsingInvalidTokenFails() { - t := rest.T() - service, controller := rest.UnSecuredControllerWithDummyDB() - - refreshToken := "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.S-vR8LZTQ92iqGCR3rNUG0MiGx2N5EBVq0frCHP_bJ8" - payload := &app.RefreshToken{RefreshToken: &refreshToken} - _, err := test.RefreshTokenUnauthorized(t, service.Context, service, controller, payload) - require.NotNil(t, err) -} - -func validateToken(t *testing.T, token *app.AuthToken) { - assert.NotNil(t, token, "Token data is nil") - assert.NotEmpty(t, token.Token.AccessToken, "Access token is empty") - assert.NotEmpty(t, token.Token.RefreshToken, "Refresh token is empty") - assert.NotEmpty(t, token.Token.TokenType, "Token type is empty") - assert.NotNil(t, token.Token.ExpiresIn, "Expires-in is nil") - assert.NotNil(t, token.Token.RefreshExpiresIn, "Refresh-expires-in is nil") - assert.NotNil(t, token.Token.NotBeforePolicy, "Not-before-policy is nil") -} - -func (rest *TestTokenRemoteREST) checkPEM(keys *app.PublicKeys) { - compareWithGolden(rest.T(), filepath.Join(rest.testDir, "keys", "ok_pem.golden.json"), keys) -} - -func (rest *TestTokenRemoteREST) checkJWK(keys *app.PublicKeys) { - compareWithGolden(rest.T(), filepath.Join(rest.testDir, "keys", "ok_jwk.golden.json"), keys) -} - -type MockDBApp struct { -} - -func (m *MockDBApp) Identities() account.IdentityRepository { return &MockIdentityRepository{} } -func (m *MockDBApp) SpaceResources() space.ResourceRepository { return nil } -func (m *MockDBApp) Users() account.UserRepository { return nil } -func (m *MockDBApp) OauthStates() auth.OauthStateReferenceRepository { return nil } -func (m *MockDBApp) ExternalTokens() provider.ExternalTokenRepository { return nil } -func (m *MockDBApp) VerificationCodes() account.VerificationCodeRepository { return nil } -func (m *MockDBApp) ResourceRepository() resource.ResourceRepository { return nil } -func (m *MockDBApp) ResourceTypeRepository() resourcetype.ResourceTypeRepository { return nil } -func (m *MockDBApp) ResourceTypeScopeRepository() scope.ResourceTypeScopeRepository { return nil } -func (m *MockDBApp) IdentityRoleRepository() identityrole.IdentityRoleRepository { return nil } -func (m *MockDBApp) RoleRepository() role.RoleRepository { return nil } - -func (m *MockDBApp) BeginTransaction() (application.Transaction, error) { - return &MockDBApp{}, nil -} - -func (m *MockDBApp) Commit() error { return nil } -func (m *MockDBApp) Rollback() error { return nil } - -type MockIdentityRepository struct { -} - -func (m *MockIdentityRepository) CheckExists(ctx context.Context, id string) error { return nil } -func (m *MockIdentityRepository) Load(ctx context.Context, id uuid.UUID) (*account.Identity, error) { - return nil, errors.NotFoundError{} -} -func (m *MockIdentityRepository) LoadWithUser(ctx context.Context, id uuid.UUID) (*account.Identity, error) { - return nil, errors.NotFoundError{} -} -func (m *MockIdentityRepository) Create(ctx context.Context, identity *account.Identity) error { - return nil -} -func (m *MockIdentityRepository) Lookup(ctx context.Context, username, profileURL, providerType string) (*account.Identity, error) { - return nil, errors.NotFoundError{} -} -func (m *MockIdentityRepository) Save(ctx context.Context, identity *account.Identity) error { - return nil -} -func (m *MockIdentityRepository) Delete(ctx context.Context, id uuid.UUID) error { return nil } -func (m *MockIdentityRepository) Query(funcs ...func(*gorm.DB) *gorm.DB) ([]account.Identity, error) { - return nil, nil -} -func (m *MockIdentityRepository) List(ctx context.Context) ([]account.Identity, error) { - return nil, nil -} -func (m *MockIdentityRepository) IsValid(context.Context, uuid.UUID) bool { return true } -func (m *MockIdentityRepository) Search(ctx context.Context, q string, start int, limit int) ([]account.Identity, int, error) { - return nil, 1, nil -} diff --git a/controller/user_test.go b/controller/user_test.go index be72a82..5118431 100644 --- a/controller/user_test.go +++ b/controller/user_test.go @@ -36,14 +36,14 @@ func TestRunUserREST(t *testing.T) { func (rest *TestUserREST) SecuredController(identity account.Identity) (*goa.Service, *UserController) { svc := testsupport.ServiceAsUser("User-Service", identity) - userInfoProvider := userinfo.NewUserInfoProvider(rest.Application.Identities(), rest.Application.Users(), testtoken.NewManager(), rest.Application) + userInfoProvider := userinfo.NewUserInfoProvider(rest.Application.Identities(), rest.Application.Users(), testtoken.TokenManager, rest.Application) controller := NewUserController(svc, userInfoProvider, rest.Application, testtoken.TokenManager, rest.Configuration) return svc, controller } func (rest *TestUserREST) UnsecuredController() (*goa.Service, *UserController) { svc := goa.New("User-Service") - userInfoProvider := userinfo.NewUserInfoProvider(rest.Application.Identities(), rest.Application.Users(), testtoken.NewManager(), rest.Application) + userInfoProvider := userinfo.NewUserInfoProvider(rest.Application.Identities(), rest.Application.Users(), testtoken.TokenManager, rest.Application) controller := NewUserController(svc, userInfoProvider, rest.Application, testtoken.TokenManager, rest.Configuration) return svc, controller } diff --git a/controller/userinfo_test.go b/controller/userinfo_test.go index a60b09e..438f30e 100644 --- a/controller/userinfo_test.go +++ b/controller/userinfo_test.go @@ -34,14 +34,14 @@ func TestRunUserInfoREST(t *testing.T) { func (rest *TestUserInfoREST) SecuredController(identity account.Identity) (*goa.Service, *UserinfoController) { svc := testsupport.ServiceAsUser("Userinfo-Service", identity) - userInfoProvider := userinfo.NewUserInfoProvider(rest.Application.Identities(), rest.Application.Users(), testtoken.NewManager(), rest.Application) + userInfoProvider := userinfo.NewUserInfoProvider(rest.Application.Identities(), rest.Application.Users(), testtoken.TokenManager, rest.Application) controller := NewUserinfoController(svc, userInfoProvider, rest.Application, testtoken.TokenManager) return svc, controller } func (rest *TestUserInfoREST) UnsecuredController() (*goa.Service, *UserinfoController) { svc := goa.New("Userinfo-Service") - userInfoProvider := userinfo.NewUserInfoProvider(rest.Application.Identities(), rest.Application.Users(), testtoken.NewManager(), rest.Application) + userInfoProvider := userinfo.NewUserInfoProvider(rest.Application.Identities(), rest.Application.Users(), testtoken.TokenManager, rest.Application) controller := NewUserinfoController(svc, userInfoProvider, rest.Application, testtoken.TokenManager) return svc, controller } diff --git a/controller/users.go b/controller/users.go index 6757fd0..e3e6781 100644 --- a/controller/users.go +++ b/controller/users.go @@ -1219,7 +1219,7 @@ func ConvertUserSimple(request *goa.RequestData, identityID interface{}) *app.Ge } func createUserLinks(request *goa.RequestData, identityID interface{}) *app.GenericLinks { - relatedURL := rest.AbsoluteURL(request, app.UsersHref(identityID)) + relatedURL := rest.AbsoluteURL(request, app.UsersHref(identityID), nil) return &app.GenericLinks{ Self: &relatedURL, Related: &relatedURL, diff --git a/login/service.go b/login/service.go index c95130b..1eb4b0c 100644 --- a/login/service.go +++ b/login/service.go @@ -28,6 +28,7 @@ import ( errs "github.com/pkg/errors" "github.com/satori/go.uuid" "golang.org/x/oauth2" + "reflect" ) type LoginServiceConfiguration interface { @@ -41,6 +42,7 @@ type LoginServiceConfiguration interface { GetWITURL(*goa.RequestData) (string, error) GetOpenShiftClientApiUrl() string GetKeycloakAccountEndpoint(*goa.RequestData) (string, error) + IsPostgresDeveloperModeEnabled() bool } // NewKeycloakOAuthProvider creates a new login.Service capable of using keycloak for authorization @@ -75,7 +77,7 @@ type KeycloakOAuthService interface { ExchangeRefreshToken(ctx context.Context, refreshToken string, endpoint string, serviceConfig LoginServiceConfiguration) (*token.TokenSet, error) AuthCodeCallback(ctx *app.CallbackAuthorizeContext) (*string, error) CreateOrUpdateIdentityInDB(ctx context.Context, accessToken string, configuration LoginServiceConfiguration) (*account.Identity, bool, error) - CreateOrUpdateIdentityAndUser(ctx context.Context, referrerURL *url.URL, keycloakToken *oauth2.Token, request *goa.RequestData, serviceConfig LoginServiceConfiguration) (*string, error) + CreateOrUpdateIdentityAndUser(ctx context.Context, referrerURL *url.URL, keycloakToken *oauth2.Token, request *goa.RequestData, serviceConfig LoginServiceConfiguration) (*string, *oauth2.Token, error) } const ( @@ -115,7 +117,7 @@ func (keycloak *KeycloakOAuthProvider) Login(ctx *app.LoginLoginContext, config return ctx.TemporaryRedirect() } - redirectTo, err := keycloak.CreateOrUpdateIdentityAndUser(ctx, referrerURL, keycloakToken, ctx.RequestData, serviceConfig) + redirectTo, _, err := keycloak.CreateOrUpdateIdentityAndUser(ctx, referrerURL, keycloakToken, ctx.RequestData, serviceConfig) if err != nil { jsonapi.JSONErrorResponse(ctx, err) } @@ -180,9 +182,10 @@ func (keycloak *KeycloakOAuthProvider) AuthCodeURL(ctx context.Context, redirect return &redirectTo, err } -// Exchange returns token and referralURL on receiving code and state +// Exchange exchanges the given code for OAuth2 token with Keycloak func (keycloak *KeycloakOAuthProvider) Exchange(ctx context.Context, code string, config oauth.OauthConfig) (*oauth2.Token, error) { + // Exchange the code for a Keycloak token keycloakToken, err := config.Exchange(ctx, code) if err != nil { log.Error(ctx, map[string]interface{}{ @@ -201,7 +204,32 @@ func (keycloak *KeycloakOAuthProvider) Exchange(ctx context.Context, code string // ExchangeRefreshToken exchanges refreshToken for OauthToken func (keycloak *KeycloakOAuthProvider) ExchangeRefreshToken(ctx context.Context, refreshToken string, endpoint string, serviceConfig LoginServiceConfiguration) (*token.TokenSet, error) { - identity, err := LoadContextIdentityAndUser(ctx, keycloak.DB) + + // Load identity for the refresh token + var identity *account.Identity + claims, err := keycloak.TokenManager.ParseTokenWithMapClaims(ctx, refreshToken) + if err != nil { + return nil, autherrors.NewUnauthorizedError(err.Error()) + } + sub := claims["sub"] + if sub == nil { + return nil, autherrors.NewUnauthorizedError("missing 'sub' claim in the refresh token") + } + identityID, err := uuid.FromString(fmt.Sprintf("%s", sub)) + if err != nil { + return nil, autherrors.NewUnauthorizedError(err.Error()) + } + err = application.Transactional(keycloak.DB, func(appl application.Application) error { + identity, err = appl.Identities().LoadWithUser(ctx, identityID) + return err + }) + if err != nil { + // That's OK if we didn't find the identity if the token was issued for an API client + // Just log it and proceed. + log.Warn(ctx, map[string]interface{}{ + "err": err, + }, "failed to load identity when refreshing token; it's OK if the token was issued for an API client") + } if identity != nil && identity.User.Deprovisioned { log.Warn(ctx, map[string]interface{}{ "identity_id": identity.ID, @@ -209,24 +237,38 @@ func (keycloak *KeycloakOAuthProvider) ExchangeRefreshToken(ctx context.Context, }, "deprovisioned user tried to refresh token") return nil, autherrors.NewUnauthorizedError("unauthorized access") } + + // Refresh token in Keycloak + tokeSet, err := keycloak.keycloakTokenService.RefreshToken(ctx, endpoint, serviceConfig.GetKeycloakClientID(), serviceConfig.GetKeycloakSecret(), refreshToken) if err != nil { - // That's OK if we didn't find the identity if the token was issued for an API client - // Just log it and proceed. - log.Warn(ctx, map[string]interface{}{ - "err": err, - }, "failed to load identity when refreshing token; it's OK if the token was issued for an API client") + if serviceConfig.IsPostgresDeveloperModeEnabled() && identity != nil && reflect.TypeOf(keycloak.keycloakTokenService) == reflect.TypeOf(&keycloaktoken.KeycloakTokenService{}) { + // If running in dev mode but not in a test then we ignore an error from Keycloak and just generate a refresh token + generatedToken, err := keycloak.TokenManager.GenerateUserTokenForIdentity(ctx, *identity) + if err != nil { + return nil, err + } + return keycloak.TokenManager.ConvertToken(*generatedToken) + } + return nil, err + } + + // Generate token based on the Keycloak token + oauthToken := keycloak.TokenManager.ConvertTokenSet(*tokeSet) + generatedToken, err := keycloak.TokenManager.GenerateUserToken(ctx, *oauthToken, identity) + if err != nil { + return nil, err } - return keycloak.keycloakTokenService.RefreshToken(ctx, endpoint, serviceConfig.GetKeycloakClientID(), serviceConfig.GetKeycloakSecret(), refreshToken) + return keycloak.TokenManager.ConvertToken(*generatedToken) } // CreateOrUpdateIdentityAndUser creates or updates user and identity, checks whether the user is approved, // encodes the token and returns final URL to which we are supposed to redirect -func (keycloak *KeycloakOAuthProvider) CreateOrUpdateIdentityAndUser(ctx context.Context, referrerURL *url.URL, keycloakToken *oauth2.Token, request *goa.RequestData, config LoginServiceConfiguration) (*string, error) { +func (keycloak *KeycloakOAuthProvider) CreateOrUpdateIdentityAndUser(ctx context.Context, referrerURL *url.URL, keycloakToken *oauth2.Token, request *goa.RequestData, config LoginServiceConfiguration) (*string, *oauth2.Token, error) { witURL, err := config.GetWITURL(request) if err != nil { - return nil, autherrors.NewInternalError(ctx, err) + return nil, nil, autherrors.NewInternalError(ctx, err) } apiClient := referrerURL.Query().Get(apiClientParam) @@ -241,19 +283,22 @@ func (keycloak *KeycloakOAuthProvider) CreateOrUpdateIdentityAndUser(ctx context case autherrors.UnauthorizedError: if apiClient != "" { // Return the api token - err = encodeToken(ctx, referrerURL, keycloakToken, apiClient) + userToken, err := keycloak.TokenManager.GenerateUserToken(ctx, *keycloakToken, nil) if err != nil { - log.Error(ctx, map[string]interface{}{ - "err": err, - }, "failed to encode token") - return nil, err + log.Error(ctx, map[string]interface{}{"err": err}, "failed to generate token") + return nil, nil, err + } + err = encodeToken(ctx, referrerURL, userToken, apiClient) + if err != nil { + log.Error(ctx, map[string]interface{}{"err": err}, "failed to encode token") + return nil, nil, err } log.Info(ctx, map[string]interface{}{ "referrerURL": referrerURL.String(), "api_client": apiClient, }, "return api token for unapproved user") redirectTo := referrerURL.String() - return &redirectTo, nil + return &redirectTo, userToken, nil } userNotApprovedRedirectURL := config.GetNotApprovedRedirect() @@ -261,11 +306,11 @@ func (keycloak *KeycloakOAuthProvider) CreateOrUpdateIdentityAndUser(ctx context log.Debug(ctx, map[string]interface{}{ "user_not_approved_redirect_url": userNotApprovedRedirectURL, }, "user not approved; redirecting to registration app") - return &userNotApprovedRedirectURL, nil + return &userNotApprovedRedirectURL, nil, nil } - return nil, autherrors.NewUnauthorizedError(err.Error()) + return nil, nil, autherrors.NewUnauthorizedError(err.Error()) } - return nil, err + return nil, nil, err } if identity.User.Deprovisioned { @@ -273,7 +318,7 @@ func (keycloak *KeycloakOAuthProvider) CreateOrUpdateIdentityAndUser(ctx context "identity_id": identity.ID, "user_name": identity.Username, }, "deprovisioned user tried to login") - return nil, autherrors.NewUnauthorizedError("unauthorized access") + return nil, nil, autherrors.NewUnauthorizedError("unauthorized access") } log.Debug(ctx, map[string]interface{}{ @@ -281,7 +326,14 @@ func (keycloak *KeycloakOAuthProvider) CreateOrUpdateIdentityAndUser(ctx context "user_name": identity.Username, }, "local user created/updated") - updatedKeycloakToken, err := keycloak.synchronizeAuthToKeycloak(ctx, request, keycloakToken, config, identity) + // Generate a new token instead of using the original Keycloak token + userToken, err := keycloak.TokenManager.GenerateUserToken(ctx, *keycloakToken, identity) + if err != nil { + log.Error(ctx, map[string]interface{}{"err": err, "identity_id": identity.ID.String()}, "failed to generate token") + return nil, nil, err + } + + _, err = keycloak.synchronizeAuthToKeycloak(ctx, request, keycloakToken, config, identity) if err != nil { log.Error(ctx, map[string]interface{}{ "err": err, @@ -289,10 +341,7 @@ func (keycloak *KeycloakOAuthProvider) CreateOrUpdateIdentityAndUser(ctx context "username": identity.Username, }, "unable to synchronize user from auth to keycloak ") - // dont wish to cause a login error if something - // goes wrong here - } else if updatedKeycloakToken != nil { - keycloakToken = updatedKeycloakToken + // don't wish to cause a login error if something goes wrong here } // new user for WIT @@ -320,13 +369,13 @@ func (keycloak *KeycloakOAuthProvider) CreateOrUpdateIdentityAndUser(ctx context } } - err = encodeToken(ctx, referrerURL, keycloakToken, apiClient) + err = encodeToken(ctx, referrerURL, userToken, apiClient) if err != nil { log.Error(ctx, map[string]interface{}{ "err": err, }, "failed to encode token") redirectTo := referrerURL.String() + err.Error() - return &redirectTo, autherrors.NewInternalError(ctx, err) + return &redirectTo, nil, autherrors.NewInternalError(ctx, err) } log.Debug(ctx, map[string]interface{}{ "referrerURL": referrerURL.String(), @@ -334,7 +383,7 @@ func (keycloak *KeycloakOAuthProvider) CreateOrUpdateIdentityAndUser(ctx context }, "token encoded") redirectTo := referrerURL.String() - return &redirectTo, nil + return &redirectTo, userToken, nil } func (keycloak *KeycloakOAuthProvider) updateUserInKeycloak(ctx context.Context, request *goa.RequestData, keycloakUser KeytcloakUserRequest, config LoginServiceConfiguration, identity *account.Identity) error { @@ -455,6 +504,7 @@ func (keycloak *KeycloakOAuthProvider) synchronizeAuthToKeycloak(ctx context.Con oauth2Token = oauth2Token.WithExtra(map[string]interface{}{ "expires_in": *tokenSet.ExpiresIn, "refresh_expires_in": *tokenSet.RefreshExpiresIn, + "not_before_policy": *tokenSet.NotBeforePolicy, }) return oauth2Token, nil } diff --git a/login/service_blackbox_test.go b/login/service_blackbox_test.go index b008bb4..d5f5291 100644 --- a/login/service_blackbox_test.go +++ b/login/service_blackbox_test.go @@ -75,7 +75,12 @@ func (s *serviceBlackBoxTest) SetupSuite() { }, } claims := make(map[string]interface{}) - accessToken, err := testtoken.GenerateTokenWithClaims(claims) + claims["sub"] = uuid.NewV4().String() + accessToken, err := testtoken.GenerateAccessTokenWithClaims(claims) + if err != nil { + panic(err) + } + refreshToken, err := testtoken.GenerateRefreshTokenWithClaims(claims) if err != nil { panic(err) } @@ -89,15 +94,15 @@ func (s *serviceBlackBoxTest) SetupSuite() { TokenURL: tokenEndpoint, }, }, - accessToken: accessToken, + accessToken: accessToken, + refreshToken: refreshToken, } userRepository := account.NewUserRepository(s.DB) identityRepository := account.NewIdentityRepository(s.DB) userProfileClient := NewKeycloakUserProfileClient() - refreshToken := uuid.NewV4().String() - refreshTokenSet := token.TokenSet{AccessToken: &refreshToken} + refreshTokenSet := token.TokenSet{AccessToken: &accessToken, RefreshToken: &refreshToken} s.keycloakTokenService = &DummyTokenService{tokenSet: refreshTokenSet} s.loginService = NewKeycloakOAuthProvider(identityRepository, userRepository, testtoken.TokenManager, s.Application, userProfileClient, s.keycloakTokenService) @@ -200,11 +205,15 @@ func (s *serviceBlackBoxTest) unapprovedUserRedirected() (*string, error) { claims := make(map[string]interface{}) claims["approved"] = false - tokenStr, err := testtoken.GenerateTokenWithClaims(claims) + accessTokenStr, err := testtoken.GenerateAccessTokenWithClaims(claims) require.Nil(s.T(), err) - token := &oauth2.Token{AccessToken: tokenStr, RefreshToken: tokenStr} - return s.loginService.CreateOrUpdateIdentityAndUser(context.Background(), redirect, token, req, s.Configuration) + refreshTokenStr, err := testtoken.GenerateRefreshTokenWithClaims(claims) + require.Nil(s.T(), err) + + token := &oauth2.Token{AccessToken: accessTokenStr, RefreshToken: refreshTokenStr} + redirectURL, _, err := s.loginService.CreateOrUpdateIdentityAndUser(context.Background(), redirect, token, req, s.Configuration) + return redirectURL, err } func (s *serviceBlackBoxTest) resetConfiguration() { @@ -499,35 +508,32 @@ func (s *serviceBlackBoxTest) TestUnapprovedUserLoginUnauthorized() { } func (s *serviceBlackBoxTest) TestAPIClientForApprovedUsersReturnOK() { - extra := make(map[string]string) - extra["api_client"] = "vscode" - rw, authorizeCtx := s.loginCallback(extra) - - claims := make(map[string]interface{}) - accessToken, err := testtoken.GenerateTokenWithClaims(claims) - require.Nil(s.T(), err) - - dummyOauth := &dummyOauth2Config{ - Config: oauth2.Config{}, - accessToken: accessToken, - } - - s.checkLoginCallback(dummyOauth, rw, authorizeCtx, "api_token") + s.checkAPIClientForUsersReturnOK(true) } func (s *serviceBlackBoxTest) TestAPIClientForUnapprovedUsersReturnOK() { + s.checkAPIClientForUsersReturnOK(false) +} + +func (s *serviceBlackBoxTest) checkAPIClientForUsersReturnOK(approved bool) { extra := make(map[string]string) extra["api_client"] = "vscode" rw, authorizeCtx := s.loginCallback(extra) claims := make(map[string]interface{}) - claims["approved"] = nil + if !approved { + claims["approved"] = nil + } + claims["sub"] = uuid.NewV4().String() accessToken, err := testtoken.GenerateTokenWithClaims(claims) require.Nil(s.T(), err) + refreshToken, err := testtoken.GenerateRefreshTokenWithClaims(claims) + require.Nil(s.T(), err) dummyOauth := &dummyOauth2Config{ - Config: oauth2.Config{}, - accessToken: accessToken, + Config: oauth2.Config{}, + accessToken: accessToken, + refreshToken: refreshToken, } s.checkLoginCallback(dummyOauth, rw, authorizeCtx, "api_token") @@ -575,10 +581,13 @@ func (s *serviceBlackBoxTest) TestNotDeprovisionedUserLoginOK() { claims["email"] = identity.User.Email accessToken, err := testtoken.GenerateTokenWithClaims(claims) require.Nil(s.T(), err) + refreshToken, err := testtoken.GenerateRefreshTokenWithClaims(claims) + require.Nil(s.T(), err) dummyOauth := &dummyOauth2Config{ - Config: oauth2.Config{}, - accessToken: accessToken, + Config: oauth2.Config{}, + accessToken: accessToken, + refreshToken: refreshToken, } err = s.loginService.Login(authorizeCtx, dummyOauth, s.Configuration) @@ -587,40 +596,97 @@ func (s *serviceBlackBoxTest) TestNotDeprovisionedUserLoginOK() { assert.Equal(s.T(), 307, rw.Code) } -func (s *serviceBlackBoxTest) TestExchangeRefreshTokenForDeprovisionedUser() { - // Fails if no token in context because Keycloak service returns 401 - s.keycloakTokenService.fail = true +func (s *serviceBlackBoxTest) TestExchangeRefreshTokenFailsIfInvalidToken() { + // Fails if invalid format of refresh token + s.keycloakTokenService.fail = false _, err := s.loginService.ExchangeRefreshToken(context.Background(), "", "", s.Configuration) - require.NotNil(s.T(), err) + require.EqualError(s.T(), err, "token contains an invalid number of segments") require.IsType(s.T(), errors.NewUnauthorizedError(""), err) - require.Equal(s.T(), "kc refresh failed", err.Error()) - // Fails if identity is deprovisioned + // Fails if refresh token is expired + identity, err := testsupport.CreateTestIdentityAndUserWithDefaultProviderType(s.DB, "TestExchangeRefreshTokenFailsIfInvalidToken-"+uuid.NewV4().String()) + require.NoError(s.T(), err) + + claims := make(map[string]interface{}) + claims["sub"] = identity.ID.String() + claims["iat"] = time.Now().Unix() - 60*60 // Issued 1h ago + claims["exp"] = time.Now().Unix() - 60 // Expired 1m ago + refreshToken, err := testtoken.GenerateRefreshTokenWithClaims(claims) + require.NoError(s.T(), err) + + ctx := testtoken.ContextWithRequest(nil) + _, err = s.loginService.ExchangeRefreshToken(ctx, refreshToken, "", s.Configuration) + require.EqualError(s.T(), err, "Token is expired") + require.IsType(s.T(), errors.NewUnauthorizedError(""), err) + + // OK if not expired + claims["exp"] = time.Now().Unix() + 60*60 // Expires in 1h + refreshToken, err = testtoken.GenerateRefreshTokenWithClaims(claims) + require.NoError(s.T(), err) + + _, err = s.loginService.ExchangeRefreshToken(ctx, refreshToken, "", s.Configuration) + require.NoError(s.T(), err) + + // Fails if KC fails + s.keycloakTokenService.fail = true + _, err = s.loginService.ExchangeRefreshToken(context.Background(), refreshToken, "", s.Configuration) + require.EqualError(s.T(), err, "kc refresh failed") + require.IsType(s.T(), errors.NewUnauthorizedError(""), err) +} + +func (s *serviceBlackBoxTest) TestExchangeRefreshTokenForDeprovisionedUser() { + // 1. Fails if identity is deprovisioned s.keycloakTokenService.fail = false identity, err := testsupport.CreateDeprovisionedTestIdentityAndUser(s.DB, "TestExchangeRefreshTokenForDeprovisionedUser-"+uuid.NewV4().String()) require.NoError(s.T(), err) - ctx, err := testtoken.EmbedIdentityInContext(identity) + + // Refresh tokens + ctx := testtoken.ContextWithRequest(nil) + generatedToken, err := testtoken.TokenManager.GenerateUserTokenForIdentity(ctx, identity) require.NoError(s.T(), err) - _, err = s.loginService.ExchangeRefreshToken(ctx, "", "", s.Configuration) + _, err = s.loginService.ExchangeRefreshToken(ctx, generatedToken.RefreshToken, "", s.Configuration) require.NotNil(s.T(), err) require.IsType(s.T(), errors.NewUnauthorizedError(""), err) require.Equal(s.T(), "unauthorized access", err.Error()) - // OK if identity is not deprovisioned + // 2. OK if identity is not deprovisioned identity, err = testsupport.CreateTestIdentityAndUserWithDefaultProviderType(s.DB, "TestExchangeRefreshTokenForDeprovisionedUser-"+uuid.NewV4().String()) require.NoError(s.T(), err) - ctx, err = testtoken.EmbedIdentityInContext(identity) + + // Generate expected tokens returned by dummy KC service + claims := make(map[string]interface{}) + claims["sub"] = identity.ID.String() + accessToken, err := testtoken.GenerateAccessTokenWithClaims(claims) + require.NoError(s.T(), err) + refreshToken, err := testtoken.GenerateRefreshTokenWithClaims(claims) require.NoError(s.T(), err) - tokenSet, err := s.loginService.ExchangeRefreshToken(ctx, "", "", s.Configuration) + typ := "bearer" + var in30days int64 + in30days = 30 * 24 * 60 * 60 + s.keycloakTokenService.tokenSet = token.TokenSet{AccessToken: &accessToken, RefreshToken: &refreshToken, TokenType: &typ, ExpiresIn: &in30days, RefreshExpiresIn: &in30days} + + // Refresh tokens + generatedToken, err = testtoken.TokenManager.GenerateUserTokenForIdentity(ctx, identity) + require.NoError(s.T(), err) + tokenSet, err := s.loginService.ExchangeRefreshToken(ctx, generatedToken.RefreshToken, "", s.Configuration) require.NoError(s.T(), err) require.NotNil(s.T(), tokenSet) - require.Equal(s.T(), s.keycloakTokenService.tokenSet, *tokenSet) + + // Compare tokens + err = testtoken.EqualAccessTokens(ctx, *s.keycloakTokenService.tokenSet.RefreshToken, *tokenSet.RefreshToken) + require.NoError(s.T(), err) + err = testtoken.EqualRefreshTokens(ctx, *s.keycloakTokenService.tokenSet.AccessToken, *tokenSet.AccessToken) + require.NoError(s.T(), err) + assert.Equal(s.T(), typ, *tokenSet.TokenType) + assert.Equal(s.T(), in30days, *tokenSet.ExpiresIn) + assert.Equal(s.T(), in30days, *tokenSet.RefreshExpiresIn) } func (s *serviceBlackBoxTest) loginCallback(extraParams map[string]string) (*httptest.ResponseRecorder, *app.LoginLoginContext) { // Setup request context rw := httptest.NewRecorder() u := &url.URL{ + Host: "openshift.io", Path: fmt.Sprintf("/api/login"), } req, err := http.NewRequest("GET", u.String(), nil) @@ -695,9 +761,10 @@ func (s *serviceBlackBoxTest) checkLoginCallback(dummyOauth *dummyOauth2Config, require.True(s.T(), len(tokenJson) > 0) tokenSet, err := token.ReadTokenSetFromJson(context.Background(), tokenJson[0]) - require.Nil(s.T(), err) - assert.Equal(s.T(), dummyOauth.accessToken, *tokenSet.AccessToken) - assert.Equal(s.T(), "someRefreshToken", *tokenSet.RefreshToken) + require.NoError(s.T(), err) + + assert.NoError(s.T(), testtoken.EqualAccessTokens(context.Background(), dummyOauth.accessToken, *tokenSet.AccessToken)) + assert.NoError(s.T(), testtoken.EqualAccessTokens(context.Background(), dummyOauth.refreshToken, *tokenSet.RefreshToken)) assert.NotContains(s.T(), locationString, "https://keycloak-url.example.org/path-of-login") assert.Contains(s.T(), locationString, "https://openshift.io/somepath") @@ -705,21 +772,23 @@ func (s *serviceBlackBoxTest) checkLoginCallback(dummyOauth *dummyOauth2Config, type dummyOauth2Config struct { oauth2.Config - accessToken string + accessToken string + refreshToken string } func (c *dummyOauth2Config) Exchange(ctx netcontext.Context, code string) (*oauth2.Token, error) { - var thirtyDays int64 + var thirtyDays, nbf int64 thirtyDays = 60 * 60 * 24 * 30 token := &oauth2.Token{ TokenType: "bearer", AccessToken: c.accessToken, - RefreshToken: "someRefreshToken", + RefreshToken: c.refreshToken, Expiry: time.Unix(time.Now().Unix()+thirtyDays, 0), } extra := make(map[string]interface{}) extra["expires_in"] = time.Now().Unix() + thirtyDays extra["refresh_expires_in"] = time.Now().Unix() + thirtyDays + extra["not_before_policy"] = nbf token = token.WithExtra(extra) return token, nil } diff --git a/login/service_whitebox_test.go b/login/service_whitebox_test.go index 361522e..ffd5206 100644 --- a/login/service_whitebox_test.go +++ b/login/service_whitebox_test.go @@ -59,6 +59,7 @@ func TestEncodeTokenOK(t *testing.T) { var refreshExpiresIn float64 refreshExpiresIn = 2.59e6 + var nbf int64 outhToken := &oauth2.Token{ AccessToken: accessToken, RefreshToken: refreshToken, @@ -67,6 +68,7 @@ func TestEncodeTokenOK(t *testing.T) { extra := map[string]interface{}{ "expires_in": expiresIn, "refresh_expires_in": refreshExpiresIn, + "not_before_policy": nbf, } tokenJson, err := TokenToJson(context.Background(), outhToken.WithExtra(extra)) assert.Nil(t, err) diff --git a/main.go b/main.go index a8f1331..aebe214 100644 --- a/main.go +++ b/main.go @@ -126,9 +126,6 @@ func main() { os.Exit(0) } - // Load service accounts - // application.s - // Create service service := goa.New("auth") diff --git a/openshift/auth.app.yaml b/openshift/auth.app.yaml index ee88b01..f8d7c6b 100644 --- a/openshift/auth.app.yaml +++ b/openshift/auth.app.yaml @@ -69,6 +69,16 @@ objects: secretKeyRef: name: auth key: serviceaccount.privatekeyid + - name: AUTH_USERACCOUNT_PRIVATEKEY + valueFrom: + secretKeyRef: + name: auth + key: useraccount.privatekey + - name: AUTH_USERACCOUNT_PRIVATEKEYID + valueFrom: + secretKeyRef: + name: auth + key: useraccount.privatekeyid - name: AUTH_GITHUB_CLIENT_ID valueFrom: secretKeyRef: diff --git a/openshift/auth.config.yaml b/openshift/auth.config.yaml index 77306ef..d57abc4 100644 --- a/openshift/auth.config.yaml +++ b/openshift/auth.config.yaml @@ -17,6 +17,8 @@ objects: keycloak.secret: Cg== serviceaccount.privatekey: Cg== serviceaccount.privatekeyid: Cg== + useraccount.privatekey: Cg== + useraccount.privatekeyid: Cg== github.client.id: Cg== github.client.secret: Cg== sentry.dsn: c2VjcmV0 diff --git a/rest/url.go b/rest/url.go index 965e62d..5de1f0d 100644 --- a/rest/url.go +++ b/rest/url.go @@ -16,8 +16,42 @@ import ( "github.com/goadesign/goa" ) +type configuration interface { + IsPostgresDeveloperModeEnabled() bool +} + +// Host returns the host from the given request if run in prod mode or if config is nil +// and "auth.openshift.io" if run in dev mode +func Host(req *goa.RequestData, config configuration) string { + if config != nil && config.IsPostgresDeveloperModeEnabled() { + return "auth.openshift.io" + } + return req.Host +} + // AbsoluteURL prefixes a relative URL with absolute address -func AbsoluteURL(req *goa.RequestData, relative string) string { +// If config is not nil and run in dev mode then host is replaced by "auth.openshift.io" +func AbsoluteURL(req *goa.RequestData, relative string, config configuration) string { + host := Host(req, config) + return absoluteURLForHost(req, host, relative) +} + +// ReplaceDomainPrefixInAbsoluteURL replaces the last name in the host of the URL by a new name. +// Example: https://api.service.domain.org -> https://sso.service.domain.org +// If replaceBy == "" then return trim the last name. +// Example: https://api.service.domain.org -> https://service.domain.org +// Also prefixes a relative URL with absolute address +// If config is not nil and run in dev mode then "auth.openshift.io" is used as a host +func ReplaceDomainPrefixInAbsoluteURL(req *goa.RequestData, replaceBy, relative string, config configuration) (string, error) { + host := Host(req, config) + newHost, err := ReplaceDomainPrefix(host, replaceBy) + if err != nil { + return "", err + } + return absoluteURLForHost(req, newHost, relative), nil +} + +func absoluteURLForHost(req *goa.RequestData, host, relative string) string { scheme := "http" if req.URL != nil && req.URL.Scheme == "https" { // isHTTPS scheme = "https" @@ -26,15 +60,19 @@ func AbsoluteURL(req *goa.RequestData, relative string) string { if xForwardProto != "" { scheme = xForwardProto } - return fmt.Sprintf("%s://%s%s", scheme, req.Host, relative) + return fmt.Sprintf("%s://%s%s", scheme, host, relative) } // ReplaceDomainPrefix replaces the last name in the host by a new name. Example: api.service.domain.org -> sso.service.domain.org +// If replaceBy == "" then return trim the last name. Example: api.service.domain.org -> service.domain.org func ReplaceDomainPrefix(host string, replaceBy string) (string, error) { split := strings.SplitN(host, ".", 2) if len(split) < 2 { return host, errors.NewBadParameterError("host", host).Expected("must contain more than one domain") } + if replaceBy == "" { + return split[1], nil + } return replaceBy + "." + split[1], nil } diff --git a/rest/url_blackbox_test.go b/rest/url_blackbox_test.go index 5069678..1573bb4 100644 --- a/rest/url_blackbox_test.go +++ b/rest/url_blackbox_test.go @@ -12,6 +12,14 @@ import ( "github.com/stretchr/testify/require" ) +type dummyConfig struct { + devMode bool +} + +func (c *dummyConfig) IsPostgresDeveloperModeEnabled() bool { + return c.devMode +} + func TestAbsoluteURLOK(t *testing.T) { resource.Require(t, resource.UnitTest) t.Parallel() @@ -20,7 +28,7 @@ func TestAbsoluteURLOK(t *testing.T) { Request: &http.Request{Host: "api.service.domain.org"}, } // HTTP - urlStr := AbsoluteURL(req, "/testpath") + urlStr := AbsoluteURL(req, "/testpath", nil) assert.Equal(t, "http://api.service.domain.org/testpath", urlStr) // HTTPS @@ -29,8 +37,16 @@ func TestAbsoluteURLOK(t *testing.T) { req = &goa.RequestData{ Request: r, } - urlStr = AbsoluteURL(req, "/testpath2") + urlStr = AbsoluteURL(req, "/testpath2", nil) + assert.Equal(t, "https://api.service.domain.org/testpath2", urlStr) + + // Prod mode + urlStr = AbsoluteURL(req, "/testpath2", &dummyConfig{}) assert.Equal(t, "https://api.service.domain.org/testpath2", urlStr) + + // Dev mode + urlStr = AbsoluteURL(req, "/testpath2", &dummyConfig{true}) + assert.Equal(t, "https://auth.openshift.io/testpath2", urlStr) } func TestAbsoluteURLOKWithProxyForward(t *testing.T) { @@ -48,17 +64,82 @@ func TestAbsoluteURLOKWithProxyForward(t *testing.T) { req = &goa.RequestData{ Request: r, } - urlStr := AbsoluteURL(req, "/testpath2") + urlStr := AbsoluteURL(req, "/testpath2", nil) assert.Equal(t, "https://api.service.domain.org/testpath2", urlStr) } +func TestReplaceDomainPrefixInAbsoluteURLOK(t *testing.T) { + resource.Require(t, resource.UnitTest) + t.Parallel() + + req := &goa.RequestData{ + Request: &http.Request{Host: "api.service.domain.org"}, + } + // HTTP + urlStr, err := ReplaceDomainPrefixInAbsoluteURL(req, "auth", "/testpath", nil) + require.NoError(t, err) + assert.Equal(t, "http://auth.service.domain.org/testpath", urlStr) + + // HTTPS + r, err := http.NewRequest("", "https://api.service.domain.org", nil) + require.Nil(t, err) + req = &goa.RequestData{ + Request: r, + } + urlStr, err = ReplaceDomainPrefixInAbsoluteURL(req, "auth", "/testpath2", nil) + require.NoError(t, err) + assert.Equal(t, "https://auth.service.domain.org/testpath2", urlStr) + + urlStr, err = ReplaceDomainPrefixInAbsoluteURL(req, "", "/testpath3", nil) + require.NoError(t, err) + assert.Equal(t, "https://service.domain.org/testpath3", urlStr) + + // Prod mode + urlStr, err = ReplaceDomainPrefixInAbsoluteURL(req, "", "/testpath4", &dummyConfig{}) + require.NoError(t, err) + assert.Equal(t, "https://service.domain.org/testpath4", urlStr) + + // Dev mode + urlStr, err = ReplaceDomainPrefixInAbsoluteURL(req, "", "/testpath5", &dummyConfig{true}) + require.NoError(t, err) + assert.Equal(t, "https://openshift.io/testpath5", urlStr) + urlStr, err = ReplaceDomainPrefixInAbsoluteURL(req, "core", "/testpath6", &dummyConfig{true}) + require.NoError(t, err) + assert.Equal(t, "https://core.openshift.io/testpath6", urlStr) +} + +func TestHostOK(t *testing.T) { + resource.Require(t, resource.UnitTest) + t.Parallel() + + req := &goa.RequestData{ + Request: &http.Request{Host: "api.service.domain.org"}, + } + + // Prod mode + host := Host(req, &dummyConfig{}) + assert.Equal(t, "api.service.domain.org", host) + + // Config is nil + host = Host(req, nil) + assert.Equal(t, "api.service.domain.org", host) + + // Dev mode + host = Host(req, &dummyConfig{true}) + assert.Equal(t, "auth.openshift.io", host) +} + func TestReplaceDomainPrefixOK(t *testing.T) { resource.Require(t, resource.UnitTest) t.Parallel() host, err := ReplaceDomainPrefix("api.service.domain.org", "sso") - assert.Nil(t, err) + require.NoError(t, err) assert.Equal(t, "sso.service.domain.org", host) + + host, err = ReplaceDomainPrefix("api.service.domain.org", "") + require.NoError(t, err) + assert.Equal(t, "service.domain.org", host) } func TestReplaceDomainPrefixInTooShortHostFails(t *testing.T) { diff --git a/space/authz/authz_test.go b/space/authz/authz_test.go index 4d6b208..0dc88c7 100644 --- a/space/authz/authz_test.go +++ b/space/authz/authz_test.go @@ -54,7 +54,7 @@ func (s *TestAuthzSuite) SetupSuite() { panic(fmt.Errorf("failed to get endpoint from configuration: %s", err.Error())) } - token, err := controller.GenerateUserToken(context.Background(), tokenEndpoint, s.Config, s.Config.GetKeycloakTestUserName(), s.Config.GetKeycloakTestUserSecret()) + token, err := controller.ObtainKeycloakUserToken(context.Background(), tokenEndpoint, s.Config, s.Config.GetKeycloakTestUserName(), s.Config.GetKeycloakTestUserSecret()) if err != nil { panic(fmt.Errorf("failed to generate token: %s", err.Error())) } @@ -64,7 +64,7 @@ func (s *TestAuthzSuite) SetupSuite() { s.test1Token = *token.Token.AccessToken - token, err = controller.GenerateUserToken(context.Background(), tokenEndpoint, s.Config, s.Config.GetKeycloakTestUser2Name(), s.Config.GetKeycloakTestUser2Secret()) + token, err = controller.ObtainKeycloakUserToken(context.Background(), tokenEndpoint, s.Config, s.Config.GetKeycloakTestUser2Name(), s.Config.GetKeycloakTestUser2Secret()) if err != nil { panic(fmt.Errorf("failed to generate token: %s", err.Error())) } diff --git a/test/token/token.go b/test/token/token.go index c726eba..8ecbe47 100644 --- a/test/token/token.go +++ b/test/token/token.go @@ -12,23 +12,23 @@ import ( "github.com/fabric8-services/fabric8-auth/token" "github.com/dgrijalva/jwt-go" + "github.com/goadesign/goa" jwtgoa "github.com/goadesign/goa/middleware/security/jwt" "github.com/pkg/errors" "github.com/satori/go.uuid" + "golang.org/x/oauth2" + "net/http" + "net/http/httptest" + "net/url" ) -var ( - TokenManager token.Manager -) - -func init() { - TokenManager = NewManager() -} +var config = configurationData() +var TokenManager = newManager() // EmbedTokenInContext generates a token and embed it into the context func EmbedTokenInContext(sub, username string) (context.Context, error) { // Generate Token with an identity that doesn't exist in the database - tokenString, err := GenerateToken(sub, username, PrivateKey()) + tokenString, err := GenerateToken(sub, username) if err != nil { return nil, err } @@ -49,18 +49,24 @@ func EmbedIdentityInContext(identity account.Identity) (context.Context, error) if err != nil { return nil, err } + ctx = ContextWithRequest(ctx) return tokencontext.ContextWithTokenManager(ctx, TokenManager), nil } -// GenerateToken generates a JWT token and signs it using the given private key -func GenerateToken(identityID string, identityUsername string, privateKey *rsa.PrivateKey) (string, error) { +// GenerateToken generates a JWT token and signs it using the default private key +func GenerateToken(identityID string, identityUsername string) (string, error) { token := jwt.New(jwt.SigningMethodRS256) token.Claims.(jwt.MapClaims)["uuid"] = identityID token.Claims.(jwt.MapClaims)["preferred_username"] = identityUsername token.Claims.(jwt.MapClaims)["sub"] = identityID - token.Header["kid"] = "test-key" - tokenStr, err := token.SignedString(privateKey) + key, kid, err := privateKey() + if err != nil { + return "", errors.WithStack(err) + } + token.Header["kid"] = kid + tokenStr, err := token.SignedString(key) + if err != nil { return "", errors.WithStack(err) } @@ -90,18 +96,49 @@ func GenerateTokenWithClaims(claims map[string]interface{}) (string, error) { token.Claims.(jwt.MapClaims)["given_name"] = "Test" token.Claims.(jwt.MapClaims)["family_name"] = "User" token.Claims.(jwt.MapClaims)["email"] = fmt.Sprintf("testuser+%s@email.com", uuid.NewV4().String()) + token.Claims.(jwt.MapClaims)["email_verified"] = true for key, value := range claims { token.Claims.(jwt.MapClaims)[key] = value } - token.Header["kid"] = "test-key" - tokenStr, err := token.SignedString(PrivateKey()) + key, kid, err := privateKey() + if err != nil { + return "", errors.WithStack(err) + } + token.Header["kid"] = kid + tokenStr, err := token.SignedString(key) if err != nil { return "", errors.WithStack(err) } return tokenStr, nil } +func GenerateAccessTokenWithClaims(claims map[string]interface{}) (string, error) { + return GenerateTokenWithClaims(claims) +} + +func GenerateRefreshTokenWithClaims(claims map[string]interface{}) (string, error) { + claims["approved"] = nil + claims["company"] = nil + claims["email"] = nil + claims["email_verified"] = nil + claims["typ"] = "Refresh" + claims["preferred_username"] = nil + claims["name"] = nil + return GenerateTokenWithClaims(claims) +} + +func GenerateUserTokenForIdentity(ctx context.Context, identity account.Identity) (*oauth2.Token, error) { + rw := httptest.NewRecorder() + u := &url.URL{Host: "auth.openshift.io"} + req, err := http.NewRequest("GET", u.String(), nil) + if err != nil { + return nil, err + } + goaCtx := goa.NewContext(ctx, rw, req, url.Values{}) + return TokenManager.GenerateUserTokenForIdentity(goaCtx, identity) +} + // UpdateToken generates a new token based on the existing one with additional claims func UpdateToken(tokenString string, claims map[string]interface{}) (string, error) { newToken := jwt.New(jwt.SigningMethodRS256) @@ -125,60 +162,124 @@ func UpdateToken(tokenString string, claims map[string]interface{}) (string, err for key, value := range claims { newToken.Claims.(jwt.MapClaims)[key] = value } - newToken.Header["kid"] = "test-key" - tokenStr, err := newToken.SignedString(PrivateKey()) + key, kid, err := privateKey() + if err != nil { + return "", errors.WithStack(err) + } + newToken.Header["kid"] = kid + tokenStr, err := newToken.SignedString(key) if err != nil { return "", errors.WithStack(err) } return tokenStr, nil } -// NewManager returns a new token Manager for handling tokens -func NewManager() token.Manager { - publicKey := &token.PublicKey{KeyID: "test-key", Key: &PrivateKey().PublicKey} - rsaServiceAccountKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(configuration.DefaultServiceAccountPrivateKey)) +func ContextWithRequest(ctx context.Context) context.Context { + if ctx == nil { + ctx = context.Background() + } + u := &url.URL{ + Scheme: "https", + Host: "auth.openshift.io", + } + rw := httptest.NewRecorder() + req, err := http.NewRequest("GET", u.String(), nil) if err != nil { - panic(fmt.Errorf("failed to setup parse priviate key: %s", err.Error())) + panic("invalid test " + err.Error()) // bug } - serviceAccountKey := &token.PrivateKey{KeyID: "9MLnViaRkhVj1GT9kpWUkwHIwUD-wZfUxR-3CpkE-Xs", Key: rsaServiceAccountKey} + return goa.NewContext(goa.WithAction(ctx, "Test"), rw, req, url.Values{}) +} - return token.NewManagerWithPublicKey(publicKey, serviceAccountKey) +func configurationData() *configuration.ConfigurationData { + config, err := configuration.GetConfigurationData() + if err != nil { + panic("failed to load configuration: " + err.Error()) + } + return config } -func PrivateKey() *rsa.PrivateKey { - rsaKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(rsaPrivateKey)) +func newManager() token.Manager { + tm, err := token.NewManager(config) if err != nil { - panic("Failed: " + err.Error()) + panic("failed to create token manager: " + err.Error()) } - return rsaKey + return tm +} + +func privateKey() (*rsa.PrivateKey, string, error) { + key, kid := config.GetUserAccountPrivateKey() + pk, err := jwt.ParseRSAPrivateKeyFromPEM(key) + return pk, kid, err } -// rsaPrivateKey for signing JWT Tokens -// ssh-keygen -f alm_rsa -var rsaPrivateKey = `-----BEGIN RSA PRIVATE KEY----- -MIIEpQIBAAKCAQEAnwrjH5iTSErw9xUptp6QSFoUfpHUXZ+PaslYSUrpLjw1q27O -DSFwmhV4+dAaTMO5chFv/kM36H3ZOyA146nwxBobS723okFaIkshRrf6qgtD6coT -HlVUSBTAcwKEjNn4C9jtEpyOl+eSgxhMzRH3bwTIFlLlVMiZf7XVE7P3yuOCpqkk -2rdYVSpQWQWKU+ZRywJkYcLwjEYjc70AoNpjO5QnY+Exx98E30iEdPHZpsfNhsjh -9Z7IX5TrMYgz7zBTw8+niO/uq3RBaHyIhDbvenbR9Q59d88lbnEeHKgSMe2RQpFR -3rxFRkc/64Rn/bMuL/ptNowPqh1P+9GjYzWmPwIDAQABAoIBAQCBCl5ZpnvprhRx -BVTA/Upnyd7TCxNZmzrME+10Gjmz79pD7DV25ejsu/taBYUxP6TZbliF3pggJOv6 -UxomTB4znlMDUz0JgyjUpkyril7xVQ6XRAPbGrS1f1Def+54MepWAn3oGeqASb3Q -bAj0Yl12UFTf+AZmkhQpUKk/wUeN718EIY4GRHHQ6ykMSqCKvdnVbMyb9sIzbSTl -v+l1nQFnB/neyJq6P0Q7cxlhVj03IhYj/AxveNlKqZd2Ih3m/CJo0Abtwhx+qHZp -cCBrYj7VelEaGARTmfoIVoGxFGKZNCcNzn7R2ic7safxXqeEnxugsAYX/UmMoq1b -vMYLcaLRAoGBAMqMbbgejbD8Cy6wa5yg7XquqOP5gPdIYYS88TkQTp+razDqKPIU -hPKetnTDJ7PZleOLE6eJ+dQJ8gl6D/dtOsl4lVRy/BU74dk0fYMiEfiJMYEYuAU0 -MCramo3HAeySTP8pxSLFYqJVhcTpL9+NQgbpJBUlx5bLDlJPl7auY077AoGBAMkD -UpJRIv/0gYSz5btVheEyDzcqzOMZUVsngabH7aoQ49VjKrfLzJ9WznzJS5gZF58P -vB7RLuIA8m8Y4FUwxOr4w9WOevzlFh0gyzgNY4gCwrzEryOZqYYqCN+8QLWfq/hL -+gYFYpEW5pJ/lAy2i8kPanC3DyoqiZCsUmlg6JKNAoGBAIdCkf6zgKGhHwKV07cs -DIqx2p0rQEFid6UB3ADkb+zWt2VZ6fAHXeT7shJ1RK0o75ydgomObWR5I8XKWqE7 -s1dZjDdx9f9kFuVK1Upd1SxoycNRM4peGJB1nWJydEl8RajcRwZ6U+zeOc+OfWbH -WUFuLadlrEx5212CQ2k+OZlDAoGAdsH2w6kZ83xCFOOv41ioqx5HLQGlYLpxfVg+ -2gkeWa523HglIcdPEghYIBNRDQAuG3RRYSeW+kEy+f4Jc2tHu8bS9FWkRcsWoIji -ZzBJ0G5JHPtaub6sEC6/ZWe0F1nJYP2KLop57FxKRt0G2+fxeA0ahpMwa2oMMiQM -4GM3pHUCgYEAj2ZjjsF2MXYA6kuPUG1vyY9pvj1n4fyEEoV/zxY1k56UKboVOtYr -BA/cKaLPqUF+08Tz/9MPBw51UH4GYfppA/x0ktc8998984FeIpfIFX6I2U9yUnoQ -OCCAgsB8g8yTB4qntAYyfofEoDiseKrngQT5DSdxd51A/jw7B8WyBK8= ------END RSA PRIVATE KEY-----` +// EqualAccessTokens returns an error if the tokens are not equal +func EqualAccessTokens(ctx context.Context, expectedToken, actualToken string) error { + expectedParsed, err := TokenManager.ParseToken(ctx, expectedToken) + if err != nil { + return err + } + expectedClaims, err := TokenManager.ParseTokenWithMapClaims(ctx, expectedToken) + if err != nil { + return err + } + actualClaims, err := TokenManager.ParseTokenWithMapClaims(ctx, actualToken) + if err != nil { + return err + } + actualParsed, err := TokenManager.ParseToken(ctx, actualToken) + if err != nil { + return err + } + + err = equalTokenClaim("typ", expectedClaims, actualClaims) + if err != nil { + return err + } + if expectedParsed.Approved != actualParsed.Approved { + return errors.Errorf("'approved' claims are not equal. Expected: %v. Actual: %v", expectedParsed.Approved, actualParsed.Approved) + } + err = equalTokenClaim("email", expectedClaims, actualClaims) + if err != nil { + return err + } + err = equalTokenClaim("email_verified", expectedClaims, actualClaims) + if err != nil { + return err + } + err = equalTokenClaim("preferred_username", expectedClaims, actualClaims) + if err != nil { + return err + } + err = equalTokenClaim("name", expectedClaims, actualClaims) + if err != nil { + return err + } + return equalTokenClaim("sub", expectedClaims, actualClaims) +} + +// EqualRefreshTokens returns an error if the refresh tokens are not equal +func EqualRefreshTokens(ctx context.Context, expectedToken, actualToken string) error { + expectedClaims, err := TokenManager.ParseTokenWithMapClaims(ctx, expectedToken) + if err != nil { + return err + } + actualClaims, err := TokenManager.ParseTokenWithMapClaims(ctx, actualToken) + if err != nil { + return err + } + + err = equalTokenClaim("typ", expectedClaims, actualClaims) + if err != nil { + return err + } + return equalTokenClaim("sub", expectedClaims, actualClaims) + + return nil +} + +func equalTokenClaim(claimName string, expectedToken, actualToken jwt.MapClaims) error { + if expectedToken[claimName] != actualToken[claimName] { + return errors.Errorf("'%s' claims are not equal. Expected: %v. Actual: %v", claimName, expectedToken[claimName], actualToken[claimName]) + } + return nil +} diff --git a/token/link/link.go b/token/link/link.go index b668533..e4aae38 100644 --- a/token/link/link.go +++ b/token/link/link.go @@ -267,7 +267,7 @@ func (service *LinkService) Callback(ctx context.Context, req *goa.RequestData, // NewOauthProvider creates a new oauth provider for the given resource URL or provider alias func (service *OauthProviderFactoryService) NewOauthProvider(ctx context.Context, identityID uuid.UUID, req *goa.RequestData, forResource string) (ProviderConfig, error) { - authURL := rest.AbsoluteURL(req, "") + authURL := rest.AbsoluteURL(req, "", nil) // Check if the forResource is actually a provider alias like "github" or "openshift" if forResource == GitHubProviderAlias { return NewGitHubIdentityProvider(service.config.GetGitHubClientID(), service.config.GetGitHubClientSecret(), service.config.GetGitHubClientDefaultScopes(), authURL), nil diff --git a/token/token.go b/token/token.go index 64c226e..0606827 100644 --- a/token/token.go +++ b/token/token.go @@ -1,34 +1,33 @@ package token import ( + "bytes" "context" "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/json" "fmt" + "io" "net/http" + "strconv" + "strings" "sync" + "time" + + "github.com/pkg/errors" + "github.com/fabric8-services/fabric8-auth/account" autherrors "github.com/fabric8-services/fabric8-auth/errors" "github.com/fabric8-services/fabric8-auth/log" logintokencontext "github.com/fabric8-services/fabric8-auth/login/tokencontext" "github.com/fabric8-services/fabric8-auth/rest" - errs "github.com/pkg/errors" - - "bytes" - "io" - "strconv" - "strings" - - "time" - "github.com/dgrijalva/jwt-go" "github.com/goadesign/goa" goajwt "github.com/goadesign/goa/middleware/security/jwt" - "github.com/pkg/errors" "github.com/satori/go.uuid" + "golang.org/x/oauth2" "gopkg.in/square/go-jose.v2" ) @@ -47,9 +46,14 @@ const ( // configuration represents configuration needed to construct a token manager type configuration interface { - GetKeycloakEndpointCerts() string GetServiceAccountPrivateKey() ([]byte, string) GetDeprecatedServiceAccountPrivateKey() ([]byte, string) + GetUserAccountPrivateKey() ([]byte, string) + GetDeprecatedUserAccountPrivateKey() ([]byte, string) + GetDevModePublicKey() (bool, []byte, string) + IsPostgresDeveloperModeEnabled() bool + GetAccessTokenExpiresIn() int64 + GetRefreshTokenExpiresIn() int64 } type JsonKeys struct { @@ -100,6 +104,10 @@ type Manager interface { AuthServiceAccountToken(req *goa.RequestData) (string, error) GenerateServiceAccountToken(req *goa.RequestData, saID string, saName string) (string, error) GenerateUnsignedServiceAccountToken(req *goa.RequestData, saID string, saName string) *jwt.Token + GenerateUserToken(ctx context.Context, keycloakToken oauth2.Token, identity *account.Identity) (*oauth2.Token, error) + GenerateUserTokenForIdentity(ctx context.Context, identity account.Identity) (*oauth2.Token, error) + ConvertTokenSet(tokenSet TokenSet) *oauth2.Token + ConvertToken(oauthToken oauth2.Token) (*TokenSet, error) } // PrivateKey represents an RSA private key with a Key ID @@ -117,87 +125,66 @@ type tokenManager struct { publicKeysMap map[string]*rsa.PublicKey publicKeys []*PublicKey serviceAccountPrivateKey *PrivateKey + userAccountPrivateKey *PrivateKey jsonWebKeys JsonKeys pemKeys JsonKeys serviceAccountToken string serviceAccountLock sync.RWMutex + config configuration } // NewManager returns a new token Manager for handling tokens func NewManager(config configuration) (Manager, error) { - // Load public keys from Keycloak and add them to the manager tm := &tokenManager{ publicKeysMap: map[string]*rsa.PublicKey{}, } + tm.config = config - keycloakKeys, err := FetchKeys(config.GetKeycloakEndpointCerts()) + // Load the user account private key and add it to the manager. + // Extract the public key from it and add it to the map of public keys. + var err error + key, kid := config.GetUserAccountPrivateKey() + deprecatedKey, deprecatedKid := config.GetDeprecatedUserAccountPrivateKey() + tm.userAccountPrivateKey, err = LoadPrivateKey(tm, key, kid, deprecatedKey, deprecatedKid) if err != nil { - log.Error(nil, map[string]interface{}{}, "unable to load Keycloak public keys") - return nil, errors.New("unable to load Keycloak public keys") - } - for _, keycloakKey := range keycloakKeys { - tm.publicKeysMap[keycloakKey.KeyID] = keycloakKey.Key - tm.publicKeys = append(tm.publicKeys, &PublicKey{KeyID: keycloakKey.KeyID, Key: keycloakKey.Key}) - log.Info(nil, map[string]interface{}{ - "kid": keycloakKey.KeyID, - }, "Public key added") + log.Error(nil, map[string]interface{}{"err": err}, "unable to load user account private keys") + return nil, err } - // Load the service account private key and add it to the manager. // Extract the public key from it and add it to the map of public keys. - key, kid := config.GetServiceAccountPrivateKey() - if len(key) == 0 || kid == "" { - log.Error(nil, map[string]interface{}{ - "kid": kid, - "key_length": len(key), - }, "Service account private key or its ID are not set up") - return nil, errors.New("Service account private key or its ID are not set up") - } - rsaServiceAccountKey, err := jwt.ParseRSAPrivateKeyFromPEM(key) + key, kid = config.GetServiceAccountPrivateKey() + deprecatedKey, deprecatedKid = config.GetDeprecatedServiceAccountPrivateKey() + tm.serviceAccountPrivateKey, err = LoadPrivateKey(tm, key, kid, deprecatedKey, deprecatedKid) if err != nil { + log.Error(nil, map[string]interface{}{"err": err}, "unable to load service account private keys") return nil, err } - tm.serviceAccountPrivateKey = &PrivateKey{KeyID: kid, Key: rsaServiceAccountKey} - pk := &rsaServiceAccountKey.PublicKey - tm.publicKeysMap[kid] = pk - tm.publicKeys = append(tm.publicKeys, &PublicKey{KeyID: kid, Key: pk}) - log.Info(nil, map[string]interface{}{ - "kid": kid, - }, "Service account private key added") - // Extract public key from deprecated service account private key if any and add it to the manager - key, kid = config.GetDeprecatedServiceAccountPrivateKey() - if len(key) == 0 || kid == "" { - log.Debug(nil, map[string]interface{}{ - "kid": kid, - "key_length": len(key), - }, "No deprecated service account private key found") - } else { - rsaServiceAccountKey, err := jwt.ParseRSAPrivateKeyFromPEM(key) + + // Load Keycloak public key if run in dev mode. + devMode, key, kid := config.GetDevModePublicKey() + if devMode { + rsaKey, err := jwt.ParseRSAPublicKeyFromPEM(key) if err != nil { + log.Error(nil, map[string]interface{}{"err": err}, "unable to load dev mode public key") return nil, err } - pk := &rsaServiceAccountKey.PublicKey - tm.publicKeysMap[kid] = pk - tm.publicKeys = append(tm.publicKeys, &PublicKey{KeyID: kid, Key: pk}) - log.Info(nil, map[string]interface{}{ - "kid": kid, - }, "Deprecated service account private key added") + tm.publicKeysMap[kid] = rsaKey + tm.publicKeys = append(tm.publicKeys, &PublicKey{KeyID: kid, Key: rsaKey}) + log.Info(nil, map[string]interface{}{"kid": kid}, "dev mode public key added") } + // Convert public keys to JWK format jsonKeys, err := toJsonWebKeys(tm.publicKeys) if err != nil { - log.Error(nil, map[string]interface{}{ - "err": err, - }, "unable to convert public keys to JSON Web Keys") + log.Error(nil, map[string]interface{}{"err": err}, "unable to convert public keys to JSON Web Keys") return nil, errors.New("unable to convert public keys to JSON Web Keys") } tm.jsonWebKeys = jsonKeys + // Convert public keys to PEM format jsonKeys, err = toPemKeys(tm.publicKeys) if err != nil { - log.Error(nil, map[string]interface{}{ - "err": err, - }, "unable to convert public keys to PEM Keys") + log.Error(nil, map[string]interface{}{"err": err}, "unable to convert public keys to PEM Keys") return nil, errors.New("unable to convert public keys to PEM Keys") } tm.pemKeys = jsonKeys @@ -205,14 +192,48 @@ func NewManager(config configuration) (Manager, error) { return tm, nil } -// NewManagerWithPublicKey returns a new token Manager for handling tokens with the only public key -func NewManagerWithPublicKey(key *PublicKey, serviceAccountKey *PrivateKey) Manager { - saPublicKey := &serviceAccountKey.Key.PublicKey - return &tokenManager{ - publicKeysMap: map[string]*rsa.PublicKey{key.KeyID: key.Key, serviceAccountKey.KeyID: saPublicKey}, - publicKeys: []*PublicKey{key, {KeyID: serviceAccountKey.KeyID, Key: saPublicKey}}, - serviceAccountPrivateKey: serviceAccountKey, +// LoadPrivateKey loads a private key and a deprecated private key. +// Extracts public keys from them and adds them to the manager +// Returns the loaded private key. +func LoadPrivateKey(tm *tokenManager, key []byte, kid string, deprecatedKey []byte, deprecatedKid string) (*PrivateKey, error) { + if len(key) == 0 || kid == "" { + log.Error(nil, map[string]interface{}{ + "kid": kid, + "key_length": len(key), + }, "private key or its ID are not set up") + return nil, errors.New("private key or its ID are not set up") } + + // Load the private key. Extract the public key from it + rsaServiceAccountKey, err := jwt.ParseRSAPrivateKeyFromPEM(key) + if err != nil { + log.Error(nil, map[string]interface{}{"err": err}, "unable to parse private key") + return nil, err + } + privateKey := &PrivateKey{KeyID: kid, Key: rsaServiceAccountKey} + pk := &rsaServiceAccountKey.PublicKey + tm.publicKeysMap[kid] = pk + tm.publicKeys = append(tm.publicKeys, &PublicKey{KeyID: kid, Key: pk}) + log.Info(nil, map[string]interface{}{"kid": kid}, "public key added") + + // Extract public key from the deprecated key if any and add it to the manager + if len(deprecatedKey) == 0 || deprecatedKid == "" { + log.Debug(nil, map[string]interface{}{ + "kid": deprecatedKid, + "key_length": len(deprecatedKey), + }, "no deprecated private key found") + } else { + rsaServiceAccountKey, err := jwt.ParseRSAPrivateKeyFromPEM(deprecatedKey) + if err != nil { + log.Error(nil, map[string]interface{}{"err": err}, "unable to parse deprecated private key") + return nil, err + } + pk := &rsaServiceAccountKey.PublicKey + tm.publicKeysMap[deprecatedKid] = pk + tm.publicKeys = append(tm.publicKeys, &PublicKey{KeyID: deprecatedKid, Key: pk}) + log.Info(nil, map[string]interface{}{"kid": deprecatedKid}, "deprecated public key added") + } + return privateKey, nil } // FetchKeys fetches public JSON WEB Keys from a remote service @@ -447,15 +468,365 @@ func (mgm *tokenManager) GenerateServiceAccountToken(req *goa.RequestData, saID func (mgm *tokenManager) GenerateUnsignedServiceAccountToken(req *goa.RequestData, saID string, saName string) *jwt.Token { token := jwt.New(jwt.SigningMethodRS256) token.Header["kid"] = mgm.serviceAccountPrivateKey.KeyID - token.Claims.(jwt.MapClaims)["service_accountname"] = saName - token.Claims.(jwt.MapClaims)["sub"] = saID - token.Claims.(jwt.MapClaims)["jti"] = uuid.NewV4().String() - token.Claims.(jwt.MapClaims)["iat"] = time.Now().Unix() - token.Claims.(jwt.MapClaims)["iss"] = rest.AbsoluteURL(req, "") - token.Claims.(jwt.MapClaims)["scopes"] = []string{"uma_protection"} + claims := token.Claims.(jwt.MapClaims) + claims["service_accountname"] = saName + claims["sub"] = saID + claims["jti"] = uuid.NewV4().String() + claims["iat"] = time.Now().Unix() + claims["iss"] = rest.AbsoluteURL(req, "", nil) + claims["scopes"] = []string{"uma_protection"} return token } +// GenerateUserToken generates an OAuth2 user token for the given identity based on the Keycloak token +func (mgm *tokenManager) GenerateUserToken(ctx context.Context, keycloakToken oauth2.Token, identity *account.Identity) (*oauth2.Token, error) { + unsignedAccessToken, err := mgm.GenerateUnsignedUserAccessToken(ctx, keycloakToken.AccessToken, identity) + if err != nil { + return nil, errors.WithStack(err) + } + accessToken, err := unsignedAccessToken.SignedString(mgm.userAccountPrivateKey.Key) + if err != nil { + return nil, errors.WithStack(err) + } + unsignedRefreshToken, err := mgm.GenerateUnsignedUserRefreshToken(ctx, keycloakToken.RefreshToken, identity) + if err != nil { + return nil, errors.WithStack(err) + } + refreshToken, err := unsignedRefreshToken.SignedString(mgm.userAccountPrivateKey.Key) + if err != nil { + return nil, errors.WithStack(err) + } + token := &oauth2.Token{ + AccessToken: accessToken, + RefreshToken: refreshToken, + Expiry: keycloakToken.Expiry, + TokenType: "bearer", + } + + // Derivative OAuth2 claims "expires_in" and "refresh_expires_in" + extra := make(map[string]interface{}) + expiresIn := keycloakToken.Extra("expires_in") + if expiresIn != nil { + extra["expires_in"] = expiresIn + } + refreshExpiresIn := keycloakToken.Extra("refresh_expires_in") + if refreshExpiresIn != nil { + extra["refresh_expires_in"] = refreshExpiresIn + } + notBeforePolicy := keycloakToken.Extra("not_before_policy") + if notBeforePolicy != nil { + extra["not_before_policy"] = notBeforePolicy + } + if len(extra) > 0 { + token = token.WithExtra(extra) + } + + return token, nil +} + +// GenerateUserTokenForIdentity generates an OAuth2 user token for the given identity +func (mgm *tokenManager) GenerateUserTokenForIdentity(ctx context.Context, identity account.Identity) (*oauth2.Token, error) { + nowTime := time.Now().Unix() + unsignedAccessToken, err := mgm.GenerateUnsignedUserAccessTokenForIdentity(ctx, identity) + if err != nil { + return nil, errors.WithStack(err) + } + accessToken, err := unsignedAccessToken.SignedString(mgm.userAccountPrivateKey.Key) + if err != nil { + return nil, errors.WithStack(err) + } + unsignedRefreshToken, err := mgm.GenerateUnsignedUserRefreshTokenForIdentity(ctx, identity) + if err != nil { + return nil, errors.WithStack(err) + } + refreshToken, err := unsignedRefreshToken.SignedString(mgm.userAccountPrivateKey.Key) + if err != nil { + return nil, errors.WithStack(err) + } + + var nbf int64 + + token := &oauth2.Token{ + AccessToken: accessToken, + RefreshToken: refreshToken, + Expiry: time.Unix(nowTime+mgm.config.GetAccessTokenExpiresIn(), 0), + TokenType: "bearer", + } + + // Derivative OAuth2 claims "expires_in" and "refresh_expires_in" + extra := make(map[string]interface{}) + extra["expires_in"] = mgm.config.GetAccessTokenExpiresIn() + extra["refresh_expires_in"] = mgm.config.GetRefreshTokenExpiresIn() + extra["not_before_policy"] = nbf + + token = token.WithExtra(extra) + + return token, nil +} + +// GenerateUnsignedUserAccessToken generates an unsigned OAuth2 user access token for the given identity based on the Keycloak token +func (mgm *tokenManager) GenerateUnsignedUserAccessToken(ctx context.Context, keycloakAccessToken string, identity *account.Identity) (*jwt.Token, error) { + token := jwt.New(jwt.SigningMethodRS256) + token.Header["kid"] = mgm.userAccountPrivateKey.KeyID + + kcClaims, err := mgm.ParseToken(ctx, keycloakAccessToken) + if err != nil { + return nil, errors.WithStack(err) + } + + req := goa.ContextRequest(ctx) + if req == nil { + return nil, errors.New("missing request in context") + } + + authOpenshiftIO := rest.AbsoluteURL(req, "", mgm.config) + openshiftIO, err := rest.ReplaceDomainPrefixInAbsoluteURL(req, "", "", mgm.config) + if err != nil { + return nil, errors.WithStack(err) + } + + claims := token.Claims.(jwt.MapClaims) + claims["jti"] = uuid.NewV4().String() + claims["exp"] = kcClaims.ExpiresAt + claims["nbf"] = kcClaims.NotBefore + claims["iat"] = kcClaims.IssuedAt + claims["iss"] = kcClaims.Issuer + claims["aud"] = kcClaims.Audience + claims["typ"] = "Bearer" + claims["auth_time"] = kcClaims.IssuedAt + claims["approved"] = identity != nil && !identity.User.Deprovisioned && kcClaims.Approved + if identity != nil { + claims["sub"] = identity.ID.String() + claims["email_verified"] = identity.User.EmailVerified + claims["name"] = identity.User.FullName + claims["preferred_username"] = identity.Username + firstName, lastName := account.SplitFullName(identity.User.FullName) + claims["given_name"] = firstName + claims["family_name"] = lastName + claims["email"] = identity.User.Email + } else { + claims["sub"] = kcClaims.Subject + claims["email_verified"] = kcClaims.EmailVerified + claims["name"] = kcClaims.Name + claims["preferred_username"] = kcClaims.Username + claims["given_name"] = kcClaims.GivenName + claims["family_name"] = kcClaims.FamilyName + claims["email"] = kcClaims.Email + } + + claims["allowed-origins"] = []string{ + authOpenshiftIO, + openshiftIO, + } + + claims["azp"] = kcClaims.Audience + claims["session_state"] = kcClaims.SessionState + claims["acr"] = "0" + + realmAccess := make(map[string]interface{}) + realmAccess["roles"] = []string{"uma_authorization"} + claims["realm_access"] = realmAccess + + resourceAccess := make(map[string]interface{}) + broker := make(map[string]interface{}) + broker["roles"] = []string{"read-token"} + resourceAccess["broker"] = broker + + account := make(map[string]interface{}) + account["roles"] = []string{"manage-account", "manage-account-links", "view-profile"} + resourceAccess["account"] = account + + claims["resource_access"] = resourceAccess + + return token, nil +} + +// GenerateUnsignedUserAccessTokenForIdentity generates an unsigned OAuth2 user access token for the given identity +func (mgm *tokenManager) GenerateUnsignedUserAccessTokenForIdentity(ctx context.Context, identity account.Identity) (*jwt.Token, error) { + token := jwt.New(jwt.SigningMethodRS256) + token.Header["kid"] = mgm.userAccountPrivateKey.KeyID + + req := goa.ContextRequest(ctx) + if req == nil { + return nil, errors.New("missing request in context") + } + + authOpenshiftIO := rest.AbsoluteURL(req, "", mgm.config) + openshiftIO, err := rest.ReplaceDomainPrefixInAbsoluteURL(req, "", "", mgm.config) + if err != nil { + return nil, errors.WithStack(err) + } + + claims := token.Claims.(jwt.MapClaims) + claims["jti"] = uuid.NewV4().String() + iat := time.Now().Unix() + claims["exp"] = iat + mgm.config.GetAccessTokenExpiresIn() + claims["nbf"] = 0 + claims["iat"] = iat + claims["iss"] = authOpenshiftIO + claims["aud"] = openshiftIO + claims["typ"] = "Bearer" + claims["auth_time"] = iat // TODO should use the time when user actually logged-in the last time. Will need to get this time from the RHD token + claims["approved"] = !identity.User.Deprovisioned + claims["sub"] = identity.ID.String() + claims["email_verified"] = identity.User.EmailVerified + claims["name"] = identity.User.FullName + claims["preferred_username"] = identity.Username + firstName, lastName := account.SplitFullName(identity.User.FullName) + claims["given_name"] = firstName + claims["family_name"] = lastName + claims["email"] = identity.User.Email + claims["allowed-origins"] = []string{ + authOpenshiftIO, + openshiftIO, + } + + return token, nil +} + +// GenerateUnsignedUserRefreshToken generates an unsigned OAuth2 user refresh token for the given identity based on the Keycloak token +func (mgm *tokenManager) GenerateUnsignedUserRefreshToken(ctx context.Context, keycloakRefreshToken string, identity *account.Identity) (*jwt.Token, error) { + token := jwt.New(jwt.SigningMethodRS256) + token.Header["kid"] = mgm.userAccountPrivateKey.KeyID + + kcClaims, err := mgm.ParseToken(ctx, keycloakRefreshToken) + if err != nil { + return nil, errors.WithStack(err) + } + + req := goa.ContextRequest(ctx) + if req == nil { + return nil, errors.New("missing request in context") + } + + claims := token.Claims.(jwt.MapClaims) + claims["jti"] = uuid.NewV4().String() + claims["exp"] = kcClaims.ExpiresAt + claims["nbf"] = kcClaims.NotBefore + claims["iat"] = kcClaims.IssuedAt + claims["iss"] = kcClaims.Issuer + claims["aud"] = kcClaims.Audience + claims["typ"] = "Refresh" + claims["auth_time"] = 0 + + if identity != nil { + claims["sub"] = identity.ID.String() + } else { + claims["sub"] = kcClaims.Subject + } + + claims["azp"] = kcClaims.Audience + claims["session_state"] = kcClaims.SessionState + + return token, nil +} + +// GenerateUnsignedUserRefreshTokenForIdentity generates an unsigned OAuth2 user refresh token for the given identity +func (mgm *tokenManager) GenerateUnsignedUserRefreshTokenForIdentity(ctx context.Context, identity account.Identity) (*jwt.Token, error) { + token := jwt.New(jwt.SigningMethodRS256) + token.Header["kid"] = mgm.userAccountPrivateKey.KeyID + + req := goa.ContextRequest(ctx) + if req == nil { + return nil, errors.New("missing request in context") + } + + authOpenshiftIO := rest.AbsoluteURL(req, "", mgm.config) + openshiftIO, err := rest.ReplaceDomainPrefixInAbsoluteURL(req, "", "", mgm.config) + if err != nil { + return nil, errors.WithStack(err) + } + + claims := token.Claims.(jwt.MapClaims) + claims["jti"] = uuid.NewV4().String() + iat := time.Now().Unix() + exp := iat + mgm.config.GetRefreshTokenExpiresIn() + claims["exp"] = exp + claims["nbf"] = 0 + claims["iat"] = iat + claims["iss"] = authOpenshiftIO + claims["aud"] = openshiftIO + claims["typ"] = "Refresh" + claims["auth_time"] = 0 + claims["sub"] = identity.ID.String() + + return token, nil +} + +// ConvertTokenSet converts the token set to oauth2.Token +func (mgm *tokenManager) ConvertTokenSet(tokenSet TokenSet) *oauth2.Token { + var accessToken, refreshToken, tokenType string + extra := make(map[string]interface{}) + if tokenSet.AccessToken != nil { + accessToken = *tokenSet.AccessToken + } + if tokenSet.RefreshToken != nil { + refreshToken = *tokenSet.RefreshToken + } + if tokenSet.TokenType != nil { + tokenType = *tokenSet.TokenType + } + var expire time.Time + if tokenSet.ExpiresIn != nil { + expire = time.Now().Add(time.Duration(*tokenSet.ExpiresIn) * time.Second) + extra["expires_in"] = *tokenSet.ExpiresIn + } + if tokenSet.RefreshExpiresIn != nil { + extra["refresh_expires_in"] = *tokenSet.RefreshExpiresIn + } + if tokenSet.NotBeforePolicy != nil { + extra["not_before_policy"] = *tokenSet.NotBeforePolicy + } + + oauth2Token := &oauth2.Token{ + AccessToken: accessToken, + RefreshToken: refreshToken, + TokenType: tokenType, + Expiry: expire, + } + oauth2Token = oauth2Token.WithExtra(extra) + + return oauth2Token +} + +// ConvertToken converts the oauth2.Token to a token set +func (mgm *tokenManager) ConvertToken(oauthToken oauth2.Token) (*TokenSet, error) { + + tokenSet := &TokenSet{ + AccessToken: &oauthToken.AccessToken, + RefreshToken: &oauthToken.RefreshToken, + TokenType: &oauthToken.TokenType, + } + + var err error + tokenSet.ExpiresIn, err = mgm.extraInt(oauthToken, "expires_in") + if err != nil { + return nil, err + } + tokenSet.RefreshExpiresIn, err = mgm.extraInt(oauthToken, "refresh_expires_in") + if err != nil { + return nil, err + } + tokenSet.NotBeforePolicy, err = mgm.extraInt(oauthToken, "not_before_policy") + if err != nil { + return nil, err + } + + return tokenSet, nil +} + +func (mgm *tokenManager) extraInt(oauthToken oauth2.Token, claimName string) (*int64, error) { + claim := oauthToken.Extra(claimName) + if claim != nil { + claimInt, err := NumberToInt(claim) + if err != nil { + return nil, err + } + return &claimInt, nil + } + return nil, nil +} + func (mgm *tokenManager) Parse(ctx context.Context, tokenString string) (*jwt.Token, error) { keyFunc := mgm.keyFunction(ctx) jwtToken, err := jwt.Parse(tokenString, keyFunc) @@ -529,7 +900,7 @@ func ReadManagerFromContext(ctx context.Context) (*tokenManager, error) { "token": tm, }, "missing token manager") - return nil, errs.New("missing token manager") + return nil, errors.New("missing token manager") } return tm.(*tokenManager), nil } @@ -577,7 +948,7 @@ func ReadTokenSetFromJson(ctx context.Context, jsonString string) (*TokenSet, er var token TokenSet err := json.Unmarshal([]byte(jsonString), &token) if err != nil { - return nil, errs.Wrapf(err, "error when unmarshal json with access token %s ", jsonString) + return nil, errors.Wrapf(err, "error when unmarshal json with access token %s ", jsonString) } return &token, nil } diff --git a/token/token_remote_test.go b/token/token_remote_test.go deleted file mode 100644 index ea92824..0000000 --- a/token/token_remote_test.go +++ /dev/null @@ -1,100 +0,0 @@ -package token - -import ( - "errors" - "fmt" - "net/http" - "testing" - - testsuite "github.com/fabric8-services/fabric8-auth/test/suite" - - "github.com/dgrijalva/jwt-go" - "github.com/goadesign/goa" - "github.com/satori/go.uuid" - "github.com/stretchr/testify/require" - "github.com/stretchr/testify/suite" -) - -type TokenWhiteBoxTest struct { - testsuite.RemoteTestSuite - manager *tokenManager -} - -func TestRunTokenWhiteBoxTest(t *testing.T) { - suite.Run(t, &TokenWhiteBoxTest{RemoteTestSuite: testsuite.NewRemoteTestSuite()}) -} - -func (s *TokenWhiteBoxTest) SetupSuite() { - s.RemoteTestSuite.SetupSuite() - var err error - m, err := NewManager(s.Config) - require.Nil(s.T(), err) - require.NotNil(s.T(), m) - tm, ok := m.(*tokenManager) - require.True(s.T(), ok) - s.manager = tm -} - -func (s *TokenWhiteBoxTest) TestKeycloakTokensLoaded() { - minKeyNumber := 2 // At least one service account key and one Keycloak key - _, serviceAccountKid := s.Config.GetServiceAccountPrivateKey() - require.NotEqual(s.T(), "", serviceAccountKid) - require.NotNil(s.T(), s.manager.PublicKey(serviceAccountKid)) - - _, dServiceAccountKid := s.Config.GetDeprecatedServiceAccountPrivateKey() - if dServiceAccountKid != "" { - minKeyNumber++ - require.NotNil(s.T(), s.manager.PublicKey(dServiceAccountKid)) - } - require.True(s.T(), len(s.manager.PublicKeys()) >= minKeyNumber) - - require.Equal(s.T(), len(s.manager.publicKeys), len(s.manager.PublicKeys())) - require.Equal(s.T(), len(s.manager.publicKeys), len(s.manager.publicKeysMap)) - for i, k := range s.manager.publicKeys { - require.NotEqual(s.T(), "", k.KeyID) - require.NotNil(s.T(), s.manager.PublicKey(k.KeyID)) - require.Equal(s.T(), s.manager.PublicKeys()[i], k.Key) - } - - jwKeys := s.manager.JsonWebKeys() - require.NotEmpty(s.T(), jwKeys.Keys) - - pemKeys := s.manager.PemKeys() - require.NotEmpty(s.T(), pemKeys.Keys) -} - -func (s *TokenWhiteBoxTest) TestAuthServiceAccount() { - r := &goa.RequestData{ - Request: &http.Request{Host: "example.com"}, - } - - tokenString, err := s.manager.AuthServiceAccountToken(r) - require.Nil(s.T(), err) - - token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { - kid := token.Header["kid"] - if kid == nil { - return nil, errors.New("There is no 'kid' header in the token") - } - if fmt.Sprintf("%s", kid) != s.manager.serviceAccountPrivateKey.KeyID { - return nil, errors.New(fmt.Sprintf("The key ID %s doesn't match the private key ID %s", kid, s.manager.serviceAccountPrivateKey.KeyID)) - } - key := s.manager.PublicKey(fmt.Sprintf("%s", kid)) - if key == nil { - return nil, errors.New(fmt.Sprintf("There is no public key with such ID: %s", kid)) - } - return key, nil - }) - require.Nil(s.T(), err) - - claims := token.Claims.(jwt.MapClaims) - require.Equal(s.T(), AuthServiceAccountID, claims["sub"]) - require.Equal(s.T(), "fabric8-auth", claims["service_accountname"]) - require.Equal(s.T(), []interface{}{"uma_protection"}, claims["scopes"]) - jti, ok := claims["jti"].(string) - require.True(s.T(), ok) - _, err = uuid.FromString(jti) - require.Nil(s.T(), err) - require.NotEmpty(s.T(), claims["iat"]) - require.Equal(s.T(), "http://example.com", claims["iss"]) -} diff --git a/token/token_test.go b/token/token_test.go index 062122f..01c1300 100644 --- a/token/token_test.go +++ b/token/token_test.go @@ -2,7 +2,6 @@ package token_test import ( "context" - "crypto/rsa" "fmt" "testing" "time" @@ -19,6 +18,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" + "golang.org/x/oauth2" ) func TestToken(t *testing.T) { @@ -28,9 +28,7 @@ func TestToken(t *testing.T) { type TestTokenSuite struct { suite.Suite - config *configuration.ConfigurationData - privateKey *rsa.PrivateKey - tokenManager token.Manager + config *configuration.ConfigurationData } func (s *TestTokenSuite) SetupSuite() { @@ -39,11 +37,173 @@ func (s *TestTokenSuite) SetupSuite() { if err != nil { panic(fmt.Errorf("Failed to setup the configuration: %s", err.Error())) } - s.privateKey = testtoken.PrivateKey() - s.tokenManager = testtoken.NewManager() } -func (s *TestTokenSuite) TearDownSuite() { +func (s *TestTokenSuite) TestGenerateUserTokenForIdentity() { + token, identity, ctx := s.generateToken() + s.assertGeneratedToken(token, identity) + + // With verified email + identity.User.EmailVerified = true + token, err := testtoken.TokenManager.GenerateUserTokenForIdentity(ctx, identity) + require.NoError(s.T(), err) + s.assertGeneratedToken(token, identity) +} + +func (s *TestTokenSuite) assertGeneratedToken(generatedToken *oauth2.Token, identity account.Identity) { + require.NotNil(s.T(), generatedToken) + assert.Equal(s.T(), "bearer", generatedToken.TokenType) + + assert.True(s.T(), generatedToken.Valid()) + + // Extra + s.assertInt(30*24*60*60, generatedToken.Extra("expires_in")) + s.assertInt(30*24*60*60, generatedToken.Extra("refresh_expires_in")) + s.assertInt(0, generatedToken.Extra("not_before_policy")) + + // Access token + + accessToken, err := testtoken.TokenManager.ParseTokenWithMapClaims(context.Background(), generatedToken.AccessToken) + require.NoError(s.T(), err) + + // Headers + s.assertHeaders(generatedToken.AccessToken) + + // Claims + s.assertJti(accessToken) + iat := s.assertIat(accessToken) + s.assertExpiresIn(accessToken["exp"]) + s.assertIntClaim(accessToken, "nbf", 0) + s.assertClaim(accessToken, "iss", "https://auth.openshift.io") + s.assertClaim(accessToken, "aud", "https://openshift.io") + s.assertClaim(accessToken, "typ", "Bearer") + s.assertClaim(accessToken, "auth_time", iat) + s.assertClaim(accessToken, "approved", !identity.User.Deprovisioned) + s.assertClaim(accessToken, "sub", identity.ID.String()) + s.assertClaim(accessToken, "email", identity.User.Email) + s.assertClaim(accessToken, "email_verified", identity.User.EmailVerified) + s.assertClaim(accessToken, "preferred_username", identity.Username) + + firstName, lastName := account.SplitFullName(identity.User.FullName) + s.assertClaim(accessToken, "given_name", firstName) + s.assertClaim(accessToken, "family_name", lastName) + + s.assertClaim(accessToken, "allowed-origins", []interface{}{ + "https://auth.openshift.io", + "https://openshift.io", + }) + + // Refresh token + + refreshToken, err := testtoken.TokenManager.ParseTokenWithMapClaims(context.Background(), generatedToken.RefreshToken) + require.NoError(s.T(), err) + + // Headers + s.assertHeaders(generatedToken.RefreshToken) + + // Claims + s.assertJti(refreshToken) + s.assertIat(refreshToken) + s.assertExpiresIn(refreshToken["exp"]) + s.assertIntClaim(refreshToken, "nbf", 0) + s.assertClaim(refreshToken, "iss", "https://auth.openshift.io") + s.assertClaim(refreshToken, "aud", "https://openshift.io") + s.assertClaim(refreshToken, "typ", "Refresh") + s.assertIntClaim(refreshToken, "auth_time", 0) + s.assertClaim(refreshToken, "sub", identity.ID.String()) +} + +func (s *TestTokenSuite) assertHeaders(tokenString string) { + jwtToken, err := testtoken.TokenManager.Parse(context.Background(), tokenString) + assert.NoError(s.T(), err) + assert.Equal(s.T(), "aUGv8mQA85jg4V1DU8Uk1W0uKsxn187KQONAGl6AMtc", jwtToken.Header["kid"]) + assert.Equal(s.T(), "RS256", jwtToken.Header["alg"]) + assert.Equal(s.T(), "JWT", jwtToken.Header["typ"]) +} + +func (s *TestTokenSuite) assertExpiresIn(actualValue interface{}) { + require.NotNil(s.T(), actualValue) + now := time.Now().Unix() + expInt, err := token.NumberToInt(actualValue) + require.NoError(s.T(), err) + assert.True(s.T(), expInt >= now+30*24*60*60-60 && expInt < now+30*24*60*60+60, "expiration claim is not in 30 days (%d +/- 1m): %d", now+30*24*60*60, expInt) // Between 30 days from now and 30 days + 1 minute +} + +func (s *TestTokenSuite) assertJti(claims jwt.MapClaims) { + jti := claims["jti"] + require.NotNil(s.T(), jti) + require.IsType(s.T(), "", jti) + _, err := uuid.FromString(jti.(string)) + assert.NoError(s.T(), err) +} + +func (s *TestTokenSuite) assertIat(claims jwt.MapClaims) interface{} { + iat := claims["iat"] + require.NotNil(s.T(), iat) + iatInt, err := token.NumberToInt(iat) + require.NoError(s.T(), err) + now := time.Now().Unix() + assert.True(s.T(), iatInt <= now && iatInt > now-60, "'issued at' claim is not within one minute interval from now (%d): %d", now, iatInt) // Between now and 1 minute ago + return iat +} + +func (s *TestTokenSuite) assertClaim(claims jwt.MapClaims, claimName string, expectedValue interface{}) { + clm := claims[claimName] + require.NotNil(s.T(), clm) + assert.Equal(s.T(), expectedValue, clm) +} + +func (s *TestTokenSuite) assertIntClaim(claims jwt.MapClaims, claimName string, expectedValue interface{}) { + clm := claims[claimName] + require.NotNil(s.T(), clm) + clmInt, err := token.NumberToInt(clm) + expectedInt, err := token.NumberToInt(expectedValue) + require.NoError(s.T(), err) + assert.Equal(s.T(), expectedInt, clmInt) +} + +func (s *TestTokenSuite) assertInt(expectedValue, actualValue interface{}) { + require.NotNil(s.T(), actualValue) + actInt, err := token.NumberToInt(actualValue) + require.NoError(s.T(), err) + expInt, err := token.NumberToInt(expectedValue) + require.NoError(s.T(), err) + assert.Equal(s.T(), actInt, expInt) +} + +func (s *TestTokenSuite) TestConvertToken() { + // Generate an oauth token first + generatedToken, identity, _ := s.generateToken() + + // Now convert it to a token set + tokenSet, err := testtoken.TokenManager.ConvertToken(*generatedToken) + require.NoError(s.T(), err) + + // Convert the token set back to an oauth token + token := testtoken.TokenManager.ConvertTokenSet(*tokenSet) + require.NoError(s.T(), err) + + // Check the converted token + s.assertGeneratedToken(token, identity) +} + +func (s *TestTokenSuite) generateToken() (*oauth2.Token, account.Identity, context.Context) { + ctx := testtoken.ContextWithRequest(nil) + user := account.User{ + ID: uuid.NewV4(), + Email: uuid.NewV4().String(), + FullName: uuid.NewV4().String(), + Cluster: uuid.NewV4().String(), + } + identity := account.Identity{ + ID: uuid.NewV4(), + User: user, + Username: uuid.NewV4().String(), + } + token, err := testtoken.TokenManager.GenerateUserTokenForIdentity(ctx, identity) + require.NoError(s.T(), err) + + return token, identity, ctx } func (s *TestTokenSuite) TestValidOAuthAccessToken() { @@ -51,15 +211,15 @@ func (s *TestTokenSuite) TestValidOAuthAccessToken() { ID: uuid.NewV4(), Username: "testuser", } - generatedToken, err := testtoken.GenerateToken(identity.ID.String(), identity.Username, s.privateKey) + generatedToken, err := testtoken.GenerateToken(identity.ID.String(), identity.Username) assert.Nil(s.T(), err) - claims, err := s.tokenManager.ParseToken(context.Background(), generatedToken) + claims, err := testtoken.TokenManager.ParseToken(context.Background(), generatedToken) require.Nil(s.T(), err) assert.Equal(s.T(), identity.ID.String(), claims.Subject) assert.Equal(s.T(), identity.Username, claims.Username) - jwtToken, err := s.tokenManager.Parse(context.Background(), generatedToken) + jwtToken, err := testtoken.TokenManager.Parse(context.Background(), generatedToken) require.Nil(s.T(), err) s.checkClaim(jwtToken, "sub", identity.ID.String()) @@ -96,11 +256,11 @@ func (s *TestTokenSuite) TestInvalidOAuthAccessTokenFails() { } func (s *TestTokenSuite) checkInvalidToken(token string) { - _, err := s.tokenManager.ParseToken(context.Background(), token) + _, err := testtoken.TokenManager.ParseToken(context.Background(), token) assert.NotNil(s.T(), err) - _, err = s.tokenManager.ParseTokenWithMapClaims(context.Background(), token) + _, err = testtoken.TokenManager.ParseTokenWithMapClaims(context.Background(), token) assert.NotNil(s.T(), err) - _, err = s.tokenManager.Parse(context.Background(), token) + _, err = testtoken.TokenManager.Parse(context.Background(), token) assert.NotNil(s.T(), err) } @@ -141,7 +301,7 @@ func (s *TestTokenSuite) TestLocateTokenInContex() { tk.Claims.(jwt.MapClaims)["sub"] = id.String() ctx := goajwt.WithJWT(context.Background(), tk) - foundId, err := s.tokenManager.Locate(ctx) + foundId, err := testtoken.TokenManager.Locate(ctx) require.Nil(s.T(), err) assert.Equal(s.T(), id, foundId, "ID in created context not equal") } @@ -149,7 +309,7 @@ func (s *TestTokenSuite) TestLocateTokenInContex() { func (s *TestTokenSuite) TestLocateMissingTokenInContext() { ctx := context.Background() - _, err := s.tokenManager.Locate(ctx) + _, err := testtoken.TokenManager.Locate(ctx) if err == nil { s.T().Error("Should have returned error on missing token in contex", err) } @@ -159,7 +319,7 @@ func (s *TestTokenSuite) TestLocateMissingUUIDInTokenInContext() { tk := jwt.New(jwt.SigningMethodRS256) ctx := goajwt.WithJWT(context.Background(), tk) - _, err := s.tokenManager.Locate(ctx) + _, err := testtoken.TokenManager.Locate(ctx) require.NotNil(s.T(), err) } @@ -168,7 +328,7 @@ func (s *TestTokenSuite) TestLocateInvalidUUIDInTokenInContext() { tk.Claims.(jwt.MapClaims)["sub"] = "131" ctx := goajwt.WithJWT(context.Background(), tk) - _, err := s.tokenManager.Locate(ctx) + _, err := testtoken.TokenManager.Locate(ctx) require.NotNil(s.T(), err) } func (s *TestTokenSuite) TestInt32ToInt64OK() { diff --git a/token/token_whitebox_test.go b/token/token_whitebox_test.go index 2306233..a4108ab 100644 --- a/token/token_whitebox_test.go +++ b/token/token_whitebox_test.go @@ -10,6 +10,8 @@ import ( config "github.com/fabric8-services/fabric8-auth/configuration" "github.com/fabric8-services/fabric8-auth/resource" + "os" + "github.com/dgrijalva/jwt-go" "github.com/goadesign/goa" goajwt "github.com/goadesign/goa/middleware/security/jwt" @@ -38,24 +40,19 @@ func (s *TestWhiteboxTokenSuite) SetupSuite() { if err != nil { panic(fmt.Errorf("failed to setup the configuration: %s", err.Error())) } - s.tokenManager = newTestTokenManager() -} - -func (s *TestWhiteboxTokenSuite) TearDownSuite() { + m, err := NewManager(s.config) + require.Nil(s.T(), err) + require.NotNil(s.T(), m) + tm, ok := m.(*tokenManager) + require.True(s.T(), ok) + s.tokenManager = tm } -func newTestTokenManager() *tokenManager { - rsaServiceAccountKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(config.DefaultServiceAccountPrivateKey)) +func (s *TestWhiteboxTokenSuite) SetupTest() { + var err error + s.config, err = config.GetConfigurationData() if err != nil { - panic(fmt.Errorf("failed to parse priviate key: %s", err.Error())) - } - serviceAccountKey := &PrivateKey{KeyID: "9MLnViaRkhVj1GT9kpWUkwHIwUD-wZfUxR-3CpkE-Xs", Key: rsaServiceAccountKey} - saPublicKey := &serviceAccountKey.Key.PublicKey - - return &tokenManager{ - publicKeysMap: map[string]*rsa.PublicKey{serviceAccountKey.KeyID: saPublicKey}, - publicKeys: []*PublicKey{{KeyID: serviceAccountKey.KeyID, Key: saPublicKey}}, - serviceAccountPrivateKey: serviceAccountKey, + panic(fmt.Errorf("failed to setup the configuration: %s", err.Error())) } } @@ -132,3 +129,161 @@ func createInvalidSAContext() context.Context { token := jwt.NewWithClaims(jwt.SigningMethodRS512, claims) return goajwt.WithJWT(context.Background(), token) } + +func (s *TestWhiteboxTokenSuite) TestPrivateKeysLoaded() { + // One service account key, one user key, and one dev mode key + require.Equal(s.T(), 3, len(s.tokenManager.PublicKeys())) + + // SA key + _, serviceAccountKid := s.config.GetServiceAccountPrivateKey() + require.NotEqual(s.T(), "", serviceAccountKid) + require.NotNil(s.T(), s.tokenManager.PublicKey(serviceAccountKid)) + + // User key + _, userAccountKid := s.config.GetUserAccountPrivateKey() + require.NotEqual(s.T(), "", userAccountKid) + require.NotNil(s.T(), s.tokenManager.PublicKey(userAccountKid)) + + // Check all arrays and maps + require.Equal(s.T(), len(s.tokenManager.publicKeys), len(s.tokenManager.PublicKeys())) + require.Equal(s.T(), len(s.tokenManager.publicKeys), len(s.tokenManager.publicKeysMap)) + for i, k := range s.tokenManager.publicKeys { + require.NotEqual(s.T(), "", k.KeyID) + require.NotNil(s.T(), s.tokenManager.PublicKey(k.KeyID)) + require.Equal(s.T(), s.tokenManager.PublicKeys()[i], k.Key) + } + + // Check JWK and PEM formats + jwKeys := s.tokenManager.JsonWebKeys() + require.NotEmpty(s.T(), jwKeys.Keys) + + pemKeys := s.tokenManager.PemKeys() + require.NotEmpty(s.T(), pemKeys.Keys) +} + +func (s *TestWhiteboxTokenSuite) TestPrivateKeysLoadedFromEnvVars() { + s.checkPrivateKeyLoaded("AUTH_SERVICEACCOUNT_PRIVATEKEY", config.DefaultServiceAccountPrivateKey, "AUTH_SERVICEACCOUNT_PRIVATEKEYID", "9MLnViaRkhVj1GT9kpWUkwHIwUD-wZfUxR-3CpkE-Xs") + s.checkPrivateKeyLoaded("AUTH_USERACCOUNT_PRIVATEKEY", config.DefaultUserAccountPrivateKey, "AUTH_USERACCOUNT_PRIVATEKEYID", "aUGv8mQA85jg4V1DU8Uk1W0uKsxn187KQONAGl6AMtc") + s.checkPrivateKeyLoaded("AUTH_SERVICEACCOUNT_PRIVATEKEY_DEPRECATED", deprecatedServiceAccountPrivateKey, "AUTH_SERVICEACCOUNT_PRIVATEKEYID_DEPRECATED", "bMa8r5iGklldtlb23HE6DBAeIwD1SpmCTEwm2TqyUTo") + s.checkPrivateKeyLoaded("AUTH_USERACCOUNT_PRIVATEKEY_DEPRECATED", deprecatedUserAccountPrivateKey, "AUTH_USERACCOUNT_PRIVATEKEYID_DEPRECATED", "ATXsLMBt9YD8ZgSqCq84PMWNVai_Q2LjIp-lAneSi4s") +} + +func (s *TestWhiteboxTokenSuite) checkPrivateKeyLoaded(keyEnvVarName, keyEnvVarValue, kidEnvVarName, kidEnvVarValue string) { + keyEnv := os.Getenv(keyEnvVarName) + kidEnv := os.Getenv(kidEnvVarName) + defer func() { + os.Setenv(keyEnvVarName, keyEnv) + os.Setenv(kidEnvVarName, kidEnv) + }() + + os.Setenv(keyEnvVarName, keyEnvVarValue) + os.Setenv(kidEnvVarName, kidEnvVarValue) + c, err := config.GetConfigurationData() + require.NoError(s.T(), err) + + m, err := NewManager(c) + require.NoError(s.T(), err) + tm, ok := m.(*tokenManager) + require.True(s.T(), ok) + + publicKey := tm.PublicKey(kidEnvVarValue) + require.NotNil(s.T(), publicKey) + + rsaServiceAccountKey, err := jwt.ParseRSAPrivateKeyFromPEM([]byte(keyEnvVarValue)) + require.NoError(s.T(), err) + require.Equal(s.T(), rsaServiceAccountKey.PublicKey, *publicKey) +} + +func (s *TestWhiteboxTokenSuite) TestAuthServiceAccount() { + r := &goa.RequestData{ + Request: &http.Request{Host: "example.com"}, + } + + tokenString, err := s.tokenManager.AuthServiceAccountToken(r) + require.Nil(s.T(), err) + + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { + kid := token.Header["kid"] + if kid == nil { + return nil, errors.New("There is no 'kid' header in the token") + } + if fmt.Sprintf("%s", kid) != s.tokenManager.serviceAccountPrivateKey.KeyID { + return nil, errors.New(fmt.Sprintf("The key ID %s doesn't match the private key ID %s", kid, s.tokenManager.serviceAccountPrivateKey.KeyID)) + } + key := s.tokenManager.PublicKey(fmt.Sprintf("%s", kid)) + if key == nil { + return nil, errors.New(fmt.Sprintf("There is no public key with such ID: %s", kid)) + } + return key, nil + }) + require.Nil(s.T(), err) + + claims := token.Claims.(jwt.MapClaims) + require.Equal(s.T(), AuthServiceAccountID, claims["sub"]) + require.Equal(s.T(), "fabric8-auth", claims["service_accountname"]) + require.Equal(s.T(), []interface{}{"uma_protection"}, claims["scopes"]) + jti, ok := claims["jti"].(string) + require.True(s.T(), ok) + _, err = uuid.FromString(jti) + require.Nil(s.T(), err) + require.NotEmpty(s.T(), claims["iat"]) + require.Equal(s.T(), "http://example.com", claims["iss"]) +} + +const ( + deprecatedServiceAccountPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAgYUCOars5k/zvFcm+GkLCviNftWWtXiva0Sp+mKwTRUpTw6+ +B6Fz8gPv2WbmcKFi02YiEETDKx5uNkJixMj0ujxYh7C8c0uAvcdEPVIlgcaP8mnV +48my3Br278uhP2wsw51K/nehE829tRRpguMNQtjqqZerHqdEkFWAcRgsrSJVt2vP +ojgIJKMd0F+kYpvhHUpgIhwXL6iaTpyo7nVyE/T/6UENpe5PRo/Yszg+/dgkPJG7 +RVLboNMsTiCzfTIdMlllrBR5BhBz6JXA/9mbNBfnB02j1oKuwV3jq9PhaSeprcmL +CZUuxclj4Au7oDuwrp7MfwcAlr5kd2L6nmPUEQIDAQABAoIBABaf3Vuld+xjWvgz +YSNTdhJciJr3RHQ+uKXMQMT0KEfOwoCE2r0Kfu5vsZ4QU4CpMFItLRYabN1DW40u +24H0eItvrydEwCaDseF0xX7QsqyQuuRliG9Z9FxueWQ59djWVJt3Bnqc+w4yikjv +X971OoPK0HL/g2y/W0K7LMyUpHk5noNH2s0G9qf4FHIo1Lwfpe9hIvs4CZxtRqO9 +RsRDRoJEF6vFEd+qkNsJNwXynXap9SA1KTh2u1m1rVyDypsP+icHspRgeN2qTygp +i+z8c8eWl7KxE80GvepiAuGlNyR/udrDEYkPkIfqRnmJbsdwX0KieO1+RsDqfg4G +ZKTDMWECgYEAvfBMew1RFsQ+ifg3DmmHd/DwB++X10nJ+c3d8Mf8GWxxlMWzthRI +tEyAvd5ZcbIidHvDDeyBcxFSZvuaa2bzzNgbXiqZWQ4oQJCmGBQ9szCKudRaBIVZ +2aUe+AH5yGRC5D40hr4gCUcTEDYlYFZxrIjf6xvpxF4/YAYvJ+NfdMMCgYEArpEh +4LoSsKPdaZMqZkafXeyvzdA8Obcz5EggVyeAcK5un472aBmv+Dw0ROQKMoWvVya6 +EnnqMI5oGptPM4ocNQDukEUf8xtvsiAa3Hmk1X9LW9y03RQZYbKFG+IztPpdNHl9 +fD3WQFdK2K1NCSfRYUjzCdGCwMzzrjPrqx+9NpsCgYB3vpgo98NIjB41U1Q6dNNg +DXj2N9nNc4qvP1eNpjbMPG768Q0UXINdj+GWUiinojtQnnnhPFp8Fc6SeErpLTXE +zfWrD0YwO9mqosbj5VbkslSzRSofMYbszMnSZ0R3TqZRSNpKnHCMCM/+53P24Wi2 +8m/gxG9DSnu/6QYvqowSiwKBgBK2Sexd5bz7g7NabBQUg+a8hUfJh3skUTKqLJVL +DbCGciM2XuFfx4YTZgLwcsthmx77brymRt03lp8rgLzklAt2cxwR3M/hZAKzAE4b +1/husbRCHz0Hd4UKbsxDXgmLQMxsLXBQ7JNvB/3b7cMKep40BKFLzPk/vuswc5Wf +TFf7AoGBAJWMsF0Fxu1APVfrgeEE+1vWdyUDAINlNLuD2wWSu+8J1Kzg0p4LmyR2 +UOuQUESja64DUJcIEMzgB3xngApvNL/3PnQlM6+ZL3fS+MXGOrpofNhxBLJbLuoN +WA2V2idzoQRfDRW1xzJu11xJKMUAmnyU17iUePgZ2m0vO+EY4Tgc +-----END RSA PRIVATE KEY-----` + + deprecatedUserAccountPrivateKey = `-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAk08FgfnXRPpH7muT60xJrvsYXkqJ3LIPKyEBR9wWWHmN8bR8 +c3SPPCsmI6ykAsa4IavnwS5vY64/4kcL2IFz9EqMbdNqSjT1dly3a7rwi/lT9E85 +fe6mzaBz7460SANmFdB/e/9e6NVQWZvSsZ2T0QQFmDq+peQg4CbdbuQ95cb5vnDI +pWApiRS2zKUo9SLgrF1xgDSmSMHINn4SswS4Zaory/VmsElWPCxBs30k35qRfq5l +NZFjCgfC875CUHylnv+uddycdnP4Dw1aN31D7zyLkTsFh4DG62D0ui+SO8Vzecd5 +erpgFm5Q0E1fKYFSotlvInXjHnaU2cer6UhkEwIDAQABAoIBAArCg+F5kVrNeUGW +BAj02pD4cFA625UOQINi9sf78Hnn7xFPoKOCSRAZCsEiVByLzVlQSC5ZKPO7/5iU +ne3jjseyRk2jWqku8xsBLLimv/lJbfNzcfyb2P0+EhnWb56u+N7xCs7Q2WriYesZ +sasdmnVy+MGk0NYnMquMyzHVZBwLb0JZ28Rfg1krs3Ot8kWScGKBjlSEXftf11hz +pNcoidyNx5UrPplGNFn+uSZ2YqKp/D3b91pmCTaGWETC3NX3DPqyVB6An5Rto+yg +wIY/KDEQVdyxYbxKzIji6Y2QNgogxjY8Bf8kJt8m3xU6+rcGY766j/6yxnv3pKr0 +l4y2UXECgYEAyH80IBLFPRoHSW3As9elo2hhUCc+b5do0G5nD3F4dPMUj/oX7mrr +S3CVczX2duRd/L/Gso/6i3I0cWbA/DtPmqE6iDoJogGF/Ht1HwxLZFcPgsYEXiSp +7NgdxS/7bkAoCKwBKh0lZdrpJGJf334v8zhfcdR43s4/QirgiJKx87sCgYEAvBZ6 +9EkOxa3VS5sIt+G3UaTIlghwmTaf5RogP7JriKS2b90NIZvMcO2LMa6yhsd9dIG3 +BXcK22nbnmSo6CF7ZDTWDu0eN71vKsiOK0ko3Zqbk+OWJJiOZe//FaGIY9FS2beR +kXStM8/vlSJcGDBP/p01+uJZXSeDLK6Dv6N/D4kCgYByE/ZrnWJ+bpXg0MLJURTc +0iI0ge/DfKnVlkurfMul9z0m4ozFSi6Q4QEX6YdPhIZ5rgB3TvamaxetwmJh4blc +aQotwqACfs1mqDQus0ceU27u4I5RppjMuvbNYIy14Wkl7gBHnwfNWW44FoUoW9sa +j2O3F8aiN0XE9zKEYrs/ywKBgQC7AMLYdHa6df3WgNrnMAS6qNJB0TxaKKRK/XHI +wtUFc3Zru+TdYHCgapz1FZMsS9Vg68MTLOtfgV04my4QNZHf7GRTTM+5bZ/Ecshf +Iwr9YUWDgUh7NC6IDViZohPf4nO0QT36132JQRkcNqBH8GjoZlgQC9H7u1hBKXWW +KLEguQKBgDJAG9NfRiIAtUKxcNhg09UUP0jNWdGF2k9BI75HWEeUsgE++TZcC/Po +DN+17hUNEB2VOWVydpTkRCl5ws+ankX5jvvVRAxqbfB+Kf33J5o/kINzzo+NFccA +bHzHsuuOvQwzlLS06P/VkVlF8bAsA/ajgNCDz1vC8lBrcmEugrYC +-----END RSA PRIVATE KEY-----` +)