From 8bd48391cacf12a882f74a5bd59da43a3613a1ee Mon Sep 17 00:00:00 2001 From: Thomas Darimont Date: Sat, 18 May 2019 10:14:15 +0200 Subject: [PATCH] KEYCLOAK-10313 Add PKCE support to KeycloakInstalled Adpater This adds PKCE support for Desktop Apps as a followup to KEYCLOAK-1033 #6047. --- .../adapters/installed/KeycloakInstalled.java | 102 ++++++++++++++---- 1 file changed, 81 insertions(+), 21 deletions(-) diff --git a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java index 2472dfe193..e6e3f4ec72 100644 --- a/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java +++ b/adapters/oidc/installed/src/main/java/org/keycloak/adapters/installed/KeycloakInstalled.java @@ -26,7 +26,9 @@ import org.keycloak.adapters.KeycloakDeploymentBuilder; import org.keycloak.adapters.ServerRequest; import org.keycloak.adapters.rotation.AdapterTokenVerifier; import org.keycloak.common.VerificationException; +import org.keycloak.common.util.Base64Url; import org.keycloak.common.util.KeycloakUriBuilder; +import org.keycloak.common.util.RandomString; import org.keycloak.representations.AccessToken; import org.keycloak.representations.AccessTokenResponse; import org.keycloak.representations.IDToken; @@ -41,6 +43,8 @@ import java.net.ServerSocket; import java.net.Socket; import java.net.URI; import java.net.URISyntaxException; +import java.security.MessageDigest; +import java.security.SecureRandom; import java.util.Locale; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -161,17 +165,9 @@ public class KeycloakInstalled { String redirectUri = "http://localhost:" + callback.server.getLocalPort(); String state = UUID.randomUUID().toString(); + Pkce pkce = generatePkce(); - KeycloakUriBuilder builder = deployment.getAuthUrl().clone() - .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE) - .queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName()) - .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri) - .queryParam(OAuth2Constants.STATE, state) - .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID); - if (locale != null) { - builder.queryParam(OAuth2Constants.UI_LOCALES_PARAM, locale.getLanguage()); - } - String authUrl = builder.build().toString(); + String authUrl = createAuthUrl(redirectUri, state, pkce); Desktop.getDesktop().browse(new URI(authUrl)); @@ -189,11 +185,39 @@ public class KeycloakInstalled { throw callback.errorException; } - processCode(callback.code, redirectUri); + processCode(callback.code, redirectUri, pkce); status = Status.LOGGED_DESKTOP; } + protected String createAuthUrl(String redirectUri, String state, Pkce pkce) { + + KeycloakUriBuilder builder = deployment.getAuthUrl().clone() + .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE) + .queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName()) + .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri) + .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID); + + if (state != null) { + builder.queryParam(OAuth2Constants.STATE, state); + } + + if (locale != null) { + builder.queryParam(OAuth2Constants.UI_LOCALES_PARAM, locale.getLanguage()); + } + + if (pkce != null) { + builder.queryParam(OAuth2Constants.CODE_CHALLENGE, pkce.getCodeChallenge()); + builder.queryParam(OAuth2Constants.CODE_CHALLENGE_METHOD, "S256"); + } + + return builder.build().toString(); + } + + protected Pkce generatePkce(){ + return Pkce.generatePkce(); + } + private void logoutDesktop() throws IOException, URISyntaxException, InterruptedException { CallbackListener callback = new CallbackListener(getLogoutResponseWriter()); callback.start(); @@ -218,14 +242,12 @@ public class KeycloakInstalled { } public void loginManual(PrintStream printer, Reader reader) throws IOException, ServerRequest.HttpFailure, VerificationException { + String redirectUri = "urn:ietf:wg:oauth:2.0:oob"; - String authUrl = deployment.getAuthUrl().clone() - .queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE) - .queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName()) - .queryParam(OAuth2Constants.REDIRECT_URI, redirectUri) - .queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID) - .build().toString(); + Pkce pkce = generatePkce(); + + String authUrl = createAuthUrl(redirectUri, null, pkce); printer.println("Open the following URL in a browser. After login copy/paste the code back and press "); printer.println(authUrl); @@ -233,7 +255,7 @@ public class KeycloakInstalled { printer.print("Code: "); String code = readCode(reader); - processCode(code, redirectUri); + processCode(code, redirectUri, pkce); status = Status.LOGGED_MANUAL; } @@ -467,7 +489,7 @@ public class KeycloakInstalled { response.close(); client.close(); String code = m.group(1); - processCode(code, redirectUri); + processCode(code, redirectUri, null); return true; } if (response.getStatus() == 302 && redirectCount++ > 4) { @@ -568,8 +590,9 @@ public class KeycloakInstalled { } - private void processCode(String code, String redirectUri) throws IOException, ServerRequest.HttpFailure, VerificationException { - AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null); + private void processCode(String code, String redirectUri, Pkce pkce) throws IOException, ServerRequest.HttpFailure, VerificationException { + + AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null, pkce == null ? null : pkce.getCodeVerifier()); parseAccessToken(tokenResponse); } @@ -677,5 +700,42 @@ public class KeycloakInstalled { } + public static class Pkce { + // https://tools.ietf.org/html/rfc7636#section-4.1 + public static final int PKCE_CODE_VERIFIER_MAX_LENGTH = 128; + + private final String codeChallenge; + private final String codeVerifier; + + public Pkce(String codeVerifier, String codeChallenge) { + this.codeChallenge = codeChallenge; + this.codeVerifier = codeVerifier; + } + + public String getCodeChallenge() { + return codeChallenge; + } + + public String getCodeVerifier() { + return codeVerifier; + } + + public static Pkce generatePkce() { + try { + String codeVerifier = new RandomString(PKCE_CODE_VERIFIER_MAX_LENGTH, new SecureRandom()).nextString(); + String codeChallenge = generateS256CodeChallenge(codeVerifier); + return new Pkce(codeVerifier, codeChallenge); + } catch (Exception ex){ + throw new RuntimeException("Could not generate PKCE", ex); + } + } + + // https://tools.ietf.org/html/rfc7636#section-4.6 + private static String generateS256CodeChallenge(String codeVerifier) throws Exception { + MessageDigest md = MessageDigest.getInstance("SHA-256"); + md.update(codeVerifier.getBytes("ISO_8859_1")); + return Base64Url.encode(md.digest()); + } + } }