keycloak-aplcache

Changes

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/OAuth2Constants.java b/core/src/main/java/org/keycloak/OAuth2Constants.java
index cf52423..234b632 100644
--- a/core/src/main/java/org/keycloak/OAuth2Constants.java
+++ b/core/src/main/java/org/keycloak/OAuth2Constants.java
@@ -83,6 +83,14 @@ public interface OAuth2Constants {
 
     String JWT = "JWT";
 
+    // https://tools.ietf.org/html/rfc7636#section-6.1
+    String CODE_VERIFIER = "code_verifier";
+    String CODE_CHALLENGE = "code_challenge";
+    String CODE_CHALLENGE_METHOD = "code_challenge_method";
+
+    // https://tools.ietf.org/html/rfc7636#section-6.2.2
+    String PKCE_METHOD_PLAIN = "plain";
+    String PKCE_METHOD_S256 = "S256";
 
 }
 
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
diff --git a/server-spi-private/src/main/java/org/keycloak/events/Errors.java b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
index e17743e..e82421f 100755
--- a/server-spi-private/src/main/java/org/keycloak/events/Errors.java
+++ b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
@@ -75,7 +75,16 @@ public interface Errors {
     String PASSWORD_CONFIRM_ERROR = "password_confirm_error";
     String PASSWORD_MISSING = "password_missing";
     String PASSWORD_REJECTED = "password_rejected";
+
+    // https://tools.ietf.org/html/rfc7636
+    String CODE_VERIFIER_MISSING = "code_verifier_missing";
+    String INVALID_CODE_VERIFIER = "invalid_code_verifier";
+    String PKCE_VERIFICATION_FAILED = "pkce_verification_failed";
+    String INVALID_CODE_CHALLENGE_METHOD = "invalid_code_challenge_method";
+
+
     String NOT_LOGGED_IN = "not_logged_in";
     String UNKNOWN_IDENTITY_PROVIDER = "unknown_identity_provider";
     String ILLEGAL_ORIGIN = "illegal_origin";
+
 }
diff --git a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_0_0.java b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_0_0.java
index 6d2f651..4daf755 100644
--- a/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_0_0.java
+++ b/server-spi-private/src/main/java/org/keycloak/migration/migrators/MigrateTo3_0_0.java
@@ -20,13 +20,18 @@ package org.keycloak.migration.migrators;
 
 import org.keycloak.migration.ModelVersion;
 import org.keycloak.models.ClientModel;
+import org.keycloak.models.Constants;
 import org.keycloak.models.KeycloakSession;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
+import org.keycloak.representations.oidc.OIDCClientRepresentation;
+
+import java.util.Objects;
 
 import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT;
 import static org.keycloak.models.AccountRoles.MANAGE_ACCOUNT_LINKS;
 import static org.keycloak.models.Constants.ACCOUNT_MANAGEMENT_CLIENT_ID;
+import static org.keycloak.models.Constants.defaultClients;
 
 /**
  * @author <a href="mailto:bburke@redhat.com">Bill Burke</a>
@@ -38,6 +43,12 @@ public class MigrateTo3_0_0 implements Migration {
     @Override
     public void migrate(KeycloakSession session) {
         for (RealmModel realm : session.realms().getRealms()) {
+
+            realm.getClients().stream()
+                    .filter(clientModel -> defaultClients.contains(clientModel.getId()))
+                    .filter(clientModel -> Objects.isNull(clientModel.getProtocol()))
+                    .forEach(clientModel -> clientModel.setProtocol("openid-connect"));
+
             ClientModel client = realm.getClientByClientId(ACCOUNT_MANAGEMENT_CLIENT_ID);
             if (client == null) continue;
             RoleModel linkRole = client.getRole(MANAGE_ACCOUNT_LINKS);
diff --git a/server-spi-private/src/main/java/org/keycloak/models/Constants.java b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
index 68c83b2..260ac1d 100755
--- a/server-spi-private/src/main/java/org/keycloak/models/Constants.java
+++ b/server-spi-private/src/main/java/org/keycloak/models/Constants.java
@@ -19,6 +19,9 @@ package org.keycloak.models;
 
 import org.keycloak.OAuth2Constants;
 
+import java.util.Arrays;
+import java.util.Collection;
+
 /**
  * @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
  * @version $Revision: 1 $
@@ -31,6 +34,8 @@ public interface Constants {
     String BROKER_SERVICE_CLIENT_ID = "broker";
     String REALM_MANAGEMENT_CLIENT_ID = "realm-management";
 
+    Collection<String> defaultClients = Arrays.asList(ACCOUNT_MANAGEMENT_CLIENT_ID, ADMIN_CLI_CLIENT_ID, BROKER_SERVICE_CLIENT_ID, REALM_MANAGEMENT_CLIENT_ID, ADMIN_CONSOLE_CLIENT_ID);
+
     String INSTALLED_APP_URN = "urn:ietf:wg:oauth:2.0:oob";
     String INSTALLED_APP_URL = "http://localhost";
     String READ_TOKEN_ROLE = "read-token";
diff --git a/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java b/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
index ef11479..f98f8fe 100755
--- a/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
+++ b/server-spi-private/src/main/java/org/keycloak/services/managers/ClientSessionCode.java
@@ -27,6 +27,7 @@ import org.keycloak.models.ProtocolMapperModel;
 import org.keycloak.models.RealmModel;
 import org.keycloak.models.RoleModel;
 import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.OAuth2Constants;
 
 import java.security.MessageDigest;
 import java.util.HashSet;
@@ -233,6 +234,19 @@ public class ClientSessionCode {
             sb.append('.');
             sb.append(clientSession.getId());
 
+            // https://tools.ietf.org/html/rfc7636#section-4
+            String codeChallenge = clientSession.getNote(OAuth2Constants.CODE_CHALLENGE);
+            String codeChallengeMethod = clientSession.getNote(OAuth2Constants.CODE_CHALLENGE_METHOD);
+            if (codeChallenge != null) {
+                logger.debugf("PKCE received codeChallenge = %s", codeChallenge);
+                if (codeChallengeMethod == null) {
+                    logger.debug("PKCE not received codeChallengeMethod, treating plain");
+                    codeChallengeMethod = OAuth2Constants.PKCE_METHOD_PLAIN;
+                } else {
+                    logger.debugf("PKCE received codeChallengeMethod = %s", codeChallengeMethod);
+                }
+            }
+
             String code = sb.toString();
 
             clientSession.setNote(ACTIVE_CODE, code);
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
index 467f7d1..1588321 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/AuthorizationEndpoint.java
@@ -50,6 +50,9 @@ import javax.ws.rs.GET;
 import javax.ws.rs.core.MultivaluedMap;
 import javax.ws.rs.core.Response;
 
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
  */
@@ -67,6 +70,9 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
      */
     public static final String CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX = "client_request_param_";
 
+    // https://tools.ietf.org/html/rfc7636#section-4.2
+    private static final Pattern VALID_CODE_CHALLENGE_PATTERN = Pattern.compile("^[0-9a-zA-Z\\-\\.~_]+$");
+
     private enum Action {
         REGISTER, CODE, FORGOT_CREDENTIALS
     }
@@ -113,6 +119,12 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
             return errorResponse;
         }
 
+        // https://tools.ietf.org/html/rfc7636#section-4
+        errorResponse = checkPKCEParams();
+        if (errorResponse != null) {
+            return errorResponse;
+        }
+
         createClientSession();
         // So back button doesn't work
         CacheControlUtil.noBackButtonCacheControlHeader();
@@ -258,6 +270,65 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
         return null;
     }
 
+    // https://tools.ietf.org/html/rfc7636#section-4
+    private Response checkPKCEParams() {
+        String codeChallenge = request.getCodeChallenge();
+        String codeChallengeMethod = request.getCodeChallengeMethod();
+        
+        // PKCE not adopted to OAuth2 Implicit Grant and OIDC Implicit Flow,
+        // adopted to OAuth2 Authorization Code Grant and OIDC Authorization Code Flow, Hybrid Flow
+        // Namely, flows using authorization code.
+        if (parsedResponseType.isImplicitFlow()) return null;
+        
+        if (codeChallenge == null && codeChallengeMethod != null) {
+            logger.info("PKCE supporting Client without code challenge");
+            event.error(Errors.INVALID_REQUEST);
+            return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Missing parameter: code_challenge");
+        }
+        
+        // based on code_challenge value decide whether this client(RP) supports PKCE
+        if (codeChallenge == null) {
+            logger.debug("PKCE non-supporting Client");
+            return null;
+        }
+        
+        if (codeChallengeMethod != null) {
+        	// https://tools.ietf.org/html/rfc7636#section-4.2
+        	// plain or S256
+            if (!codeChallengeMethod.equals(OIDCLoginProtocol.PKCE_METHOD_S256) && !codeChallengeMethod.equals(OIDCLoginProtocol.PKCE_METHOD_PLAIN)) {
+                logger.infof("PKCE supporting Client with invalid code challenge method not specified in PKCE, codeChallengeMethod = %s", codeChallengeMethod);
+                event.error(Errors.INVALID_REQUEST);
+                return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: code_challenge_method");
+            }
+        } else {
+        	// https://tools.ietf.org/html/rfc7636#section-4.3
+        	// default code_challenge_method is plane
+        	codeChallengeMethod = OIDCLoginProtocol.PKCE_METHOD_PLAIN;
+        }
+        
+        if (!isValidPkceCodeChallenge(codeChallenge)) {
+            logger.infof("PKCE supporting Client with invalid code challenge specified in PKCE, codeChallenge = %s", codeChallenge);
+            event.error(Errors.INVALID_REQUEST);
+            return redirectErrorToClient(parsedResponseMode, OAuthErrorException.INVALID_REQUEST, "Invalid parameter: code_challenge");
+        }
+        
+        return null;
+    }
+
+    // https://tools.ietf.org/html/rfc7636#section-4
+    private boolean isValidPkceCodeChallenge(String codeChallenge) {
+        if (codeChallenge.length() < OIDCLoginProtocol.PKCE_CODE_CHALLENGE_MIN_LENGTH) {
+           logger.debugf("PKCE codeChallenge length under lower limit , codeChallenge = %s", codeChallenge);
+           return false;
+       }
+       if (codeChallenge.length() > OIDCLoginProtocol.PKCE_CODE_CHALLENGE_MAX_LENGTH) {
+           logger.debugf("PKCE codeChallenge length over upper limit , codeChallenge = %s", codeChallenge);
+           return false;
+       }
+       Matcher m = VALID_CODE_CHALLENGE_PATTERN.matcher(codeChallenge);
+       return m.matches() ? true : false;
+    }
+
     private Response redirectErrorToClient(OIDCResponseMode responseMode, String error, String errorDescription) {
         OIDCRedirectUriBuilder errorResponseBuilder = OIDCRedirectUriBuilder.fromUri(redirectUri, responseMode)
                 .addParam(OAuth2Constants.ERROR, error);
@@ -303,6 +374,14 @@ public class AuthorizationEndpoint extends AuthorizationEndpointBase {
         if (request.getIdpHint() != null) clientSession.setNote(AdapterConstants.KC_IDP_HINT, request.getIdpHint());
         if (request.getResponseMode() != null) clientSession.setNote(OIDCLoginProtocol.RESPONSE_MODE_PARAM, request.getResponseMode());
 
+        // https://tools.ietf.org/html/rfc7636#section-4
+        if (request.getCodeChallenge() != null) clientSession.setNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM, request.getCodeChallenge());
+        if (request.getCodeChallengeMethod() != null) {
+        	clientSession.setNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, request.getCodeChallengeMethod());
+        } else {
+        	clientSession.setNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM, OIDCLoginProtocol.PKCE_METHOD_PLAIN);
+        }
+
         if (request.getAdditionalReqParams() != null) {
             for (String paramName : request.getAdditionalReqParams().keySet()) {
                 clientSession.setNote(CLIENT_SESSION_NOTE_ADDITIONAL_REQ_PARAMS_PREFIX + paramName, request.getAdditionalReqParams().get(paramName));
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
index 998a58c..a0f874b 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java
@@ -38,6 +38,10 @@ public class AuthorizationEndpointRequest {
     String idpHint;
     Map<String, String> additionalReqParams = new HashMap<>();
 
+    // https://tools.ietf.org/html/rfc7636#section-6.1
+    String codeChallenge;
+    String codeChallengeMethod;
+
     public String getClientId() {
         return clientId;
     }
@@ -85,4 +89,15 @@ public class AuthorizationEndpointRequest {
     public Map<String, String> getAdditionalReqParams() {
         return additionalReqParams;
     }
+
+    // https://tools.ietf.org/html/rfc7636#section-6.1
+    public String getCodeChallenge() {
+        return codeChallenge;
+    }
+
+    // https://tools.ietf.org/html/rfc7636#section-6.1
+    public String getCodeChallengeMethod() {
+        return codeChallengeMethod;
+    }
+
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
index ea1c35e..346b1a6 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthzEndpointRequestParser.java
@@ -61,6 +61,11 @@ abstract class AuthzEndpointRequestParser {
         KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.UI_LOCALES_PARAM);
         KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_PARAM);
         KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.REQUEST_URI_PARAM);
+
+        // https://tools.ietf.org/html/rfc7636#section-6.1
+        KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CODE_CHALLENGE_PARAM);
+        KNOWN_REQ_PARAMS.add(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM);
+
     }
 
 
@@ -83,6 +88,10 @@ abstract class AuthzEndpointRequestParser {
         request.nonce = replaceIfNotNull(request.nonce, getParameter(OIDCLoginProtocol.NONCE_PARAM));
         request.maxAge = replaceIfNotNull(request.maxAge, getIntParameter(OIDCLoginProtocol.MAX_AGE_PARAM));
 
+        // https://tools.ietf.org/html/rfc7636#section-6.1
+        request.codeChallenge = replaceIfNotNull(request.codeChallenge, getParameter(OIDCLoginProtocol.CODE_CHALLENGE_PARAM));
+        request.codeChallengeMethod = replaceIfNotNull(request.codeChallengeMethod, getParameter(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM));
+
         extractAdditionalReqParams(request.additionalReqParams);
     }
 
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
index 03be086..8fa4341 100644
--- a/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/endpoints/TokenEndpoint.java
@@ -25,6 +25,7 @@ import org.keycloak.OAuthErrorException;
 import org.keycloak.authentication.AuthenticationProcessor;
 import org.keycloak.common.ClientConnection;
 import org.keycloak.common.constants.ServiceAccountConstants;
+import org.keycloak.common.util.Base64Url;
 import org.keycloak.constants.AdapterConstants;
 import org.keycloak.events.Details;
 import org.keycloak.events.Errors;
@@ -63,6 +64,9 @@ import javax.ws.rs.core.Response;
 import javax.ws.rs.core.UriInfo;
 import java.util.List;
 import java.util.Map;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.security.MessageDigest;
 
 /**
  * @author <a href="mailto:sthorger@redhat.com">Stian Thorgersen</a>
@@ -78,6 +82,9 @@ public class TokenEndpoint {
         AUTHORIZATION_CODE, REFRESH_TOKEN, PASSWORD, CLIENT_CREDENTIALS
     }
 
+    // https://tools.ietf.org/html/rfc7636#section-4.2
+    private static final Pattern VALID_CODE_VERIFIER_PATTERN = Pattern.compile("^[0-9a-zA-Z\\-\\.~_]+$");
+
     @Context
     private KeycloakSession session;
 
@@ -266,6 +273,60 @@ public class TokenEndpoint {
             throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "Session not active", Response.Status.BAD_REQUEST);
         }
 
+        // https://tools.ietf.org/html/rfc7636#section-4.6
+        String codeVerifier = formParams.getFirst(OAuth2Constants.CODE_VERIFIER);
+        String codeChallenge = clientSession.getNote(OIDCLoginProtocol.CODE_CHALLENGE_PARAM);
+        String codeChallengeMethod = clientSession.getNote(OIDCLoginProtocol.CODE_CHALLENGE_METHOD_PARAM);
+        String authUserId = user.getId();
+        String authUsername = user.getUsername();
+        if (authUserId == null) {
+            authUserId = "unknown";
+        }
+        if (authUsername == null) {
+            authUsername = "unknown";
+        }
+        if (codeChallenge != null && codeVerifier == null) {
+            logger.warnf("PKCE code verifier not specified, authUserId = %s, authUsername = %s", authUserId, authUsername);
+            event.error(Errors.CODE_VERIFIER_MISSING);
+            throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "PKCE code verifier not specified", Response.Status.BAD_REQUEST);
+        }
+
+        if (codeChallenge != null) {
+            // based on whether code_challenge has been stored at corresponding authorization code request previously
+        	// decide whether this client(RP) supports PKCE
+            if (!isValidPkceCodeVerifier(codeVerifier)) {
+                logger.infof("PKCE invalid code verifier");
+                event.error(Errors.INVALID_CODE_VERIFIER);
+                throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "PKCE invalid code verifier", Response.Status.BAD_REQUEST);
+            }
+            
+            logger.debugf("PKCE supporting Client, codeVerifier = %s", codeVerifier);
+            String codeVerifierEncoded = codeVerifier;
+            try {
+            	// https://tools.ietf.org/html/rfc7636#section-4.2
+            	// plain or S256
+                if (codeChallengeMethod != null && codeChallengeMethod.equals(OAuth2Constants.PKCE_METHOD_S256)) {
+                    logger.debugf("PKCE codeChallengeMethod = %s", codeChallengeMethod);                    
+                    codeVerifierEncoded = generateS256CodeChallenge(codeVerifier);
+                } else {
+                    logger.debug("PKCE codeChallengeMethod is plain");
+                    codeVerifierEncoded = codeVerifier;
+                }
+            } catch (Exception nae) {
+                logger.infof("PKCE code verification failed, not supported algorithm specified");
+                event.error(Errors.PKCE_VERIFICATION_FAILED);
+                throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "PKCE code verification failed, not supported algorithm specified", Response.Status.BAD_REQUEST);
+            }
+            if (!codeChallenge.equals(codeVerifierEncoded)) {
+                logger.warnf("PKCE verification failed. authUserId = %s, authUsername = %s", authUserId, authUsername);
+                event.error(Errors.PKCE_VERIFICATION_FAILED);
+                throw new ErrorResponseException(OAuthErrorException.INVALID_GRANT, "PKCE verification failed", Response.Status.BAD_REQUEST);
+            } else {
+                logger.debugf("PKCE verification success. codeVerifierEncoded = %s, codeChallenge = %s", codeVerifierEncoded, codeChallenge);
+            }
+        }
+
+
         updateClientSession(clientSession);
         updateUserSessionFromClientAuth(userSession);
 
@@ -474,4 +535,31 @@ public class TokenEndpoint {
         return Cors.add(request, Response.ok(res, MediaType.APPLICATION_JSON_TYPE)).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
     }
 
+    // https://tools.ietf.org/html/rfc7636#section-4.1
+    private boolean isValidPkceCodeVerifier(String codeVerifier) {
+        if (codeVerifier.length() < OIDCLoginProtocol.PKCE_CODE_VERIFIER_MIN_LENGTH) {
+            logger.infof(" Error: PKCE codeVerifier length under lower limit , codeVerifier = %s", codeVerifier);
+            return false;
+        }
+        if (codeVerifier.length() > OIDCLoginProtocol.PKCE_CODE_VERIFIER_MAX_LENGTH) {
+            logger.infof(" Error: PKCE codeVerifier length over upper limit , codeVerifier = %s", codeVerifier);
+            return false;
+        }
+        Matcher m = VALID_CODE_VERIFIER_PATTERN.matcher(codeVerifier);
+        return m.matches() ? true : false;
+    }
+
+    // https://tools.ietf.org/html/rfc7636#section-4.6
+    private String generateS256CodeChallenge(String codeVerifier) throws Exception {
+        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);
+        }
+        String codeVerifierEncoded = Base64Url.encode(sb.toString().getBytes());
+        return codeVerifierEncoded;
+    }
+ 
 }
diff --git a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
index 7e41411..4c0691a 100755
--- a/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
+++ b/services/src/main/java/org/keycloak/protocol/oidc/OIDCLoginProtocol.java
@@ -85,6 +85,22 @@ public class OIDCLoginProtocol implements LoginProtocol {
     public static final String CLIENT_SECRET_JWT = "client_secret_jwt";
     public static final String PRIVATE_KEY_JWT = "private_key_jwt";
 
+    // https://tools.ietf.org/html/rfc7636#section-4.3
+    public static final String CODE_CHALLENGE_PARAM = "code_challenge";
+    public static final String CODE_CHALLENGE_METHOD_PARAM = "code_challenge_method";
+
+    // https://tools.ietf.org/html/rfc7636#section-4.2
+    public static final int PKCE_CODE_CHALLENGE_MIN_LENGTH = 43;
+    public static final int PKCE_CODE_CHALLENGE_MAX_LENGTH = 128;
+
+    // https://tools.ietf.org/html/rfc7636#section-4.1
+    public static final int PKCE_CODE_VERIFIER_MIN_LENGTH = 43;
+    public static final int PKCE_CODE_VERIFIER_MAX_LENGTH = 128;    
+    
+    // https://tools.ietf.org/html/rfc7636#section-6.2.2
+    public static final String PKCE_METHOD_PLAIN = "plain";
+    public static final String PKCE_METHOD_S256 = "S256";
+
     private static final Logger logger = Logger.getLogger(OIDCLoginProtocol.class);
 
     protected KeycloakSession session;
diff --git a/services/src/main/java/org/keycloak/services/managers/RealmManager.java b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
index 0921c60..b28cf2f 100755
--- a/services/src/main/java/org/keycloak/services/managers/RealmManager.java
+++ b/services/src/main/java/org/keycloak/services/managers/RealmManager.java
@@ -148,6 +148,7 @@ public class RealmManager {
         adminConsole.setPublicClient(true);
         adminConsole.addRedirectUri(baseUrl + "/*");
         adminConsole.setFullScopeAllowed(false);
+        adminConsole.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
 
         RoleModel adminRole;
         if (realm.getName().equals(Config.getAdminRealm())) {
@@ -182,6 +183,7 @@ public class RealmManager {
             adminCli.setFullScopeAllowed(false);
             adminCli.setStandardFlowEnabled(false);
             adminCli.setDirectAccessGrantsEnabled(true);
+            adminCli.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
 
             RoleModel adminRole;
             if (realm.getName().equals(Config.getAdminRealm())) {
@@ -348,6 +350,7 @@ public class RealmManager {
         adminRole.setScopeParamRequired(false);
         realmAdminClient.setBearerOnly(true);
         realmAdminClient.setFullScopeAllowed(false);
+        realmAdminClient.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
 
         for (String r : AdminRoles.ALL_REALM_ROLES) {
             addAndSetAdminRole(r, realmAdminClient, adminRole);
@@ -389,6 +392,7 @@ public class RealmManager {
             String redirectUri = base + "/*";
             client.addRedirectUri(redirectUri);
             client.setBaseUrl(base);
+            client.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
 
             for (String role : AccountRoles.ALL) {
                 client.addDefaultRole(role);
@@ -415,6 +419,7 @@ public class RealmManager {
             client.setEnabled(true);
             client.setName("${client_" + Constants.BROKER_SERVICE_CLIENT_ID + "}");
             client.setFullScopeAllowed(false);
+            client.setProtocol(OIDCLoginProtocol.LOGIN_PROTOCOL);
 
             for (String role : Constants.BROKER_SERVICE_ROLES) {
                 RoleModel roleModel = client.addRole(role);
diff --git a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java
index 8bc4bd5..bc83af1 100644
--- a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java
+++ b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProvider.java
@@ -16,7 +16,7 @@ import java.util.Optional;
 /**
  * Identity provider for Openshift V3. Check <a href="https://docs.openshift.com/enterprise/3.0/architecture/additional_concepts/authentication.html">official documentation</a> for more details.
  */
-public class OpenshiftV3IdentityProvider extends AbstractOAuth2IdentityProvider<OpenshifV3IdentityProviderConfig> implements SocialIdentityProvider<OpenshifV3IdentityProviderConfig> {
+public class OpenshiftV3IdentityProvider extends AbstractOAuth2IdentityProvider<OpenshiftV3IdentityProviderConfig> implements SocialIdentityProvider<OpenshiftV3IdentityProviderConfig> {
 
     public static final String BASE_URL = "https://api.preview.openshift.com";
     private static final String AUTH_RESOURCE = "/oauth/authorize";
@@ -24,7 +24,7 @@ public class OpenshiftV3IdentityProvider extends AbstractOAuth2IdentityProvider<
     private static final String PROFILE_RESOURCE = "/oapi/v1/users/~";
     private static final String DEFAULT_SCOPE = "user:info";
 
-    public OpenshiftV3IdentityProvider(KeycloakSession session, OpenshifV3IdentityProviderConfig config) {
+    public OpenshiftV3IdentityProvider(KeycloakSession session, OpenshiftV3IdentityProviderConfig config) {
         super(session, config);
         final String baseUrl = Optional.ofNullable(config.getBaseUrl()).orElse(BASE_URL);
         config.setAuthorizationUrl(baseUrl + AUTH_RESOURCE);
diff --git a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderFactory.java b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderFactory.java
index b370530..d9708a1 100644
--- a/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderFactory.java
+++ b/services/src/main/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderFactory.java
@@ -16,7 +16,7 @@ public class OpenshiftV3IdentityProviderFactory extends AbstractIdentityProvider
 
     @Override
     public OpenshiftV3IdentityProvider create(KeycloakSession keycloakSession, IdentityProviderModel identityProviderModel) {
-        return new OpenshiftV3IdentityProvider(keycloakSession, new OpenshifV3IdentityProviderConfig(identityProviderModel));
+        return new OpenshiftV3IdentityProvider(keycloakSession, new OpenshiftV3IdentityProviderConfig(identityProviderModel));
     }
 
     @Override
diff --git a/services/src/test/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderTest.java b/services/src/test/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderTest.java
index e39f157..8a6ac67 100644
--- a/services/src/test/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderTest.java
+++ b/services/src/test/java/org/keycloak/social/openshift/OpenshiftV3IdentityProviderTest.java
@@ -8,7 +8,7 @@ public class OpenshiftV3IdentityProviderTest {
 
     @Test
     public void shouldConstructProviderUrls() throws Exception {
-        final OpenshifV3IdentityProviderConfig config = new OpenshifV3IdentityProviderConfig(new IdentityProviderModel());
+        final OpenshiftV3IdentityProviderConfig config = new OpenshiftV3IdentityProviderConfig(new IdentityProviderModel());
         config.setBaseUrl("http://openshift.io:8443");
         final OpenshiftV3IdentityProvider openshiftV3IdentityProvider = new OpenshiftV3IdentityProvider(null, config);
 
@@ -17,7 +17,7 @@ public class OpenshiftV3IdentityProviderTest {
 
     @Test
     public void shouldConstructProviderUrlsForBaseUrlWithTrailingSlash() throws Exception {
-        final OpenshifV3IdentityProviderConfig config = new OpenshifV3IdentityProviderConfig(new IdentityProviderModel());
+        final OpenshiftV3IdentityProviderConfig config = new OpenshiftV3IdentityProviderConfig(new IdentityProviderModel());
         config.setBaseUrl("http://openshift.io:8443/");
         final OpenshiftV3IdentityProvider openshiftV3IdentityProvider = new OpenshiftV3IdentityProvider(null, config);
 
diff --git a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
index 1fb0a63..8d70f07 100755
--- a/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
+++ b/testsuite/integration/src/test/java/org/keycloak/testsuite/broker/ImportIdentityProviderTest.java
@@ -37,7 +37,7 @@ import org.keycloak.social.google.GoogleIdentityProvider;
 import org.keycloak.social.google.GoogleIdentityProviderFactory;
 import org.keycloak.social.linkedin.LinkedInIdentityProvider;
 import org.keycloak.social.linkedin.LinkedInIdentityProviderFactory;
-import org.keycloak.social.openshift.OpenshifV3IdentityProviderConfig;
+import org.keycloak.social.openshift.OpenshiftV3IdentityProviderConfig;
 import org.keycloak.social.openshift.OpenshiftV3IdentityProvider;
 import org.keycloak.social.openshift.OpenshiftV3IdentityProviderFactory;
 import org.keycloak.social.stackoverflow.StackOverflowIdentityProviderConfig;
@@ -290,7 +290,7 @@ public class ImportIdentityProviderTest extends AbstractIdentityProviderModelTes
 
     private void assertOpenshiftIdentityProviderConfig(IdentityProviderModel identityProvider) {
         OpenshiftV3IdentityProvider osoIdentityProvider = new OpenshiftV3IdentityProviderFactory().create(session, identityProvider);
-        OpenshifV3IdentityProviderConfig config = osoIdentityProvider.getConfig();
+        OpenshiftV3IdentityProviderConfig config = osoIdentityProvider.getConfig();
 
         assertEquals("model-openshift-v3", config.getAlias());
         assertEquals(OpenshiftV3IdentityProviderFactory.PROVIDER_ID, config.getProviderId());
diff --git a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
index e47deac..682e745 100644
--- a/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
+++ b/testsuite/integration-arquillian/tests/base/src/main/java/org/keycloak/testsuite/util/OAuthClient.java
@@ -113,6 +113,11 @@ public class OAuthClient {
 
     private Map<String, PublicKey> publicKeys = new HashMap<>();
 
+    // https://tools.ietf.org/html/rfc7636#section-4
+    private String codeVerifier;
+    private String codeChallenge;
+    private String codeChallengeMethod;
+
     public class LogoutUrlBuilder {
         private final UriBuilder b = OIDCLoginProtocolService.logoutUrl(UriBuilder.fromUri(baseUrl));
 
@@ -166,6 +171,10 @@ public class OAuthClient {
         nonce = null;
         request = null;
         requestUri = null;
+        // https://tools.ietf.org/html/rfc7636#section-4
+        codeVerifier = null;
+        codeChallenge = null;
+        codeChallengeMethod = null;
     }
 
     public AuthorizationEndpointResponse doLogin(String username, String password) {
@@ -251,6 +260,11 @@ public class OAuthClient {
                 parameters.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, clientSessionHost));
             }
 
+            // https://tools.ietf.org/html/rfc7636#section-4.5
+            if (codeVerifier != null) {
+                parameters.add(new BasicNameValuePair(OAuth2Constants.CODE_VERIFIER, codeVerifier));
+            }
+
             UrlEncodedFormEntity formEntity = null;
             try {
                 formEntity = new UrlEncodedFormEntity(parameters, "UTF-8");
@@ -615,6 +629,13 @@ public class OAuthClient {
         if (requestUri != null) {
             b.queryParam(OIDCLoginProtocol.REQUEST_URI_PARAM, requestUri);
         }
+        // https://tools.ietf.org/html/rfc7636#section-4.3
+        if (codeChallenge != null) {
+            b.queryParam(OAuth2Constants.CODE_CHALLENGE, codeChallenge);
+        }
+        if (codeChallengeMethod != null) {
+            b.queryParam(OAuth2Constants.CODE_CHALLENGE_METHOD, codeChallengeMethod);
+        }  
         return b.build(realm).toString();
     }
 
@@ -730,6 +751,20 @@ public class OAuthClient {
         return realm;
     }
 
+    // https://tools.ietf.org/html/rfc7636#section-4
+    public OAuthClient codeVerifier(String codeVerifier) {
+    	this.codeVerifier = codeVerifier;
+    	return this;
+    }
+    public OAuthClient codeChallenge(String codeChallenge) {
+    	this.codeChallenge = codeChallenge;
+    	return this;
+    }
+    public OAuthClient codeChallengeMethod(String codeChallengeMethod) {
+    	this.codeChallengeMethod = codeChallengeMethod;
+    	return this;
+    }
+
     public static class AuthorizationEndpointResponse {
 
         private boolean isRedirected;
diff --git a/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java
new file mode 100644
index 0000000..a72aa3a
--- /dev/null
+++ b/testsuite/integration-arquillian/tests/base/src/test/java/org/keycloak/testsuite/oauth/OAuthProofKeyForCodeExchangeTest.java
@@ -0,0 +1,549 @@
+package org.keycloak.testsuite.oauth;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.entity.UrlEncodedFormEntity;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicNameValuePair;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.keycloak.OAuth2Constants;
+import org.keycloak.OAuthErrorException;
+import org.keycloak.admin.client.resource.ClientResource;
+import org.keycloak.admin.client.resource.ClientTemplateResource;
+import org.keycloak.admin.client.resource.RealmResource;
+import org.keycloak.admin.client.resource.UserResource;
+import org.keycloak.common.enums.SslRequired;
+import org.keycloak.common.util.Base64Url;
+import org.keycloak.events.Details;
+import org.keycloak.events.Errors;
+import org.keycloak.jose.jws.JWSHeader;
+import org.keycloak.jose.jws.JWSInput;
+import org.keycloak.jose.jws.JWSInputException;
+import org.keycloak.models.Constants;
+import org.keycloak.models.ProtocolMapperModel;
+import org.keycloak.models.UserModel;
+import org.keycloak.models.utils.KeycloakModelUtils;
+import org.keycloak.models.utils.ModelToRepresentation;
+import org.keycloak.protocol.oidc.OIDCLoginProtocolService;
+import org.keycloak.protocol.oidc.mappers.HardcodedClaim;
+import org.keycloak.representations.AccessToken;
+import org.keycloak.representations.IDToken;
+import org.keycloak.representations.RefreshToken;
+import org.keycloak.representations.idm.ClientRepresentation;
+import org.keycloak.representations.idm.ClientTemplateRepresentation;
+import org.keycloak.representations.idm.EventRepresentation;
+import org.keycloak.representations.idm.ProtocolMapperRepresentation;
+import org.keycloak.representations.idm.RealmRepresentation;
+import org.keycloak.representations.idm.RoleRepresentation;
+import org.keycloak.representations.idm.UserRepresentation;
+import org.keycloak.testsuite.AbstractKeycloakTest;
+import org.keycloak.testsuite.AssertEvents;
+import org.keycloak.testsuite.arquillian.AuthServerTestEnricher;
+import org.keycloak.testsuite.util.ClientBuilder;
+import org.keycloak.testsuite.util.ClientManager;
+import org.keycloak.testsuite.util.OAuthClient;
+import org.keycloak.testsuite.util.RealmManager;
+import org.keycloak.testsuite.util.RoleBuilder;
+import org.keycloak.testsuite.util.UserBuilder;
+import org.keycloak.testsuite.util.UserInfoClientUtil;
+import org.keycloak.testsuite.util.UserManager;
+import org.keycloak.util.BasicAuthHelper;
+
+import javax.ws.rs.client.Client;
+import javax.ws.rs.client.Entity;
+import javax.ws.rs.client.WebTarget;
+import javax.ws.rs.core.Form;
+import javax.ws.rs.core.HttpHeaders;
+import javax.ws.rs.core.Response;
+import javax.ws.rs.core.UriBuilder;
+import java.io.IOException;
+import java.net.URI;
+import java.security.MessageDigest;
+import java.util.LinkedList;
+import java.util.List;
+
+import static org.hamcrest.Matchers.allOf;
+import static org.hamcrest.Matchers.greaterThanOrEqualTo;
+import static org.hamcrest.Matchers.lessThanOrEqualTo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.keycloak.testsuite.admin.AbstractAdminTest.loadJson;
+import static org.keycloak.testsuite.admin.ApiUtil.findClientByClientId;
+import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
+import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsernameId;
+import static org.keycloak.testsuite.util.OAuthClient.AUTH_SERVER_ROOT;
+import static org.keycloak.testsuite.util.ProtocolMapperUtil.createRoleNameMapper;
+
+//https://tools.ietf.org/html/rfc7636
+
+/**
+ * @author <a href="takashi.norimatsu.ws@hitachi.com">Takashi Norimatsu</a>
+ */
+public class OAuthProofKeyForCodeExchangeTest extends AbstractKeycloakTest {
+
+    @Rule
+    public AssertEvents events = new AssertEvents(this);
+
+
+    @Override
+    public void beforeAbstractKeycloakTest() throws Exception {
+        super.beforeAbstractKeycloakTest();
+    }
+
+    @Before
+    public void clientConfiguration() {
+        ClientManager.realm(adminClient.realm("test")).clientId("test-app").directAccessGrant(true);
+        /*
+         * Configure the default client ID. Seems like OAuthClient is keeping the state of clientID
+         * For example: If some test case configure oauth.clientId("sample-public-client"), other tests
+         * will faile and the clientID will always be "sample-public-client
+         * @see AccessTokenTest#testAuthorizationNegotiateHeaderIgnored()
+         */
+        oauth.clientId("test-app");
+    }
+
+    @Override
+    public void addTestRealms(List<RealmRepresentation> testRealms) {
+
+        RealmRepresentation realm = loadJson(getClass().getResourceAsStream("/testrealm.json"), RealmRepresentation.class);
+
+        UserBuilder user = UserBuilder.create()
+                .id(KeycloakModelUtils.generateId())
+                .username("no-permissions")
+                .addRoles("user")
+                .password("password");
+        realm.getUsers().add(user.build());
+
+        testRealms.add(realm);
+
+    }
+
+    @Test
+    public void accessTokenRequestWithoutPKCE() throws Exception {
+    	// test case : success : A-1-1
+        oauth.doLogin("test-user@localhost", "password");
+
+        EventRepresentation loginEvent = events.expectLogin().assertEvent();
+        
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        
+        expectSuccessfulResponseFromTokenEndpoint(codeId, sessionId, code);
+    }
+
+    @Test
+    public void accessTokenRequestInPKCEValidS256CodeChallengeMethod() throws Exception {
+    	// test case : success : A-1-2
+    	String codeVerifier = "1234567890123456789012345678901234567890123"; // 43
+    	String codeChallenge = generateS256CodeChallenge(codeVerifier);
+    	oauth.codeChallenge(codeChallenge);
+    	oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
+    	
+        oauth.doLogin("test-user@localhost", "password");
+
+        EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+        oauth.codeVerifier(codeVerifier);
+        
+        expectSuccessfulResponseFromTokenEndpoint(codeId, sessionId, code);
+    }
+
+    @Test
+    public void accessTokenRequestInPKCEUnmatchedCodeVerifierWithS256CodeChallengeMethod() throws Exception {
+    	// test case : failure : A-1-5
+    	String codeVerifier = "1234567890123456789012345678901234567890123";
+    	String codeChallenge = codeVerifier;
+    	oauth.codeChallenge(codeChallenge);
+    	oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
+    	
+        oauth.doLogin("test-user@localhost", "password");
+
+        EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+        oauth.codeVerifier(codeVerifier);
+        
+        OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+        
+        assertEquals(400, response.getStatusCode());
+        assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
+        assertEquals("PKCE verification failed", response.getErrorDescription());
+        
+        events.expectCodeToToken(codeId, sessionId).error(Errors.PKCE_VERIFICATION_FAILED).clearDetails().assertEvent();
+    }
+    
+    @Test
+    public void accessTokenRequestInPKCEValidPlainCodeChallengeMethod() throws Exception {
+    	// test case : success : A-1-3
+    	oauth.codeChallenge(".234567890-234567890~234567890_234567890123");
+    	oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_PLAIN);
+    	
+        oauth.doLogin("test-user@localhost", "password");
+
+        EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        
+        oauth.codeVerifier(".234567890-234567890~234567890_234567890123");
+        
+        expectSuccessfulResponseFromTokenEndpoint(codeId, sessionId, code);
+    }
+
+    @Test
+    public void accessTokenRequestInPKCEUnmachedCodeVerifierWithPlainCodeChallengeMethod() throws Exception {
+    	// test case : failure : A-1-6
+    	oauth.codeChallenge("1234567890123456789012345678901234567890123");
+    	oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_PLAIN);
+    	
+        oauth.doLogin("test-user@localhost", "password");
+
+        EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+        
+        oauth.codeVerifier("aZ_-.~1234567890123456789012345678901234567890123Za");
+        
+        OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+        
+        assertEquals(400, response.getStatusCode());
+        assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
+        assertEquals("PKCE verification failed", response.getErrorDescription());
+        
+        events.expectCodeToToken(codeId, sessionId).error(Errors.PKCE_VERIFICATION_FAILED).clearDetails().assertEvent();
+    }
+    
+    @Test
+    public void accessTokenRequestInPKCEValidDefaultCodeChallengeMethod() throws Exception {
+    	// test case : success : A-1-4
+    	oauth.codeChallenge("1234567890123456789012345678901234567890123");
+    	
+        oauth.doLogin("test-user@localhost", "password");
+
+        EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+        oauth.codeVerifier("1234567890123456789012345678901234567890123");
+        
+        expectSuccessfulResponseFromTokenEndpoint(codeId, sessionId, code);
+    }
+    
+    @Test
+    public void accessTokenRequestInPKCEWithoutCodeChallengeWithValidCodeChallengeMethod() throws Exception {
+    	// test case : failure : A-1-7
+    	oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_PLAIN);
+        UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
+        
+        driver.navigate().to(b.build().toURL());
+    	
+        OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth);
+
+        Assert.assertTrue(errorResponse.isRedirected());
+        Assert.assertEquals(errorResponse.getError(), OAuthErrorException.INVALID_REQUEST);
+        Assert.assertEquals(errorResponse.getErrorDescription(), "Missing parameter: code_challenge");
+        
+        events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().assertEvent();
+    }
+    
+    @Test
+    public void accessTokenRequestInPKCEInvalidUnderCodeChallengeWithS256CodeChallengeMethod() throws Exception {
+    	// test case : failure : A-1-8
+    	oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
+    	oauth.codeChallenge("ABCDEFGabcdefg1234567ABCDEFGabcdefg1234567"); // 42
+        UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
+        
+        driver.navigate().to(b.build().toURL());
+    	
+        OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth);
+
+        Assert.assertTrue(errorResponse.isRedirected());
+        Assert.assertEquals(errorResponse.getError(), OAuthErrorException.INVALID_REQUEST);
+        Assert.assertEquals(errorResponse.getErrorDescription(), "Invalid parameter: code_challenge");
+        
+        events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().assertEvent();
+    }
+    
+    @Test
+    public void accessTokenRequestInPKCEInvalidOverCodeChallengeWithPlainCodeChallengeMethod() throws Exception {
+    	// test case : failure : A-1-9
+    	oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_PLAIN);
+    	oauth.codeChallenge("3fRc92kac_keic8c7al-3ncbdoaie.DDeizlck3~3fRc92kac_keic8c7al-3ncbdoaie.DDeizlck3~3fRc92kac_keic8c7al-3ncbdoaie.DDeizlck3~123456789"); // 129
+
+    	UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
+        
+        driver.navigate().to(b.build().toURL());
+    	
+        OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth);
+
+        Assert.assertTrue(errorResponse.isRedirected());
+        Assert.assertEquals(errorResponse.getError(), OAuthErrorException.INVALID_REQUEST);
+        Assert.assertEquals(errorResponse.getErrorDescription(), "Invalid parameter: code_challenge");
+        
+        events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().assertEvent();
+    }
+    
+    @Test
+    public void accessTokenRequestInPKCEInvalidUnderCodeVerifierWithS256CodeChallengeMethod() throws Exception {
+    	// test case : success : A-1-10
+    	String codeVerifier = "ABCDEFGabcdefg1234567ABCDEFGabcdefg1234567"; // 42
+    	String codeChallenge = generateS256CodeChallenge(codeVerifier);
+
+    	oauth.codeChallenge(codeChallenge);
+    	oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
+    	
+        oauth.doLogin("test-user@localhost", "password");
+
+        EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+        oauth.codeVerifier(codeVerifier);
+        
+        OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+        
+        assertEquals(400, response.getStatusCode());
+        assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
+        assertEquals("PKCE invalid code verifier", response.getErrorDescription());
+        
+        events.expectCodeToToken(codeId, sessionId).error(Errors.INVALID_CODE_VERIFIER).clearDetails().assertEvent();
+    }
+    
+    @Test
+    public void accessTokenRequestInPKCEInvalidOverCodeVerifierWithS256CodeChallengeMethod() throws Exception {
+    	// test case : success : A-1-11
+    	String codeVerifier = "3fRc92kac_keic8c7al-3ncbdoaie.DDeizlck3~3fRc92kac_keic8c7al-3ncbdoaie.DDeizlck3~3fRc92kac_keic8c7al-3ncbdoaie.DDeizlck3~123456789"; // 129
+    	String codeChallenge = generateS256CodeChallenge(codeVerifier);
+    	oauth.codeChallenge(codeChallenge);
+    	oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
+    	
+        oauth.doLogin("test-user@localhost", "password");
+
+        EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+        oauth.codeVerifier(codeVerifier);
+        
+        OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+        
+        assertEquals(400, response.getStatusCode());
+        assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
+        assertEquals("PKCE invalid code verifier", response.getErrorDescription());
+        
+        events.expectCodeToToken(codeId, sessionId).error(Errors.INVALID_CODE_VERIFIER).clearDetails().assertEvent();
+    }
+
+    @Test
+    public void accessTokenRequestInPKCEWIthoutCodeVerifierWithS256CodeChallengeMethod() throws Exception {
+    	// test case : failure : A-1-12
+    	String codeVerifier = "1234567890123456789012345678901234567890123";
+    	String codeChallenge = codeVerifier;
+    	oauth.codeChallenge(codeChallenge);
+    	oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
+    	
+        oauth.doLogin("test-user@localhost", "password");
+
+        EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+       
+        OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+        
+        assertEquals(400, response.getStatusCode());
+        assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
+        assertEquals("PKCE code verifier not specified", response.getErrorDescription());
+        
+        events.expectCodeToToken(codeId, sessionId).error(Errors.CODE_VERIFIER_MISSING).clearDetails().assertEvent();
+    }
+
+    @Test
+    public void accessTokenRequestInPKCEInvalidCodeChallengeWithS256CodeChallengeMethod() throws Exception {
+    	// test case : failure : A-1-13
+    	String codeVerifier = "1234567890123456789=12345678901234567890123";
+    	String codeChallenge = codeVerifier;
+    	oauth.codeChallenge(codeChallenge);
+    	oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
+    	
+    	UriBuilder b = UriBuilder.fromUri(oauth.getLoginFormUrl());
+        
+        driver.navigate().to(b.build().toURL());
+    	
+        OAuthClient.AuthorizationEndpointResponse errorResponse = new OAuthClient.AuthorizationEndpointResponse(oauth);
+
+        Assert.assertTrue(errorResponse.isRedirected());
+        Assert.assertEquals(errorResponse.getError(), OAuthErrorException.INVALID_REQUEST);
+        Assert.assertEquals(errorResponse.getErrorDescription(), "Invalid parameter: code_challenge");
+        
+        events.expectLogin().error(Errors.INVALID_REQUEST).user((String) null).session((String) null).clearDetails().assertEvent();
+    }
+
+    @Test
+    public void accessTokenRequestInPKCEInvalidCodeVerifierWithS256CodeChallengeMethod() throws Exception {
+    	// test case : failure : A-1-14
+    	String codeVerifier = "123456789.123456789-123456789~1234$6789_123";
+    	String codeChallenge = generateS256CodeChallenge(codeVerifier);
+    	oauth.codeChallenge(codeChallenge);
+    	oauth.codeChallengeMethod(OAuth2Constants.PKCE_METHOD_S256);
+    	
+        oauth.doLogin("test-user@localhost", "password");
+
+        EventRepresentation loginEvent = events.expectLogin().assertEvent();
+
+        String sessionId = loginEvent.getSessionId();
+        String codeId = loginEvent.getDetails().get(Details.CODE_ID);
+
+        String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
+
+        oauth.codeVerifier(codeVerifier);
+        
+        OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+        
+        assertEquals(400, response.getStatusCode());
+        assertEquals(OAuthErrorException.INVALID_GRANT, response.getError());
+        assertEquals("PKCE invalid code verifier", response.getErrorDescription());
+        
+        events.expectCodeToToken(codeId, sessionId).error(Errors.INVALID_CODE_VERIFIER).clearDetails().assertEvent();
+    }
+    
+    private String generateS256CodeChallenge(String codeVerifier) throws Exception {
+        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);
+        }
+        String codeChallenge = Base64Url.encode(sb.toString().getBytes());
+    	return codeChallenge;
+    }
+ 
+    private void expectSuccessfulResponseFromTokenEndpoint(String codeId, String sessionId, String code)  throws Exception {
+        OAuthClient.AccessTokenResponse response = oauth.doAccessTokenRequest(code, "password");
+
+        assertEquals(200, response.getStatusCode());
+        Assert.assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
+        Assert.assertThat(response.getRefreshExpiresIn(), allOf(greaterThanOrEqualTo(1750), lessThanOrEqualTo(1800)));
+        assertEquals("bearer", response.getTokenType());
+
+        String expectedKid = oauth.doCertsRequest("test").getKeys()[0].getKeyId();
+
+        JWSHeader header = new JWSInput(response.getAccessToken()).getHeader();
+        assertEquals("RS256", header.getAlgorithm().name());
+        assertEquals("JWT", header.getType());
+        assertEquals(expectedKid, header.getKeyId());
+        assertNull(header.getContentType());
+
+        header = new JWSInput(response.getIdToken()).getHeader();
+        assertEquals("RS256", header.getAlgorithm().name());
+        assertEquals("JWT", header.getType());
+        assertEquals(expectedKid, header.getKeyId());
+        assertNull(header.getContentType());
+
+        header = new JWSInput(response.getRefreshToken()).getHeader();
+        assertEquals("RS256", header.getAlgorithm().name());
+        assertEquals("JWT", header.getType());
+        assertEquals(expectedKid, header.getKeyId());
+        assertNull(header.getContentType());
+
+        AccessToken token = oauth.verifyToken(response.getAccessToken());
+
+        assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), token.getSubject());
+        Assert.assertNotEquals("test-user@localhost", token.getSubject());
+        assertEquals(sessionId, token.getSessionState());
+        assertEquals(1, token.getRealmAccess().getRoles().size());
+        assertTrue(token.getRealmAccess().isUserInRole("user"));
+        assertEquals(1, token.getResourceAccess(oauth.getClientId()).getRoles().size());
+        assertTrue(token.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user"));
+
+        EventRepresentation event = events.expectCodeToToken(codeId, sessionId).assertEvent();
+        
+        assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID));
+        assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID));
+        assertEquals(sessionId, token.getSessionState());
+        
+        // make sure PKCE does not affect token refresh on Token Endpoint
+        
+        String refreshTokenString = response.getRefreshToken();
+        RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString);
+
+        Assert.assertNotNull(refreshTokenString);
+        Assert.assertThat(token.getExpiration() - getCurrentTime(), allOf(greaterThanOrEqualTo(200), lessThanOrEqualTo(350)));
+        int actual = refreshToken.getExpiration() - getCurrentTime();
+        Assert.assertThat(actual, allOf(greaterThanOrEqualTo(1799), lessThanOrEqualTo(1800)));
+        assertEquals(sessionId, refreshToken.getSessionState());
+
+        setTimeOffset(2);
+
+        OAuthClient.AccessTokenResponse refreshResponse = oauth.doRefreshTokenRequest(refreshTokenString, "password");
+        
+        AccessToken refreshedToken = oauth.verifyToken(refreshResponse.getAccessToken());
+        RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(refreshResponse.getRefreshToken());
+
+        assertEquals(200, refreshResponse.getStatusCode());
+        assertEquals(sessionId, refreshedToken.getSessionState());
+        assertEquals(sessionId, refreshedRefreshToken.getSessionState());
+
+        Assert.assertThat(refreshResponse.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
+        Assert.assertThat(refreshedToken.getExpiration() - getCurrentTime(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
+
+        Assert.assertThat(refreshedToken.getExpiration() - token.getExpiration(), allOf(greaterThanOrEqualTo(1), lessThanOrEqualTo(10)));
+        Assert.assertThat(refreshedRefreshToken.getExpiration() - refreshToken.getExpiration(), allOf(greaterThanOrEqualTo(1), lessThanOrEqualTo(10)));
+
+        Assert.assertNotEquals(token.getId(), refreshedToken.getId());
+        Assert.assertNotEquals(refreshToken.getId(), refreshedRefreshToken.getId());
+
+        assertEquals("bearer", refreshResponse.getTokenType());
+
+        assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), refreshedToken.getSubject());
+        Assert.assertNotEquals("test-user@localhost", refreshedToken.getSubject());
+
+        assertEquals(1, refreshedToken.getRealmAccess().getRoles().size());
+        Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole("user"));
+
+        assertEquals(1, refreshedToken.getResourceAccess(oauth.getClientId()).getRoles().size());
+        Assert.assertTrue(refreshedToken.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user"));
+
+        EventRepresentation refreshEvent = events.expectRefresh(event.getDetails().get(Details.REFRESH_TOKEN_ID), sessionId).assertEvent();
+        Assert.assertNotEquals(event.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID));
+        Assert.assertNotEquals(event.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID));
+
+        setTimeOffset(0);
+    }
+}