diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java index 58d49ea29a..676a103351 100755 --- a/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java +++ b/core/src/main/java/org/keycloak/representations/adapters/config/BaseAdapterConfig.java @@ -16,7 +16,7 @@ import java.util.Map; "resource", "public-client", "credentials", "use-resource-role-mappings", "enable-cors", "cors-max-age", "cors-allowed-methods", - "expose-token", "bearer-only"}) + "expose-token", "bearer-only", "enable-basic-auth"}) public class BaseAdapterConfig extends BaseRealmConfig { @JsonProperty("resource") protected String resource; @@ -34,6 +34,8 @@ public class BaseAdapterConfig extends BaseRealmConfig { protected boolean exposeToken; @JsonProperty("bearer-only") protected boolean bearerOnly; + @JsonProperty("enable-basic-auth") + protected boolean enableBasicAuth; @JsonProperty("public-client") protected boolean publicClient; @JsonProperty("credentials") @@ -97,12 +99,20 @@ public class BaseAdapterConfig extends BaseRealmConfig { } public boolean isBearerOnly() { - return bearerOnly; - } + return bearerOnly; + } public void setBearerOnly(boolean bearerOnly) { - this.bearerOnly = bearerOnly; - } + this.bearerOnly = bearerOnly; + } + + public boolean isEnableBasicAuth() { + return enableBasicAuth; + } + + public void setEnableBasicAuth(boolean enableBasicAuth) { + this.enableBasicAuth = enableBasicAuth; + } public Map getCredentials() { return credentials; diff --git a/distribution/examples-docs-zip/build.xml b/distribution/examples-docs-zip/build.xml index 16f93f5546..503dc8e4d8 100755 --- a/distribution/examples-docs-zip/build.xml +++ b/distribution/examples-docs-zip/build.xml @@ -50,6 +50,14 @@ + + + + + + + + diff --git a/distribution/modules/build.xml b/distribution/modules/build.xml index 14ed2cd3d6..a9da469d1a 100755 --- a/distribution/modules/build.xml +++ b/distribution/modules/build.xml @@ -86,9 +86,6 @@ - - - diff --git a/distribution/modules/pom.xml b/distribution/modules/pom.xml index 5a286abf34..f3a2a088c9 100755 --- a/distribution/modules/pom.xml +++ b/distribution/modules/pom.xml @@ -52,11 +52,6 @@ keycloak-subsystem ${project.version} - - org.keycloak - keycloak-as7-subsystem - ${project.version} - org.bouncycastle bcprov-jdk16 diff --git a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml index 5323ea3207..2af6613ec2 100755 --- a/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml +++ b/distribution/modules/src/main/resources/modules/org/keycloak/keycloak-adapter-core/main/module.xml @@ -34,6 +34,7 @@ + diff --git a/docbook/reference/en/en-US/modules/adapter-config.xml b/docbook/reference/en/en-US/modules/adapter-config.xml index 6eddb77a54..979a9d550f 100755 --- a/docbook/reference/en/en-US/modules/adapter-config.xml +++ b/docbook/reference/en/en-US/modules/adapter-config.xml @@ -16,6 +16,7 @@ "cors-max-age" : 1000, "cors-allowed-methods" : [ "POST", "PUT", "DELETE", "GET" ], "bearer-only" : false, + "enable-basic-auth" : false, "expose-token" : true, "credentials" : { "secret" : "234234-234234-234234" @@ -157,6 +158,16 @@ + + enable-basic-auth + + + This tells the adapter to also support basic authentication. If this option is enabled, + then secret must also be provided. + This is OPTIONAL. The default value is false. + + + expose-token diff --git a/examples/basic-auth/README.md b/examples/basic-auth/README.md new file mode 100644 index 0000000000..c79becbd42 --- /dev/null +++ b/examples/basic-auth/README.md @@ -0,0 +1,29 @@ +Keycloak Example - Basic Authentication +======================================= + +The following example was tested on Wildfly 8.1.0.Final and JBoss EAP 6.3. It should be compatible with any JBoss AS, JBoss EAP or Wildfly that supports Java EE 7. + +This example demonstrates basic authentication support for a Keycloak protected REST service. However, more importantly it enables a REST service to be secured using both basic and bearer token authentication, which is useful where the service needs to be accessed both as part of a single signon session, and also as a standalone REST service. + + +Step 1: Setup a basic Keycloak server +-------------------------------------------------------------- +Install Keycloak server and start it on port 8080. Check the Reference Guide if unsure on how to do it. + +Once the Keycloak server is up and running, import the realm basicauthrealm.json. + + +Step 2: Deploy and run the example +-------------------------------------------------------------- + +- Build and deploy this sample's WAR file. For this example, deploy on the same server that is running the Keycloak Server, although this is not required for real world scenarios. + +- Open a command window and perform the following command: + + curl http://admin:password@localhost:8080/basicauth/service/echo?value=hello + +This should result in the value 'hello' being returned as a response. + +Simply change the username (currently 'admin') or password (currently 'password') in the command to see an "Unauthorized" response. + + diff --git a/examples/basic-auth/basicauthrealm.json b/examples/basic-auth/basicauthrealm.json new file mode 100644 index 0000000000..d738fd2ca2 --- /dev/null +++ b/examples/basic-auth/basicauthrealm.json @@ -0,0 +1,56 @@ +{ + "realm": "basic-auth", + "enabled": true, + "accessTokenLifespan": 60, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "ssoSessionIdleTimeout": 600, + "ssoSessionMaxLifespan": 36000, + "passwordCredentialGrantAllowed": true, + "sslRequired": "external", + "registrationAllowed": false, + "social": false, + "updateProfileOnInitialSocialLogin": false, + "privateKey": "MIICXAIBAAKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQABAoGAfmO8gVhyBxdqlxmIuglbz8bcjQbhXJLR2EoS8ngTXmN1bo2L90M0mUKSdc7qF10LgETBzqL8jYlQIbt+e6TH8fcEpKCjUlyq0Mf/vVbfZSNaVycY13nTzo27iPyWQHK5NLuJzn1xvxxrUeXI6A2WFpGEBLbHjwpx5WQG9A+2scECQQDvdn9NE75HPTVPxBqsEd2z10TKkl9CZxu10Qby3iQQmWLEJ9LNmy3acvKrE3gMiYNWb6xHPKiIqOR1as7L24aTAkEAtyvQOlCvr5kAjVqrEKXalj0Tzewjweuxc0pskvArTI2Oo070h65GpoIKLc9jf+UA69cRtquwP93aZKtW06U8dQJAF2Y44ks/mK5+eyDqik3koCI08qaC8HYq2wVl7G2QkJ6sbAaILtcvD92ToOvyGyeE0flvmDZxMYlvaZnaQ0lcSQJBAKZU6umJi3/xeEbkJqMfeLclD27XGEFoPeNrmdx0q10Azp4NfJAY+Z8KRyQCR2BEG+oNitBOZ+YXF9KCpH3cdmECQHEigJhYg+ykOvr1aiZUMFT72HU0jnmQe2FVekuG+LJUt2Tm7GtMjTFoGpf0JwrVuZN39fOYAlo+nTixgeW7X8Y=", + "publicKey": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "requiredCredentials": [ "password" ], + "users" : [ + { + "username" : "admin", + "enabled": true, + "email" : "admin@admin.com", + "firstName": "Admin", + "lastName": "Burke", + "credentials" : [ + { "type" : "password", + "value" : "password" } + ], + "realmRoles": [ "user","admin" ], + "applicationRoles": { + "realm-management": [ "realm-admin" ] + } + } + ], + "roles" : { + "realm" : [ + { + "name": "user", + "description": "User privileges" + }, + { + "name": "admin", + "description": "Administrator privileges" + } + ] + }, + "applications": [ + { + "name": "basic-auth-service", + "enabled": true, + "adminUrl": "/basicauth", + "baseUrl": "/basicauth", + "secret": "password" + } + ] + +} diff --git a/examples/basic-auth/pom.xml b/examples/basic-auth/pom.xml new file mode 100644 index 0000000000..5e7095d802 --- /dev/null +++ b/examples/basic-auth/pom.xml @@ -0,0 +1,94 @@ + + + 4.0.0 + + + keycloak-parent + org.keycloak + 1.1.0.Beta2-SNAPSHOT + ../../pom.xml + + + Keycloak Examples - Basic Auth + examples-basicauth + war + + + Keycloak Basic Auth Example + + + + + jboss + jboss repo + http://repository.jboss.org/nexus/content/groups/public/ + + + + + + org.jboss.resteasy + resteasy-jaxrs + provided + + + org.jboss.spec.javax.servlet + jboss-servlet-api_3.0_spec + provided + + + org.keycloak + keycloak-core + ${project.version} + provided + + + org.keycloak + keycloak-adapter-core + ${project.version} + provided + + + org.apache.httpcomponents + httpclient + ${keycloak.apache.httpcomponents.version} + provided + + + + + basicauth + + + org.jboss.as.plugins + jboss-as-maven-plugin + + false + + + + org.wildfly.plugins + wildfly-maven-plugin + + false + + + + org.apache.maven.plugins + maven-deploy-plugin + + true + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${maven.compiler.source} + ${maven.compiler.target} + + + + + diff --git a/examples/basic-auth/src/main/java/org/keycloak/example/basicauth/BasicAuthService.java b/examples/basic-auth/src/main/java/org/keycloak/example/basicauth/BasicAuthService.java new file mode 100644 index 0000000000..d722da9679 --- /dev/null +++ b/examples/basic-auth/src/main/java/org/keycloak/example/basicauth/BasicAuthService.java @@ -0,0 +1,24 @@ +package org.keycloak.example.basicauth; + +import org.jboss.resteasy.annotations.cache.NoCache; + +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Bill Burke + * @version $Revision: 1 $ + */ +@Path("service") +public class BasicAuthService { + @GET + @NoCache + @Path("echo") + public String echo(@QueryParam("value") String value) { + return value; + } +} diff --git a/examples/basic-auth/src/main/java/org/keycloak/example/basicauth/BasicAuthServiceApplication.java b/examples/basic-auth/src/main/java/org/keycloak/example/basicauth/BasicAuthServiceApplication.java new file mode 100644 index 0000000000..c0c0382c81 --- /dev/null +++ b/examples/basic-auth/src/main/java/org/keycloak/example/basicauth/BasicAuthServiceApplication.java @@ -0,0 +1,12 @@ +package org.keycloak.example.basicauth; + +import javax.ws.rs.ApplicationPath; +import javax.ws.rs.core.Application; + +/** + * Basic auth app. + */ +@ApplicationPath("/") +public class BasicAuthServiceApplication extends Application +{ +} diff --git a/examples/basic-auth/src/main/webapp/WEB-INF/keycloak.json b/examples/basic-auth/src/main/webapp/WEB-INF/keycloak.json new file mode 100644 index 0000000000..4502199d70 --- /dev/null +++ b/examples/basic-auth/src/main/webapp/WEB-INF/keycloak.json @@ -0,0 +1,11 @@ +{ + "realm" : "basic-auth", + "resource" : "basic-auth-service", + "realm-public-key" : "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url": "/auth", + "ssl-required" : "external", + "enable-basic-auth" : "true", + "credentials": { + "secret": "password" + } +} diff --git a/examples/basic-auth/src/main/webapp/WEB-INF/web.xml b/examples/basic-auth/src/main/webapp/WEB-INF/web.xml new file mode 100644 index 0000000000..f25eb4656d --- /dev/null +++ b/examples/basic-auth/src/main/webapp/WEB-INF/web.xml @@ -0,0 +1,29 @@ + + + + basicauth + + + + /* + + + + user + + + + + KEYCLOAK + basic-auth + + + + user + + diff --git a/examples/pom.xml b/examples/pom.xml index 61c47af28b..97d1b9ed4f 100755 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -31,5 +31,6 @@ providers js-console multi-tenant + basic-auth diff --git a/integration/adapter-core/pom.xml b/integration/adapter-core/pom.xml index 9262be18d3..e6f4b0bec8 100755 --- a/integration/adapter-core/pom.xml +++ b/integration/adapter-core/pom.xml @@ -41,6 +41,11 @@ jackson-xc provided + + net.iharder + base64 + provided + junit junit diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java index 6258645273..d8a2141f69 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/AdapterDeploymentContext.java @@ -237,6 +237,16 @@ public class AdapterDeploymentContext { delegate.setBearerOnly(bearerOnly); } + @Override + public boolean isEnableBasicAuth() { + return delegate.isEnableBasicAuth(); + } + + @Override + public void setEnableBasicAuth(boolean enableBasicAuth) { + delegate.setEnableBasicAuth(enableBasicAuth); + } + @Override public boolean isPublicClient() { return delegate.isPublicClient(); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java new file mode 100644 index 0000000000..55dc847bf7 --- /dev/null +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/BasicAuthRequestAuthenticator.java @@ -0,0 +1,112 @@ +package org.keycloak.adapters; + +import org.apache.http.HttpEntity; +import org.apache.http.HttpResponse; +import org.apache.http.NameValuePair; +import org.apache.http.client.HttpClient; +import org.apache.http.client.entity.UrlEncodedFormEntity; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.message.BasicNameValuePair; +import org.jboss.logging.Logger; +import org.keycloak.OAuth2Constants; +import org.keycloak.constants.ServiceUrlConstants; +import org.keycloak.representations.AccessTokenResponse; +import org.keycloak.util.BasicAuthHelper; +import org.keycloak.util.JsonSerialization; +import org.keycloak.util.KeycloakUriBuilder; + +import java.util.List; + +/** + * Basic auth request authenticator. + */ +public class BasicAuthRequestAuthenticator extends BearerTokenRequestAuthenticator { + protected Logger log = Logger.getLogger(BasicAuthRequestAuthenticator.class); + + public BasicAuthRequestAuthenticator(KeycloakDeployment deployment) { + super(deployment); + } + + public AuthOutcome authenticate(HttpFacade exchange) { + List authHeaders = exchange.getRequest().getHeaders("Authorization"); + if (authHeaders == null || authHeaders.size() == 0) { + challenge = challengeResponse(exchange, null, null); + return AuthOutcome.NOT_ATTEMPTED; + } + + tokenString = null; + for (String authHeader : authHeaders) { + String[] split = authHeader.trim().split("\\s+"); + if (split == null || split.length != 2) continue; + if (!split[0].equalsIgnoreCase("Basic")) continue; + tokenString = split[1]; + } + + if (tokenString == null) { + challenge = challengeResponse(exchange, null, null); + return AuthOutcome.NOT_ATTEMPTED; + } + + AccessTokenResponse atr=null; + try { + String userpw=new String(net.iharder.Base64.decode(tokenString)); + String[] parts=userpw.split(":"); + + atr = getToken(parts[0], parts[1]); + } catch (Exception e) { + log.debug("Failed to obtain token", e); + challenge = challengeResponse(exchange, "no_token", e.getMessage()); + return AuthOutcome.FAILED; + } + + return authenticateToken(exchange, atr.getToken()); + } + + private AccessTokenResponse getToken(String username, String password) throws Exception { + AccessTokenResponse tokenResponse=null; + HttpClient client = new HttpClientBuilder().disableTrustManager().build(); + + try { + HttpPost post = new HttpPost( + KeycloakUriBuilder.fromUri(deployment.getAuthServerBaseUrl()) + .path(ServiceUrlConstants.TOKEN_SERVICE_DIRECT_GRANT_PATH).build(deployment.getRealm())); + java.util.List formparams = new java.util.ArrayList (); + formparams.add(new BasicNameValuePair("username", username)); + formparams.add(new BasicNameValuePair("password", password)); + + if (deployment.isPublicClient()) { + formparams.add(new BasicNameValuePair(OAuth2Constants.CLIENT_ID, deployment.getResourceName())); + } else { + String authorization = BasicAuthHelper.createHeader(deployment.getResourceName(), + deployment.getResourceCredentials().get("secret")); + post.setHeader("Authorization", authorization); + } + + UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8"); + post.setEntity(form); + + HttpResponse response = client.execute(post); + int status = response.getStatusLine().getStatusCode(); + HttpEntity entity = response.getEntity(); + if (status != 200) { + throw new java.io.IOException("Bad status: " + status); + } + if (entity == null) { + throw new java.io.IOException("No Entity"); + } + java.io.InputStream is = entity.getContent(); + try { + tokenResponse = JsonSerialization.readValue(is, AccessTokenResponse.class); + } finally { + try { + is.close(); + } catch (java.io.IOException ignored) { } + } + } finally { + client.getConnectionManager().shutdown(); + } + + return (tokenResponse); + } + +} diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java index 0aafa00ca1..ccbe5961b2 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/BearerTokenRequestAuthenticator.java @@ -59,6 +59,10 @@ public class BearerTokenRequestAuthenticator { return AuthOutcome.NOT_ATTEMPTED; } + return (authenticateToken(exchange, tokenString)); + } + + protected AuthOutcome authenticateToken(HttpFacade exchange, String tokenString) { try { token = RSATokenVerifier.verifyToken(tokenString, deployment.getRealmKey(), deployment.getRealm()); } catch (VerificationException e) { diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java index f4b9c9029a..e9dfaf608f 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java @@ -38,6 +38,7 @@ public class KeycloakDeployment { protected String resourceName; protected boolean bearerOnly; + protected boolean enableBasicAuth; protected boolean publicClient; protected Map resourceCredentials = new HashMap(); protected HttpClient client; @@ -199,6 +200,14 @@ public class KeycloakDeployment { this.bearerOnly = bearerOnly; } + public boolean isEnableBasicAuth() { + return enableBasicAuth; + } + + public void setEnableBasicAuth(boolean enableBasicAuth) { + this.enableBasicAuth = enableBasicAuth; + } + public boolean isPublicClient() { return publicClient; } diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java index e03eb51d3a..1f00b91c07 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java @@ -67,6 +67,7 @@ public class KeycloakDeploymentBuilder { } deployment.setBearerOnly(adapterConfig.isBearerOnly()); + deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth()); deployment.setAlwaysRefreshToken(adapterConfig.isAlwaysRefreshToken()); deployment.setRegisterNodeAtStartup(adapterConfig.isRegisterNodeAtStartup()); deployment.setRegisterNodePeriod(adapterConfig.getRegisterNodePeriod()); diff --git a/integration/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java b/integration/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java index d5c7119821..bd853da5f6 100755 --- a/integration/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java +++ b/integration/adapter-core/src/main/java/org/keycloak/adapters/RequestAuthenticator.java @@ -36,10 +36,12 @@ public abstract class RequestAuthenticator { if (log.isTraceEnabled()) { log.trace("--> authenticate()"); } + BearerTokenRequestAuthenticator bearer = createBearerTokenAuthenticator(); if (log.isTraceEnabled()) { log.trace("try bearer"); } + AuthOutcome outcome = bearer.authenticate(facade); if (outcome == AuthOutcome.FAILED) { challenge = bearer.getChallenge(); @@ -47,7 +49,7 @@ public abstract class RequestAuthenticator { return AuthOutcome.FAILED; } else if (outcome == AuthOutcome.AUTHENTICATED) { if (verifySSL()) return AuthOutcome.FAILED; - completeAuthentication(bearer); + completeAuthentication(bearer, "KEYCLOAK"); log.debug("Bearer AUTHENTICATED"); return AuthOutcome.AUTHENTICATED; } else if (deployment.isBearerOnly()) { @@ -56,6 +58,24 @@ public abstract class RequestAuthenticator { return AuthOutcome.NOT_ATTEMPTED; } + if (deployment.isEnableBasicAuth()) { + BasicAuthRequestAuthenticator basicAuth = createBasicAuthAuthenticator(); + if (log.isTraceEnabled()) { + log.trace("try basic auth"); + } + + outcome = basicAuth.authenticate(facade); + if (outcome == AuthOutcome.FAILED) { + challenge = basicAuth.getChallenge(); + log.debug("BasicAuth FAILED"); + return AuthOutcome.FAILED; + } else if (outcome == AuthOutcome.AUTHENTICATED) { + log.debug("BasicAuth AUTHENTICATED"); + completeAuthentication(basicAuth, "BASIC"); + return AuthOutcome.AUTHENTICATED; + } + } + if (log.isTraceEnabled()) { log.trace("try oauth"); } @@ -104,6 +124,10 @@ public abstract class RequestAuthenticator { return new BearerTokenRequestAuthenticator(deployment); } + protected BasicAuthRequestAuthenticator createBasicAuthAuthenticator() { + return new BasicAuthRequestAuthenticator(deployment); + } + protected void completeAuthentication(OAuthRequestAuthenticator oauth) { RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, tokenStore, oauth.getTokenString(), oauth.getToken(), oauth.getIdTokenString(), oauth.getIdToken(), oauth.getRefreshToken()); final KeycloakPrincipal principal = new KeycloakPrincipal(AdapterUtils.getPrincipalName(deployment, oauth.getToken()), session); @@ -111,13 +135,13 @@ public abstract class RequestAuthenticator { } protected abstract void completeOAuthAuthentication(KeycloakPrincipal principal); - protected abstract void completeBearerAuthentication(KeycloakPrincipal principal); + protected abstract void completeBearerAuthentication(KeycloakPrincipal principal, String method); protected abstract String getHttpSessionId(boolean create); - protected void completeAuthentication(BearerTokenRequestAuthenticator bearer) { + protected void completeAuthentication(BearerTokenRequestAuthenticator bearer, String method) { RefreshableKeycloakSecurityContext session = new RefreshableKeycloakSecurityContext(deployment, null, bearer.getTokenString(), bearer.getToken(), null, null, null); final KeycloakPrincipal principal = new KeycloakPrincipal(AdapterUtils.getPrincipalName(deployment, bearer.getToken()), session); - completeBearerAuthentication(principal); + completeBearerAuthentication(principal, method); } } diff --git a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java index 74e0e63919..7544e66725 100755 --- a/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java +++ b/integration/as7-eap6/adapter/src/main/java/org/keycloak/adapters/as7/CatalinaRequestAuthenticator.java @@ -84,7 +84,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { } @Override - protected void completeBearerAuthentication(KeycloakPrincipal principal) { + protected void completeBearerAuthentication(KeycloakPrincipal principal, String method) { RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); if (log.isDebugEnabled()) { @@ -92,7 +92,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { } Principal generalPrincipal = new CatalinaSecurityContextHelper().createPrincipal(request.getContext().getRealm(), principal, roles, securityContext); request.setUserPrincipal(generalPrincipal); - request.setAuthType("KEYCLOAK"); + request.setAuthType(method); request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); } 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 33795c298f..1d070cfd1b 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 @@ -6,6 +6,7 @@ import org.keycloak.adapters.AdapterUtils; import org.keycloak.adapters.AuthChallenge; import org.keycloak.adapters.AuthOutcome; import org.keycloak.adapters.AuthenticatedActionsHandler; +import org.keycloak.adapters.BasicAuthRequestAuthenticator; import org.keycloak.adapters.BearerTokenRequestAuthenticator; import org.keycloak.adapters.KeycloakConfigResolver; import org.keycloak.adapters.KeycloakDeployment; @@ -188,10 +189,16 @@ public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter { } protected void bearerAuthentication(JaxrsHttpFacade facade, ContainerRequestContext request, KeycloakDeployment resolvedDeployment) { - BearerTokenRequestAuthenticator bearer = new BearerTokenRequestAuthenticator(resolvedDeployment); - AuthOutcome outcome = bearer.authenticate(facade); + BearerTokenRequestAuthenticator authenticator = new BearerTokenRequestAuthenticator(resolvedDeployment); + AuthOutcome outcome = authenticator.authenticate(facade); + + if (outcome == AuthOutcome.NOT_ATTEMPTED && resolvedDeployment.isEnableBasicAuth()) { + authenticator = new BasicAuthRequestAuthenticator(resolvedDeployment); + outcome = authenticator.authenticate(facade); + } + if (outcome == AuthOutcome.FAILED || outcome == AuthOutcome.NOT_ATTEMPTED) { - AuthChallenge challenge = bearer.getChallenge(); + AuthChallenge challenge = authenticator.getChallenge(); log.fine("Authentication outcome: " + outcome); boolean challengeSent = challenge.challenge(facade); if (!challengeSent) { @@ -210,7 +217,7 @@ public class JaxrsBearerTokenFilterImpl implements JaxrsBearerTokenFilter { } } - propagateSecurityContext(facade, request, resolvedDeployment, bearer); + propagateSecurityContext(facade, request, resolvedDeployment, authenticator); handleAuthActions(facade, resolvedDeployment); } diff --git a/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/AbstractJettyRequestAuthenticator.java b/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/AbstractJettyRequestAuthenticator.java index ee6ffaeb09..c78169e181 100755 --- a/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/AbstractJettyRequestAuthenticator.java +++ b/integration/jetty/jetty-core/src/main/java/org/keycloak/adapters/jetty/AbstractJettyRequestAuthenticator.java @@ -79,7 +79,7 @@ public abstract class AbstractJettyRequestAuthenticator extends RequestAuthentic } @Override - protected void completeBearerAuthentication(KeycloakPrincipal principal) { + protected void completeBearerAuthentication(KeycloakPrincipal principal, String method) { this.principal = principal; RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); diff --git a/integration/keycloak-subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentDefinition.java b/integration/keycloak-subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentDefinition.java index 2254dd6d3b..9cd606c34c 100755 --- a/integration/keycloak-subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentDefinition.java +++ b/integration/keycloak-subsystem/src/main/java/org/keycloak/subsystem/extension/SecureDeploymentDefinition.java @@ -65,6 +65,12 @@ public class SecureDeploymentDefinition extends SimpleResourceDefinition { .setAllowExpression(true) .setDefaultValue(new ModelNode(false)) .build(); + protected static final SimpleAttributeDefinition ENABLE_BASIC_AUTH = + new SimpleAttributeDefinitionBuilder("enable-basic-auth", ModelType.BOOLEAN, true) + .setXmlName("enable-basic-auth") + .setAllowExpression(true) + .setDefaultValue(new ModelNode(false)) + .build(); protected static final SimpleAttributeDefinition PUBLIC_CLIENT = new SimpleAttributeDefinitionBuilder("public-client", ModelType.BOOLEAN, true) .setXmlName("public-client") @@ -78,6 +84,7 @@ public class SecureDeploymentDefinition extends SimpleResourceDefinition { DEPLOYMENT_ONLY_ATTRIBUTES.add(RESOURCE); DEPLOYMENT_ONLY_ATTRIBUTES.add(USE_RESOURCE_ROLE_MAPPINGS); DEPLOYMENT_ONLY_ATTRIBUTES.add(BEARER_ONLY); + DEPLOYMENT_ONLY_ATTRIBUTES.add(ENABLE_BASIC_AUTH); DEPLOYMENT_ONLY_ATTRIBUTES.add(PUBLIC_CLIENT); } diff --git a/integration/keycloak-subsystem/src/main/resources/org/keycloak/subsystem/extension/LocalDescriptions.properties b/integration/keycloak-subsystem/src/main/resources/org/keycloak/subsystem/extension/LocalDescriptions.properties index 1756381104..46d254d7ee 100755 --- a/integration/keycloak-subsystem/src/main/resources/org/keycloak/subsystem/extension/LocalDescriptions.properties +++ b/integration/keycloak-subsystem/src/main/resources/org/keycloak/subsystem/extension/LocalDescriptions.properties @@ -68,6 +68,7 @@ keycloak.secure-deployment.resource=Application name keycloak.secure-deployment.use-resource-role-mappings=Use resource level permissions from token keycloak.secure-deployment.credentials=Adapter credentials keycloak.secure-deployment.bearer-only=Bearer Token Auth only +keycloak.secure-deployment.enable-basic-auth=Enable Basic Authentication keycloak.secure-deployment.public-client=Public client keycloak.secure-deployment.enable-cors=Enable Keycloak CORS support keycloak.secure-deployment.client-keystore=n/a diff --git a/integration/keycloak-subsystem/src/main/resources/schema/wildfly-keycloak_1_0.xsd b/integration/keycloak-subsystem/src/main/resources/schema/wildfly-keycloak_1_0.xsd index ff7c16e8d5..17d6aa6f23 100755 --- a/integration/keycloak-subsystem/src/main/resources/schema/wildfly-keycloak_1_0.xsd +++ b/integration/keycloak-subsystem/src/main/resources/schema/wildfly-keycloak_1_0.xsd @@ -86,6 +86,7 @@ + diff --git a/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaRequestAuthenticator.java b/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaRequestAuthenticator.java index fdfe16f493..e7884b82b0 100755 --- a/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaRequestAuthenticator.java +++ b/integration/tomcat/tomcat-core/src/main/java/org/keycloak/adapters/tomcat/CatalinaRequestAuthenticator.java @@ -86,7 +86,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { } @Override - protected void completeBearerAuthentication(KeycloakPrincipal principal) { + protected void completeBearerAuthentication(KeycloakPrincipal principal, String method) { RefreshableKeycloakSecurityContext securityContext = principal.getKeycloakSecurityContext(); Set roles = AdapterUtils.getRolesFromSecurityContext(securityContext); if (log.isLoggable(Level.FINE)) { @@ -94,7 +94,7 @@ public class CatalinaRequestAuthenticator extends RequestAuthenticator { } Principal generalPrincipal = principalFactory.createPrincipal(request.getContext().getRealm(), principal, roles, securityContext); request.setUserPrincipal(generalPrincipal); - request.setAuthType("KEYCLOAK"); + request.setAuthType(method); request.setAttribute(KeycloakSecurityContext.class.getName(), securityContext); } diff --git a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowRequestAuthenticator.java b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowRequestAuthenticator.java index b9761e1397..5ef0734800 100755 --- a/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowRequestAuthenticator.java +++ b/integration/undertow/src/main/java/org/keycloak/adapters/undertow/AbstractUndertowRequestAuthenticator.java @@ -20,6 +20,7 @@ import io.undertow.security.api.SecurityContext; import io.undertow.server.HttpServerExchange; import io.undertow.server.session.Session; import io.undertow.util.Sessions; + import org.keycloak.KeycloakPrincipal; import org.keycloak.adapters.AdapterTokenStore; import org.keycloak.adapters.HttpFacade; @@ -69,9 +70,9 @@ public abstract class AbstractUndertowRequestAuthenticator extends RequestAuthen } @Override - protected void completeBearerAuthentication(KeycloakPrincipal principal) { + protected void completeBearerAuthentication(KeycloakPrincipal principal, String method) { KeycloakUndertowAccount account = createAccount(principal); - securityContext.authenticationComplete(account, "KEYCLOAK", false); + securityContext.authenticationComplete(account, method, false); propagateKeycloakContext(account); } diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/jaxrs/JaxrsBasicAuthTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/jaxrs/JaxrsBasicAuthTest.java new file mode 100644 index 0000000000..5857c00c53 --- /dev/null +++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/jaxrs/JaxrsBasicAuthTest.java @@ -0,0 +1,145 @@ +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.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.services.managers.RealmManager; +import org.keycloak.testsuite.Constants; +import org.keycloak.testsuite.rule.KeycloakRule; +import org.keycloak.testsuite.rule.WebResource; +import org.keycloak.testsuite.rule.WebRule; +import org.openqa.selenium.WebDriver; + +/** + * Test for basic authentication. + */ +public class JaxrsBasicAuthTest { + + private static final String JAXRS_APP_URL = Constants.SERVER_ROOT + "/jaxrs-simple/res"; + + 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); + app.setSecret("password"); + + JaxrsBasicAuthTest.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 initParams = new TreeMap(); + initParams.put(CONFIG_FILE_INIT_PARAM, "classpath:jaxrs-test/jaxrs-keycloak-basicauth.json"); + keycloakRule.deployJaxrsApplication("JaxrsSimpleApp", "/jaxrs-simple", JaxrsTestApplication.class, initParams); + } + + }); + + // Send GET request without credentials, it should fail + Response getResp = client.target(JAXRS_APP_URL).request().get(); + Assert.assertEquals(getResp.getStatus(), 401); + getResp.close(); + + // Send POST request without credentials, 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 + String incorrectAuthHeader = "Basic "+encodeCredentials("invalid-user", "password"); + + // Send GET request with incorrect credentials, it shojuld fail + getResp = client.target(JAXRS_APP_URL).request() + .header(HttpHeaders.AUTHORIZATION, incorrectAuthHeader) + .get(); + Assert.assertEquals(getResp.getStatus(), 401); + getResp.close(); + + // Retrieve token + String authHeader = "Basic "+encodeCredentials("test-user@localhost", "password"); + + // 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()); + + // TODO: SHOULD HAVE USER ROLE + //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()); + } + + private String encodeCredentials(String username, String password) { + String text=username+":"+password; + return (net.iharder.Base64.encodeBytes(text.getBytes())); + } +} diff --git a/testsuite/integration/src/test/resources/jaxrs-test/jaxrs-keycloak-basicauth.json b/testsuite/integration/src/test/resources/jaxrs-test/jaxrs-keycloak-basicauth.json new file mode 100644 index 0000000000..949b720643 --- /dev/null +++ b/testsuite/integration/src/test/resources/jaxrs-test/jaxrs-keycloak-basicauth.json @@ -0,0 +1,11 @@ +{ + "realm": "test", + "resource": "jaxrs-app", + "realm-public-key": "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrVrCuTtArbgaZzL1hvh0xtL5mc7o0NqPVnYXkLvgcwiC3BjLGw1tGEGoJaXDuSaRllobm53JBhjx33UNv+5z/UMG4kytBWxheNVKnL6GgqlNabMaFfPLPCF8kAgKnsi79NMo+n6KnSY8YeUmec/p2vjO2NjsSAVcWEQMVhJ31LwIDAQAB", + "auth-server-url": "http://localhost:8081/auth", + "ssl-required" : "external", + "enable-basic-auth": true, + "credentials": { + "secret": "password" + } +} \ No newline at end of file