Skip to content

Commit 0e8398e

Browse files
authored
[v8] Allow CF Authentication based on Tokens - user and client tokens (#3455)
1 parent 28400e3 commit 0e8398e

File tree

9 files changed

+143
-12
lines changed

9 files changed

+143
-12
lines changed

actor/v7action/auth.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ func NewDefaultAuthActor(config Config, uaaClient UAAClient) AuthActor {
2525
}
2626

2727
func (actor defaultAuthActor) Authenticate(credentials map[string]string, origin string, grantType constant.GrantType) error {
28-
if grantType == constant.GrantTypePassword && actor.config.UAAGrantType() == string(constant.GrantTypeClientCredentials) {
28+
if (grantType == constant.GrantTypePassword || grantType == constant.GrantTypeJwtBearer) && actor.config.UAAGrantType() == string(constant.GrantTypeClientCredentials) {
2929
return actionerror.PasswordGrantTypeLogoutRequiredError{}
3030
}
3131

@@ -45,7 +45,7 @@ func (actor defaultAuthActor) Authenticate(credentials map[string]string, origin
4545
actor.config.SetUAAGrantType(string(grantType))
4646
}
4747

48-
if grantType == constant.GrantTypeClientCredentials {
48+
if (grantType == constant.GrantTypeClientCredentials || grantType == constant.GrantTypeJwtBearer) && credentials["client_id"] != "" {
4949
actor.config.SetUAAClientCredentials(credentials["client_id"], "")
5050
}
5151

api/uaa/auth.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ func (client Client) Authenticate(creds map[string]string, origin string, grantT
6262

6363
if grantType == constant.GrantTypePassword {
6464
request.SetBasicAuth(client.config.UAAOAuthClient(), client.config.UAAOAuthClientSecret())
65+
} else if grantType == constant.GrantTypeJwtBearer {
66+
// overwrite client authentication in case of provided parameters in cf auth clientid clientsecret or use defaults as done in password grant
67+
clientId := client.config.UAAOAuthClient()
68+
clientSecret := client.config.UAAOAuthClientSecret()
69+
if creds["client_id"] != "" {
70+
clientId = creds["client_id"]
71+
}
72+
if creds["client_secret"] != "" {
73+
clientSecret = creds["client_secret"]
74+
}
75+
request.SetBasicAuth(clientId, clientSecret)
6576
}
6677

6778
responseBody := AuthResponse{}

api/uaa/auth_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,37 @@ var _ = Describe("Auth", func() {
161161
Expect(refreshToken).To(BeEmpty())
162162
})
163163
})
164+
165+
When("the grant type is jwt bearer assertion", func() {
166+
BeforeEach(func() {
167+
response := `{
168+
"access_token":"some-access-token"
169+
}`
170+
171+
credentials = map[string]string{
172+
"client_id": "some-client-id",
173+
"client_secret": "some-client-secret",
174+
}
175+
origin = ""
176+
grantType = constant.GrantTypeJwtBearer
177+
server.AppendHandlers(
178+
CombineHandlers(
179+
verifyRequestHost(TestAuthorizationResource),
180+
VerifyRequest(http.MethodPost, "/oauth/token"),
181+
VerifyHeaderKV("Content-Type", "application/x-www-form-urlencoded"),
182+
VerifyHeaderKV("Authorization", "Basic c29tZS1jbGllbnQtaWQ6c29tZS1jbGllbnQtc2VjcmV0"),
183+
VerifyBody([]byte(fmt.Sprintf("client_id=%s&client_secret=%s&grant_type=%s", credentials["client_id"], credentials["client_secret"], url.QueryEscape(string(grantType))))),
184+
RespondWith(http.StatusOK, response),
185+
))
186+
})
187+
188+
It("authenticates with the assertion provided", func() {
189+
Expect(executeErr).NotTo(HaveOccurred())
190+
191+
Expect(accessToken).To(Equal("some-access-token"))
192+
Expect(refreshToken).To(BeEmpty())
193+
})
194+
})
164195
})
165196

166197
When("an error occurs", func() {

api/uaa/constant/grant_type.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,6 @@ const (
1010
// GrantTypePassword is used for user's username/password authentication.
1111
GrantTypePassword GrantType = "password"
1212
GrantTypeRefreshToken GrantType = "refresh_token"
13+
// GrantTypeJwtBearer is used for token based user authentication
14+
GrantTypeJwtBearer GrantType = "urn:ietf:params:oauth:grant-type:jwt-bearer"
1315
)

api/uaa/refresh_token.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ func (client *Client) RefreshAccessToken(refreshToken string) (RefreshedTokens,
2929
switch client.config.UAAGrantType() {
3030
case string(constant.GrantTypeClientCredentials):
3131
values = client.clientCredentialRefreshBody()
32-
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
32+
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
3333
values = client.refreshTokenBody(refreshToken)
3434
}
3535

api/uaa/wrapper/uaa_authentication.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,5 +109,6 @@ func skipAuthenticationHeader(request *http.Request, body []byte) bool {
109109
request.Method == http.MethodPost &&
110110
(strings.Contains(stringBody, "grant_type=refresh_token") ||
111111
strings.Contains(stringBody, "grant_type=password") ||
112+
strings.Contains(stringBody, "grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Ajwt-bearer") ||
112113
strings.Contains(stringBody, "grant_type=client_credentials"))
113114
}

command/v7/auth_command.go

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ type AuthCommand struct {
1717
RequiredArgs flag.Authentication `positional-args:"yes"`
1818
ClientCredentials bool `long:"client-credentials" description:"Use (non-user) service account (also called client credentials)"`
1919
Origin string `long:"origin" description:"Indicates the identity provider to be used for authentication"`
20-
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)"`
20+
Assertion string `long:"assertion" description:"Token based authentication with assertion (user) or in combination with client-credentials (non-user)"`
21+
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)"`
2122
relatedCommands interface{} `related_commands:"api, login, target"`
2223
}
2324

@@ -62,7 +63,7 @@ func (cmd AuthCommand) Execute(args []string) error {
6263
if !cmd.ClientCredentials {
6364
if cmd.Config.UAAGrantType() == string(constant.GrantTypeClientCredentials) {
6465
return translatableerror.PasswordGrantTypeLogoutRequiredError{}
65-
} else if cmd.Config.UAAOAuthClient() != "cf" || cmd.Config.UAAOAuthClientSecret() != "" {
66+
} else if (cmd.Assertion == "" && cmd.Config.UAAOAuthClient() != "cf") || cmd.Config.UAAOAuthClientSecret() != "" {
6667
return translatableerror.ManualClientCredentialsError{}
6768
}
6869
}
@@ -75,12 +76,35 @@ func (cmd AuthCommand) Execute(args []string) error {
7576
grantType := constant.GrantTypePassword
7677
if cmd.ClientCredentials {
7778
grantType = constant.GrantTypeClientCredentials
79+
if cmd.Assertion != "" {
80+
if username == "" {
81+
username = cmd.Config.UAAOAuthClient()
82+
}
83+
// use assertion as client_assertion - replacing client_secret - but stay with client credentials grant type
84+
credentials["client_assertion"] = cmd.Assertion
85+
credentials["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
86+
} else {
87+
credentials["client_secret"] = password
88+
}
7889
credentials["client_id"] = username
79-
credentials["client_secret"] = password
8090
} else {
81-
credentials = map[string]string{
82-
"username": username,
83-
"password": password,
91+
if cmd.Assertion != "" {
92+
// use assertion as user authentication using JWT bearer grant type
93+
grantType = constant.GrantTypeJwtBearer
94+
credentials = map[string]string{
95+
"assertion": cmd.Assertion,
96+
}
97+
if username != "" {
98+
credentials["client_id"] = username
99+
}
100+
if password != "" {
101+
credentials["client_secret"] = password
102+
}
103+
} else {
104+
credentials = map[string]string{
105+
"username": username,
106+
"password": password,
107+
}
84108
}
85109
}
86110

@@ -130,7 +154,7 @@ func (cmd AuthCommand) getUsernamePassword() (string, string, error) {
130154
passwordMissing = false
131155
}
132156

133-
if userMissing || passwordMissing {
157+
if cmd.Assertion == "" && (userMissing || passwordMissing) {
134158
return "", "", translatableerror.MissingCredentialsError{
135159
MissingUsername: userMissing,
136160
MissingPassword: passwordMissing,

command/v7/auth_command_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,65 @@ var _ = Describe("auth Command", func() {
280280
})
281281
})
282282

283+
When("--assertion is set", func() {
284+
BeforeEach(func() {
285+
cmd.ClientCredentials = false
286+
cmd.Assertion = "jwt-token"
287+
cmd.RequiredArgs.Username = testID
288+
cmd.RequiredArgs.Password = testSecret
289+
})
290+
291+
It("outputs API target information and clears the targeted org and space", func() {
292+
Expect(err).ToNot(HaveOccurred())
293+
294+
Expect(testUI.Out).To(Say("API endpoint: %s", fakeConfig.Target()))
295+
Expect(testUI.Out).To(Say(`Authenticating\.\.\.`))
296+
Expect(testUI.Out).To(Say("OK"))
297+
Expect(testUI.Out).To(Say("Use '%s target' to view or set your target org and space", binaryName))
298+
299+
Expect(fakeActor.AuthenticateCallCount()).To(Equal(1))
300+
credentials, origin, grantType := fakeActor.AuthenticateArgsForCall(0)
301+
ID := credentials["client_id"]
302+
secret := credentials["client_secret"]
303+
Expect(ID).To(Equal(testID))
304+
Expect(secret).To(Equal(testSecret))
305+
Expect(origin).To(BeEmpty())
306+
Expect(grantType).To(Equal(constant.GrantTypeJwtBearer))
307+
})
308+
})
309+
310+
When("--assertion and --client-credentials is set", func() {
311+
BeforeEach(func() {
312+
cmd.ClientCredentials = true
313+
cmd.Assertion = "client-jwt-token"
314+
cmd.RequiredArgs.Username = testID
315+
cmd.RequiredArgs.Password = testSecret
316+
})
317+
318+
It("outputs API target information and clears the targeted org and space", func() {
319+
Expect(err).ToNot(HaveOccurred())
320+
321+
Expect(testUI.Out).To(Say("API endpoint: %s", fakeConfig.Target()))
322+
Expect(testUI.Out).To(Say(`Authenticating\.\.\.`))
323+
Expect(testUI.Out).To(Say("OK"))
324+
Expect(testUI.Out).To(Say("Use '%s target' to view or set your target org and space", binaryName))
325+
326+
Expect(fakeActor.AuthenticateCallCount()).To(Equal(1))
327+
credentials, origin, grantType := fakeActor.AuthenticateArgsForCall(0)
328+
ID := credentials["client_id"]
329+
secret := credentials["client_secret"]
330+
clientAssertion := credentials["client_assertion"]
331+
clientAssertionType := credentials["client_assertion_type"]
332+
Expect(ID).To(Equal(testID))
333+
Expect(secret).To(BeEmpty())
334+
Expect(origin).To(BeEmpty())
335+
Expect(secret).To(Equal(""))
336+
Expect(clientAssertion).To(Equal("client-jwt-token"))
337+
Expect(clientAssertionType).To(Equal("urn:ietf:params:oauth:client-assertion-type:jwt-bearer"))
338+
Expect(grantType).To(Equal(constant.GrantTypeClientCredentials))
339+
})
340+
})
341+
283342
When("the username and password are provided in env variables", func() {
284343
var (
285344
envUsername string

integration/v7/isolated/auth_command_test.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ var _ = Describe("auth command", func() {
2727

2828
Eventually(session).Should(Say("USAGE:"))
2929
Eventually(session).Should(Say("cf auth USERNAME PASSWORD\n"))
30-
Eventually(session).Should(Say("cf auth CLIENT_ID CLIENT_SECRET --client-credentials\n\n"))
30+
Eventually(session).Should(Say("cf auth CLIENT_ID CLIENT_SECRET --client-credentials\n"))
31+
Eventually(session).Should(Say("cf auth CLIENT_ID CLIENT_SECRET --assertion ID-TOKEN\n"))
32+
Eventually(session).Should(Say("cf auth CLIENT_ID --client-credentials --assertion ACCESS-TOKEN\n"))
3133

3234
Eventually(session).Should(Say("ENVIRONMENT VARIABLES:"))
3335
Eventually(session).Should(Say(`CF_USERNAME=user\s+Authenticating user. Overridden if USERNAME argument is provided.`))
@@ -44,7 +46,8 @@ var _ = Describe("auth command", func() {
4446

4547
Eventually(session).Should(Say("OPTIONS:"))
4648
Eventually(session).Should(Say("--client-credentials\\s+Use \\(non-user\\) service account \\(also called client credentials\\)\n"))
47-
Eventually(session).Should(Say("--origin\\s+Indicates the identity provider to be used for authentication\n\n"))
49+
Eventually(session).Should(Say("--origin\\s+Indicates the identity provider to be used for authentication\n"))
50+
Eventually(session).Should(Say("--assertion\\s+Token based authentication with assertion \\(user\\) or in combination with client-credentials \\(non-user\\)\n"))
4851

4952
Eventually(session).Should(Say("SEE ALSO:"))
5053
Eventually(session).Should(Say("api, login, target"))

0 commit comments

Comments
 (0)