KEYCLOAK-836 Refactoring of JaxrsBearerTokenFilter to work with both resteasy and Apache CXF. Added test

This commit is contained in:
mposolda 2014-11-06 21:01:24 +01:00
parent ebb795af5a
commit d1e819cef1
18 changed files with 962 additions and 129 deletions

View file

@ -0,0 +1,10 @@
package org.keycloak.util;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class GenericConstants {
public static final String PROTOCOL_CLASSPATH = "classpath:";
}

View file

@ -11,12 +11,10 @@ import java.security.KeyStore;
*/
public class KeystoreUtil {
private static final String PROTOCOL_CLASSPATH = "classpath:";
public static KeyStore loadKeyStore(String filename, String password) throws Exception {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
InputStream trustStream = (filename.startsWith(PROTOCOL_CLASSPATH))
?KeystoreUtil.class.getResourceAsStream(filename.replace(PROTOCOL_CLASSPATH, ""))
InputStream trustStream = (filename.startsWith(GenericConstants.PROTOCOL_CLASSPATH))
?KeystoreUtil.class.getResourceAsStream(filename.replace(GenericConstants.PROTOCOL_CLASSPATH, ""))
:new FileInputStream(new File(filename));
trustStore.load(trustStream, password.toCharArray());
trustStream.close();

View file

@ -166,7 +166,7 @@ public class PreAuthActionsHandler {
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
log.warn("SSL is required for adapter admin action");
facade.getResponse().sendError(403, "ssl required");
return null;
}
String token = StreamUtil.readString(facade.getRequest().getInputStream());
if (token == null) {

View file

@ -14,11 +14,6 @@
<description/>
<dependencies>
<dependency>
<groupId>org.jboss.logging</groupId>
<artifactId>jboss-logging</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>jaxrs-api</artifactId>
@ -43,6 +38,12 @@
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-adapter-core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.codehaus.jackson</groupId>
<artifactId>jackson-core-asl</artifactId>

View file

@ -1,114 +1,14 @@
package org.keycloak.jaxrs;
import org.jboss.logging.Logger;
import org.jboss.resteasy.spi.ResteasyProviderFactory;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.RSATokenVerifier;
import org.keycloak.VerificationException;
import org.keycloak.representations.AccessToken;
import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import java.io.IOException;
import java.security.Principal;
import java.security.PublicKey;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@Priority(Priorities.AUTHENTICATION)
public class JaxrsBearerTokenFilter implements ContainerRequestFilter {
private static Logger log = Logger.getLogger(JaxrsBearerTokenFilter.class);
protected String realm;
protected PublicKey realmPublicKey;
protected String resourceName;
public JaxrsBearerTokenFilter(String realm, PublicKey realmPublicKey, String resourceName) {
this.realm = realm;
this.realmPublicKey = realmPublicKey;
this.resourceName = resourceName;
}
protected void challengeResponse(ContainerRequestContext request, String error, String description) {
StringBuilder header = new StringBuilder("Bearer realm=\"");
header.append(realm).append("\"");
if (error != null) {
header.append(", error=\"").append(error).append("\"");
}
if (description != null) {
header.append(", error_description=\"").append(description).append("\"");
}
request.abortWith(Response.status(Response.Status.UNAUTHORIZED).header(HttpHeaders.WWW_AUTHENTICATE, header.toString()).build());
return;
}
@Context
protected SecurityContext securityContext;
@Override
public void filter(ContainerRequestContext request) throws IOException {
String authHeader = request.getHeaderString(HttpHeaders.AUTHORIZATION);
if (authHeader == null) {
challengeResponse(request, null, null);
return;
}
String[] split = authHeader.trim().split("\\s+");
if (split == null || split.length != 2) challengeResponse(request, null, null);
if (!split[0].equalsIgnoreCase("Bearer")) challengeResponse(request, null, null);
String tokenString = split[1];
try {
AccessToken token = RSATokenVerifier.verifyToken(tokenString, realmPublicKey, realm);
KeycloakSecurityContext skSession = new KeycloakSecurityContext(tokenString, token, null, null);
ResteasyProviderFactory.pushContext(KeycloakSecurityContext.class, skSession);
final KeycloakPrincipal<KeycloakSecurityContext> principal = new KeycloakPrincipal<KeycloakSecurityContext>(token.getSubject(), skSession);
final boolean isSecure = securityContext.isSecure();
final AccessToken.Access access;
if (resourceName != null) {
access = token.getResourceAccess(resourceName);
} else {
access = token.getRealmAccess();
}
SecurityContext ctx = new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return principal;
}
@Override
public boolean isUserInRole(String role) {
if (access.getRoles() == null) return false;
return access.getRoles().contains(role);
}
@Override
public boolean isSecure() {
return isSecure;
}
@Override
public String getAuthenticationScheme() {
return "OAUTH_BEARER";
}
};
request.setSecurityContext(ctx);
} catch (VerificationException e) {
log.error("Failed to verify token", e);
challengeResponse(request, "invalid_token", e.getMessage());
}
}
}
package org.keycloak.jaxrs;
import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.container.PreMatching;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@PreMatching
@Priority(Priorities.AUTHENTICATION)
public interface JaxrsBearerTokenFilter extends ContainerRequestFilter {
}

View file

@ -0,0 +1,254 @@
package org.keycloak.jaxrs;
import org.keycloak.KeycloakPrincipal;
import org.keycloak.adapters.AdapterDeploymentContext;
import org.keycloak.adapters.AdapterUtils;
import org.keycloak.adapters.AuthChallenge;
import org.keycloak.adapters.AuthOutcome;
import org.keycloak.adapters.BearerTokenRequestAuthenticator;
import org.keycloak.adapters.KeycloakConfigResolver;
import org.keycloak.adapters.KeycloakDeployment;
import org.keycloak.adapters.KeycloakDeploymentBuilder;
import org.keycloak.adapters.NodesRegistrationManagement;
import org.keycloak.adapters.PreAuthActionsHandler;
import org.keycloak.adapters.RefreshableKeycloakSecurityContext;
import org.keycloak.adapters.UserSessionManagement;
import org.keycloak.util.GenericConstants;
import javax.annotation.Priority;
import javax.ws.rs.Priorities;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.PreMatching;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.security.Principal;
import java.util.List;
import java.util.Set;
import java.util.logging.Logger;
/**
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
* @version $Revision: 1 $
*/
@PreMatching
@Priority(Priorities.AUTHENTICATION)
public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter {
private final static Logger log = Logger.getLogger("" + JaxrsBearerTokenFilterImpl.class);
private String keycloakConfigFile;
private String keycloakConfigResolverClass;
private volatile boolean started;
protected AdapterDeploymentContext deploymentContext;
// TODO: Should also handle stop lifecycle for de-registration
protected NodesRegistrationManagement nodesRegistrationManagement;
protected UserSessionManagement userSessionManagement = new EmptyUserSessionManagement();
public void setKeycloakConfigFile(String configFile) {
this.keycloakConfigFile = configFile;
start();
}
public String getKeycloakConfigFile() {
return this.keycloakConfigFile;
}
public String getKeycloakConfigResolverClass() {
return keycloakConfigResolverClass;
}
public void setKeycloakConfigResolverClass(String keycloakConfigResolverClass) {
this.keycloakConfigResolverClass = keycloakConfigResolverClass;
start();
}
protected void start() {
if (started) {
throw new IllegalStateException("Filter already started. Make sure to specify just keycloakConfigResolver or keycloakConfigFile but not both");
}
if (keycloakConfigResolverClass != null) {
Class<?> clazz;
try {
clazz = getClass().getClassLoader().loadClass(keycloakConfigResolverClass);
} catch (ClassNotFoundException cnfe) {
// Fallback to tccl
try {
clazz = Thread.currentThread().getContextClassLoader().loadClass(keycloakConfigResolverClass);
} catch (ClassNotFoundException cnfe2) {
throw new RuntimeException("Unable to find resolver class: " + keycloakConfigResolverClass);
}
}
try {
KeycloakConfigResolver resolver = (KeycloakConfigResolver) clazz.newInstance();
log.info("Using " + resolver + " to resolve Keycloak configuration on a per-request basis.");
this.deploymentContext = new AdapterDeploymentContext(resolver);
} catch (Exception e) {
throw new RuntimeException("Unable to instantiate resolver " + clazz);
}
} else {
if (keycloakConfigFile == null) {
throw new IllegalArgumentException("You need to specify either keycloakConfigResolverClass or keycloakConfigFile in configuration");
}
InputStream is = readConfigFile();
KeycloakDeployment kd = KeycloakDeploymentBuilder.build(is);
deploymentContext = new AdapterDeploymentContext(kd);
log.info("Keycloak is using a per-deployment configuration loaded from: " + keycloakConfigFile);
}
nodesRegistrationManagement = new NodesRegistrationManagement();
started = true;
}
protected InputStream readConfigFile() {
if (keycloakConfigFile.startsWith(GenericConstants.PROTOCOL_CLASSPATH)) {
String classPathLocation = keycloakConfigFile.replace(GenericConstants.PROTOCOL_CLASSPATH, "");
log.fine("Loading config from classpath on location: " + classPathLocation);
// Try current class classloader first
InputStream is = getClass().getClassLoader().getResourceAsStream(classPathLocation);
if (is == null) {
is = Thread.currentThread().getContextClassLoader().getResourceAsStream(classPathLocation);
}
if (is != null) {
return is;
} else {
throw new RuntimeException("Unable to find config from classpath: " + keycloakConfigFile);
}
} else {
// Fallback to file
try {
log.fine("Loading config from file: " + keycloakConfigFile);
return new FileInputStream(keycloakConfigFile);
} catch (FileNotFoundException fnfe) {
log.severe("Config not found on " + keycloakConfigFile);
throw new RuntimeException(fnfe);
}
}
}
@Override
public void filter(ContainerRequestContext request) throws IOException {
SecurityContext securityContext = getRequestSecurityContext(request);
JaxrsHttpFacade facade = new JaxrsHttpFacade(request, securityContext);
if (handlePreauth(request, facade)) {
return;
}
KeycloakDeployment resolvedDeployment = deploymentContext.resolveDeployment(facade);
nodesRegistrationManagement.tryRegister(resolvedDeployment);
bearerAuthentication(facade, request, resolvedDeployment);
}
protected boolean handlePreauth(ContainerRequestContext request, JaxrsHttpFacade facade) {
PreAuthActionsHandler handler = new PreAuthActionsHandler(userSessionManagement, deploymentContext, facade);
if (handler.handleRequest()) {
// Response might be already finished if error was sent
if (!facade.isResponseFinished()) {
request.abortWith(facade.getResponseBuilder().build());
}
return true;
}
return false;
}
protected void bearerAuthentication(JaxrsHttpFacade facade, ContainerRequestContext request, KeycloakDeployment resolvedDeployment) {
BearerTokenRequestAuthenticator bearer = new BearerTokenRequestAuthenticator(resolvedDeployment);
AuthOutcome outcome = bearer.authenticate(facade);
if (outcome == AuthOutcome.FAILED || outcome == AuthOutcome.NOT_ATTEMPTED) {
AuthChallenge challenge = bearer.getChallenge();
log.fine("Authentication outcome: " + outcome);
boolean challengeSent = challenge.challenge(facade);
if (!challengeSent) {
// Use some default status code
facade.getResponse().setStatus(Response.Status.UNAUTHORIZED.getStatusCode());
}
// Send response now
if (!facade.isResponseFinished()) {
request.abortWith(facade.getResponseBuilder().build());
}
return;
} else {
if (verifySslFailed(facade, resolvedDeployment)) {
return;
}
}
propagateSecurityContext(facade, request, resolvedDeployment, bearer);
}
protected void propagateSecurityContext(JaxrsHttpFacade facade, ContainerRequestContext request, KeycloakDeployment resolvedDeployment, BearerTokenRequestAuthenticator bearer) {
RefreshableKeycloakSecurityContext skSession = new RefreshableKeycloakSecurityContext(resolvedDeployment, null, bearer.getTokenString(), bearer.getToken(), null, null, null);
// Not needed to do resteasy specifics as KeycloakSecurityContext can be always retrieved from SecurityContext by typecast SecurityContext.getUserPrincipal to KeycloakPrincipal
// ResteasyProviderFactory.pushContext(KeycloakSecurityContext.class, skSession);
facade.setSecurityContext(skSession);
String principalName = AdapterUtils.getPrincipalName(resolvedDeployment, bearer.getToken());
final KeycloakPrincipal<RefreshableKeycloakSecurityContext> principal = new KeycloakPrincipal<RefreshableKeycloakSecurityContext>(principalName, skSession);
SecurityContext anonymousSecurityContext = getRequestSecurityContext(request);
final boolean isSecure = anonymousSecurityContext.isSecure();
final Set<String> roles = AdapterUtils.getRolesFromSecurityContext(skSession);
SecurityContext ctx = new SecurityContext() {
@Override
public Principal getUserPrincipal() {
return principal;
}
@Override
public boolean isUserInRole(String role) {
return roles.contains(role);
}
@Override
public boolean isSecure() {
return isSecure;
}
@Override
public String getAuthenticationScheme() {
return "OAUTH_BEARER";
}
};
request.setSecurityContext(ctx);
}
protected boolean verifySslFailed(JaxrsHttpFacade facade, KeycloakDeployment deployment) {
if (!facade.getRequest().isSecure() && deployment.getSslRequired().isRequired(facade.getRequest().getRemoteAddr())) {
log.warning("SSL is required to authenticate, but request is not secured");
facade.getResponse().sendError(403, "SSL required!");
return true;
}
return false;
}
protected SecurityContext getRequestSecurityContext(ContainerRequestContext request) {
return request.getSecurityContext();
}
// We don't have any sessions to manage with pure jaxrs filter
private static class EmptyUserSessionManagement implements UserSessionManagement {
@Override
public void logoutAll() {
}
@Override
public void logoutHttpSessions(List<String> ids) {
}
}
}

View file

@ -0,0 +1,175 @@
package org.keycloak.jaxrs;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import java.util.Map;
import javax.security.cert.X509Certificate;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.SecurityContext;
import org.keycloak.KeycloakSecurityContext;
import org.keycloak.adapters.HttpFacade;
import org.keycloak.util.HostUtils;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class JaxrsHttpFacade implements HttpFacade {
protected final ContainerRequestContext requestContext;
protected final SecurityContext securityContext;
protected final RequestFacade requestFacade = new RequestFacade();
protected final ResponseFacade responseFacade = new ResponseFacade();
protected KeycloakSecurityContext keycloakSecurityContext;
protected boolean responseFinished;
public JaxrsHttpFacade(ContainerRequestContext containerRequestContext, SecurityContext securityContext) {
this.requestContext = containerRequestContext;
this.securityContext = securityContext;
}
protected class RequestFacade implements HttpFacade.Request {
@Override
public String getMethod() {
return requestContext.getMethod();
}
@Override
public String getURI() {
return requestContext.getUriInfo().getRequestUri().toString();
}
@Override
public boolean isSecure() {
return securityContext.isSecure();
}
@Override
public String getQueryParamValue(String param) {
MultivaluedMap<String, String> queryParams = requestContext.getUriInfo().getQueryParameters();
if (queryParams == null)
return null;
return queryParams.getFirst(param);
}
@Override
public Cookie getCookie(String cookieName) {
Map<String, javax.ws.rs.core.Cookie> cookies = requestContext.getCookies();
if (cookies == null)
return null;
javax.ws.rs.core.Cookie cookie = cookies.get(cookieName);
if (cookie == null)
return null;
return new Cookie(cookie.getName(), cookie.getValue(), cookie.getVersion(), cookie.getDomain(), cookie.getPath());
}
@Override
public String getHeader(String name) {
return requestContext.getHeaderString(name);
}
@Override
public List<String> getHeaders(String name) {
MultivaluedMap<String, String> headers = requestContext.getHeaders();
return (headers == null) ? null : headers.get(name);
}
@Override
public InputStream getInputStream() {
return requestContext.getEntityStream();
}
@Override
public String getRemoteAddr() {
// TODO: implement properly
return HostUtils.getIpAddress();
}
}
protected class ResponseFacade implements HttpFacade.Response {
private javax.ws.rs.core.Response.ResponseBuilder responseBuilder = javax.ws.rs.core.Response.status(204);
@Override
public void setStatus(int status) {
responseBuilder.status(status);
}
@Override
public void addHeader(String name, String value) {
responseBuilder.header(name, value);
}
@Override
public void setHeader(String name, String value) {
responseBuilder.header(name, value);
}
@Override
public void resetCookie(String name, String path) {
// For now doesn't need to be supported
throw new IllegalStateException("Not supported yet");
}
@Override
public void setCookie(String name, String value, String path, String domain, int maxAge, boolean secure, boolean httpOnly) {
// For now doesn't need to be supported
throw new IllegalStateException("Not supported yet");
}
@Override
public OutputStream getOutputStream() {
// For now doesn't need to be supported
throw new IllegalStateException("Not supported yet");
}
@Override
public void sendError(int code, String message) {
javax.ws.rs.core.Response response = responseBuilder.status(code).entity(message).build();
requestContext.abortWith(response);
responseFinished = true;
}
@Override
public void end() {
// For now doesn't need to be supported
throw new IllegalStateException("Not supported yet");
}
}
@Override
public KeycloakSecurityContext getSecurityContext() {
return keycloakSecurityContext;
}
public void setSecurityContext(KeycloakSecurityContext securityContext) {
this.keycloakSecurityContext = securityContext;
}
@Override
public Request getRequest() {
return requestFacade;
}
@Override
public Response getResponse() {
return responseFacade;
}
@Override
public X509Certificate[] getCertificateChain() {
throw new IllegalStateException("Not supported yet");
}
public boolean isResponseFinished() {
return responseFinished;
}
public javax.ws.rs.core.Response.ResponseBuilder getResponseBuilder() {
return responseFacade.responseBuilder;
}
}

View file

@ -1,6 +1,5 @@
package org.keycloak.jaxrs;
import org.jboss.logging.Logger;
import org.keycloak.AbstractOAuthClient;
import org.keycloak.OAuth2Constants;
import org.keycloak.representations.AccessTokenResponse;
@ -18,6 +17,7 @@ import javax.ws.rs.core.UriBuilder;
import javax.ws.rs.core.UriInfo;
import java.net.URI;
import java.util.Map;
import java.util.logging.Logger;
/**
* Helper code to obtain oauth access tokens via browser redirects
@ -26,7 +26,7 @@ import java.util.Map;
* @version $Revision: 1 $
*/
public class JaxrsOAuthClient extends AbstractOAuthClient {
protected static final Logger logger = Logger.getLogger(JaxrsOAuthClient.class);
private final static Logger logger = Logger.getLogger("" + JaxrsOAuthClient.class);
protected Client client;
/**
@ -80,8 +80,8 @@ public class JaxrsOAuthClient extends AbstractOAuthClient {
URI url = uriBuilder.build();
NewCookie cookie = new NewCookie(getStateCookieName(), state, getStateCookiePath(uriInfo), null, null, -1, isSecure, true);
logger.debug("NewCookie: " + cookie.toString());
logger.debug("Oauth Redirect to: " + url);
logger.fine("NewCookie: " + cookie.toString());
logger.fine("Oauth Redirect to: " + url);
return Response.status(302)
.location(url)
.cookie(cookie).build();

View file

@ -124,6 +124,11 @@
<artifactId>keycloak-undertow-adapter</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>keycloak-jaxrs-oauth-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.keycloak</groupId>
<artifactId>federation-properties-example</artifactId>

View file

@ -26,6 +26,7 @@ package org.keycloak.testsuite;
*/
public class Constants {
public static String AUTH_SERVER_ROOT = "http://localhost:8081/auth";
public static String SERVER_ROOT = "http://localhost:8081";
public static String AUTH_SERVER_ROOT = SERVER_ROOT + "/auth";
}

View file

@ -0,0 +1,298 @@
package org.keycloak.testsuite.jaxrs;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;
import javax.ws.rs.client.Entity;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.HttpHeaders;
import javax.ws.rs.core.Response;
import org.apache.http.impl.client.DefaultHttpClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClient;
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.client.jaxrs.engines.ApacheHttpClient4Engine;
import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExternalResource;
import org.keycloak.OAuth2Constants;
import org.keycloak.TokenIdGenerator;
import org.keycloak.adapters.AdapterConstants;
import org.keycloak.adapters.HttpClientBuilder;
import org.keycloak.models.ApplicationModel;
import org.keycloak.models.RealmModel;
import org.keycloak.models.RoleModel;
import org.keycloak.models.UserModel;
import org.keycloak.protocol.oidc.TokenManager;
import org.keycloak.representations.adapters.action.PushNotBeforeAction;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.Constants;
import org.keycloak.testsuite.OAuthClient;
import org.keycloak.testsuite.rule.KeycloakRule;
import org.keycloak.testsuite.rule.WebResource;
import org.keycloak.testsuite.rule.WebRule;
import org.keycloak.util.Time;
import org.openqa.selenium.WebDriver;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class JaxrsFilterTest {
private static final String JAXRS_APP_URL = Constants.SERVER_ROOT + "/jaxrs-simple/res";
private static final String JAXRS_APP_PUSN_NOT_BEFORE_URL = Constants.SERVER_ROOT + "/jaxrs-simple/" + AdapterConstants.K_PUSH_NOT_BEFORE;
public static final String CONFIG_FILE_INIT_PARAM = "config-file";
@ClassRule
public static KeycloakRule keycloakRule = new KeycloakRule(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
ApplicationModel app = appRealm.addApplication("jaxrs-app");
app.setEnabled(true);
RoleModel role = app.addRole("jaxrs-app-user");
UserModel user = manager.getSession().users().getUserByUsername("test-user@localhost", appRealm);
user.grantRole(role);
JaxrsFilterTest.appRealm = appRealm;
}
});
@ClassRule
public static ExternalResource clientRule = new ExternalResource() {
@Override
protected void before() throws Throwable {
DefaultHttpClient httpClient = (DefaultHttpClient) new HttpClientBuilder().build();
ApacheHttpClient4Engine engine = new ApacheHttpClient4Engine(httpClient);
client = new ResteasyClientBuilder().httpEngine(engine).build();
}
@Override
protected void after() {
client.close();
}
};
private static ResteasyClient client;
@Rule
public WebRule webRule = new WebRule(this);
@WebResource
protected WebDriver driver;
// Used for signing admin action
protected static RealmModel appRealm;
@Test
public void testBasic() {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
Map<String,String> initParams = new TreeMap<String,String>();
initParams.put(CONFIG_FILE_INIT_PARAM, "classpath:jaxrs-test/jaxrs-keycloak.json");
keycloakRule.deployJaxrsApplication("JaxrsSimpleApp", "/jaxrs-simple", JaxrsTestApplication.class, initParams);
}
});
// Send GET request without token, it should fail
Response getResp = client.target(JAXRS_APP_URL).request().get();
Assert.assertEquals(getResp.getStatus(), 401);
getResp.close();
// Send POST request without token, it should fail
Response postResp = client.target(JAXRS_APP_URL).request().post(Entity.form(new Form()));
Assert.assertEquals(postResp.getStatus(), 401);
postResp.close();
// Retrieve token
OAuthClient.AccessTokenResponse accessTokenResp = retrieveAccessToken();
String authHeader = "Bearer " + accessTokenResp.getAccessToken();
// Send GET request with token and assert it's passing
JaxrsTestResource.SimpleRepresentation getRep = client.target(JAXRS_APP_URL).request()
.header(HttpHeaders.AUTHORIZATION, authHeader)
.get(JaxrsTestResource.SimpleRepresentation.class);
Assert.assertEquals("get", getRep.getMethod());
Assert.assertTrue(getRep.getHasUserRole());
Assert.assertFalse(getRep.getHasAdminRole());
Assert.assertFalse(getRep.getHasJaxrsAppRole());
// Assert that principal is ID of user (should be in UUID format)
UUID.fromString(getRep.getPrincipal());
// Send POST request with token and assert it's passing
JaxrsTestResource.SimpleRepresentation postRep = client.target(JAXRS_APP_URL).request()
.header(HttpHeaders.AUTHORIZATION, authHeader)
.post(Entity.form(new Form()), JaxrsTestResource.SimpleRepresentation.class);
Assert.assertEquals("post", postRep.getMethod());
Assert.assertEquals(getRep.getPrincipal(), postRep.getPrincipal());
}
@Test
public void testRelativeUriAndPublicKey() {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
Map<String,String> initParams = new TreeMap<String,String>();
initParams.put(CONFIG_FILE_INIT_PARAM, "classpath:jaxrs-test/jaxrs-keycloak-relative.json");
keycloakRule.deployJaxrsApplication("JaxrsSimpleApp", "/jaxrs-simple", JaxrsTestApplication.class, initParams);
}
});
// Send GET request without token, it should fail
Response getResp = client.target(JAXRS_APP_URL).request().get();
Assert.assertEquals(getResp.getStatus(), 401);
getResp.close();
// Retrieve token
OAuthClient.AccessTokenResponse accessTokenResp = retrieveAccessToken();
String authHeader = "Bearer " + accessTokenResp.getAccessToken();
// Send GET request with token and assert it's passing
JaxrsTestResource.SimpleRepresentation getRep = client.target(JAXRS_APP_URL).request()
.header(HttpHeaders.AUTHORIZATION, authHeader)
.get(JaxrsTestResource.SimpleRepresentation.class);
Assert.assertEquals("get", getRep.getMethod());
Assert.assertTrue(getRep.getHasUserRole());
Assert.assertFalse(getRep.getHasAdminRole());
Assert.assertFalse(getRep.getHasJaxrsAppRole());
// Assert that principal is ID of user (should be in UUID format)
UUID.fromString(getRep.getPrincipal());
}
@Test
public void testSslRequired() {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
Map<String, String> initParams = new TreeMap<String, String>();
initParams.put(CONFIG_FILE_INIT_PARAM, "classpath:jaxrs-test/jaxrs-keycloak-ssl.json");
keycloakRule.deployJaxrsApplication("JaxrsSimpleApp", "/jaxrs-simple", JaxrsTestApplication.class, initParams);
}
});
// Retrieve token
OAuthClient.AccessTokenResponse accessTokenResp = retrieveAccessToken();
String authHeader = "Bearer " + accessTokenResp.getAccessToken();
// Fail due to non-https
Response getResp = client.target(JAXRS_APP_URL).request()
.header(HttpHeaders.AUTHORIZATION, authHeader)
.get();
Assert.assertEquals(getResp.getStatus(), 403);
getResp.close();
}
@Test
public void testResourceRoleMappings() {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
Map<String, String> initParams = new TreeMap<String, String>();
initParams.put(CONFIG_FILE_INIT_PARAM, "classpath:jaxrs-test/jaxrs-keycloak-resource-mappings.json");
keycloakRule.deployJaxrsApplication("JaxrsSimpleApp", "/jaxrs-simple", JaxrsTestApplication.class, initParams);
}
});
// Retrieve token
OAuthClient.AccessTokenResponse accessTokenResp = retrieveAccessToken();
String authHeader = "Bearer " + accessTokenResp.getAccessToken();
// Send GET request with token and assert it's passing
JaxrsTestResource.SimpleRepresentation getRep = client.target(JAXRS_APP_URL).request()
.header(HttpHeaders.AUTHORIZATION, authHeader)
.get(JaxrsTestResource.SimpleRepresentation.class);
Assert.assertEquals("get", getRep.getMethod());
// principal is username
Assert.assertEquals("test-user@localhost", getRep.getPrincipal());
// User is in jaxrs-app-user role thanks to use-resource-role-mappings
Assert.assertFalse(getRep.getHasUserRole());
Assert.assertFalse(getRep.getHasAdminRole());
Assert.assertTrue(getRep.getHasJaxrsAppRole());
}
@Test
public void testPushNotBefore() {
keycloakRule.update(new KeycloakRule.KeycloakSetup() {
@Override
public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) {
Map<String,String> initParams = new TreeMap<String,String>();
initParams.put(CONFIG_FILE_INIT_PARAM, "classpath:jaxrs-test/jaxrs-keycloak.json");
keycloakRule.deployJaxrsApplication("JaxrsSimpleApp", "/jaxrs-simple", JaxrsTestApplication.class, initParams);
}
});
// Retrieve token
OAuthClient.AccessTokenResponse accessTokenResp = retrieveAccessToken();
String authHeader = "Bearer " + accessTokenResp.getAccessToken();
// Send GET request with token and assert it's passing
JaxrsTestResource.SimpleRepresentation getRep = client.target(JAXRS_APP_URL).request()
.header(HttpHeaders.AUTHORIZATION, authHeader)
.get(JaxrsTestResource.SimpleRepresentation.class);
Assert.assertEquals("get", getRep.getMethod());
Assert.assertTrue(getRep.getHasUserRole());
// Push new notBefore now TODO: should use admin console (admin client) instead..
int currentTime = Time.currentTime();
PushNotBeforeAction action = new PushNotBeforeAction(TokenIdGenerator.generateId(), currentTime + 30, "jaxrs-app", currentTime + 1);
String token = new TokenManager().encodeToken(appRealm, action);
Response response = client.target(JAXRS_APP_PUSN_NOT_BEFORE_URL).request().post(Entity.text(token));
Assert.assertEquals(204, response.getStatus());
response.close();
// Assert that previous token shouldn't pass anymore
response = client.target(JAXRS_APP_URL).request()
.header(HttpHeaders.AUTHORIZATION, authHeader)
.get();
Assert.assertEquals(401, response.getStatus());
response.close();
}
// @Test
public void testCxfExample() {
String uri = "http://localhost:9000/customerservice/customers/123";
Response resp = client.target(uri).request()
.get();
Assert.assertEquals(resp.getStatus(), 401);
resp.close();
// Retrieve token
OAuthClient.AccessTokenResponse accessTokenResp = retrieveAccessToken();
String authHeader = "Bearer " + accessTokenResp.getAccessToken();
String resp2 = client.target(uri).request()
.header(HttpHeaders.AUTHORIZATION, authHeader)
.get(String.class);
System.out.println(resp2);
}
private OAuthClient.AccessTokenResponse retrieveAccessToken() {
OAuthClient oauth = new OAuthClient(driver);
oauth.doLogin("test-user@localhost", "password");
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
Assert.assertEquals(200, response.getStatusCode());
return response;
}
}

View file

@ -0,0 +1,38 @@
package org.keycloak.testsuite.jaxrs;
import java.util.HashSet;
import java.util.Set;
import javax.servlet.ServletContext;
import javax.ws.rs.core.Application;
import javax.ws.rs.core.Context;
import org.keycloak.jaxrs.JaxrsBearerTokenFilterImpl;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
public class JaxrsTestApplication extends Application {
protected Set<Class<?>> classes = new HashSet<Class<?>>();
protected Set<Object> singletons = new HashSet<Object>();
public JaxrsTestApplication(@Context ServletContext context) throws Exception {
singletons.add(new JaxrsTestResource());
String configFile = context.getInitParameter(JaxrsFilterTest.CONFIG_FILE_INIT_PARAM);
JaxrsBearerTokenFilterImpl filter = new JaxrsBearerTokenFilterImpl();
filter.setKeycloakConfigFile(configFile);
singletons.add(filter);
}
@Override
public Set<Class<?>> getClasses() {
return classes;
}
@Override
public Set<Object> getSingletons() {
return singletons;
}
}

View file

@ -0,0 +1,93 @@
package org.keycloak.testsuite.jaxrs;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.SecurityContext;
/**
* @author <a href="mailto:mposolda@redhat.com">Marek Posolda</a>
*/
@Path("res")
public class JaxrsTestResource {
@Context
protected SecurityContext securityContext;
@GET
@Produces("application/json")
public SimpleRepresentation get() {
return new SimpleRepresentation("get", securityContext.getUserPrincipal().getName(), securityContext.isUserInRole("user"),
securityContext.isUserInRole("admin"), securityContext.isUserInRole("jaxrs-app-user"));
}
@POST
@Produces("application/json")
public SimpleRepresentation post() {
return new SimpleRepresentation("post", securityContext.getUserPrincipal().getName(), securityContext.isUserInRole("user"),
securityContext.isUserInRole("admin"), securityContext.isUserInRole("jaxrs-app-user"));
}
public static class SimpleRepresentation {
private String method;
private String principal;
private Boolean hasUserRole;
private Boolean hasAdminRole;
private Boolean hasJaxrsAppRole;
public SimpleRepresentation() {
}
public SimpleRepresentation(String method, String principal, boolean hasUserRole, boolean hasAdminRole,
boolean hasJaxrsAppRole) {
this.method = method;
this.principal = principal;
this.hasUserRole = hasUserRole;
this.hasAdminRole = hasAdminRole;
this.hasJaxrsAppRole = hasJaxrsAppRole;
}
public String getMethod() {
return method;
}
public void setMethod(String method) {
this.method = method;
}
public String getPrincipal() {
return principal;
}
public void setPrincipal(String principal) {
this.principal = principal;
}
public Boolean getHasUserRole() {
return hasUserRole;
}
public void setHasUserRole(Boolean hasUserRole) {
this.hasUserRole = hasUserRole;
}
public Boolean getHasAdminRole() {
return hasAdminRole;
}
public void setHasAdminRole(Boolean hasAdminRole) {
this.hasAdminRole = hasAdminRole;
}
public Boolean getHasJaxrsAppRole() {
return hasJaxrsAppRole;
}
public void setHasJaxrsAppRole(Boolean hasJaxrsAppRole) {
this.hasJaxrsAppRole = hasJaxrsAppRole;
}
}
}

View file

@ -1,10 +1,14 @@
package org.keycloak.testsuite.rule;
import io.undertow.servlet.Servlets;
import io.undertow.servlet.api.DeploymentInfo;
import io.undertow.servlet.api.FilterInfo;
import io.undertow.servlet.api.LoginConfig;
import io.undertow.servlet.api.SecurityConstraint;
import io.undertow.servlet.api.ServletInfo;
import io.undertow.servlet.api.WebResourceCollection;
import org.jboss.resteasy.plugins.server.undertow.UndertowJaxrsServer;
import org.jboss.resteasy.spi.ResteasyDeployment;
import org.junit.rules.ExternalResource;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
@ -14,16 +18,23 @@ import org.keycloak.models.UserModel;
import org.keycloak.models.utils.ModelToRepresentation;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.representations.idm.UserRepresentation;
import org.keycloak.services.filters.ClientConnectionFilter;
import org.keycloak.services.filters.KeycloakSessionServletFilter;
import org.keycloak.services.managers.RealmManager;
import org.keycloak.testsuite.Retry;
import org.keycloak.testutils.KeycloakServer;
import org.keycloak.util.JsonSerialization;
import javax.servlet.DispatcherType;
import javax.servlet.Servlet;
import javax.ws.rs.core.Application;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.Socket;
import java.util.Map;
import org.keycloak.adapters.KeycloakConfigResolver;
/**
@ -158,6 +169,22 @@ public abstract class AbstractKeycloakRule extends ExternalResource {
server.getServer().deploy(di);
}
public void deployJaxrsApplication(String name, String contextPath, Class<? extends Application> applicationClass, Map<String,String> initParams) {
ResteasyDeployment deployment = new ResteasyDeployment();
deployment.setApplicationClass(applicationClass.getName());
DeploymentInfo di = server.getServer().undertowDeployment(deployment, "");
di.setClassLoader(getClass().getClassLoader());
di.setContextPath(contextPath);
di.setDeploymentName(name);
for (Map.Entry<String,String> param : initParams.entrySet()) {
di.addInitParameter(param.getKey(), param.getValue());
}
server.getServer().deploy(di);
}
@Override
protected void after() {
removeTestRealms();

View file

@ -0,0 +1,7 @@
{
"realm": "test",
"resource": "jaxrs-app",
"auth-server-url": "/auth",
"ssl-required" : "external",
"bearer-only": true
}

View file

@ -0,0 +1,10 @@
{
"realm": "test",
"resource": "jaxrs-app",
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url": "http://localhost:8081/auth",
"ssl-required" : "external",
"bearer-only": true,
"principal-attribute": "preferred_username",
"use-resource-role-mappings": true
}

View file

@ -0,0 +1,8 @@
{
"realm": "test",
"resource": "jaxrs-app",
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url": "http://localhost:8081/auth",
"ssl-required" : "all",
"bearer-only": true
}

View file

@ -0,0 +1,8 @@
{
"realm": "test",
"resource": "jaxrs-app",
"realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB",
"auth-server-url": "http://localhost:8081/auth",
"ssl-required" : "external",
"bearer-only": true
}