Skip to content

Commit 9c27b40

Browse files
authored
HLRC: Add security Create Token API (#34791)
This adds the Create Token API (POST /_xpack/security/oauth2/token) to the High Level Rest Client. Relates: #29827
1 parent bb5b590 commit 9c27b40

File tree

11 files changed

+662
-5
lines changed

11 files changed

+662
-5
lines changed

client/rest-high-level/build.gradle

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ integTestCluster {
8888
systemProperty 'es.scripting.update.ctx_in_params', 'false'
8989
setting 'xpack.license.self_generated.type', 'trial'
9090
setting 'xpack.security.enabled', 'true'
91+
setting 'xpack.security.authc.token.enabled', 'true'
9192
// Truststore settings are not used since TLS is not enabled. Included for testing the get certificates API
9293
setting 'xpack.ssl.certificate_authorities', 'testnode.crt'
9394
setting 'xpack.security.transport.ssl.truststore.path', 'testnode.jks'

client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityClient.java

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,24 +20,26 @@
2020
package org.elasticsearch.client;
2121

2222
import org.elasticsearch.action.ActionListener;
23+
import org.elasticsearch.client.security.ChangePasswordRequest;
2324
import org.elasticsearch.client.security.ClearRolesCacheRequest;
2425
import org.elasticsearch.client.security.ClearRolesCacheResponse;
26+
import org.elasticsearch.client.security.CreateTokenRequest;
27+
import org.elasticsearch.client.security.CreateTokenResponse;
28+
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
29+
import org.elasticsearch.client.security.DeleteRoleMappingResponse;
2530
import org.elasticsearch.client.security.DeleteRoleRequest;
2631
import org.elasticsearch.client.security.DeleteRoleResponse;
27-
import org.elasticsearch.client.security.PutRoleMappingRequest;
28-
import org.elasticsearch.client.security.PutRoleMappingResponse;
2932
import org.elasticsearch.client.security.DisableUserRequest;
3033
import org.elasticsearch.client.security.EmptyResponse;
3134
import org.elasticsearch.client.security.EnableUserRequest;
3235
import org.elasticsearch.client.security.GetRoleMappingsRequest;
3336
import org.elasticsearch.client.security.GetRoleMappingsResponse;
3437
import org.elasticsearch.client.security.GetSslCertificatesRequest;
3538
import org.elasticsearch.client.security.GetSslCertificatesResponse;
39+
import org.elasticsearch.client.security.PutRoleMappingRequest;
40+
import org.elasticsearch.client.security.PutRoleMappingResponse;
3641
import org.elasticsearch.client.security.PutUserRequest;
3742
import org.elasticsearch.client.security.PutUserResponse;
38-
import org.elasticsearch.client.security.ChangePasswordRequest;
39-
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
40-
import org.elasticsearch.client.security.DeleteRoleMappingResponse;
4143

4244
import java.io.IOException;
4345

@@ -350,4 +352,32 @@ public void deleteRoleAsync(DeleteRoleRequest request, RequestOptions options, A
350352
DeleteRoleResponse::fromXContent, listener, singleton(404));
351353
}
352354

355+
/**
356+
* Creates an OAuth2 token.
357+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-token.html">
358+
* the docs</a> for more.
359+
*
360+
* @param request the request for the token
361+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
362+
* @return the response from the create token call
363+
* @throws IOException in case there is a problem sending the request or parsing back the response
364+
*/
365+
public CreateTokenResponse createToken(CreateTokenRequest request, RequestOptions options) throws IOException {
366+
return restHighLevelClient.performRequestAndParseEntity(request, SecurityRequestConverters::createToken, options,
367+
CreateTokenResponse::fromXContent, emptySet());
368+
}
369+
370+
/**
371+
* Asynchronously creates an OAuth2 token.
372+
* See <a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/security-api-get-token.html">
373+
* the docs</a> for more.
374+
*
375+
* @param request the request for the token
376+
* @param options the request options (e.g. headers), use {@link RequestOptions#DEFAULT} if nothing needs to be customized
377+
* @param listener the listener to be notified upon request completion
378+
*/
379+
public void createTokenAsync(CreateTokenRequest request, RequestOptions options, ActionListener<CreateTokenResponse> listener) {
380+
restHighLevelClient.performRequestAsyncAndParseEntity(request, SecurityRequestConverters::createToken, options,
381+
CreateTokenResponse::fromXContent, listener, emptySet());
382+
}
353383
}

client/rest-high-level/src/main/java/org/elasticsearch/client/SecurityRequestConverters.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import org.apache.http.client.methods.HttpPost;
2525
import org.apache.http.client.methods.HttpPut;
2626
import org.elasticsearch.client.security.ClearRolesCacheRequest;
27+
import org.elasticsearch.client.security.CreateTokenRequest;
2728
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
2829
import org.elasticsearch.client.security.DeleteRoleRequest;
2930
import org.elasticsearch.client.security.PutRoleMappingRequest;
@@ -140,4 +141,10 @@ static Request deleteRole(DeleteRoleRequest deleteRoleRequest) {
140141
params.withRefreshPolicy(deleteRoleRequest.getRefreshPolicy());
141142
return request;
142143
}
144+
145+
static Request createToken(CreateTokenRequest createTokenRequest) throws IOException {
146+
Request request = new Request(HttpPost.METHOD_NAME, "/_xpack/security/oauth2/token");
147+
request.setEntity(createEntity(createTokenRequest, REQUEST_BODY_CONTENT_TYPE));
148+
return request;
149+
}
143150
}
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client.security;
21+
22+
import org.elasticsearch.client.Validatable;
23+
import org.elasticsearch.common.CharArrays;
24+
import org.elasticsearch.common.Nullable;
25+
import org.elasticsearch.common.Strings;
26+
import org.elasticsearch.common.xcontent.ToXContentObject;
27+
import org.elasticsearch.common.xcontent.XContentBuilder;
28+
29+
import java.io.IOException;
30+
import java.util.Arrays;
31+
import java.util.Objects;
32+
33+
/**
34+
* Request to create a new OAuth2 token from the Elasticsearch cluster.
35+
*/
36+
public final class CreateTokenRequest implements Validatable, ToXContentObject {
37+
38+
private final String grantType;
39+
private final String scope;
40+
private final String username;
41+
private final char[] password;
42+
private final String refreshToken;
43+
44+
/**
45+
* General purpose constructor. This constructor is typically not useful, and one of the following factory methods should be used
46+
* instead:
47+
* <ul>
48+
* <li>{@link #passwordGrant(String, char[])}</li>
49+
* <li>{@link #refreshTokenGrant(String)}</li>
50+
* <li>{@link #clientCredentialsGrant()}</li>
51+
* </ul>
52+
*/
53+
public CreateTokenRequest(String grantType, @Nullable String scope, @Nullable String username, @Nullable char[] password,
54+
@Nullable String refreshToken) {
55+
if (Strings.isNullOrEmpty(grantType)) {
56+
throw new IllegalArgumentException("grant_type is required");
57+
}
58+
this.grantType = grantType;
59+
this.username = username;
60+
this.password = password;
61+
this.scope = scope;
62+
this.refreshToken = refreshToken;
63+
}
64+
65+
public static CreateTokenRequest passwordGrant(String username, char[] password) {
66+
if (Strings.isNullOrEmpty(username)) {
67+
throw new IllegalArgumentException("username is required");
68+
}
69+
if (password == null || password.length == 0) {
70+
throw new IllegalArgumentException("password is required");
71+
}
72+
return new CreateTokenRequest("password", null, username, password, null);
73+
}
74+
75+
public static CreateTokenRequest refreshTokenGrant(String refreshToken) {
76+
if (Strings.isNullOrEmpty(refreshToken)) {
77+
throw new IllegalArgumentException("refresh_token is required");
78+
}
79+
return new CreateTokenRequest("refresh_token", null, null, null, refreshToken);
80+
}
81+
82+
public static CreateTokenRequest clientCredentialsGrant() {
83+
return new CreateTokenRequest("client_credentials", null, null, null, null);
84+
}
85+
86+
public String getGrantType() {
87+
return grantType;
88+
}
89+
90+
public String getScope() {
91+
return scope;
92+
}
93+
94+
public String getUsername() {
95+
return username;
96+
}
97+
98+
public char[] getPassword() {
99+
return password;
100+
}
101+
102+
public String getRefreshToken() {
103+
return refreshToken;
104+
}
105+
106+
@Override
107+
public XContentBuilder toXContent(XContentBuilder builder, Params params) throws IOException {
108+
builder.startObject()
109+
.field("grant_type", grantType);
110+
if (scope != null) {
111+
builder.field("scope", scope);
112+
}
113+
if (username != null) {
114+
builder.field("username", username);
115+
}
116+
if (password != null) {
117+
byte[] passwordBytes = CharArrays.toUtf8Bytes(password);
118+
try {
119+
builder.field("password").utf8Value(passwordBytes, 0, passwordBytes.length);
120+
} finally {
121+
Arrays.fill(passwordBytes, (byte) 0);
122+
}
123+
}
124+
if (refreshToken != null) {
125+
builder.field("refresh_token", refreshToken);
126+
}
127+
return builder.endObject();
128+
}
129+
130+
@Override
131+
public boolean equals(Object o) {
132+
if (this == o) {
133+
return true;
134+
}
135+
if (o == null || getClass() != o.getClass()) {
136+
return false;
137+
}
138+
final CreateTokenRequest that = (CreateTokenRequest) o;
139+
return Objects.equals(grantType, that.grantType) &&
140+
Objects.equals(scope, that.scope) &&
141+
Objects.equals(username, that.username) &&
142+
Arrays.equals(password, that.password) &&
143+
Objects.equals(refreshToken, that.refreshToken);
144+
}
145+
146+
@Override
147+
public int hashCode() {
148+
int result = Objects.hash(grantType, scope, username, refreshToken);
149+
result = 31 * result + Arrays.hashCode(password);
150+
return result;
151+
}
152+
}
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
/*
2+
* Licensed to Elasticsearch under one or more contributor
3+
* license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright
5+
* ownership. Elasticsearch licenses this file to you under
6+
* the Apache License, Version 2.0 (the "License"); you may
7+
* not use this file except in compliance with the License.
8+
* You may obtain a copy of the License at
9+
*
10+
* http://www.apache.org/licenses/LICENSE-2.0
11+
*
12+
* Unless required by applicable law or agreed to in writing,
13+
* software distributed under the License is distributed on an
14+
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15+
* KIND, either express or implied. See the License for the
16+
* specific language governing permissions and limitations
17+
* under the License.
18+
*/
19+
20+
package org.elasticsearch.client.security;
21+
22+
import org.elasticsearch.common.ParseField;
23+
import org.elasticsearch.common.unit.TimeValue;
24+
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
25+
import org.elasticsearch.common.xcontent.XContentParser;
26+
27+
import java.io.IOException;
28+
import java.util.Objects;
29+
30+
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.constructorArg;
31+
import static org.elasticsearch.common.xcontent.ConstructingObjectParser.optionalConstructorArg;
32+
33+
/**
34+
* Response when creating a new OAuth2 token in the Elasticsearch cluster. Contains an access token, the token's expiry, and an optional
35+
* refresh token.
36+
*/
37+
public final class CreateTokenResponse {
38+
39+
private final String accessToken;
40+
private final String type;
41+
private final TimeValue expiresIn;
42+
private final String scope;
43+
private final String refreshToken;
44+
45+
public CreateTokenResponse(String accessToken, String type, TimeValue expiresIn, String scope, String refreshToken) {
46+
this.accessToken = accessToken;
47+
this.type = type;
48+
this.expiresIn = expiresIn;
49+
this.scope = scope;
50+
this.refreshToken = refreshToken;
51+
}
52+
53+
public String getAccessToken() {
54+
return accessToken;
55+
}
56+
57+
public String getType() {
58+
return type;
59+
}
60+
61+
public TimeValue getExpiresIn() {
62+
return expiresIn;
63+
}
64+
65+
public String getScope() {
66+
return scope;
67+
}
68+
69+
public String getRefreshToken() {
70+
return refreshToken;
71+
}
72+
73+
@Override
74+
public boolean equals(Object o) {
75+
if (this == o) {
76+
return true;
77+
}
78+
if (o == null || getClass() != o.getClass()) {
79+
return false;
80+
}
81+
final CreateTokenResponse that = (CreateTokenResponse) o;
82+
return Objects.equals(accessToken, that.accessToken) &&
83+
Objects.equals(type, that.type) &&
84+
Objects.equals(expiresIn, that.expiresIn) &&
85+
Objects.equals(scope, that.scope) &&
86+
Objects.equals(refreshToken, that.refreshToken);
87+
}
88+
89+
@Override
90+
public int hashCode() {
91+
return Objects.hash(accessToken, type, expiresIn, scope, refreshToken);
92+
}
93+
94+
private static final ConstructingObjectParser<CreateTokenResponse, Void> PARSER = new ConstructingObjectParser<>(
95+
"create_token_response", true, args -> new CreateTokenResponse(
96+
(String) args[0], (String) args[1], TimeValue.timeValueSeconds((Long) args[2]), (String) args[3], (String) args[4]));
97+
98+
static {
99+
PARSER.declareString(constructorArg(), new ParseField("access_token"));
100+
PARSER.declareString(constructorArg(), new ParseField("type"));
101+
PARSER.declareLong(constructorArg(), new ParseField("expires_in"));
102+
PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("scope"));
103+
PARSER.declareStringOrNull(optionalConstructorArg(), new ParseField("refresh_token"));
104+
}
105+
106+
public static CreateTokenResponse fromXContent(XContentParser parser) throws IOException {
107+
return PARSER.parse(parser, null);
108+
}
109+
}
110+

client/rest-high-level/src/test/java/org/elasticsearch/client/SecurityRequestConvertersTests.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.apache.http.client.methods.HttpDelete;
2424
import org.apache.http.client.methods.HttpPost;
2525
import org.apache.http.client.methods.HttpPut;
26+
import org.elasticsearch.client.security.CreateTokenRequest;
2627
import org.elasticsearch.client.security.DeleteRoleMappingRequest;
2728
import org.elasticsearch.client.security.DeleteRoleRequest;
2829
import org.elasticsearch.client.security.DisableUserRequest;
@@ -211,4 +212,34 @@ public void testDeleteRole() {
211212
assertEquals(expectedParams, request.getParameters());
212213
assertNull(request.getEntity());
213214
}
215+
216+
public void testCreateTokenWithPasswordGrant() throws Exception {
217+
final String username = randomAlphaOfLengthBetween(1, 12);
218+
final String password = randomAlphaOfLengthBetween(8, 12);
219+
CreateTokenRequest createTokenRequest = CreateTokenRequest.passwordGrant(username, password.toCharArray());
220+
Request request = SecurityRequestConverters.createToken(createTokenRequest);
221+
assertEquals(HttpPost.METHOD_NAME, request.getMethod());
222+
assertEquals("/_xpack/security/oauth2/token", request.getEndpoint());
223+
assertEquals(0, request.getParameters().size());
224+
assertToXContentBody(createTokenRequest, request.getEntity());
225+
}
226+
227+
public void testCreateTokenWithRefreshTokenGrant() throws Exception {
228+
final String refreshToken = randomAlphaOfLengthBetween(8, 24);
229+
CreateTokenRequest createTokenRequest = CreateTokenRequest.refreshTokenGrant(refreshToken);
230+
Request request = SecurityRequestConverters.createToken(createTokenRequest);
231+
assertEquals(HttpPost.METHOD_NAME, request.getMethod());
232+
assertEquals("/_xpack/security/oauth2/token", request.getEndpoint());
233+
assertEquals(0, request.getParameters().size());
234+
assertToXContentBody(createTokenRequest, request.getEntity());
235+
}
236+
237+
public void testCreateTokenWithClientCredentialsGrant() throws Exception {
238+
CreateTokenRequest createTokenRequest = CreateTokenRequest.clientCredentialsGrant();
239+
Request request = SecurityRequestConverters.createToken(createTokenRequest);
240+
assertEquals(HttpPost.METHOD_NAME, request.getMethod());
241+
assertEquals("/_xpack/security/oauth2/token", request.getEndpoint());
242+
assertEquals(0, request.getParameters().size());
243+
assertToXContentBody(createTokenRequest, request.getEntity());
244+
}
214245
}

0 commit comments

Comments
 (0)