From 2f08df5f3321081484a29e034dd03023352731ea Mon Sep 17 00:00:00 2001 From: lasanthaS Date: Thu, 16 Mar 2023 14:39:03 +0530 Subject: [PATCH] Enable PKCE in OIDC federated flow --- .../oidc/OIDCAuthenticatorConstants.java | 3 + .../oidc/OpenIDConnectAuthenticator.java | 96 +++++++++++++++++-- 2 files changed, 92 insertions(+), 7 deletions(-) diff --git a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OIDCAuthenticatorConstants.java b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OIDCAuthenticatorConstants.java index 22d0eab3..39863a94 100644 --- a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OIDCAuthenticatorConstants.java +++ b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OIDCAuthenticatorConstants.java @@ -58,6 +58,9 @@ private OIDCAuthenticatorConstants() { public static final String LOGOUT_TOKEN = "logout_token"; public static final Pattern OIDC_BACKCHANNEL_LOGOUT_ENDPOINT_URL_PATTERN = Pattern.compile("(.*)/identity/oidc" + "/slo(.*)"); + + public static final String OAUTH_FEDERATED_PKCE_CODE_VERIFIER = "OAUTH_PKCE_CODE_VERIFIER"; + public static final String ENABLE_FEDERATED_PKCE = "IsPKCEEnabled"; public class AuthenticatorConfParams { diff --git a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticator.java b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticator.java index c522d605..ad30550c 100644 --- a/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticator.java +++ b/components/org.wso2.carbon.identity.application.authenticator.oidc/src/main/java/org/wso2/carbon/identity/application/authenticator/oidc/OpenIDConnectAuthenticator.java @@ -72,6 +72,9 @@ import java.net.URL; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; import java.text.ParseException; import java.util.ArrayList; import java.util.HashMap; @@ -371,6 +374,8 @@ protected void initiateAuthenticationRequest(HttpServletRequest request, HttpSer String authorizationEP = getOIDCAuthzEndpoint(authenticatorProperties); String callbackurl = getCallbackUrl(authenticatorProperties); String state = getStateParameter(context, authenticatorProperties); + boolean isPKCEEnabled = Boolean.parseBoolean( + authenticatorProperties.get(OIDCAuthenticatorConstants.ENABLE_FEDERATED_PKCE)); OAuthClientRequest authzRequest; @@ -427,6 +432,18 @@ protected void initiateAuthenticationRequest(HttpServletRequest request, HttpSer loginPage = loginPage + "&fidp=" + domain; } + // If PKCE is enabled, add code_challenge and code_challenge_method to the request. + if (isPKCEEnabled) { + String codeVerifier = generateCodeVerifier(); + context.setProperty(OIDCAuthenticatorConstants.OAUTH_FEDERATED_PKCE_CODE_VERIFIER, codeVerifier); + try { + String codeChallenge = generateCodeChallenge(codeVerifier); + loginPage += "&code_challenge=" + codeChallenge + "&code_challenge_method=S256"; + } catch (NoSuchAlgorithmException e) { + log.error("Error while generating the code challenge", e); + } + } + if (StringUtils.isNotBlank(queryString)) { if (!queryString.startsWith("&")) { loginPage = loginPage + "&" + queryString; @@ -795,6 +812,9 @@ protected OAuthClientRequest getAccessTokenRequest(AuthenticationContext context String clientId = authenticatorProperties.get(OIDCAuthenticatorConstants.CLIENT_ID); String clientSecret = authenticatorProperties.get(OIDCAuthenticatorConstants.CLIENT_SECRET); String tokenEndPoint = getTokenEndpoint(authenticatorProperties); + boolean isPKCEEnabled = Boolean.parseBoolean( + authenticatorProperties.get(OIDCAuthenticatorConstants.ENABLE_FEDERATED_PKCE)); + Object codeVerifier = context.getProperty(OIDCAuthenticatorConstants.OAUTH_FEDERATED_PKCE_CODE_VERIFIER); String callbackUrl = getCallbackUrlFromInitialRequestParamMap(context); if (StringUtils.isBlank(callbackUrl)) { @@ -813,9 +833,21 @@ protected OAuthClientRequest getAccessTokenRequest(AuthenticationContext context "authentication scheme."); } - accessTokenRequest = OAuthClientRequest.tokenLocation(tokenEndPoint).setGrantType(GrantType - .AUTHORIZATION_CODE).setRedirectURI(callbackUrl).setCode(authzResponse.getCode()) - .buildBodyMessage(); + OAuthClientRequest.TokenRequestBuilder tokenRequestBuilder = OAuthClientRequest + .tokenLocation(tokenEndPoint) + .setGrantType(GrantType.AUTHORIZATION_CODE) + .setRedirectURI(callbackUrl) + .setCode(authzResponse.getCode()); + + if (isPKCEEnabled) { + if (codeVerifier != null) { + tokenRequestBuilder.setParameter("code_verifier", codeVerifier.toString()); + } else { + log.warn("PKCE is enabled, but the code verifier is not found."); + } + } + + accessTokenRequest = tokenRequestBuilder.buildBodyMessage(); String base64EncodedCredential = new String(Base64.encodeBase64((clientId + ":" + clientSecret).getBytes())); accessTokenRequest.addHeader(OAuth.HeaderType.AUTHORIZATION, "Basic " + base64EncodedCredential); @@ -825,10 +857,23 @@ protected OAuthClientRequest getAccessTokenRequest(AuthenticationContext context LOG.debug("Authenticating to token endpoint: " + tokenEndPoint + " including client credentials " + "in request body."); } - accessTokenRequest = OAuthClientRequest.tokenLocation(tokenEndPoint).setGrantType(GrantType - .AUTHORIZATION_CODE).setClientId(clientId).setClientSecret(clientSecret).setRedirectURI - (callbackUrl).setCode(authzResponse.getCode()).buildBodyMessage(); + OAuthClientRequest.TokenRequestBuilder tokenRequestBuilder = OAuthClientRequest + .tokenLocation(tokenEndPoint) + .setGrantType(GrantType.AUTHORIZATION_CODE) + .setClientId(clientId) + .setClientSecret(clientSecret) + .setRedirectURI(callbackUrl) + .setCode(authzResponse.getCode()); + if (isPKCEEnabled) { + if (codeVerifier != null) { + tokenRequestBuilder.setParameter("code_verifier", codeVerifier.toString()); + } else { + log.warn("PKCE is enabled, but the code verifier is not found."); + } + } + accessTokenRequest = tokenRequestBuilder.buildBodyMessage(); } + context.removeProperty(OIDCAuthenticatorConstants.OAUTH_FEDERATED_PKCE_CODE_VERIFIER); // set 'Origin' header to access token request. if (accessTokenRequest != null) { // fetch the 'Hostname' configured in carbon.xml @@ -844,7 +889,6 @@ protected OAuthClientRequest getAccessTokenRequest(AuthenticationContext context } catch (URLBuilderException e) { throw new RuntimeException("Error occurred while building URL in tenant qualified mode.", e); } - return accessTokenRequest; } @@ -1008,6 +1052,15 @@ public List getConfigurationProperties() { enableBasicAuth.setDisplayOrder(10); configProperties.add(enableBasicAuth); + Property enablePKCE = new Property(); + enablePKCE.setName("isPKCEEnabled"); + enablePKCE.setDisplayName("Enable PKCE"); + enablePKCE.setRequired(false); + enablePKCE.setDescription("Specifies that PKCE should be used for client authentication"); + enablePKCE.setType("boolean"); + enablePKCE.setDisplayOrder(10); + configProperties.add(enablePKCE); + return configProperties; } @@ -1260,4 +1313,33 @@ private String getCallbackUrlFromInitialRequestParamMap(AuthenticationContext co return null; } + + /** + * Generate code verifier for PKCE + * + * @return code verifier + */ + private String generateCodeVerifier() { + SecureRandom secureRandom = new SecureRandom(); + byte[] codeVerifier = new byte[32]; + secureRandom.nextBytes(codeVerifier); + return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(codeVerifier); + } + + /** + * Generate code challenge for PKCE + * + * @param codeVerifier code verifier + * @return code challenge + * @throws UnsupportedEncodingException + * @throws NoSuchAlgorithmException + */ + private String generateCodeChallenge(String codeVerifier) + throws UnsupportedEncodingException, NoSuchAlgorithmException { + byte[] bytes = codeVerifier.getBytes("US-ASCII"); + MessageDigest messageDigest = MessageDigest.getInstance("SHA-256"); + messageDigest.update(bytes, 0, bytes.length); + byte[] digest = messageDigest.digest(); + return java.util.Base64.getUrlEncoder().withoutPadding().encodeToString(digest); + } }