Skip to content

Commit de83208

Browse files
authored
Allow CF Authentication based on Tokens - user and client tokens (#3397)
1 parent 9ae1f41 commit de83208

File tree

8 files changed

+138
-10
lines changed

8 files changed

+138
-10
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
@@ -18,7 +18,8 @@ type AuthCommand struct {
1818
RequiredArgs flag.Authentication `positional-args:"yes"`
1919
ClientCredentials bool `long:"client-credentials" description:"Use (non-user) service account (also called client credentials)"`
2020
Origin string `long:"origin" description:"Indicates the identity provider to be used for authentication"`
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\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)"`
21+
Assertion string `long:"assertion" description:"Token based authentication with assertion (user) or in combination with client-credentials (non-user)"`
22+
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)"`
2223
relatedCommands interface{} `related_commands:"api, login, target"`
2324
}
2425

@@ -63,7 +64,7 @@ func (cmd AuthCommand) Execute(args []string) error {
6364
if !cmd.ClientCredentials {
6465
if cmd.Config.UAAGrantType() == string(constant.GrantTypeClientCredentials) {
6566
return translatableerror.PasswordGrantTypeLogoutRequiredError{}
66-
} else if cmd.Config.UAAOAuthClient() != "cf" || cmd.Config.UAAOAuthClientSecret() != "" {
67+
} else if (cmd.Assertion == "" && cmd.Config.UAAOAuthClient() != "cf") || cmd.Config.UAAOAuthClientSecret() != "" {
6768
return translatableerror.ManualClientCredentialsError{}
6869
}
6970
}
@@ -76,8 +77,17 @@ func (cmd AuthCommand) Execute(args []string) error {
7677
grantType := constant.GrantTypePassword
7778
if cmd.ClientCredentials {
7879
grantType = constant.GrantTypeClientCredentials
80+
if cmd.Assertion != "" {
81+
if username == "" {
82+
username = cmd.Config.UAAOAuthClient()
83+
}
84+
// use assertion as client_assertion - replacing client_secret - but stay with client credentials grant type
85+
credentials["client_assertion"] = cmd.Assertion
86+
credentials["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
87+
} else {
88+
credentials["client_secret"] = password
89+
}
7990
credentials["client_id"] = username
80-
credentials["client_secret"] = password
8191
} else if cmd.Config.IsCFOnK8s() {
8292
prompts, err := cmd.Actor.GetLoginPrompts()
8393
if err != nil {
@@ -102,9 +112,23 @@ func (cmd AuthCommand) Execute(args []string) error {
102112
"k8s-auth-info": username,
103113
}
104114
} else {
105-
credentials = map[string]string{
106-
"username": username,
107-
"password": password,
115+
if cmd.Assertion != "" {
116+
// use assertion as user authentication using JWT bearer grant type
117+
grantType = constant.GrantTypeJwtBearer
118+
credentials = map[string]string{
119+
"assertion": cmd.Assertion,
120+
}
121+
if username != "" {
122+
credentials["client_id"] = username
123+
}
124+
if password != "" {
125+
credentials["client_secret"] = password
126+
}
127+
} else {
128+
credentials = map[string]string{
129+
"username": username,
130+
"password": password,
131+
}
108132
}
109133
}
110134

@@ -147,7 +171,7 @@ func (cmd AuthCommand) getUsernamePassword() (string, string, error) {
147171
}
148172
}
149173

150-
if userMissing || passwordMissing {
174+
if cmd.Assertion == "" && (userMissing || passwordMissing) {
151175
return "", "", translatableerror.MissingCredentialsError{
152176
MissingUsername: userMissing,
153177
MissingPassword: passwordMissing,

command/v7/auth_command_test.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,65 @@ var _ = Describe("auth Command", func() {
271271
})
272272
})
273273

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

0 commit comments

Comments
 (0)