Skip to content

Commit 5d9c270

Browse files
authored
Token API supports the client_credentials grant (#33106)
This change adds support for the client credentials grant type to the token api. The client credentials grant allows for a client to authenticate with the authorization server and obtain a token to access as itself. Per RFC 6749, a refresh token should not be included with the access token and as such a refresh token is not issued when the client credentials grant is used. The addition of the client credentials grant will allow users authenticated with mechanisms such as kerberos or PKI to obtain a token that can be used for subsequent access.
1 parent 309fb22 commit 5d9c270

File tree

15 files changed

+567
-81
lines changed

15 files changed

+567
-81
lines changed

x-pack/docs/en/rest-api/security/get-tokens.asciidoc

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -38,36 +38,39 @@ The following parameters can be specified in the body of a POST request and
3838
pertain to creating a token:
3939

4040
`grant_type`::
41-
(string) The type of grant. Valid grant types are: `password` and `refresh_token`.
41+
(string) The type of grant. Supported grant types are: `password`,
42+
`client_credentials` and `refresh_token`.
4243

4344
`password`::
4445
(string) The user's password. If you specify the `password` grant type, this
45-
parameter is required.
46+
parameter is required. This parameter is not valid with any other supported
47+
grant type.
4648

4749
`refresh_token`::
4850
(string) If you specify the `refresh_token` grant type, this parameter is
4951
required. It contains the string that was returned when you created the token
50-
and enables you to extend its life.
52+
and enables you to extend its life. This parameter is not valid with any other
53+
supported grant type.
5154

5255
`scope`::
5356
(string) The scope of the token. Currently tokens are only issued for a scope of
5457
`FULL` regardless of the value sent with the request.
5558

5659
`username`::
5760
(string) The username that identifies the user. If you specify the `password`
58-
grant type, this parameter is required.
61+
grant type, this parameter is required. This parameter is not valid with any
62+
other supported grant type.
5963

6064
==== Examples
6165

62-
The following example obtains a token for the `test_admin` user:
66+
The following example obtains a token using the `client_credentials` grant type,
67+
which simply creates a token as the authenticated user:
6368

6469
[source,js]
6570
--------------------------------------------------
6671
POST /_xpack/security/oauth2/token
6772
{
68-
"grant_type" : "password",
69-
"username" : "test_admin",
70-
"password" : "x-pack-test-password"
73+
"grant_type" : "client_credentials"
7174
}
7275
--------------------------------------------------
7376
// CONSOLE
@@ -80,12 +83,10 @@ seconds) that the token expires in, and the type:
8083
{
8184
"access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==",
8285
"type" : "Bearer",
83-
"expires_in" : 1200,
84-
"refresh_token": "vLBPvmAB6KvwvJZr27cS"
86+
"expires_in" : 1200
8587
}
8688
--------------------------------------------------
8789
// TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/]
88-
// TESTRESPONSE[s/vLBPvmAB6KvwvJZr27cS/$body.refresh_token/]
8990

9091
The token returned by this API can be used by sending a request with a
9192
`Authorization` header with a value having the prefix `Bearer ` followed
@@ -97,9 +98,39 @@ curl -H "Authorization: Bearer dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvb
9798
--------------------------------------------------
9899
// NOTCONSOLE
99100

101+
The following example obtains a token for the `test_admin` user using the
102+
`password` grant type:
103+
104+
[source,js]
105+
--------------------------------------------------
106+
POST /_xpack/security/oauth2/token
107+
{
108+
"grant_type" : "password",
109+
"username" : "test_admin",
110+
"password" : "x-pack-test-password"
111+
}
112+
--------------------------------------------------
113+
// CONSOLE
114+
115+
The following example output contains the access token, the amount of time (in
116+
seconds) that the token expires in, the type, and the refresh token:
117+
118+
[source,js]
119+
--------------------------------------------------
120+
{
121+
"access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==",
122+
"type" : "Bearer",
123+
"expires_in" : 1200,
124+
"refresh_token": "vLBPvmAB6KvwvJZr27cS"
125+
}
126+
--------------------------------------------------
127+
// TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/]
128+
// TESTRESPONSE[s/vLBPvmAB6KvwvJZr27cS/$body.refresh_token/]
129+
100130
[[security-api-refresh-token]]
101-
To extend the life of an existing token, you can call the API again with the
102-
refresh token within 24 hours of the token's creation. For example:
131+
To extend the life of an existing token obtained using the `password` grant type,
132+
you can call the API again with the refresh token within 24 hours of the token's
133+
creation. For example:
103134

104135
[source,js]
105136
--------------------------------------------------

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenRequest.java

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919

2020
import java.io.IOException;
2121
import java.util.Arrays;
22+
import java.util.Collections;
23+
import java.util.EnumSet;
24+
import java.util.Set;
25+
import java.util.stream.Collectors;
2226

2327
import static org.elasticsearch.action.ValidateActions.addValidationError;
2428

@@ -29,6 +33,37 @@
2933
*/
3034
public final class CreateTokenRequest extends ActionRequest {
3135

36+
public enum GrantType {
37+
PASSWORD("password"),
38+
REFRESH_TOKEN("refresh_token"),
39+
AUTHORIZATION_CODE("authorization_code"),
40+
CLIENT_CREDENTIALS("client_credentials");
41+
42+
private final String value;
43+
44+
GrantType(String value) {
45+
this.value = value;
46+
}
47+
48+
public String getValue() {
49+
return value;
50+
}
51+
52+
public static GrantType fromString(String grantType) {
53+
if (grantType != null) {
54+
for (GrantType type : values()) {
55+
if (type.getValue().equals(grantType)) {
56+
return type;
57+
}
58+
}
59+
}
60+
return null;
61+
}
62+
}
63+
64+
private static final Set<GrantType> SUPPORTED_GRANT_TYPES = Collections.unmodifiableSet(
65+
EnumSet.of(GrantType.PASSWORD, GrantType.REFRESH_TOKEN, GrantType.CLIENT_CREDENTIALS));
66+
3267
private String grantType;
3368
private String username;
3469
private SecureString password;
@@ -49,33 +84,58 @@ public CreateTokenRequest(String grantType, @Nullable String username, @Nullable
4984
@Override
5085
public ActionRequestValidationException validate() {
5186
ActionRequestValidationException validationException = null;
52-
if ("password".equals(grantType)) {
53-
if (Strings.isNullOrEmpty(username)) {
54-
validationException = addValidationError("username is missing", validationException);
55-
}
56-
if (password == null || password.getChars() == null || password.getChars().length == 0) {
57-
validationException = addValidationError("password is missing", validationException);
58-
}
59-
if (refreshToken != null) {
60-
validationException =
61-
addValidationError("refresh_token is not supported with the password grant_type", validationException);
62-
}
63-
} else if ("refresh_token".equals(grantType)) {
64-
if (username != null) {
65-
validationException =
66-
addValidationError("username is not supported with the refresh_token grant_type", validationException);
67-
}
68-
if (password != null) {
69-
validationException =
70-
addValidationError("password is not supported with the refresh_token grant_type", validationException);
71-
}
72-
if (refreshToken == null) {
73-
validationException = addValidationError("refresh_token is missing", validationException);
87+
GrantType type = GrantType.fromString(grantType);
88+
if (type != null) {
89+
switch (type) {
90+
case PASSWORD:
91+
if (Strings.isNullOrEmpty(username)) {
92+
validationException = addValidationError("username is missing", validationException);
93+
}
94+
if (password == null || password.getChars() == null || password.getChars().length == 0) {
95+
validationException = addValidationError("password is missing", validationException);
96+
}
97+
if (refreshToken != null) {
98+
validationException =
99+
addValidationError("refresh_token is not supported with the password grant_type", validationException);
100+
}
101+
break;
102+
case REFRESH_TOKEN:
103+
if (username != null) {
104+
validationException =
105+
addValidationError("username is not supported with the refresh_token grant_type", validationException);
106+
}
107+
if (password != null) {
108+
validationException =
109+
addValidationError("password is not supported with the refresh_token grant_type", validationException);
110+
}
111+
if (refreshToken == null) {
112+
validationException = addValidationError("refresh_token is missing", validationException);
113+
}
114+
break;
115+
case CLIENT_CREDENTIALS:
116+
if (username != null) {
117+
validationException =
118+
addValidationError("username is not supported with the client_credentials grant_type", validationException);
119+
}
120+
if (password != null) {
121+
validationException =
122+
addValidationError("password is not supported with the client_credentials grant_type", validationException);
123+
}
124+
if (refreshToken != null) {
125+
validationException = addValidationError("refresh_token is not supported with the client_credentials grant_type",
126+
validationException);
127+
}
128+
break;
129+
default:
130+
validationException = addValidationError("grant_type only supports the values: [" +
131+
SUPPORTED_GRANT_TYPES.stream().map(GrantType::getValue).collect(Collectors.joining(", ")) + "]",
132+
validationException);
74133
}
75134
} else {
76-
validationException = addValidationError("grant_type only supports the values: [password, refresh_token]", validationException);
135+
validationException = addValidationError("grant_type only supports the values: [" +
136+
SUPPORTED_GRANT_TYPES.stream().map(GrantType::getValue).collect(Collectors.joining(", ")) + "]",
137+
validationException);
77138
}
78-
79139
return validationException;
80140
}
81141

@@ -126,6 +186,11 @@ public String getRefreshToken() {
126186
@Override
127187
public void writeTo(StreamOutput out) throws IOException {
128188
super.writeTo(out);
189+
if (out.getVersion().before(Version.V_7_0_0_alpha1) && GrantType.CLIENT_CREDENTIALS.getValue().equals(grantType)) {
190+
throw new IllegalArgumentException("a request with the client_credentials grant_type cannot be sent to version [" +
191+
out.getVersion() + "]");
192+
}
193+
129194
out.writeString(grantType);
130195
if (out.getVersion().onOrAfter(Version.V_6_2_0)) {
131196
out.writeOptionalString(username);

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/token/CreateTokenResponse.java

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,8 +59,14 @@ public void writeTo(StreamOutput out) throws IOException {
5959
out.writeString(tokenString);
6060
out.writeTimeValue(expiresIn);
6161
out.writeOptionalString(scope);
62-
if (out.getVersion().onOrAfter(Version.V_6_2_0)) {
63-
out.writeString(refreshToken);
62+
if (out.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { // TODO change to V_6_5_0 after backport
63+
out.writeOptionalString(refreshToken);
64+
} else if (out.getVersion().onOrAfter(Version.V_6_2_0)) {
65+
if (refreshToken == null) {
66+
out.writeString("");
67+
} else {
68+
out.writeString(refreshToken);
69+
}
6470
}
6571
}
6672

@@ -70,7 +76,9 @@ public void readFrom(StreamInput in) throws IOException {
7076
tokenString = in.readString();
7177
expiresIn = in.readTimeValue();
7278
scope = in.readOptionalString();
73-
if (in.getVersion().onOrAfter(Version.V_6_2_0)) {
79+
if (in.getVersion().onOrAfter(Version.V_7_0_0_alpha1)) { // TODO change to V_6_5_0 after backport
80+
refreshToken = in.readOptionalString();
81+
} else if (in.getVersion().onOrAfter(Version.V_6_2_0)) {
7482
refreshToken = in.readString();
7583
}
7684
}
@@ -90,4 +98,20 @@ public XContentBuilder toXContent(XContentBuilder builder, Params params) throws
9098
}
9199
return builder.endObject();
92100
}
101+
102+
@Override
103+
public boolean equals(Object o) {
104+
if (this == o) return true;
105+
if (o == null || getClass() != o.getClass()) return false;
106+
CreateTokenResponse that = (CreateTokenResponse) o;
107+
return Objects.equals(tokenString, that.tokenString) &&
108+
Objects.equals(expiresIn, that.expiresIn) &&
109+
Objects.equals(scope, that.scope) &&
110+
Objects.equals(refreshToken, that.refreshToken);
111+
}
112+
113+
@Override
114+
public int hashCode() {
115+
return Objects.hash(tokenString, expiresIn, scope, refreshToken);
116+
}
93117
}
Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* or more contributor license agreements. Licensed under the Elastic License;
44
* you may not use this file except in compliance with the Elastic License.
55
*/
6-
package org.elasticsearch.xpack.security.action.token;
6+
package org.elasticsearch.xpack.core.security.action.token;
77

88
import org.elasticsearch.action.ActionRequestValidationException;
99
import org.elasticsearch.common.settings.SecureString;
@@ -20,7 +20,7 @@ public void testRequestValidation() {
2020
ActionRequestValidationException ve = request.validate();
2121
assertNotNull(ve);
2222
assertEquals(1, ve.validationErrors().size());
23-
assertThat(ve.validationErrors().get(0), containsString("[password, refresh_token]"));
23+
assertThat(ve.validationErrors().get(0), containsString("[password, refresh_token, client_credentials]"));
2424
assertThat(ve.validationErrors().get(0), containsString("grant_type"));
2525

2626
request.setGrantType("password");
@@ -72,5 +72,19 @@ public void testRequestValidation() {
7272
assertNotNull(ve);
7373
assertEquals(1, ve.validationErrors().size());
7474
assertThat(ve.validationErrors(), hasItem("refresh_token is missing"));
75+
76+
request.setGrantType("client_credentials");
77+
ve = request.validate();
78+
assertNull(ve);
79+
80+
request.setUsername(randomAlphaOfLengthBetween(1, 32));
81+
request.setPassword(new SecureString(randomAlphaOfLengthBetween(1, 32).toCharArray()));
82+
request.setRefreshToken(randomAlphaOfLengthBetween(1, 32));
83+
ve = request.validate();
84+
assertNotNull(ve);
85+
assertEquals(3, ve.validationErrors().size());
86+
assertThat(ve.validationErrors(), hasItem(containsString("username is not supported")));
87+
assertThat(ve.validationErrors(), hasItem(containsString("password is not supported")));
88+
assertThat(ve.validationErrors(), hasItem(containsString("refresh_token is not supported")));
7589
}
7690
}

0 commit comments

Comments
 (0)