KEYCLOAK-10313 Add PKCE support to KeycloakInstalled Adpater

This adds PKCE support for Desktop Apps as
a followup to KEYCLOAK-1033 #6047.
This commit is contained in:
Thomas Darimont 2019-05-18 10:14:15 +02:00 committed by Marek Posolda
parent b32d52e62b
commit 8bd48391ca

View file

@ -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 <enter>");
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());
}
}
}