From d39e42ac2094b67eeaec9fc69ca7ebadb0458cea Mon Sep 17 00:00:00 2001 From: Alexey Kazakov Date: Thu, 29 Mar 2018 02:57:11 -0700 Subject: [PATCH] Generate user tokens in Auth service (#402) - New private keys ( added in openshift secrets ) in configuration (should match keys in KC) - We don't load public keys from KC any more - During login (/login and /token) we now generate our own keys (access & refresh) based on the KC - tokens instead of passing along the original KC tokens to client - We also generate new tokens when refreshing the token (/token/refresh) - Added new methods in token manager to generate tokens (access & refresh) based on Identity (in addition to methods to generate tokens based on KC tokens). These methods are not used atm but fully tested and ready to be used when we decommission KC - /token/generate (in Dev Mode) now doesn't use KC - /token/refresh in Dev Mode (but not in tests) doesn't use KC either fixes #229 --- account/email/verification_service.go | 2 +- auth/authz_blackbox_test.go | 4 +- configuration/configuration.go | 330 +++++++---- configuration/configuration_blackbox_test.go | 81 ++- controller/authorize.go | 2 +- controller/login.go | 2 +- controller/openid_configuration.go | 12 +- controller/paging.go | 2 +- controller/status_test.go | 4 +- .../test-files/token/keys/ok_jwk.golden.json | 12 +- .../test-files/token/keys/ok_pem.golden.json | 8 +- controller/token.go | 111 ++-- controller/token_blackbox_test.go | 92 +++- controller/token_remote_test.go | 190 ------- controller/user_test.go | 4 +- controller/userinfo_test.go | 4 +- controller/users.go | 2 +- login/service.go | 110 +++- login/service_blackbox_test.go | 157 ++++-- login/service_whitebox_test.go | 2 + main.go | 3 - openshift/auth.app.yaml | 10 + openshift/auth.config.yaml | 2 + rest/url.go | 42 +- rest/url_blackbox_test.go | 89 ++- space/authz/authz_test.go | 4 +- test/token/token.go | 213 +++++-- token/link/link.go | 2 +- token/token.go | 521 +++++++++++++++--- token/token_remote_test.go | 100 ---- token/token_test.go | 194 ++++++- token/token_whitebox_test.go | 185 ++++++- 32 files changed, 1715 insertions(+), 781 deletions(-) delete mode 100644 controller/token_remote_test.go delete mode 100644 token/token_remote_test.go 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-----` +)