diff --git a/integration/jaxrs-oauth-client/pom.xml b/integration/jaxrs-oauth-client/pom.xml index 0e23a63763..a167f99afb 100755 --- a/integration/jaxrs-oauth-client/pom.xml +++ b/integration/jaxrs-oauth-client/pom.xml @@ -70,6 +70,13 @@ junit test + + + org.osgi + org.osgi.core + 4.3.0 + provided + diff --git a/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilterImpl.java b/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilterImpl.java index 06602f1445..33795c298f 100755 --- a/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilterImpl.java +++ b/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsBearerTokenFilterImpl.java @@ -5,6 +5,7 @@ import org.keycloak.adapters.AdapterDeploymentContext; import org.keycloak.adapters.AdapterUtils; import org.keycloak.adapters.AuthChallenge; import org.keycloak.adapters.AuthOutcome; +import org.keycloak.adapters.AuthenticatedActionsHandler; import org.keycloak.adapters.BearerTokenRequestAuthenticator; import org.keycloak.adapters.KeycloakConfigResolver; import org.keycloak.adapters.KeycloakDeployment; @@ -43,17 +44,17 @@ public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter { private String keycloakConfigFile; private String keycloakConfigResolverClass; - private volatile boolean started; + protected volatile boolean started; protected AdapterDeploymentContext deploymentContext; - // TODO: Should also handle stop lifecycle for de-registration + // TODO: Should also somehow handle stop lifecycle for de-registration protected NodesRegistrationManagement nodesRegistrationManagement; protected UserSessionManagement userSessionManagement = new EmptyUserSessionManagement(); public void setKeycloakConfigFile(String configFile) { this.keycloakConfigFile = configFile; - start(); + attemptStart(); } public String getKeycloakConfigFile() { @@ -66,7 +67,25 @@ public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter { public void setKeycloakConfigResolverClass(String keycloakConfigResolverClass) { this.keycloakConfigResolverClass = keycloakConfigResolverClass; - start(); + attemptStart(); + } + + // INITIALIZATION AND STARTUP + + protected void attemptStart() { + if (started) { + throw new IllegalStateException("Filter already started. Make sure to specify just keycloakConfigResolver or keycloakConfigFile but not both"); + } + + if (isInitialized()) { + start(); + } else { + log.fine("Not yet initialized"); + } + } + + protected boolean isInitialized() { + return this.keycloakConfigFile != null || this.keycloakConfigResolverClass != null; } protected void start() { @@ -75,30 +94,20 @@ public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter { } 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); - } - } + Class resolverClass = loadResolverClass(); try { - KeycloakConfigResolver resolver = (KeycloakConfigResolver) clazz.newInstance(); + KeycloakConfigResolver resolver = resolverClass.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); + throw new RuntimeException("Unable to instantiate resolver " + resolverClass); } } else { if (keycloakConfigFile == null) { throw new IllegalArgumentException("You need to specify either keycloakConfigResolverClass or keycloakConfigFile in configuration"); } - InputStream is = readConfigFile(); + InputStream is = loadKeycloakConfigFile(); KeycloakDeployment kd = KeycloakDeploymentBuilder.build(is); deploymentContext = new AdapterDeploymentContext(kd); log.info("Keycloak is using a per-deployment configuration loaded from: " + keycloakConfigFile); @@ -108,7 +117,20 @@ public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter { started = true; } - protected InputStream readConfigFile() { + protected Class loadResolverClass() { + try { + return (Class)getClass().getClassLoader().loadClass(keycloakConfigResolverClass); + } catch (ClassNotFoundException cnfe) { + // Fallback to tccl + try { + return (Class)Thread.currentThread().getContextClassLoader().loadClass(keycloakConfigResolverClass); + } catch (ClassNotFoundException cnfe2) { + throw new RuntimeException("Unable to find resolver class: " + keycloakConfigResolverClass); + } + } + } + + protected InputStream loadKeycloakConfigFile() { if (keycloakConfigFile.startsWith(GenericConstants.PROTOCOL_CLASSPATH)) { String classPathLocation = keycloakConfigFile.replace(GenericConstants.PROTOCOL_CLASSPATH, ""); log.fine("Loading config from classpath on location: " + classPathLocation); @@ -135,11 +157,13 @@ public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter { } } + // REQUEST HANDLING + @Override public void filter(ContainerRequestContext request) throws IOException { SecurityContext securityContext = getRequestSecurityContext(request); JaxrsHttpFacade facade = new JaxrsHttpFacade(request, securityContext); - if (handlePreauth(request, facade)) { + if (handlePreauth(facade)) { return; } @@ -150,12 +174,12 @@ public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter { bearerAuthentication(facade, request, resolvedDeployment); } - protected boolean handlePreauth(ContainerRequestContext request, JaxrsHttpFacade facade) { + protected boolean handlePreauth(JaxrsHttpFacade facade) { PreAuthActionsHandler handler = new PreAuthActionsHandler(userSessionManagement, deploymentContext, facade); if (handler.handleRequest()) { - // Response might be already finished if error was sent + // Send response now (if not already sent) if (!facade.isResponseFinished()) { - request.abortWith(facade.getResponseBuilder().build()); + facade.getResponse().end(); } return true; } @@ -175,9 +199,9 @@ public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter { facade.getResponse().setStatus(Response.Status.UNAUTHORIZED.getStatusCode()); } - // Send response now + // Send response now (if not already sent) if (!facade.isResponseFinished()) { - request.abortWith(facade.getResponseBuilder().build()); + facade.getResponse().end(); } return; } else { @@ -187,6 +211,7 @@ public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter { } propagateSecurityContext(facade, request, resolvedDeployment, bearer); + handleAuthActions(facade, resolvedDeployment); } protected void propagateSecurityContext(JaxrsHttpFacade facade, ContainerRequestContext request, KeycloakDeployment resolvedDeployment, BearerTokenRequestAuthenticator bearer) { @@ -239,6 +264,16 @@ public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter { return request.getSecurityContext(); } + protected void handleAuthActions(JaxrsHttpFacade facade, KeycloakDeployment deployment) { + AuthenticatedActionsHandler authActionsHandler = new AuthenticatedActionsHandler(deployment, facade); + if (authActionsHandler.handledRequest()) { + // Send response now (if not already sent) + if (!facade.isResponseFinished()) { + facade.getResponse().end(); + } + } + } + // We don't have any sessions to manage with pure jaxrs filter private static class EmptyUserSessionManagement implements UserSessionManagement { diff --git a/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java b/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java index df72fceb24..f29935d9c4 100644 --- a/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java +++ b/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/JaxrsHttpFacade.java @@ -136,8 +136,9 @@ public class JaxrsHttpFacade implements HttpFacade { @Override public void end() { - // For now doesn't need to be supported - throw new IllegalStateException("Not supported yet"); + javax.ws.rs.core.Response response = responseBuilder.build(); + requestContext.abortWith(response); + responseFinished = true; } } @@ -168,8 +169,4 @@ public class JaxrsHttpFacade implements HttpFacade { public boolean isResponseFinished() { return responseFinished; } - - public javax.ws.rs.core.Response.ResponseBuilder getResponseBuilder() { - return responseFacade.responseBuilder; - } } diff --git a/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/OsgiJaxrsBearerTokenFilterImpl.java b/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/OsgiJaxrsBearerTokenFilterImpl.java new file mode 100644 index 0000000000..2c2f4b05c9 --- /dev/null +++ b/integration/jaxrs-oauth-client/src/main/java/org/keycloak/jaxrs/OsgiJaxrsBearerTokenFilterImpl.java @@ -0,0 +1,78 @@ +package org.keycloak.jaxrs; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.util.logging.Logger; + +import javax.annotation.Priority; +import javax.ws.rs.Priorities; +import javax.ws.rs.container.PreMatching; + +import org.keycloak.adapters.KeycloakConfigResolver; +import org.keycloak.constants.GenericConstants; +import org.osgi.framework.BundleContext; + +/** + * Variant of JaxrsBearerTokenFilter, which can be used to properly use resources from current osgi bundle + * + * @author Marek Posolda + */ +@PreMatching +@Priority(Priorities.AUTHENTICATION) +public class OsgiJaxrsBearerTokenFilterImpl extends JaxrsBearerTokenFilterImpl { + + private final static Logger log = Logger.getLogger("" + JaxrsBearerTokenFilterImpl.class); + + private BundleContext bundleContext; + + public BundleContext getBundleContext() { + return bundleContext; + } + + public void setBundleContext(BundleContext bundleContext) { + this.bundleContext = bundleContext; + attemptStart(); + } + + @Override + protected boolean isInitialized() { + return super.isInitialized() && bundleContext != null; + } + + @Override + protected Class loadResolverClass() { + String resolverClass = getKeycloakConfigResolverClass(); + try { + return (Class) bundleContext.getBundle().loadClass(resolverClass); + } catch (ClassNotFoundException cnfe) { + log.warning("Not able to find class from bundleContext. Fallback to current classloader"); + return super.loadResolverClass(); + } + } + + @Override + protected InputStream loadKeycloakConfigFile() { + String keycloakConfigFile = getKeycloakConfigFile(); + if (keycloakConfigFile.startsWith(GenericConstants.PROTOCOL_CLASSPATH)) { + + // Load from classpath of current bundle + String classPathLocation = keycloakConfigFile.replace(GenericConstants.PROTOCOL_CLASSPATH, ""); + log.fine("Loading config from classpath on location: " + classPathLocation); + + URL cfgUrl = bundleContext.getBundle().getResource(classPathLocation); + if (cfgUrl == null) { + log.warning("Not able to find configFile from bundleContext. Fallback to current classloader"); + return super.loadKeycloakConfigFile(); + } + + try { + return cfgUrl.openStream(); + } catch (IOException ioe) { + throw new RuntimeException(ioe); + } + } else { + return super.loadKeycloakConfigFile(); + } + } +} diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/jaxrs/JaxrsFilterTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/jaxrs/JaxrsFilterTest.java index 10554c54da..936792dc05 100644 --- a/testsuite/integration/src/test/java/org/keycloak/testsuite/jaxrs/JaxrsFilterTest.java +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/jaxrs/JaxrsFilterTest.java @@ -20,6 +20,7 @@ import org.junit.Test; import org.junit.rules.ExternalResource; import org.keycloak.OAuth2Constants; import org.keycloak.TokenIdGenerator; +import org.keycloak.adapters.CorsHeaders; import org.keycloak.constants.AdapterConstants; import org.keycloak.adapters.HttpClientBuilder; import org.keycloak.models.ApplicationModel; @@ -227,6 +228,52 @@ public class JaxrsFilterTest { Assert.assertTrue(getRep.getHasJaxrsAppRole()); } + @Test + public void testCors() { + keycloakRule.update(new KeycloakRule.KeycloakSetup() { + + @Override + public void config(RealmManager manager, RealmModel adminstrationRealm, RealmModel appRealm) { + Map initParams = new TreeMap(); + initParams.put(CONFIG_FILE_INIT_PARAM, "classpath:jaxrs-test/jaxrs-keycloak.json"); + keycloakRule.deployJaxrsApplication("JaxrsSimpleApp", "/jaxrs-simple", JaxrsTestApplication.class, initParams); + } + + }); + + // Send OPTIONS request + Response optionsResp = client.target(JAXRS_APP_URL).request() + .header(CorsHeaders.ORIGIN, "http://localhost:8081") + .options(); + Assert.assertEquals("true", optionsResp.getHeaderString(CorsHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + Assert.assertEquals("http://localhost:8081", optionsResp.getHeaderString(CorsHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + optionsResp.close(); + + // Retrieve token + OAuthClient.AccessTokenResponse accessTokenResp = retrieveAccessToken(); + String authHeader = "Bearer " + accessTokenResp.getAccessToken(); + + // Send GET request with token but bad origin + Response badOriginResp = client.target(JAXRS_APP_URL).request() + .header(HttpHeaders.AUTHORIZATION, authHeader) + .header(CorsHeaders.ORIGIN, "http://evil.org") + .get(); + Assert.assertEquals(403, badOriginResp.getStatus()); + badOriginResp.close(); + + // Send GET request with token and good origin + Response goodResp = client.target(JAXRS_APP_URL).request() + .header(HttpHeaders.AUTHORIZATION, authHeader) + .header(CorsHeaders.ORIGIN, "http://localhost:8081") + .get(); + Assert.assertEquals(200, goodResp.getStatus()); + Assert.assertEquals("true", optionsResp.getHeaderString(CorsHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)); + Assert.assertEquals("http://localhost:8081", optionsResp.getHeaderString(CorsHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)); + JaxrsTestResource.SimpleRepresentation getRep = goodResp.readEntity(JaxrsTestResource.SimpleRepresentation.class); + Assert.assertEquals("get", getRep.getMethod()); + goodResp.close(); + } + @Test public void testPushNotBefore() { keycloakRule.update(new KeycloakRule.KeycloakSetup() { diff --git a/testsuite/integration/src/test/resources/jaxrs-test/jaxrs-keycloak.json b/testsuite/integration/src/test/resources/jaxrs-test/jaxrs-keycloak.json index 31e97d1cdb..5a836688c1 100644 --- a/testsuite/integration/src/test/resources/jaxrs-test/jaxrs-keycloak.json +++ b/testsuite/integration/src/test/resources/jaxrs-test/jaxrs-keycloak.json @@ -4,5 +4,6 @@ "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", "auth-server-url": "http://localhost:8081/auth", "ssl-required" : "external", - "bearer-only": true + "bearer-only": true, + "enable-cors": true } \ No newline at end of file