diff --git a/actor/v7action/auth.go b/actor/v7action/auth.go index aef734c2c66..6c03cc4f366 100644 --- a/actor/v7action/auth.go +++ b/actor/v7action/auth.go @@ -25,7 +25,7 @@ func NewDefaultAuthActor(config Config, uaaClient UAAClient) AuthActor { } func (actor defaultAuthActor) Authenticate(credentials map[string]string, origin string, grantType constant.GrantType) error { - if grantType == constant.GrantTypePassword && actor.config.UAAGrantType() == string(constant.GrantTypeClientCredentials) { + if (grantType == constant.GrantTypePassword || grantType == constant.GrantTypeJwtBearer) && actor.config.UAAGrantType() == string(constant.GrantTypeClientCredentials) { return actionerror.PasswordGrantTypeLogoutRequiredError{} } @@ -45,7 +45,7 @@ func (actor defaultAuthActor) Authenticate(credentials map[string]string, origin actor.config.SetUAAGrantType(string(grantType)) } - if grantType == constant.GrantTypeClientCredentials { + if (grantType == constant.GrantTypeClientCredentials || grantType == constant.GrantTypeJwtBearer) && credentials["client_id"] != "" { actor.config.SetUAAClientCredentials(credentials["client_id"], "") } diff --git a/api/uaa/auth.go b/api/uaa/auth.go index f12da64f163..982b7f38310 100644 --- a/api/uaa/auth.go +++ b/api/uaa/auth.go @@ -62,6 +62,17 @@ func (client Client) Authenticate(creds map[string]string, origin string, grantT if grantType == constant.GrantTypePassword { request.SetBasicAuth(client.config.UAAOAuthClient(), client.config.UAAOAuthClientSecret()) + } else if grantType == constant.GrantTypeJwtBearer { + // overwrite client authentication in case of provided parameters in cf auth clientid clientsecret or use defaults as done in password grant + clientId := client.config.UAAOAuthClient() + clientSecret := client.config.UAAOAuthClientSecret() + if creds["client_id"] != "" { + clientId = creds["client_id"] + } + if creds["client_secret"] != "" { + clientSecret = creds["client_secret"] + } + request.SetBasicAuth(clientId, clientSecret) } responseBody := AuthResponse{} diff --git a/api/uaa/auth_test.go b/api/uaa/auth_test.go index b7002c3b79b..7f486a59098 100644 --- a/api/uaa/auth_test.go +++ b/api/uaa/auth_test.go @@ -161,6 +161,37 @@ var _ = Describe("Auth", func() { Expect(refreshToken).To(BeEmpty()) }) }) + + When("the grant type is jwt bearer assertion", func() { + BeforeEach(func() { + response := `{ + "access_token":"some-access-token" + }` + + credentials = map[string]string{ + "client_id": "some-client-id", + "client_secret": "some-client-secret", + } + origin = "" + grantType = constant.GrantTypeJwtBearer + server.AppendHandlers( + CombineHandlers( + verifyRequestHost(TestAuthorizationResource), + VerifyRequest(http.MethodPost, "/oauth/token"), + VerifyHeaderKV("Content-Type", "application/x-www-form-urlencoded"), + VerifyHeaderKV("Authorization", "Basic c29tZS1jbGllbnQtaWQ6c29tZS1jbGllbnQtc2VjcmV0"), + VerifyBody([]byte(fmt.Sprintf("client_id=%s&client_secret=%s&grant_type=%s", credentials["client_id"], credentials["client_secret"], url.QueryEscape(string(grantType))))), + RespondWith(http.StatusOK, response), + )) + }) + + It("authenticates with the assertion provided", func() { + Expect(executeErr).NotTo(HaveOccurred()) + + Expect(accessToken).To(Equal("some-access-token")) + Expect(refreshToken).To(BeEmpty()) + }) + }) }) When("an error occurs", func() { diff --git a/api/uaa/constant/grant_type.go b/api/uaa/constant/grant_type.go index e302d2f6562..7cc92cc1193 100644 --- a/api/uaa/constant/grant_type.go +++ b/api/uaa/constant/grant_type.go @@ -10,4 +10,6 @@ const ( // GrantTypePassword is used for user's username/password authentication. GrantTypePassword GrantType = "password" GrantTypeRefreshToken GrantType = "refresh_token" + // GrantTypeJwtBearer is used for token based user authentication + GrantTypeJwtBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer" ) diff --git a/api/uaa/refresh_token.go b/api/uaa/refresh_token.go index fba19e211ea..18407142a70 100644 --- a/api/uaa/refresh_token.go +++ b/api/uaa/refresh_token.go @@ -29,7 +29,7 @@ func (client *Client) RefreshAccessToken(refreshToken string) (RefreshedTokens, switch client.config.UAAGrantType() { case string(constant.GrantTypeClientCredentials): values = client.clientCredentialRefreshBody() - case "", string(constant.GrantTypePassword): // CLI used to write empty string for grant type in the case of password; preserve compatibility with old config.json files + case "", string(constant.GrantTypePassword), string(constant.GrantTypeJwtBearer): // CLI used to write empty string for grant type in the case of password; preserve compatibility with old config.json files values = client.refreshTokenBody(refreshToken) } diff --git a/api/uaa/wrapper/uaa_authentication.go b/api/uaa/wrapper/uaa_authentication.go index 96bc7e64d03..25f25c599c4 100644 --- a/api/uaa/wrapper/uaa_authentication.go +++ b/api/uaa/wrapper/uaa_authentication.go @@ -109,5 +109,6 @@ func skipAuthenticationHeader(request *http.Request, body []byte) bool { request.Method == http.MethodPost && (strings.Contains(stringBody, "grant_type=refresh_token") || strings.Contains(stringBody, "grant_type=password") || + strings.Contains(stringBody, "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer") || strings.Contains(stringBody, "grant_type=client_credentials")) } diff --git a/command/v7/auth_command.go b/command/v7/auth_command.go index 0d5f70c4702..95fe49fdf7a 100644 --- a/command/v7/auth_command.go +++ b/command/v7/auth_command.go @@ -18,7 +18,8 @@ type AuthCommand struct { RequiredArgs flag.Authentication `positional-args:"yes"` ClientCredentials bool `long:"client-credentials" description:"Use (non-user) service account (also called client credentials)"` Origin string `long:"origin" description:"Indicates the identity provider to be used for authentication"` - usage interface{} `usage:"CF_NAME auth USERNAME PASSWORD\n CF_NAME auth USERNAME PASSWORD --origin ORIGIN\n CF_NAME auth CLIENT_ID CLIENT_SECRET --client-credentials\n\nENVIRONMENT VARIABLES:\n CF_USERNAME=user Authenticating user. Overridden if USERNAME argument is provided.\n CF_PASSWORD=password Password associated with user. Overridden if PASSWORD argument is provided.\n\nWARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n Consider using the CF_PASSWORD environment variable instead\n\nEXAMPLES:\n CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)"` + Assertion string `long:"assertion" description:"Token based authentication with assertion (user) or in combination with client-credentials (non-user)"` + usage interface{} `usage:"CF_NAME auth USERNAME PASSWORD\n CF_NAME auth USERNAME PASSWORD --origin ORIGIN\n CF_NAME auth CLIENT_ID CLIENT_SECRET --client-credentials\n CF_NAME auth CLIENT_ID CLIENT_SECRET --assertion ID-TOKEN\n CF_NAME auth CLIENT_ID --client-credentials --assertion ACCESS-TOKEN\n\nENVIRONMENT VARIABLES:\n CF_USERNAME=user Authenticating user. Overridden if USERNAME argument is provided.\n CF_PASSWORD=password Password associated with user. Overridden if PASSWORD argument is provided.\n\nWARNING:\n Providing your password as a command line option is highly discouraged\n Your password may be visible to others and may be recorded in your shell history\n Consider using the CF_PASSWORD environment variable instead\n\nEXAMPLES:\n CF_NAME auth name@example.com \"my password\" (use quotes for passwords with a space)\n CF_NAME auth name@example.com \"\\\"password\\\"\" (escape quotes if used in password)"` relatedCommands interface{} `related_commands:"api, login, target"` } @@ -63,7 +64,7 @@ func (cmd AuthCommand) Execute(args []string) error { if !cmd.ClientCredentials { if cmd.Config.UAAGrantType() == string(constant.GrantTypeClientCredentials) { return translatableerror.PasswordGrantTypeLogoutRequiredError{} - } else if cmd.Config.UAAOAuthClient() != "cf" || cmd.Config.UAAOAuthClientSecret() != "" { + } else if (cmd.Assertion == "" && cmd.Config.UAAOAuthClient() != "cf") || cmd.Config.UAAOAuthClientSecret() != "" { return translatableerror.ManualClientCredentialsError{} } } @@ -76,8 +77,17 @@ func (cmd AuthCommand) Execute(args []string) error { grantType := constant.GrantTypePassword if cmd.ClientCredentials { grantType = constant.GrantTypeClientCredentials + if cmd.Assertion != "" { + if username == "" { + username = cmd.Config.UAAOAuthClient() + } + // use assertion as client_assertion - replacing client_secret - but stay with client credentials grant type + credentials["client_assertion"] = cmd.Assertion + credentials["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer" + } else { + credentials["client_secret"] = password + } credentials["client_id"] = username - credentials["client_secret"] = password } else if cmd.Config.IsCFOnK8s() { prompts, err := cmd.Actor.GetLoginPrompts() if err != nil { @@ -102,9 +112,23 @@ func (cmd AuthCommand) Execute(args []string) error { "k8s-auth-info": username, } } else { - credentials = map[string]string{ - "username": username, - "password": password, + if cmd.Assertion != "" { + // use assertion as user authentication using JWT bearer grant type + grantType = constant.GrantTypeJwtBearer + credentials = map[string]string{ + "assertion": cmd.Assertion, + } + if username != "" { + credentials["client_id"] = username + } + if password != "" { + credentials["client_secret"] = password + } + } else { + credentials = map[string]string{ + "username": username, + "password": password, + } } } @@ -147,7 +171,7 @@ func (cmd AuthCommand) getUsernamePassword() (string, string, error) { } } - if userMissing || passwordMissing { + if cmd.Assertion == "" && (userMissing || passwordMissing) { return "", "", translatableerror.MissingCredentialsError{ MissingUsername: userMissing, MissingPassword: passwordMissing, diff --git a/command/v7/auth_command_test.go b/command/v7/auth_command_test.go index fc945fc78ab..563bac1a046 100644 --- a/command/v7/auth_command_test.go +++ b/command/v7/auth_command_test.go @@ -271,6 +271,65 @@ var _ = Describe("auth Command", func() { }) }) + When("--assertion is set", func() { + BeforeEach(func() { + cmd.ClientCredentials = false + cmd.Assertion = "jwt-token" + cmd.RequiredArgs.Username = testID + cmd.RequiredArgs.Password = testSecret + }) + + It("outputs API target information and clears the targeted org and space", func() { + Expect(err).ToNot(HaveOccurred()) + + Expect(testUI.Out).To(Say("API endpoint: %s", fakeConfig.Target())) + Expect(testUI.Out).To(Say(`Authenticating\.\.\.`)) + Expect(testUI.Out).To(Say("OK")) + Expect(testUI.Out).To(Say("Use '%s target' to view or set your target org and space", binaryName)) + + Expect(fakeActor.AuthenticateCallCount()).To(Equal(1)) + credentials, origin, grantType := fakeActor.AuthenticateArgsForCall(0) + ID := credentials["client_id"] + secret := credentials["client_secret"] + Expect(ID).To(Equal(testID)) + Expect(secret).To(Equal(testSecret)) + Expect(origin).To(BeEmpty()) + Expect(grantType).To(Equal(constant.GrantTypeJwtBearer)) + }) + }) + + When("--assertion and --client-credentials is set", func() { + BeforeEach(func() { + cmd.ClientCredentials = true + cmd.Assertion = "client-jwt-token" + cmd.RequiredArgs.Username = testID + cmd.RequiredArgs.Password = testSecret + }) + + It("outputs API target information and clears the targeted org and space", func() { + Expect(err).ToNot(HaveOccurred()) + + Expect(testUI.Out).To(Say("API endpoint: %s", fakeConfig.Target())) + Expect(testUI.Out).To(Say(`Authenticating\.\.\.`)) + Expect(testUI.Out).To(Say("OK")) + Expect(testUI.Out).To(Say("Use '%s target' to view or set your target org and space", binaryName)) + + Expect(fakeActor.AuthenticateCallCount()).To(Equal(1)) + credentials, origin, grantType := fakeActor.AuthenticateArgsForCall(0) + ID := credentials["client_id"] + secret := credentials["client_secret"] + clientAssertion := credentials["client_assertion"] + clientAssertionType := credentials["client_assertion_type"] + Expect(ID).To(Equal(testID)) + Expect(secret).To(BeEmpty()) + Expect(origin).To(BeEmpty()) + Expect(secret).To(Equal("")) + Expect(clientAssertion).To(Equal("client-jwt-token")) + Expect(clientAssertionType).To(Equal("urn:ietf:params:oauth:client-assertion-type:jwt-bearer")) + Expect(grantType).To(Equal(constant.GrantTypeClientCredentials)) + }) + }) + When("the username and password are provided in env variables", func() { var ( envUsername string