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:
parent
b32d52e62b
commit
8bd48391ca
1 changed files with 81 additions and 21 deletions
|
@ -26,7 +26,9 @@ import org.keycloak.adapters.KeycloakDeploymentBuilder;
|
||||||
import org.keycloak.adapters.ServerRequest;
|
import org.keycloak.adapters.ServerRequest;
|
||||||
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
|
import org.keycloak.adapters.rotation.AdapterTokenVerifier;
|
||||||
import org.keycloak.common.VerificationException;
|
import org.keycloak.common.VerificationException;
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
import org.keycloak.common.util.KeycloakUriBuilder;
|
import org.keycloak.common.util.KeycloakUriBuilder;
|
||||||
|
import org.keycloak.common.util.RandomString;
|
||||||
import org.keycloak.representations.AccessToken;
|
import org.keycloak.representations.AccessToken;
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
import org.keycloak.representations.IDToken;
|
import org.keycloak.representations.IDToken;
|
||||||
|
@ -41,6 +43,8 @@ import java.net.ServerSocket;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.SecureRandom;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
@ -161,17 +165,9 @@ public class KeycloakInstalled {
|
||||||
|
|
||||||
String redirectUri = "http://localhost:" + callback.server.getLocalPort();
|
String redirectUri = "http://localhost:" + callback.server.getLocalPort();
|
||||||
String state = UUID.randomUUID().toString();
|
String state = UUID.randomUUID().toString();
|
||||||
|
Pkce pkce = generatePkce();
|
||||||
|
|
||||||
KeycloakUriBuilder builder = deployment.getAuthUrl().clone()
|
String authUrl = createAuthUrl(redirectUri, state, pkce);
|
||||||
.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();
|
|
||||||
|
|
||||||
Desktop.getDesktop().browse(new URI(authUrl));
|
Desktop.getDesktop().browse(new URI(authUrl));
|
||||||
|
|
||||||
|
@ -189,11 +185,39 @@ public class KeycloakInstalled {
|
||||||
throw callback.errorException;
|
throw callback.errorException;
|
||||||
}
|
}
|
||||||
|
|
||||||
processCode(callback.code, redirectUri);
|
processCode(callback.code, redirectUri, pkce);
|
||||||
|
|
||||||
status = Status.LOGGED_DESKTOP;
|
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 {
|
private void logoutDesktop() throws IOException, URISyntaxException, InterruptedException {
|
||||||
CallbackListener callback = new CallbackListener(getLogoutResponseWriter());
|
CallbackListener callback = new CallbackListener(getLogoutResponseWriter());
|
||||||
callback.start();
|
callback.start();
|
||||||
|
@ -218,14 +242,12 @@ public class KeycloakInstalled {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loginManual(PrintStream printer, Reader reader) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
public void loginManual(PrintStream printer, Reader reader) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||||
|
|
||||||
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
|
String redirectUri = "urn:ietf:wg:oauth:2.0:oob";
|
||||||
|
|
||||||
String authUrl = deployment.getAuthUrl().clone()
|
Pkce pkce = generatePkce();
|
||||||
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
|
||||||
.queryParam(OAuth2Constants.CLIENT_ID, deployment.getResourceName())
|
String authUrl = createAuthUrl(redirectUri, null, pkce);
|
||||||
.queryParam(OAuth2Constants.REDIRECT_URI, redirectUri)
|
|
||||||
.queryParam(OAuth2Constants.SCOPE, OAuth2Constants.SCOPE_OPENID)
|
|
||||||
.build().toString();
|
|
||||||
|
|
||||||
printer.println("Open the following URL in a browser. After login copy/paste the code back and press <enter>");
|
printer.println("Open the following URL in a browser. After login copy/paste the code back and press <enter>");
|
||||||
printer.println(authUrl);
|
printer.println(authUrl);
|
||||||
|
@ -233,7 +255,7 @@ public class KeycloakInstalled {
|
||||||
printer.print("Code: ");
|
printer.print("Code: ");
|
||||||
|
|
||||||
String code = readCode(reader);
|
String code = readCode(reader);
|
||||||
processCode(code, redirectUri);
|
processCode(code, redirectUri, pkce);
|
||||||
|
|
||||||
status = Status.LOGGED_MANUAL;
|
status = Status.LOGGED_MANUAL;
|
||||||
}
|
}
|
||||||
|
@ -467,7 +489,7 @@ public class KeycloakInstalled {
|
||||||
response.close();
|
response.close();
|
||||||
client.close();
|
client.close();
|
||||||
String code = m.group(1);
|
String code = m.group(1);
|
||||||
processCode(code, redirectUri);
|
processCode(code, redirectUri, null);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (response.getStatus() == 302 && redirectCount++ > 4) {
|
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 {
|
private void processCode(String code, String redirectUri, Pkce pkce) throws IOException, ServerRequest.HttpFailure, VerificationException {
|
||||||
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null);
|
|
||||||
|
AccessTokenResponse tokenResponse = ServerRequest.invokeAccessCodeToToken(deployment, code, redirectUri, null, pkce == null ? null : pkce.getCodeVerifier());
|
||||||
parseAccessToken(tokenResponse);
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue