mirror of
https://github.com/honeymoose/OpenSearch.git
synced 2025-02-17 10:25:15 +00:00
Rest endpoints for token based access (elastic/x-pack-elasticsearch#1235)
This commit adds rest endpoints for the creation of a new token and invalidation of an existing token. This builds upon the functionality that was introduced in elastic/x-pack-elasticsearch#1029. relates elastic/x-pack-elasticsearch#8 Original commit: elastic/x-pack-elasticsearch@d56611dfa3
This commit is contained in:
parent
f7fb02f21f
commit
1d08b4d1fb
@ -11,6 +11,7 @@ apply plugin: 'elasticsearch.docs-test'
|
||||
buildRestTests.expectedUnconvertedCandidates = [
|
||||
'en/ml/getting-started.asciidoc',
|
||||
'en/rest-api/security/users.asciidoc',
|
||||
'en/rest-api/security/tokens.asciidoc',
|
||||
'en/rest-api/watcher/put-watch.asciidoc',
|
||||
'en/rest-api/ml/post-data.asciidoc',
|
||||
'en/security/authentication/user-cache.asciidoc',
|
||||
|
@ -5,9 +5,11 @@
|
||||
* <<security-api-clear-cache>>
|
||||
* <<security-api-users>>
|
||||
* <<security-api-roles>>
|
||||
* <<security-api-tokens>>
|
||||
|
||||
include::security/authenticate.asciidoc[]
|
||||
include::security/change-password.asciidoc[]
|
||||
include::security/clear-cache.asciidoc[]
|
||||
include::security/users.asciidoc[]
|
||||
include::security/roles.asciidoc[]
|
||||
include::security/tokens.asciidoc[]
|
||||
|
91
docs/en/rest-api/security/tokens.asciidoc
Normal file
91
docs/en/rest-api/security/tokens.asciidoc
Normal file
@ -0,0 +1,91 @@
|
||||
[[security-api-tokens]]
|
||||
=== Token Management APIs
|
||||
|
||||
The `token` API enables you to create and invalidate bearer tokens for access
|
||||
without requiring basic authentication. The get token API takes the same
|
||||
parameters as a typical OAuth 2.0 token API except for the use of a JSON
|
||||
request body.
|
||||
|
||||
[[security-api-get-token]]
|
||||
To obtain a token, submit a POST request to the `/_xpack/security/oauth2/token`
|
||||
endpoint.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
POST /_xpack/security/oauth2/token
|
||||
{
|
||||
"grant_type" : "password",
|
||||
"username" : "elastic",
|
||||
"password" : "changeme"
|
||||
}
|
||||
--------------------------------------------------
|
||||
// CONSOLE
|
||||
|
||||
.Token Request Fields
|
||||
[cols="4,^2,10"]
|
||||
|=======================
|
||||
| Name | Required | Description
|
||||
| `username` | yes | The username that identifies the user.
|
||||
| `password` | yes | The user's password.
|
||||
| `grant_type`| yes | The type of grant. Currently only the `password`
|
||||
grant type is supported.
|
||||
| `scope` | no | The scope of the token. Currently tokens are only
|
||||
issued for a scope of `FULL` regardless of the value
|
||||
sent with the request.
|
||||
|=======================
|
||||
|
||||
A successful call returns a JSON structure that contains the access token, the
|
||||
amount of time (seconds) that the token expires in, the type, and the scope if
|
||||
available.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
{
|
||||
"access_token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==",
|
||||
"type" : "Bearer",
|
||||
"expires_in" : 1200
|
||||
}
|
||||
--------------------------------------------------
|
||||
// TESTRESPONSE[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/]
|
||||
|
||||
A successful call returns a JSON structure that shows whether the user has been
|
||||
created or updated.
|
||||
|
||||
The token returned by this API can be used by sending a request with a
|
||||
`Authorization` header with a value having the prefix `Bearer ` followed
|
||||
by the value of the `access_token`.
|
||||
|
||||
[source,shell]
|
||||
--------------------------------------------------
|
||||
curl -H "Authorization: Bearer dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==" http://localhost:9200/_cluster/health
|
||||
--------------------------------------------------
|
||||
|
||||
[[security-api-invalidate-token]]
|
||||
The tokens returned from this API have a finite period of time for which they
|
||||
are valid and after that time period, they can no longer be used. However, if
|
||||
a token must be invalidated immediately, you can do so by submitting a DELETE
|
||||
request to `/_xpack/security/oauth2/token`.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
DELETE /_xpack/security/oauth2/token
|
||||
{
|
||||
"token" : "dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ=="
|
||||
}
|
||||
--------------------------------------------------
|
||||
// CONSOLE
|
||||
// TEST[s/dGhpcyBpcyBub3QgYSByZWFsIHRva2VuIGJ1dCBpdCBpcyBvbmx5IHRlc3QgZGF0YS4gZG8gbm90IHRyeSB0byByZWFkIHRva2VuIQ==/$body.access_token/]
|
||||
// TEST[continued]
|
||||
|
||||
A successful call returns a JSON structure that indicates whether the token
|
||||
has already been invalidated.
|
||||
|
||||
[source,js]
|
||||
--------------------------------------------------
|
||||
{
|
||||
"created" : true <1>
|
||||
}
|
||||
--------------------------------------------------
|
||||
// TESTRESPONSE
|
||||
|
||||
<1> When a token has already been invalidated, `created` is set to false.
|
@ -123,6 +123,8 @@ import org.elasticsearch.xpack.security.bootstrap.DefaultPasswordBootstrapCheck;
|
||||
import org.elasticsearch.xpack.security.crypto.CryptoService;
|
||||
import org.elasticsearch.xpack.security.rest.SecurityRestFilter;
|
||||
import org.elasticsearch.xpack.security.rest.action.RestAuthenticateAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.oauth2.RestInvalidateTokenAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.realm.RestClearRealmCacheAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.role.RestClearRolesCacheAction;
|
||||
import org.elasticsearch.xpack.security.rest.action.role.RestDeleteRoleAction;
|
||||
@ -577,7 +579,9 @@ public class Security implements ActionPlugin, IngestPlugin, NetworkPlugin {
|
||||
new RestDeleteRoleAction(settings, restController),
|
||||
new RestChangePasswordAction(settings, restController, securityContext.get()),
|
||||
new RestSetEnabledAction(settings, restController),
|
||||
new RestHasPrivilegesAction(settings, restController, securityContext.get()));
|
||||
new RestHasPrivilegesAction(settings, restController, securityContext.get()),
|
||||
new RestGetTokenAction(settings, licenseState, restController),
|
||||
new RestInvalidateTokenAction(settings, licenseState, restController));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -0,0 +1,194 @@
|
||||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.rest.action.oauth2;
|
||||
|
||||
import org.apache.logging.log4j.Logger;
|
||||
import org.elasticsearch.action.ActionListener;
|
||||
import org.elasticsearch.action.ActionRequestValidationException;
|
||||
import org.elasticsearch.client.node.NodeClient;
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.settings.SecureString;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
|
||||
import org.elasticsearch.common.xcontent.ObjectParser.ValueType;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.license.LicenseUtils;
|
||||
import org.elasticsearch.license.XPackLicenseState;
|
||||
import org.elasticsearch.rest.BaseRestHandler;
|
||||
import org.elasticsearch.rest.BytesRestResponse;
|
||||
import org.elasticsearch.rest.RestChannel;
|
||||
import org.elasticsearch.rest.RestController;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.xpack.XPackPlugin;
|
||||
import org.elasticsearch.xpack.security.action.token.CreateTokenAction;
|
||||
import org.elasticsearch.xpack.security.action.token.CreateTokenRequest;
|
||||
import org.elasticsearch.xpack.security.action.token.CreateTokenResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Locale;
|
||||
|
||||
import static org.elasticsearch.rest.RestRequest.Method.POST;
|
||||
|
||||
/**
|
||||
* An implementation of a OAuth2-esque API for retrieval of an access token.
|
||||
* This API does not conform to the RFC completely as it uses XContent for the request body
|
||||
* instead for form encoded data. This is a relatively common modification of the OAuth2
|
||||
* specification as this aspect does not make the most sense since the response body is
|
||||
* expected to be JSON
|
||||
*/
|
||||
public final class RestGetTokenAction extends BaseRestHandler {
|
||||
|
||||
static final ConstructingObjectParser<CreateTokenRequest, Void> PARSER = new ConstructingObjectParser<>("token_request",
|
||||
a -> new CreateTokenRequest((String) a[0], (String) a[1], (SecureString) a[2], (String) a[3]));
|
||||
static {
|
||||
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("grant_type"));
|
||||
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("username"));
|
||||
PARSER.declareField(ConstructingObjectParser.optionalConstructorArg(), parser -> new SecureString(
|
||||
Arrays.copyOfRange(parser.textCharacters(), parser.textOffset(), parser.textOffset() + parser.textLength())),
|
||||
new ParseField("password"), ValueType.STRING);
|
||||
PARSER.declareString(ConstructingObjectParser.optionalConstructorArg(), new ParseField("scope"));
|
||||
}
|
||||
|
||||
private final XPackLicenseState licenseState;
|
||||
|
||||
public RestGetTokenAction(Settings settings, XPackLicenseState xPackLicenseState, RestController controller) {
|
||||
super(settings);
|
||||
this.licenseState = xPackLicenseState;
|
||||
controller.registerHandler(POST, "/_xpack/security/oauth2/token", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RestChannelConsumer prepareRequest(RestRequest request, NodeClient client)throws IOException {
|
||||
// this API shouldn't be available if security is disabled by license
|
||||
if (licenseState.isAuthAllowed() == false) {
|
||||
return channel ->
|
||||
channel.sendResponse(new BytesRestResponse(channel, LicenseUtils.newComplianceException(XPackPlugin.SECURITY)));
|
||||
}
|
||||
|
||||
try (XContentParser parser = request.contentParser()) {
|
||||
final CreateTokenRequest tokenRequest = PARSER.parse(parser, null);
|
||||
return channel -> client.execute(CreateTokenAction.INSTANCE, tokenRequest,
|
||||
// this doesn't use the RestBuilderListener since we need to override the
|
||||
// handling of failures in some cases.
|
||||
new CreateTokenResponseActionListener(channel, request, logger));
|
||||
}
|
||||
}
|
||||
|
||||
static class CreateTokenResponseActionListener implements ActionListener<CreateTokenResponse> {
|
||||
|
||||
private final RestChannel channel;
|
||||
private final RestRequest request;
|
||||
private final Logger logger;
|
||||
|
||||
CreateTokenResponseActionListener(RestChannel restChannel, RestRequest restRequest,
|
||||
Logger logger) {
|
||||
this.channel = restChannel;
|
||||
this.request = restRequest;
|
||||
this.logger = logger;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResponse(CreateTokenResponse createTokenResponse) {
|
||||
try (XContentBuilder builder = channel.newBuilder()) {
|
||||
channel.sendResponse(new BytesRestResponse(RestStatus.OK, createTokenResponse.toXContent(builder, request)));
|
||||
} catch (IOException e) {
|
||||
onFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Exception e) {
|
||||
if (e instanceof ActionRequestValidationException) {
|
||||
ActionRequestValidationException validationException = (ActionRequestValidationException) e;
|
||||
try (XContentBuilder builder = channel.newErrorBuilder()) {
|
||||
final TokenRequestError error;
|
||||
if (validationException.validationErrors().stream().anyMatch(s -> s.contains("grant_type"))) {
|
||||
error = TokenRequestError.UNSUPPORTED_GRANT_TYPE;
|
||||
} else {
|
||||
error = TokenRequestError.INVALID_REQUEST;
|
||||
}
|
||||
|
||||
// defined by https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
builder.startObject()
|
||||
.field("error",
|
||||
error.toString().toLowerCase(Locale.ROOT))
|
||||
.field("error_description",
|
||||
validationException.getMessage())
|
||||
.endObject();
|
||||
channel.sendResponse(
|
||||
new BytesRestResponse(RestStatus.BAD_REQUEST, builder));
|
||||
} catch (IOException ioe) {
|
||||
ioe.addSuppressed(e);
|
||||
sendFailure(ioe);
|
||||
}
|
||||
} else {
|
||||
sendFailure(e);
|
||||
}
|
||||
}
|
||||
|
||||
void sendFailure(Exception e) {
|
||||
try {
|
||||
channel.sendResponse(new BytesRestResponse(channel, e));
|
||||
} catch (Exception inner) {
|
||||
inner.addSuppressed(e);
|
||||
logger.error("failed to send failure response", inner);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// defined by https://tools.ietf.org/html/rfc6749#section-5.2
|
||||
enum TokenRequestError {
|
||||
/**
|
||||
* The request is missing a required parameter, includes an unsupported
|
||||
* parameter value (other than grant type), repeats a parameter,
|
||||
* includes multiple credentials, utilizes more than one mechanism for
|
||||
* authenticating the client, or is otherwise malformed.
|
||||
*/
|
||||
INVALID_REQUEST,
|
||||
|
||||
/**
|
||||
* Client authentication failed (e.g., unknown client, no client
|
||||
* authentication included, or unsupported authentication method). The
|
||||
* authorization server MAY return an HTTP 401 (Unauthorized) status
|
||||
* code to indicate which HTTP authentication schemes are supported. If
|
||||
* the client attempted to authenticate via the "Authorization" request
|
||||
* header field, the authorization server MUST respond with an HTTP 401
|
||||
* (Unauthorized) status code and include the "WWW-Authenticate"
|
||||
* response header field matching the authentication scheme used by the
|
||||
* client.
|
||||
*/
|
||||
INVALID_CLIENT,
|
||||
|
||||
/**
|
||||
* The provided authorization grant (e.g., authorization code, resource
|
||||
* owner credentials) or refresh token is invalid, expired, revoked,
|
||||
* does not match the redirection URI used in the authorization request,
|
||||
* or was issued to another client.
|
||||
*/
|
||||
INVALID_GRANT,
|
||||
|
||||
/**
|
||||
* The authenticated client is not authorized to use this authorization
|
||||
* grant type.
|
||||
*/
|
||||
UNAUTHORIZED_CLIENT,
|
||||
|
||||
/**
|
||||
* The authorization grant type is not supported by the authorization
|
||||
* server.
|
||||
*/
|
||||
UNSUPPORTED_GRANT_TYPE,
|
||||
|
||||
/**
|
||||
* The requested scope is invalid, unknown, malformed, or exceeds the
|
||||
* scope granted by the resource owner.
|
||||
*/
|
||||
INVALID_SCOPE
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.rest.action.oauth2;
|
||||
|
||||
import org.elasticsearch.client.node.NodeClient;
|
||||
import org.elasticsearch.common.ParseField;
|
||||
import org.elasticsearch.common.settings.Settings;
|
||||
import org.elasticsearch.common.xcontent.ConstructingObjectParser;
|
||||
import org.elasticsearch.common.xcontent.XContentBuilder;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.license.LicenseUtils;
|
||||
import org.elasticsearch.license.XPackLicenseState;
|
||||
import org.elasticsearch.rest.BaseRestHandler;
|
||||
import org.elasticsearch.rest.BytesRestResponse;
|
||||
import org.elasticsearch.rest.RestController;
|
||||
import org.elasticsearch.rest.RestRequest;
|
||||
import org.elasticsearch.rest.RestResponse;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.rest.action.RestBuilderListener;
|
||||
import org.elasticsearch.xpack.XPackPlugin;
|
||||
import org.elasticsearch.xpack.security.action.token.InvalidateTokenAction;
|
||||
import org.elasticsearch.xpack.security.action.token.InvalidateTokenRequest;
|
||||
import org.elasticsearch.xpack.security.action.token.InvalidateTokenResponse;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
import static org.elasticsearch.rest.RestRequest.Method.DELETE;
|
||||
|
||||
/**
|
||||
* Rest handler for handling access token invalidation requests
|
||||
*/
|
||||
public final class RestInvalidateTokenAction extends BaseRestHandler {
|
||||
|
||||
static final ConstructingObjectParser<String, Void> PARSER =
|
||||
new ConstructingObjectParser<>("invalidate_token", a -> ((String) a[0]));
|
||||
static {
|
||||
PARSER.declareString(ConstructingObjectParser.constructorArg(), new ParseField("token"));
|
||||
}
|
||||
|
||||
private final XPackLicenseState licenseState;
|
||||
|
||||
public RestInvalidateTokenAction(Settings settings, XPackLicenseState xPackLicenseState,
|
||||
RestController controller) {
|
||||
super(settings);
|
||||
this.licenseState = xPackLicenseState;
|
||||
controller.registerHandler(DELETE, "/_xpack/security/oauth2/token", this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected RestChannelConsumer prepareRequest(RestRequest request,
|
||||
NodeClient client)throws IOException {
|
||||
// this API shouldn't be available if security is disabled by license
|
||||
if (licenseState.isAuthAllowed() == false) {
|
||||
return channel ->
|
||||
channel.sendResponse(new BytesRestResponse(channel, LicenseUtils.newComplianceException(XPackPlugin.SECURITY)));
|
||||
}
|
||||
|
||||
try (XContentParser parser = request.contentParser()) {
|
||||
final String token = PARSER.parse(parser, null);
|
||||
final InvalidateTokenRequest tokenRequest = new InvalidateTokenRequest(token);
|
||||
return channel -> client.execute(InvalidateTokenAction.INSTANCE, tokenRequest,
|
||||
new RestBuilderListener<InvalidateTokenResponse>(channel) {
|
||||
@Override
|
||||
public RestResponse buildResponse(InvalidateTokenResponse invalidateResp,
|
||||
XContentBuilder builder) throws Exception {
|
||||
return new BytesRestResponse(RestStatus.OK, builder.startObject()
|
||||
.field("created", invalidateResp.isCreated())
|
||||
.endObject());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
package org.elasticsearch.xpack.security.rest.action.oauth2;
|
||||
|
||||
import org.apache.lucene.util.SetOnce;
|
||||
import org.elasticsearch.action.ActionRequestValidationException;
|
||||
import org.elasticsearch.common.settings.SecureString;
|
||||
import org.elasticsearch.common.unit.TimeValue;
|
||||
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
|
||||
import org.elasticsearch.common.xcontent.XContentHelper;
|
||||
import org.elasticsearch.common.xcontent.XContentParser;
|
||||
import org.elasticsearch.common.xcontent.XContentType;
|
||||
import org.elasticsearch.rest.AbstractRestChannel;
|
||||
import org.elasticsearch.rest.RestChannel;
|
||||
import org.elasticsearch.rest.RestResponse;
|
||||
import org.elasticsearch.rest.RestStatus;
|
||||
import org.elasticsearch.test.ESTestCase;
|
||||
import org.elasticsearch.test.rest.FakeRestRequest;
|
||||
import org.elasticsearch.xpack.security.action.token.CreateTokenRequest;
|
||||
import org.elasticsearch.xpack.security.action.token.CreateTokenResponse;
|
||||
import org.elasticsearch.xpack.security.rest.action.oauth2.RestGetTokenAction.CreateTokenResponseActionListener;
|
||||
import org.elasticsearch.xpack.security.support.NoOpLogger;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import static org.hamcrest.Matchers.hasEntry;
|
||||
|
||||
public class RestGetTokenActionTests extends ESTestCase {
|
||||
|
||||
public void testListenerHandlesExceptionProperly() {
|
||||
FakeRestRequest restRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY)
|
||||
.build();
|
||||
final SetOnce<RestResponse> responseSetOnce = new SetOnce<>();
|
||||
RestChannel restChannel = new AbstractRestChannel(restRequest, randomBoolean()) {
|
||||
@Override
|
||||
public void sendResponse(RestResponse restResponse) {
|
||||
responseSetOnce.set(restResponse);
|
||||
}
|
||||
};
|
||||
CreateTokenResponseActionListener listener = new CreateTokenResponseActionListener(restChannel, restRequest, NoOpLogger.INSTANCE);
|
||||
|
||||
ActionRequestValidationException ve = new CreateTokenRequest(null, null, null, null).validate();
|
||||
listener.onFailure(ve);
|
||||
RestResponse response = responseSetOnce.get();
|
||||
assertNotNull(response);
|
||||
|
||||
Map<String, Object> map = XContentHelper.convertToMap(response.content(), false,
|
||||
XContentType.fromMediaType(response.contentType())).v2();
|
||||
assertThat(map, hasEntry("error", "unsupported_grant_type"));
|
||||
assertThat(map, hasEntry("error_description", ve.getMessage()));
|
||||
assertEquals(2, map.size());
|
||||
assertEquals(RestStatus.BAD_REQUEST, response.status());
|
||||
}
|
||||
|
||||
public void testSendResponse() {
|
||||
FakeRestRequest restRequest = new FakeRestRequest.Builder(NamedXContentRegistry.EMPTY).build();
|
||||
final SetOnce<RestResponse> responseSetOnce = new SetOnce<>();
|
||||
RestChannel restChannel = new AbstractRestChannel(restRequest, randomBoolean()) {
|
||||
@Override
|
||||
public void sendResponse(RestResponse restResponse) {
|
||||
responseSetOnce.set(restResponse);
|
||||
}
|
||||
};
|
||||
CreateTokenResponseActionListener listener = new CreateTokenResponseActionListener(restChannel, restRequest, NoOpLogger.INSTANCE);
|
||||
CreateTokenResponse createTokenResponse =
|
||||
new CreateTokenResponse(randomAlphaOfLengthBetween(1, 256), TimeValue.timeValueHours(1L), null);
|
||||
listener.onResponse(createTokenResponse);
|
||||
|
||||
RestResponse response = responseSetOnce.get();
|
||||
assertNotNull(response);
|
||||
|
||||
Map<String, Object> map = XContentHelper.convertToMap(response.content(), false,
|
||||
XContentType.fromMediaType(response.contentType())).v2();
|
||||
assertEquals(RestStatus.OK, response.status());
|
||||
assertThat(map, hasEntry("type", "Bearer"));
|
||||
assertThat(map, hasEntry("access_token", createTokenResponse.getTokenString()));
|
||||
assertThat(map, hasEntry("expires_in", Math.toIntExact(createTokenResponse.getExpiresIn().seconds())));
|
||||
assertEquals(3, map.size());
|
||||
}
|
||||
|
||||
public void testParser() throws Exception {
|
||||
final String request = "{" +
|
||||
"\"grant_type\": \"password\"," +
|
||||
"\"username\": \"user1\"," +
|
||||
"\"password\": \"changeme\"," +
|
||||
"\"scope\": \"FULL\"" +
|
||||
"}";
|
||||
try (XContentParser parser = XContentType.JSON.xContent().createParser(NamedXContentRegistry.EMPTY, request)) {
|
||||
CreateTokenRequest createTokenRequest = RestGetTokenAction.PARSER.parse(parser, null);
|
||||
assertEquals("password", createTokenRequest.getGrantType());
|
||||
assertEquals("user1", createTokenRequest.getUsername());
|
||||
assertEquals("FULL", createTokenRequest.getScope());
|
||||
assertTrue(new SecureString("changeme".toCharArray()).equals(createTokenRequest.getPassword()));
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"xpack.security.get_token": {
|
||||
"documentation": "https://www.elastic.co/guide/en/x-pack/master/security-api-tokens.html#security-api-get-token",
|
||||
"methods": [ "POST" ],
|
||||
"url": {
|
||||
"path": "/_xpack/security/oauth2/token",
|
||||
"paths": [ "/_xpack/security/oauth2/token" ],
|
||||
"parts": {},
|
||||
"params": {}
|
||||
},
|
||||
"body": {
|
||||
"description" : "The token request to get",
|
||||
"required" : true
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"xpack.security.invalidate_token": {
|
||||
"documentation": "https://www.elastic.co/guide/en/x-pack/master/security-api-tokens.html#security-api-invalidate-token",
|
||||
"methods": [ "DELETE" ],
|
||||
"url": {
|
||||
"path": "/_xpack/security/oauth2/token",
|
||||
"paths": [ "/_xpack/security/oauth2/token" ],
|
||||
"parts": {},
|
||||
"params": {}
|
||||
},
|
||||
"body": {
|
||||
"description" : "The token to invalidate",
|
||||
"required" : true
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,88 @@
|
||||
---
|
||||
setup:
|
||||
- skip:
|
||||
features: headers
|
||||
|
||||
- do:
|
||||
cluster.health:
|
||||
wait_for_status: yellow
|
||||
|
||||
- do:
|
||||
xpack.security.put_user:
|
||||
username: "token_user"
|
||||
body: >
|
||||
{
|
||||
"password" : "changeme",
|
||||
"roles" : [ "superuser" ],
|
||||
"full_name" : "Token User"
|
||||
}
|
||||
|
||||
---
|
||||
teardown:
|
||||
- do:
|
||||
xpack.security.delete_user:
|
||||
username: "token_user"
|
||||
ignore: 404
|
||||
|
||||
---
|
||||
"Test get and use token":
|
||||
|
||||
- do:
|
||||
xpack.security.get_token:
|
||||
body:
|
||||
grant_type: "password"
|
||||
username: "token_user"
|
||||
password: "changeme"
|
||||
|
||||
- match: { type: "Bearer" }
|
||||
- is_true: access_token
|
||||
- set: { access_token: token }
|
||||
- match: { expires_in: 1200 }
|
||||
- is_false: scope
|
||||
|
||||
- do:
|
||||
headers:
|
||||
Authorization: Bearer ${token}
|
||||
xpack.security.authenticate: {}
|
||||
|
||||
- match: { username: "token_user" }
|
||||
- match: { roles.0: "superuser" }
|
||||
- match: { full_name: "Token User" }
|
||||
|
||||
---
|
||||
"Test invalidate token":
|
||||
|
||||
- do:
|
||||
xpack.security.get_token:
|
||||
body:
|
||||
grant_type: "password"
|
||||
username: "token_user"
|
||||
password: "changeme"
|
||||
|
||||
- match: { type: "Bearer" }
|
||||
- is_true: access_token
|
||||
- set: { access_token: token }
|
||||
- match: { expires_in: 1200 }
|
||||
- is_false: scope
|
||||
|
||||
- do:
|
||||
headers:
|
||||
Authorization: Bearer ${token}
|
||||
xpack.security.authenticate: {}
|
||||
|
||||
- match: { username: "token_user" }
|
||||
- match: { roles.0: "superuser" }
|
||||
- match: { full_name: "Token User" }
|
||||
|
||||
- do:
|
||||
xpack.security.invalidate_token:
|
||||
body:
|
||||
token: $token
|
||||
|
||||
- match: { created: true }
|
||||
|
||||
- do:
|
||||
catch: unauthorized
|
||||
headers:
|
||||
Authorization: Bearer ${token}
|
||||
xpack.security.authenticate: {}
|
Loading…
x
Reference in New Issue
Block a user