keycloak-aplcache

KEYCLOAK-2604 Proof Key for Code Exchange by OAuth Public Clients

2/3/2017 1:02:54 AM

Details

diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
index 8664800..ba7bc5d 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeployment.java
@@ -85,6 +85,9 @@ public class KeycloakDeployment {
     protected int publicKeyCacheTtl;
     private PolicyEnforcer policyEnforcer;
 
+    // https://tools.ietf.org/html/rfc7636
+    protected boolean pkce = false;
+
     public KeycloakDeployment() {
     }
 
@@ -414,4 +417,14 @@ public class KeycloakDeployment {
     public PolicyEnforcer getPolicyEnforcer() {
         return policyEnforcer;
     }
+
+    // https://tools.ietf.org/html/rfc7636
+    public boolean isPkce() {
+        return pkce;
+    }
+
+    public void setPkce(boolean pkce) {
+        this.pkce = pkce;
+    }
+
 }
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
index 65e9456..2fd9276 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/KeycloakDeploymentBuilder.java
@@ -98,6 +98,11 @@ public class KeycloakDeploymentBuilder {
             deployment.setCorsAllowedMethods(adapterConfig.getCorsAllowedMethods());
         }
 
+        // https://tools.ietf.org/html/rfc7636
+        if (adapterConfig.isPkce()) {
+            deployment.setPkce(true);
+        }
+
         deployment.setBearerOnly(adapterConfig.isBearerOnly());
         deployment.setAutodetectBearerOnly(adapterConfig.isAutodetectBearerOnly());
         deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth());
diff --git a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java
index 7ec546c..f5bfad0 100755
--- a/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java
+++ b/adapters/oidc/adapter-core/src/main/java/org/keycloak/adapters/ServerRequest.java
@@ -33,6 +33,8 @@ import org.keycloak.constants.AdapterConstants;
 import org.keycloak.representations.AccessTokenResponse;
 import org.keycloak.util.JsonSerialization;
 
+import org.jboss.logging.Logger;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
@@ -46,6 +48,8 @@ import java.util.List;
  */
 public class ServerRequest {
 
+	private static Logger logger = Logger.getLogger(ServerRequest.class);
+
     public static class HttpFailure extends Exception {
         private int status;
         private String error;
@@ -136,6 +140,62 @@ public class ServerRequest {
         }
     }
 
+    // https://tools.ietf.org/html/rfc7636#section-4
+    public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri, String sessionId, String codeVerifier) throws IOException, HttpFailure {
+        List<NameValuePair> formparams = new ArrayList<>();
+        redirectUri = stripOauthParametersFromRedirect(redirectUri);
+        formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "authorization_code"));
+        formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
+        formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri));
+        if (sessionId != null) {
+            formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, sessionId));
+            formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, HostUtils.getHostName()));
+        }
+        // https://tools.ietf.org/html/rfc7636#section-4
+        if (codeVerifier != null) {
+            logger.debugf("add to POST parameters of Token Request, codeVerifier = %s", codeVerifier);
+            formparams.add(new BasicNameValuePair(OAuth2Constants.CODE_VERIFIER, codeVerifier));
+        } else {
+            logger.debug("add to POST parameters of Token Request without codeVerifier");
+        }
+
+        HttpPost post = new HttpPost(deployment.getTokenUrl());
+        ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
+
+        UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
+        post.setEntity(form);
+        HttpResponse response = deployment.getClient().execute(post);
+        int status = response.getStatusLine().getStatusCode();
+        HttpEntity entity = response.getEntity();
+        if (status != 200) {
+            error(status, entity);
+        }
+        if (entity == null) {
+            throw new HttpFailure(status, null);
+        }
+        InputStream is = entity.getContent();
+        try {
+            ByteArrayOutputStream os = new ByteArrayOutputStream();
+            int c;
+            while ((c = is.read()) != -1) {
+                os.write(c);
+            }
+            byte[] bytes = os.toByteArray();
+            String json = new String(bytes);
+            try {
+                return JsonSerialization.readValue(json, AccessTokenResponse.class);
+            } catch (IOException e) {
+                throw new IOException(json, e);
+            }
+        } finally {
+            try {
+                is.close();
+            } catch (IOException ignored) {
+
+            }
+        }
+    }
+
     public static AccessTokenResponse invokeRefresh(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure {
         List<NameValuePair> formparams = new ArrayList<NameValuePair>();
         formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
diff --git a/adapters/oidc/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java b/adapters/oidc/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
index 9e4fa0a..67c9f08 100755
--- a/adapters/oidc/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
+++ b/adapters/oidc/servlet-oauth-client/src/main/java/org/keycloak/servlet/ServletOAuthClient.java
@@ -41,12 +41,60 @@ import java.io.InputStream;
 import java.net.URI;
 import java.util.List;
 
+import org.jboss.logging.Logger;
+import org.keycloak.common.util.Base64Url;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
  */
 public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
 
+	// https://tools.ietf.org/html/rfc7636#section-4
+	private String codeVerifier;
+	private String codeChallenge;
+	private String codeChallengeMethod = OAuth2Constants.PKCE_METHOD_S256;
+	private static Logger logger = Logger.getLogger(ServletOAuthClient.class);
+
+    public static String generateSecret() {
+        return generateSecret(32);
+    }
+
+    public static String generateSecret(int bytes) {
+        byte[] buf = new byte[bytes];
+        new SecureRandom().nextBytes(buf);
+        return Base64Url.encode(buf);
+    }
+
+    private void setCodeVerifier() {
+        codeVerifier = generateSecret();
+        logger.debugf("Generated codeVerifier = %s", codeVerifier);
+        return;
+    }
+
+    private void setCodeChallenge() {
+        try {
+            if (codeChallengeMethod.equals(OAuth2Constants.PKCE_METHOD_S256)) {
+                MessageDigest md = MessageDigest.getInstance("SHA-256");
+                md.update(codeVerifier.getBytes());
+                StringBuilder sb = new StringBuilder();
+                for (byte b : md.digest()) {
+                    String hex = String.format("%02x", b);
+                    sb.append(hex);
+                }
+                codeChallenge = Base64Url.encode(sb.toString().getBytes());
+            } else {
+                codeChallenge = Base64Url.encode(codeVerifier.getBytes());
+            }
+            logger.debugf("Encode codeChallenge = %s, codeChallengeMethod = %s", codeChallenge, codeChallengeMethod);
+        } catch (Exception e) {
+            logger.info("PKCE client side unknown hash algorithm");
+            codeChallenge = Base64Url.encode(codeVerifier.getBytes());
+        }
+    }
+
     /**
      * closes client
      */
@@ -57,7 +105,15 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
     private AccessTokenResponse resolveBearerToken(HttpServletRequest request, String redirectUri, String code) throws IOException, ServerRequest.HttpFailure {
         // Don't send sessionId in oauth clients for now
         KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request);
-        return ServerRequest.invokeAccessCodeToToken(resolvedDeployment, code, redirectUri, null);
+
+        // https://tools.ietf.org/html/rfc7636#section-4
+        if (codeVerifier != null) {
+            logger.debugf("Before sending Token Request, codeVerifier = %s", codeVerifier);
+            return ServerRequest.invokeAccessCodeToToken(resolvedDeployment, code, redirectUri, null, codeVerifier);
+        } else {
+            logger.debug("Before sending Token Request without codeVerifier");
+            return ServerRequest.invokeAccessCodeToToken(resolvedDeployment, code, redirectUri, null);
+        }
     }
 
     /**
@@ -94,6 +150,12 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
         String authUrl = resolvedDeployment.getAuthUrl().clone().build().toString();
         String scopeParam = TokenUtil.attachOIDCScope(scope);
 
+        // https://tools.ietf.org/html/rfc7636#section-4
+        if (resolvedDeployment.isPkce()) {
+            setCodeVerifier();
+            setCodeChallenge();
+        }
+
         KeycloakUriBuilder uriBuilder =  KeycloakUriBuilder.fromUri(authUrl)
                 .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
                 .queryParam(OAuth2Constants.CLIENT_ID, getClientId())
diff --git a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
index 0a107bb..f063962 100755
--- a/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
+++ b/core/src/main/java/org/keycloak/representations/adapters/config/AdapterConfig.java
@@ -78,6 +78,9 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
     protected int publicKeyCacheTtl = 86400; // 1 day
     @JsonProperty("policy-enforcer")
     protected PolicyEnforcerConfig policyEnforcerConfig;
+    // https://tools.ietf.org/html/rfc7636
+    @JsonProperty("enable-pkce")
+    protected boolean pkce = false;
 
     /**
      * The Proxy url to use for requests to the auth-server, configurable via the adapter config property {@code proxy-url}.
@@ -244,4 +247,14 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
     public void setPublicKeyCacheTtl(int publicKeyCacheTtl) {
         this.publicKeyCacheTtl = publicKeyCacheTtl;
     }
+
+    // https://tools.ietf.org/html/rfc7636
+    public boolean isPkce() {
+        return pkce;
+    }
+
+    public void setPkce(boolean pkce) {
+        this.pkce = pkce;
+    }
+
 }
diff --git a/examples/demo-template/third-party/src/main/webapp/WEB-INF/keycloak.json b/examples/demo-template/third-party/src/main/webapp/WEB-INF/keycloak.json
index 559df05..9f07093 100755
--- a/examples/demo-template/third-party/src/main/webapp/WEB-INF/keycloak.json
+++ b/examples/demo-template/third-party/src/main/webapp/WEB-INF/keycloak.json
@@ -5,5 +5,6 @@
   "ssl-required" : "external",
    "credentials" : {
        "secret": "password"
-   }
+   },
+  "enable-pkce" : true
 }
\ No newline at end of file