KEYCLOAK-16793 KEYCLOAK-16948 Cors on error responses for logoutEndpoint and tokenEndpoint
This commit is contained in:
parent
d452041d7d
commit
99c1ee7f5a
12 changed files with 232 additions and 21 deletions
|
@ -43,6 +43,7 @@ import org.keycloak.protocol.oidc.utils.RedirectUtils;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
import org.keycloak.representations.LogoutToken;
|
import org.keycloak.representations.LogoutToken;
|
||||||
import org.keycloak.representations.RefreshToken;
|
import org.keycloak.representations.RefreshToken;
|
||||||
|
import org.keycloak.services.CorsErrorResponseException;
|
||||||
import org.keycloak.services.ErrorPage;
|
import org.keycloak.services.ErrorPage;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
import org.keycloak.services.ErrorResponseException;
|
||||||
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
import org.keycloak.services.clientpolicy.ClientPolicyException;
|
||||||
|
@ -56,6 +57,7 @@ import org.keycloak.util.TokenUtil;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
import javax.ws.rs.OPTIONS;
|
||||||
import javax.ws.rs.POST;
|
import javax.ws.rs.POST;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
|
@ -94,12 +96,20 @@ public class LogoutEndpoint {
|
||||||
private RealmModel realm;
|
private RealmModel realm;
|
||||||
private EventBuilder event;
|
private EventBuilder event;
|
||||||
|
|
||||||
|
private Cors cors;
|
||||||
|
|
||||||
public LogoutEndpoint(TokenManager tokenManager, RealmModel realm, EventBuilder event) {
|
public LogoutEndpoint(TokenManager tokenManager, RealmModel realm, EventBuilder event) {
|
||||||
this.tokenManager = tokenManager;
|
this.tokenManager = tokenManager;
|
||||||
this.realm = realm;
|
this.realm = realm;
|
||||||
this.event = event;
|
this.event = event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Path("/")
|
||||||
|
@OPTIONS
|
||||||
|
public Response issueUserInfoPreflight() {
|
||||||
|
return Cors.add(this.request, Response.ok()).auth().preflight().build();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Logout user session. User must be logged in via a session cookie.
|
* Logout user session. User must be logged in via a session cookie.
|
||||||
*
|
*
|
||||||
|
@ -197,6 +207,8 @@ public class LogoutEndpoint {
|
||||||
@POST
|
@POST
|
||||||
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
@Consumes(MediaType.APPLICATION_FORM_URLENCODED)
|
||||||
public Response logoutToken() {
|
public Response logoutToken() {
|
||||||
|
cors = Cors.add(request).auth().allowedMethods("POST").auth().exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS);
|
||||||
|
|
||||||
MultivaluedMap<String, String> form = request.getDecodedFormParameters();
|
MultivaluedMap<String, String> form = request.getDecodedFormParameters();
|
||||||
checkSsl();
|
checkSsl();
|
||||||
|
|
||||||
|
@ -206,13 +218,13 @@ public class LogoutEndpoint {
|
||||||
String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN);
|
String refreshToken = form.getFirst(OAuth2Constants.REFRESH_TOKEN);
|
||||||
if (refreshToken == null) {
|
if (refreshToken == null) {
|
||||||
event.error(Errors.INVALID_TOKEN);
|
event.error(Errors.INVALID_TOKEN);
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST);
|
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
session.clientPolicy().triggerOnEvent(new LogoutRequestContext(form));
|
session.clientPolicy().triggerOnEvent(new LogoutRequestContext(form));
|
||||||
} catch (ClientPolicyException cpe) {
|
} catch (ClientPolicyException cpe) {
|
||||||
throw new ErrorResponseException(cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
|
throw new CorsErrorResponseException(cors, cpe.getError(), cpe.getErrorDetail(), cpe.getErrorStatus());
|
||||||
}
|
}
|
||||||
|
|
||||||
RefreshToken token = null;
|
RefreshToken token = null;
|
||||||
|
@ -238,14 +250,14 @@ public class LogoutEndpoint {
|
||||||
// KEYCLOAK-6771 Certificate Bound Token
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
if (MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC.equals(e.getDescription())) {
|
if (MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC.equals(e.getDescription())) {
|
||||||
event.error(Errors.NOT_ALLOWED);
|
event.error(Errors.NOT_ALLOWED);
|
||||||
throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.UNAUTHORIZED);
|
throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.UNAUTHORIZED);
|
||||||
} else {
|
} else {
|
||||||
event.error(Errors.INVALID_TOKEN);
|
event.error(Errors.INVALID_TOKEN);
|
||||||
throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.BAD_REQUEST);
|
throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Cors.add(request, Response.noContent()).auth().allowedOrigins(session, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
|
return cors.builder(Response.noContent()).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -416,10 +428,11 @@ public class LogoutEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
private ClientModel authorizeClient() {
|
private ClientModel authorizeClient() {
|
||||||
ClientModel client = AuthorizeClientUtil.authorizeClient(session, event).getClient();
|
ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, cors).getClient();
|
||||||
|
cors.allowedOrigins(session, client);
|
||||||
|
|
||||||
if (client.isBearerOnly()) {
|
if (client.isBearerOnly()) {
|
||||||
throw new ErrorResponseException(Errors.INVALID_CLIENT, "Bearer-only not allowed", Response.Status.BAD_REQUEST);
|
throw new CorsErrorResponseException(cors, Errors.INVALID_CLIENT, "Bearer-only not allowed", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
|
@ -427,7 +440,7 @@ public class LogoutEndpoint {
|
||||||
|
|
||||||
private void checkSsl() {
|
private void checkSsl() {
|
||||||
if (!session.getContext().getUri().getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
|
if (!session.getContext().getUri().getBaseUri().getScheme().equals("https") && realm.getSslRequired().isRequired(clientConnection)) {
|
||||||
throw new ErrorResponseException("invalid_request", "HTTPS required", Response.Status.FORBIDDEN);
|
throw new CorsErrorResponseException(cors.allowAllOrigins(), "invalid_request", "HTTPS required", Response.Status.FORBIDDEN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -263,7 +263,7 @@ public class TokenEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkClient() {
|
private void checkClient() {
|
||||||
AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event);
|
AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event, cors);
|
||||||
client = clientAuth.getClient();
|
client = clientAuth.getClient();
|
||||||
clientAuthAttributes = clientAuth.getClientAuthAttributes();
|
clientAuthAttributes = clientAuth.getClientAuthAttributes();
|
||||||
|
|
||||||
|
|
|
@ -124,7 +124,7 @@ public class TokenIntrospectionEndpoint {
|
||||||
|
|
||||||
private void authorizeClient() {
|
private void authorizeClient() {
|
||||||
try {
|
try {
|
||||||
ClientModel client = AuthorizeClientUtil.authorizeClient(session, event).getClient();
|
ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, null).getClient();
|
||||||
|
|
||||||
this.event.client(client);
|
this.event.client(client);
|
||||||
|
|
||||||
|
|
|
@ -140,7 +140,7 @@ public class TokenRevocationEndpoint {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void checkClient() {
|
private void checkClient() {
|
||||||
AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event);
|
AuthorizeClientUtil.ClientAuthResult clientAuth = AuthorizeClientUtil.authorizeClient(session, event, cors);
|
||||||
client = clientAuth.getClient();
|
client = clientAuth.getClient();
|
||||||
|
|
||||||
event.client(client);
|
event.client(client);
|
||||||
|
|
|
@ -19,6 +19,7 @@ package org.keycloak.protocol.oidc.utils;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
import org.jboss.resteasy.spi.HttpRequest;
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
import org.jboss.resteasy.spi.HttpResponse;
|
||||||
import org.keycloak.authentication.AuthenticationProcessor;
|
import org.keycloak.authentication.AuthenticationProcessor;
|
||||||
import org.keycloak.authentication.ClientAuthenticator;
|
import org.keycloak.authentication.ClientAuthenticator;
|
||||||
import org.keycloak.authentication.ClientAuthenticatorFactory;
|
import org.keycloak.authentication.ClientAuthenticatorFactory;
|
||||||
|
@ -29,7 +30,9 @@ import org.keycloak.models.ClientModel;
|
||||||
import org.keycloak.models.KeycloakSession;
|
import org.keycloak.models.KeycloakSession;
|
||||||
import org.keycloak.models.RealmModel;
|
import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
|
import org.keycloak.services.CorsErrorResponseException;
|
||||||
import org.keycloak.services.ErrorResponseException;
|
import org.keycloak.services.ErrorResponseException;
|
||||||
|
import org.keycloak.services.resources.Cors;
|
||||||
|
|
||||||
import javax.ws.rs.WebApplicationException;
|
import javax.ws.rs.WebApplicationException;
|
||||||
import javax.ws.rs.core.Response;
|
import javax.ws.rs.core.Response;
|
||||||
|
@ -42,17 +45,26 @@ public class AuthorizeClientUtil {
|
||||||
|
|
||||||
private static final Logger logger = Logger.getLogger(AuthorizeClientUtil.class);
|
private static final Logger logger = Logger.getLogger(AuthorizeClientUtil.class);
|
||||||
|
|
||||||
public static ClientAuthResult authorizeClient(KeycloakSession session, EventBuilder event) {
|
public static ClientAuthResult authorizeClient(KeycloakSession session, EventBuilder event, Cors cors) {
|
||||||
AuthenticationProcessor processor = getAuthenticationProcessor(session, event);
|
AuthenticationProcessor processor = getAuthenticationProcessor(session, event);
|
||||||
|
|
||||||
Response response = processor.authenticateClient();
|
Response response = processor.authenticateClient();
|
||||||
if (response != null) {
|
if (response != null) {
|
||||||
|
if (cors != null) {
|
||||||
|
cors.allowAllOrigins();
|
||||||
|
HttpResponse httpResponse = session.getContext().getContextObject(HttpResponse.class);
|
||||||
|
cors.build(httpResponse);
|
||||||
|
}
|
||||||
throw new WebApplicationException(response);
|
throw new WebApplicationException(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
ClientModel client = processor.getClient();
|
ClientModel client = processor.getClient();
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
throw new ErrorResponseException(Errors.INVALID_CLIENT, "Client authentication ended, but client is null", Response.Status.BAD_REQUEST);
|
throwErrorResponseException(Errors.INVALID_CLIENT, "Client authentication ended, but client is null", Response.Status.BAD_REQUEST, cors.allowAllOrigins());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cors != null) {
|
||||||
|
cors.allowedOrigins(session, client);
|
||||||
}
|
}
|
||||||
|
|
||||||
String protocol = client.getProtocol();
|
String protocol = client.getProtocol();
|
||||||
|
@ -63,7 +75,7 @@ public class AuthorizeClientUtil {
|
||||||
|
|
||||||
if (!protocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
|
if (!protocol.equals(OIDCLoginProtocol.LOGIN_PROTOCOL)) {
|
||||||
event.error(Errors.INVALID_CLIENT);
|
event.error(Errors.INVALID_CLIENT);
|
||||||
throw new ErrorResponseException(Errors.INVALID_CLIENT, "Wrong client protocol.", Response.Status.BAD_REQUEST);
|
throwErrorResponseException(Errors.INVALID_CLIENT, "Wrong client protocol.", Response.Status.BAD_REQUEST, cors);
|
||||||
}
|
}
|
||||||
|
|
||||||
session.getContext().setClient(client);
|
session.getContext().setClient(client);
|
||||||
|
@ -97,6 +109,15 @@ public class AuthorizeClientUtil {
|
||||||
.orElse(null);
|
.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void throwErrorResponseException(String error, String errorDescription, Response.Status status, Cors cors) {
|
||||||
|
if (cors == null) {
|
||||||
|
throw new ErrorResponseException(error, errorDescription, status);
|
||||||
|
} else {
|
||||||
|
cors.allowAllOrigins();
|
||||||
|
throw new CorsErrorResponseException(cors, error, errorDescription, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class ClientAuthResult {
|
public static class ClientAuthResult {
|
||||||
|
|
||||||
private final ClientModel client;
|
private final ClientModel client;
|
||||||
|
|
|
@ -146,7 +146,7 @@ public class OpenShiftTokenReviewEndpoint implements OIDCExtProvider, Environmen
|
||||||
|
|
||||||
private void authorizeClient() {
|
private void authorizeClient() {
|
||||||
try {
|
try {
|
||||||
ClientModel client = AuthorizeClientUtil.authorizeClient(session, event).getClient();
|
ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, null).getClient();
|
||||||
event.client(client);
|
event.client(client);
|
||||||
|
|
||||||
if (client == null || client.isPublicClient()) {
|
if (client == null || client.isPublicClient()) {
|
||||||
|
|
|
@ -169,7 +169,7 @@ public class ClientsManagementService {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ClientModel authorizeClient() {
|
protected ClientModel authorizeClient() {
|
||||||
ClientModel client = AuthorizeClientUtil.authorizeClient(session, event).getClient();
|
ClientModel client = AuthorizeClientUtil.authorizeClient(session, event, null).getClient();
|
||||||
|
|
||||||
if (client.isPublicClient()) {
|
if (client.isPublicClient()) {
|
||||||
OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(OAuthErrorException.INVALID_CLIENT, "Public clients not allowed");
|
OAuth2ErrorRepresentation errorRep = new OAuth2ErrorRepresentation(OAuthErrorException.INVALID_CLIENT, "Public clients not allowed");
|
||||||
|
|
|
@ -194,11 +194,7 @@ public class Cors {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allowedOrigins.contains(ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD)) {
|
response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
|
||||||
response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_ALLOW_ORIGIN_WILDCARD);
|
|
||||||
} else {
|
|
||||||
response.getOutputHeaders().add(ACCESS_CONTROL_ALLOW_ORIGIN, origin);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (preflight) {
|
if (preflight) {
|
||||||
if (allowedMethods != null) {
|
if (allowedMethods != null) {
|
||||||
|
|
|
@ -726,6 +726,9 @@ public class OAuthClient {
|
||||||
} else if (clientId != null) {
|
} else if (clientId != null) {
|
||||||
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId));
|
parameters.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, clientId));
|
||||||
}
|
}
|
||||||
|
if (origin != null) {
|
||||||
|
post.addHeader("Origin", origin);
|
||||||
|
}
|
||||||
|
|
||||||
UrlEncodedFormEntity formEntity;
|
UrlEncodedFormEntity formEntity;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
* 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 java.util.List;
|
||||||
|
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.util.ClientManager;
|
||||||
|
import org.keycloak.testsuite.util.Matchers;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
import org.keycloak.testsuite.util.RealmBuilder;
|
||||||
|
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
|
||||||
|
*/
|
||||||
|
public class LogoutCorsTest 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 beforeAbstractKeycloakTest() throws Exception {
|
||||||
|
super.beforeAbstractKeycloakTest();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Before
|
||||||
|
public void clientConfiguration() {
|
||||||
|
ClientManager.realm(adminClient.realm("test")).clientId("test-app").addWebOrigins(VALID_CORS_URL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@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());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void postLogout_validRequestWithValidOrigin() throws Exception {
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
|
||||||
|
String refreshTokenString = tokenResponse.getRefreshToken();
|
||||||
|
oauth.origin(VALID_CORS_URL);
|
||||||
|
|
||||||
|
try (CloseableHttpResponse response = oauth.doLogout(refreshTokenString, "password")) {
|
||||||
|
assertThat(response, Matchers.statusCodeIsHC(Response.Status.NO_CONTENT));
|
||||||
|
assertCors(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void postLogout_validRequestWithInValidOriginShouldFail() throws Exception {
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
|
||||||
|
String refreshTokenString = tokenResponse.getRefreshToken();
|
||||||
|
oauth.origin(INVALID_CORS_URL);
|
||||||
|
|
||||||
|
try (CloseableHttpResponse response = oauth.doLogout(refreshTokenString, "password")) {
|
||||||
|
assertThat(response, Matchers.statusCodeIsHC(Response.Status.NO_CONTENT));
|
||||||
|
assertNotCors(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void postLogout_invalidRequestWithValidOrigin() throws Exception {
|
||||||
|
OAuthClient.AccessTokenResponse tokenResponse = loginUser();
|
||||||
|
String refreshTokenString = tokenResponse.getRefreshToken();
|
||||||
|
oauth.origin(VALID_CORS_URL);
|
||||||
|
|
||||||
|
// Logout with invalid refresh token
|
||||||
|
try (CloseableHttpResponse response = oauth.doLogout("invalid-refresh-token", "password")) {
|
||||||
|
assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatusLine().getStatusCode());
|
||||||
|
assertCors(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logout with invalid client secret
|
||||||
|
try (CloseableHttpResponse response = oauth.doLogout(refreshTokenString, "invalid-secret")) {
|
||||||
|
assertEquals(Response.Status.UNAUTHORIZED.getStatusCode(), response.getStatusLine().getStatusCode());
|
||||||
|
assertCors(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private OAuthClient.AccessTokenResponse loginUser() {
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
oauth.clientSessionState("client-session");
|
||||||
|
return oauth.doAccessTokenRequest(code, "password");
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private static void assertCors(CloseableHttpResponse response) {
|
||||||
|
assertEquals("true", response.getFirstHeader("Access-Control-Allow-Credentials").getValue());
|
||||||
|
assertEquals(VALID_CORS_URL, response.getFirstHeader("Access-Control-Allow-Origin").getValue());
|
||||||
|
assertEquals("Access-Control-Allow-Methods", response.getFirstHeader("Access-Control-Expose-Headers").getValue());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void assertNotCors(CloseableHttpResponse response) {
|
||||||
|
assertNull(response.getFirstHeader("Access-Control-Allow-Credentials"));
|
||||||
|
assertNull(response.getFirstHeader("Access-Control-Allow-Origin"));
|
||||||
|
assertNull(response.getFirstHeader("Access-Control-Expose-Headers"));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -114,6 +114,29 @@ public class TokenEndpointCorsTest extends AbstractKeycloakTest {
|
||||||
assertCors(response);
|
assertCors(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void accessTokenWithConfidentialClientCorsRequest() throws Exception {
|
||||||
|
oauth.realm("test");
|
||||||
|
oauth.clientId("direct-grant");
|
||||||
|
oauth.origin(VALID_CORS_URL);
|
||||||
|
|
||||||
|
// Successful token request with correct origin - cors should work
|
||||||
|
OAuthClient.AccessTokenResponse response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
|
||||||
|
assertEquals(200, response.getStatusCode());
|
||||||
|
assertCors(response);
|
||||||
|
|
||||||
|
// Invalid client authentication with correct origin - cors should work
|
||||||
|
response = oauth.doGrantAccessTokenRequest("invalid", "test-user@localhost", "password");
|
||||||
|
assertEquals(401, response.getStatusCode());
|
||||||
|
assertCors(response);
|
||||||
|
|
||||||
|
// Successful token request with bad origin - cors should NOT work
|
||||||
|
oauth.origin(INVALID_CORS_URL);
|
||||||
|
response = oauth.doGrantAccessTokenRequest("password", "test-user@localhost", "password");
|
||||||
|
assertEquals(200, response.getStatusCode());
|
||||||
|
assertNotCors(response);
|
||||||
|
}
|
||||||
|
|
||||||
private static void assertCors(OAuthClient.AccessTokenResponse response) {
|
private static void assertCors(OAuthClient.AccessTokenResponse response) {
|
||||||
assertEquals("true", response.getHeaders().get("Access-Control-Allow-Credentials"));
|
assertEquals("true", response.getHeaders().get("Access-Control-Allow-Credentials"));
|
||||||
assertEquals(VALID_CORS_URL, response.getHeaders().get("Access-Control-Allow-Origin"));
|
assertEquals(VALID_CORS_URL, response.getHeaders().get("Access-Control-Allow-Origin"));
|
||||||
|
|
|
@ -162,6 +162,28 @@ public class ClientManager {
|
||||||
clientResource.update(app);
|
clientResource.update(app);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ClientManagerBuilder addWebOrigins(String... webOrigins) {
|
||||||
|
ClientRepresentation app = clientResource.toRepresentation();
|
||||||
|
if (app.getWebOrigins() == null) {
|
||||||
|
app.setWebOrigins(new LinkedList<String>());
|
||||||
|
}
|
||||||
|
for (String webOrigin : webOrigins) {
|
||||||
|
app.getWebOrigins().add(webOrigin);
|
||||||
|
}
|
||||||
|
clientResource.update(app);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void removeWebOrigins(String... webOrigins) {
|
||||||
|
ClientRepresentation app = clientResource.toRepresentation();
|
||||||
|
for (String webOrigin : webOrigins) {
|
||||||
|
if (app.getWebOrigins() != null) {
|
||||||
|
app.getWebOrigins().remove(webOrigin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
clientResource.update(app);
|
||||||
|
}
|
||||||
|
|
||||||
// Set valid values of "request_uri" parameter
|
// Set valid values of "request_uri" parameter
|
||||||
public void setRequestUris(String... requestUris) {
|
public void setRequestUris(String... requestUris) {
|
||||||
ClientRepresentation app = clientResource.toRepresentation();
|
ClientRepresentation app = clientResource.toRepresentation();
|
||||||
|
|
Loading…
Reference in a new issue