keycloak-aplcache
Changes
services/src/main/java/org/keycloak/protocol/oidc/endpoints/request/AuthorizationEndpointRequest.java 15(+15 -0)
Details
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/server-spi-private/src/main/java/org/keycloak/events/Errors.java b/server-spi-private/src/main/java/org/keycloak/events/Errors.java
index ea2b887..8177abf 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,4 +75,11 @@ 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";
+
}
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;