KEYCLOAK-5325 Provide OAuth token revocation capability

This commit is contained in:
Yoshiyuki Tabata 2020-01-30 11:28:48 +09:00 committed by Stian Thorgersen
parent 06d8a0a4c4
commit b40c12c712
6 changed files with 617 additions and 0 deletions

View file

@ -49,6 +49,7 @@ public class OAuthErrorException extends Exception {
public static final String INVALID_CLIENT = "invalid_client"; public static final String INVALID_CLIENT = "invalid_client";
public static final String INVALID_GRANT = "invalid_grant"; public static final String INVALID_GRANT = "invalid_grant";
public static final String UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type"; public static final String UNSUPPORTED_GRANT_TYPE = "unsupported_grant_type";
public static final String UNSUPPORTED_TOKEN_TYPE = "unsupported_token_type";
public OAuthErrorException(String error, String description, String message, Throwable cause) { public OAuthErrorException(String error, String description, String message, Throwable cause) {
super(message, cause); super(message, cause);

View file

@ -36,6 +36,7 @@ import org.keycloak.protocol.oidc.endpoints.AuthorizationEndpoint;
import org.keycloak.protocol.oidc.endpoints.LoginStatusIframeEndpoint; import org.keycloak.protocol.oidc.endpoints.LoginStatusIframeEndpoint;
import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint; import org.keycloak.protocol.oidc.endpoints.LogoutEndpoint;
import org.keycloak.protocol.oidc.endpoints.TokenEndpoint; import org.keycloak.protocol.oidc.endpoints.TokenEndpoint;
import org.keycloak.protocol.oidc.endpoints.TokenRevocationEndpoint;
import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint; import org.keycloak.protocol.oidc.endpoints.UserInfoEndpoint;
import org.keycloak.protocol.oidc.ext.OIDCExtProvider; import org.keycloak.protocol.oidc.ext.OIDCExtProvider;
import org.keycloak.services.managers.AuthenticationManager; import org.keycloak.services.managers.AuthenticationManager;
@ -138,6 +139,11 @@ public class OIDCLoginProtocolService {
return uriBuilder.path(OIDCLoginProtocolService.class, "logout"); return uriBuilder.path(OIDCLoginProtocolService.class, "logout");
} }
public static UriBuilder tokenRevocationUrl(UriBuilder baseUriBuilder) {
UriBuilder uriBuilder = tokenServiceBaseUrl(baseUriBuilder);
return uriBuilder.path(OIDCLoginProtocolService.class, "revoke");
}
/** /**
* Authorization endpoint * Authorization endpoint
*/ */
@ -233,6 +239,13 @@ public class OIDCLoginProtocolService {
return endpoint; return endpoint;
} }
@Path("revoke")
public Object revoke() {
TokenRevocationEndpoint endpoint = new TokenRevocationEndpoint(realm, event);
ResteasyProviderFactory.getInstance().injectProperties(endpoint);
return endpoint;
}
@Path("oauth/oob") @Path("oauth/oob")
@GET @GET
public Response installedAppUrnCallback(final @QueryParam("code") String code, final @QueryParam("error") String error, final @QueryParam("error_description") String errorDescription) { public Response installedAppUrnCallback(final @QueryParam("code") String code, final @QueryParam("error") String error, final @QueryParam("error_description") String errorDescription) {

View file

@ -0,0 +1,211 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.protocol.oidc.endpoints;
import java.util.List;
import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import org.jboss.resteasy.spi.HttpRequest;
import org.keycloak.OAuthErrorException;
import org.keycloak.common.ClientConnection;
import org.keycloak.events.Details;
import org.keycloak.events.Errors;
import org.keycloak.events.EventBuilder;
import org.keycloak.events.EventType;
import org.keycloak.models.AuthenticatedClientSessionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.RealmModel;
import org.keycloak.models.UserModel;
import org.keycloak.models.UserSessionModel;
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
import org.keycloak.representations.RefreshToken;
import org.keycloak.services.CorsErrorResponseException;
import org.keycloak.services.managers.UserSessionCrossDCManager;
import org.keycloak.services.managers.UserSessionManager;
import org.keycloak.services.resources.Cors;
import org.keycloak.util.TokenUtil;
/**
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
*/
public class TokenRevocationEndpoint {
private static final String PARAM_TOKEN = "token";
@Context
private KeycloakSession session;
@Context
private HttpRequest request;
@Context
private HttpHeaders headers;
@Context
private ClientConnection clientConnection;
private MultivaluedMap<String, String> formParams;
private ClientModel client;
private RealmModel realm;
private EventBuilder event;
private Cors cors;
private RefreshToken token;
private UserModel user;
public TokenRevocationEndpoint(RealmModel realm, EventBuilder event) {
this.realm = realm;
this.event = event;
}
@POST
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
public Response revoke() {
event.event(EventType.REVOKE_GRANT);
cors = Cors.add(request).auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
checkSsl();
checkRealm();
checkClient();
formParams = request.getDecodedFormParameters();
checkToken();
checkIssuedFor();
checkUser();
revokeClient();
event.detail(Details.REVOKED_CLIENT, client.getClientId()).success();
return cors.builder(Response.ok()).build();
}
private void checkSsl() {
if (!session.getContext().getUri().getBaseUri().getScheme().equals("https")
&& realm.getSslRequired().isRequired(clientConnection)) {
throw new CorsErrorResponseException(cors.allowAllOrigins(), OAuthErrorException.INVALID_REQUEST, "HTTPS required",
Response.Status.FORBIDDEN);
}
}
private void checkRealm() {
if (!realm.isEnabled()) {
throw new CorsErrorResponseException(cors.allowAllOrigins(), "access_denied", "Realm not enabled",
Response.Status.FORBIDDEN);
}
}
private void checkClient() {
AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event);
client = clientAuth.getClient();
event.client(client);
cors.allowedOrigins(session, client);
if (client.isBearerOnly()) {
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_CLIENT, "Bearer-only not allowed",
Response.Status.BAD_REQUEST);
}
}
private void checkToken() {
String encodedToken = formParams.getFirst(PARAM_TOKEN);
if (encodedToken == null) {
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Token not provided",
Response.Status.BAD_REQUEST);
}
token = session.tokens().decode(encodedToken, RefreshToken.class);
if (token == null) {
event.error(Errors.INVALID_TOKEN);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.OK);
}
if (!(TokenUtil.TOKEN_TYPE_REFRESH.equals(token.getType()) || TokenUtil.TOKEN_TYPE_OFFLINE.equals(token.getType()))) {
event.error(Errors.INVALID_TOKEN_TYPE);
throw new CorsErrorResponseException(cors, OAuthErrorException.UNSUPPORTED_TOKEN_TYPE, "Unsupported token type",
Response.Status.BAD_REQUEST);
}
}
private void checkIssuedFor() {
String issuedFor = token.getIssuedFor();
if (issuedFor == null) {
event.error(Errors.INVALID_TOKEN);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.OK);
}
if (!client.getClientId().equals(issuedFor)) {
event.error(Errors.INVALID_REQUEST);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Unmatching clients",
Response.Status.BAD_REQUEST);
}
}
private void checkUser() {
UserSessionModel userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm,
token.getSessionState(), false, client.getId());
if (userSession == null) {
userSession = new UserSessionCrossDCManager(session).getUserSessionWithClient(realm, token.getSessionState(), true,
client.getId());
if (userSession == null) {
event.error(Errors.USER_SESSION_NOT_FOUND);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_TOKEN, "Invalid token",
Response.Status.OK);
}
}
user = userSession.getUser();
if (user == null) {
event.error(Errors.USER_NOT_FOUND);
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_TOKEN, "Invalid token", Response.Status.OK);
}
event.user(user);
}
private void revokeClient() {
session.users().revokeConsentForClient(realm, user.getId(), client.getId());
if (TokenUtil.TOKEN_TYPE_OFFLINE.equals(token.getType())) {
new UserSessionManager(session).revokeOfflineToken(user, client);
}
List<UserSessionModel> userSessions = session.sessions().getUserSessions(realm, user);
for (UserSessionModel userSession : userSessions) {
AuthenticatedClientSessionModel clientSession = userSession.getAuthenticatedClientSessionByClient(client.getId());
if (clientSession != null) {
org.keycloak.protocol.oidc.TokenManager.dettachClientSession(session.sessions(), realm, clientSession);
}
}
}
}

View file

@ -653,6 +653,48 @@ public class OAuthClient {
return client.execute(post); return client.execute(post);
} }
public CloseableHttpResponse doTokenRevoke(String token, String tokenTypeHint, String clientSecret) {
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
return doTokenRevoke(token, tokenTypeHint, clientSecret, client);
} catch (IOException ex) {
throw new RuntimeException(ex);
}
}
public CloseableHttpResponse doTokenRevoke(String token, String tokenTypeHint, String clientSecret,
CloseableHttpClient client) throws IOException {
HttpPost post = new HttpPost(getTokenRevocationUrl());
List<NameValuePair> parameters = new LinkedList<>();
if (token != null) {
parameters.add(new BasicNameValuePair("token", token));
}
if (tokenTypeHint != null) {
parameters.add(new BasicNameValuePair("token_type_hint", tokenTypeHint));
}
if (origin != null) {
post.addHeader("Origin", origin);
}
if (clientId != null && clientSecret != null) {
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
post.setHeader("Authorization", authorization);
} else if (clientId != null) {
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId));
}
UrlEncodedFormEntity formEntity;
try {
formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
post.setEntity(formEntity);
return client.execute(post);
}
// KEYCLOAK-6771 Certificate Bound Token // KEYCLOAK-6771 Certificate Bound Token
public AccessTokenResponse doRefreshTokenRequest(String refreshToken, String password) { public AccessTokenResponse doRefreshTokenRequest(String refreshToken, String password) {
try (CloseableHttpClient client = HttpClientBuilder.create().build()) { try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
@ -933,6 +975,11 @@ public class OAuthClient {
return new LogoutUrlBuilder(); return new LogoutUrlBuilder();
} }
public String getTokenRevocationUrl() {
UriBuilder b = OIDCLoginProtocolService.tokenRevocationUrl(UriBuilder.fromUri(baseUrl));
return b.build(realm).toString();
}
public String getResourceOwnerPasswordCredentialGrantUrl() { public String getResourceOwnerPasswordCredentialGrantUrl() {
UriBuilder b = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(baseUrl)); UriBuilder b = OIDCLoginProtocolService.tokenUrl(UriBuilder.fromUri(baseUrl));
return b.build(realm).toString(); return b.build(realm).toString();

View file

@ -0,0 +1,110 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.oauth;
import static org.junit.Assert.*;
import static org.keycloak.testsuite.admin.AbstractAdminTest.*;
import java.io.IOException;
import java.util.List;
import javax.ws.rs.core.Response.Status;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.junit.Test;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.oidc.TokenMetadataRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.util.ClientBuilder;
import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
*/
public class TokenRevocationCorsTest extends AbstractKeycloakTest {
private static final String VALID_CORS_URL = "http://localtest.me:8180";
private static final String INVALID_CORS_URL = "http://invalid.localtest.me:8180";
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
realm.getClients().add(ClientBuilder.create().redirectUris(VALID_CORS_URL + "/realms/master/app")
.addWebOrigin(VALID_CORS_URL).id("test-app2").clientId("test-app2").publicClient().directAccessGrants().build());
testRealms.add(realm);
}
@Test
public void testTokenRevocationCorsRequestWithValidUrl() throws Exception {
oauth.realm("test");
oauth.clientId("test-app2");
oauth.redirectUri(VALID_CORS_URL + "/realms/master/app");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest(null, "test-user@localhost",
"password");
oauth.origin(VALID_CORS_URL);
CloseableHttpResponse response = oauth.doTokenRevoke(tokenResponse.getRefreshToken(), "refresh_token", "password");
assertThat(response, Matchers.statusCodeIsHC(Status.OK));
assertCors(response);
isTokenDisabled(tokenResponse, "test-app2");
}
@Test
public void userTokenRevocationCorsRequestWithInvalidUrlShouldFail() throws Exception {
oauth.realm("test");
oauth.clientId("test-app2");
oauth.redirectUri(VALID_CORS_URL + "/realms/master/app");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest(null, "test-user@localhost",
"password");
oauth.origin(INVALID_CORS_URL);
CloseableHttpResponse response = oauth.doTokenRevoke(tokenResponse.getRefreshToken(), "refresh_token", "password");
assertThat(response, Matchers.statusCodeIsHC(Status.OK));
assertNotCors(response);
isTokenDisabled(tokenResponse, "test-app2");
}
private static void assertCors(CloseableHttpResponse response) {
assertEquals("true", response.getHeaders("Access-Control-Allow-Credentials")[0].getValue());
assertEquals(VALID_CORS_URL, response.getHeaders("Access-Control-Allow-Origin")[0].getValue());
assertEquals("Access-Control-Allow-Methods", response.getHeaders("Access-Control-Expose-Headers")[0].getValue());
}
private static void assertNotCors(CloseableHttpResponse response) {
assertEquals(0, response.getHeaders("Access-Control-Allow-Credentials").length);
assertEquals(0, response.getHeaders("Access-Control-Allow-Origin").length);
assertEquals(0, response.getHeaders("Access-Control-Expose-Headers").length);
}
private void isTokenDisabled(AccessTokenResponse tokenResponse, String clientId) throws IOException {
String introspectionResponse = oauth.introspectAccessTokenWithClientCredential(clientId, "password",
tokenResponse.getAccessToken());
TokenMetadataRepresentation rep = JsonSerialization.readValue(introspectionResponse, TokenMetadataRepresentation.class);
assertFalse(rep.isActive());
oauth.clientId(clientId);
OAuthClient.AccessTokenResponse tokenRefreshResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(),
"password");
assertEquals(Status.BAD_REQUEST.getStatusCode(), tokenRefreshResponse.getStatusCode());
}
}

View file

@ -0,0 +1,235 @@
/*
* Copyright 2020 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.keycloak.testsuite.oauth;
import static org.junit.Assert.*;
import static org.keycloak.testsuite.admin.AbstractAdminTest.*;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import javax.ws.rs.core.Response.Status;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.jboss.arquillian.graphene.page.Page;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.keycloak.OAuth2Constants;
import org.keycloak.admin.client.resource.RealmResource;
import org.keycloak.admin.client.resource.UserResource;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserSessionRepresentation;
import org.keycloak.representations.oidc.TokenMetadataRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.AssertEvents;
import org.keycloak.testsuite.pages.LoginPage;
import org.keycloak.testsuite.util.ClientManager;
import org.keycloak.testsuite.util.Matchers;
import org.keycloak.testsuite.util.OAuthClient;
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
import org.keycloak.testsuite.util.RealmBuilder;
import org.keycloak.util.JsonSerialization;
/**
* @author <a href="mailto:yoshiyuki.tabata.jy@hitachi.com">Yoshiyuki Tabata</a>
*/
public class TokenRevocationTest extends AbstractKeycloakTest {
private RealmResource realm;
@Rule
public AssertEvents events = new AssertEvents(this);
@Override
public void beforeAbstractKeycloakTest() throws Exception {
super.beforeAbstractKeycloakTest();
}
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
RealmRepresentation realmRepresentation = loadJson(getClass().getResourceAsStream("/testrealm.json"),
RealmRepresentation.class);
RealmBuilder realm = RealmBuilder.edit(realmRepresentation).testEventListener();
testRealms.add(realm.build());
}
@Before
public void clientConfiguration() {
realm = adminClient.realm("test");
ClientManager.realm(realm).clientId("test-app").directAccessGrant(true);
ClientManager.realm(realm).clientId("test-app-scope").directAccessGrant(true);
}
@Page
protected LoginPage loginPage;
@Test
public void testRevokeToken() throws Exception {
oauth.clientSessionState("client-session");
OAuthClient.AccessTokenResponse tokenResponse1 = login("test-app", "test-user@localhost", "password");
OAuthClient.AccessTokenResponse tokenResponse2 = login("test-app-scope", "test-user@localhost", "password");
UserResource testUser = realm.users().get(realm.users().search("test-user@localhost").get(0).getId());
List<UserSessionRepresentation> userSessions = testUser.getUserSessions();
assertEquals(1, userSessions.size());
Map<String, String> clients = userSessions.get(0).getClients();
assertEquals("test-app", clients.get(realm.clients().findByClientId("test-app").get(0).getId()));
assertEquals("test-app-scope", clients.get(realm.clients().findByClientId("test-app-scope").get(0).getId()));
isTokenEnabled(tokenResponse1, "test-app");
isTokenEnabled(tokenResponse2, "test-app-scope");
oauth.clientId("test-app");
CloseableHttpResponse response = oauth.doTokenRevoke(tokenResponse1.getRefreshToken(), "refresh_token", "password");
assertThat(response, Matchers.statusCodeIsHC(Status.OK));
userSessions = testUser.getUserSessions();
assertEquals(1, userSessions.size());
clients = userSessions.get(0).getClients();
assertNull(clients.get(realm.clients().findByClientId("test-app").get(0).getId()));
assertEquals("test-app-scope", clients.get(realm.clients().findByClientId("test-app-scope").get(0).getId()));
isTokenDisabled(tokenResponse1, "test-app");
isTokenEnabled(tokenResponse2, "test-app-scope");
}
@Test
public void testRevokeAccessToken() throws Exception {
oauth.clientId("test-app");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost",
"password");
isTokenEnabled(tokenResponse, "test-app");
CloseableHttpResponse response = oauth.doTokenRevoke(tokenResponse.getAccessToken(), "access_token", "password");
assertThat(response, Matchers.statusCodeIsHC(Status.BAD_REQUEST));
isTokenEnabled(tokenResponse, "test-app");
}
@Test
public void testRevokeOfflineToken() throws Exception {
oauth.scope(OAuth2Constants.OFFLINE_ACCESS);
oauth.clientId("test-app");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost",
"password");
isTokenEnabled(tokenResponse, "test-app");
CloseableHttpResponse response = oauth.doTokenRevoke(tokenResponse.getRefreshToken(), "refresh_token", "password");
assertThat(response, Matchers.statusCodeIsHC(Status.OK));
isTokenDisabled(tokenResponse, "test-app");
}
@Test
public void testTokenTypeHint() throws Exception {
// different token_type_hint
oauth.clientId("test-app");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost",
"password");
isTokenEnabled(tokenResponse, "test-app");
CloseableHttpResponse response = oauth.doTokenRevoke(tokenResponse.getRefreshToken(), "access_token", "password");
assertThat(response, Matchers.statusCodeIsHC(Status.OK));
isTokenDisabled(tokenResponse, "test-app");
// invalid token_type_hint
oauth.clientId("test-app");
tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
isTokenEnabled(tokenResponse, "test-app");
response = oauth.doTokenRevoke(tokenResponse.getRefreshToken(), "invalid_token_type_hint", "password");
assertThat(response, Matchers.statusCodeIsHC(Status.OK));
isTokenDisabled(tokenResponse, "test-app");
}
@Test
public void testRevokeTokenFromDifferentClient() throws Exception {
oauth.clientId("test-app");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost",
"password");
isTokenEnabled(tokenResponse, "test-app");
oauth.clientId("test-app-scope");
CloseableHttpResponse response = oauth.doTokenRevoke(tokenResponse.getRefreshToken(), "refresh_token", "password");
assertThat(response, Matchers.statusCodeIsHC(Status.BAD_REQUEST));
isTokenEnabled(tokenResponse, "test-app");
}
@Test
public void testRevokeAlreadyRevokedToken() throws Exception {
oauth.clientId("test-app");
OAuthClient.AccessTokenResponse tokenResponse = oauth.doGrantAccessTokenRequest("password", "test-user@localhost",
"password");
isTokenEnabled(tokenResponse, "test-app");
oauth.doLogout(tokenResponse.getRefreshToken(), "password");
isTokenDisabled(tokenResponse, "test-app");
CloseableHttpResponse response = oauth.doTokenRevoke(tokenResponse.getRefreshToken(), "refresh_token", "password");
assertThat(response, Matchers.statusCodeIsHC(Status.OK));
isTokenDisabled(tokenResponse, "test-app");
}
private AccessTokenResponse login(String clientId, String username, String password) {
oauth.clientId(clientId);
oauth.openLoginForm();
if (loginPage.isCurrent()) {
loginPage.login(username, password);
}
String code = new OAuthClient.AuthorizationEndpointResponse(oauth).getCode();
return oauth.doAccessTokenRequest(code, "password");
}
private void isTokenEnabled(AccessTokenResponse tokenResponse, String clientId) throws IOException {
String introspectionResponse = oauth.introspectAccessTokenWithClientCredential(clientId, "password",
tokenResponse.getAccessToken());
TokenMetadataRepresentation rep = JsonSerialization.readValue(introspectionResponse, TokenMetadataRepresentation.class);
assertTrue(rep.isActive());
oauth.clientId(clientId);
OAuthClient.AccessTokenResponse tokenRefreshResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(),
"password");
assertEquals(Status.OK.getStatusCode(), tokenRefreshResponse.getStatusCode());
}
private void isTokenDisabled(AccessTokenResponse tokenResponse, String clientId) throws IOException {
String introspectionResponse = oauth.introspectAccessTokenWithClientCredential(clientId, "password",
tokenResponse.getAccessToken());
TokenMetadataRepresentation rep = JsonSerialization.readValue(introspectionResponse, TokenMetadataRepresentation.class);
assertFalse(rep.isActive());
oauth.clientId(clientId);
OAuthClient.AccessTokenResponse tokenRefreshResponse = oauth.doRefreshTokenRequest(tokenResponse.getRefreshToken(),
"password");
assertEquals(Status.BAD_REQUEST.getStatusCode(), tokenRefreshResponse.getStatusCode());
}
}