KEYCLOAK-2604 Proof Key for Code Exchange by OAuth Public Clients - RFC
7636 - Client Side Implementation
This commit is contained in:
parent
88bfa563df
commit
fe5fe4c968
6 changed files with 156 additions and 2 deletions
|
@ -85,6 +85,9 @@ public class KeycloakDeployment {
|
||||||
protected int publicKeyCacheTtl;
|
protected int publicKeyCacheTtl;
|
||||||
private PolicyEnforcer policyEnforcer;
|
private PolicyEnforcer policyEnforcer;
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636
|
||||||
|
protected boolean pkce = false;
|
||||||
|
|
||||||
public KeycloakDeployment() {
|
public KeycloakDeployment() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -414,4 +417,14 @@ public class KeycloakDeployment {
|
||||||
public PolicyEnforcer getPolicyEnforcer() {
|
public PolicyEnforcer getPolicyEnforcer() {
|
||||||
return policyEnforcer;
|
return policyEnforcer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636
|
||||||
|
public boolean isPkce() {
|
||||||
|
return pkce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPkce(boolean pkce) {
|
||||||
|
this.pkce = pkce;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -98,6 +98,11 @@ public class KeycloakDeploymentBuilder {
|
||||||
deployment.setCorsAllowedMethods(adapterConfig.getCorsAllowedMethods());
|
deployment.setCorsAllowedMethods(adapterConfig.getCorsAllowedMethods());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636
|
||||||
|
if (adapterConfig.isPkce()) {
|
||||||
|
deployment.setPkce(true);
|
||||||
|
}
|
||||||
|
|
||||||
deployment.setBearerOnly(adapterConfig.isBearerOnly());
|
deployment.setBearerOnly(adapterConfig.isBearerOnly());
|
||||||
deployment.setAutodetectBearerOnly(adapterConfig.isAutodetectBearerOnly());
|
deployment.setAutodetectBearerOnly(adapterConfig.isAutodetectBearerOnly());
|
||||||
deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth());
|
deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth());
|
||||||
|
|
|
@ -33,6 +33,8 @@ import org.keycloak.constants.AdapterConstants;
|
||||||
import org.keycloak.representations.AccessTokenResponse;
|
import org.keycloak.representations.AccessTokenResponse;
|
||||||
import org.keycloak.util.JsonSerialization;
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
|
@ -46,6 +48,8 @@ import java.util.List;
|
||||||
*/
|
*/
|
||||||
public class ServerRequest {
|
public class ServerRequest {
|
||||||
|
|
||||||
|
private static Logger logger = Logger.getLogger(ServerRequest.class);
|
||||||
|
|
||||||
public static class HttpFailure extends Exception {
|
public static class HttpFailure extends Exception {
|
||||||
private int status;
|
private int status;
|
||||||
private String error;
|
private String error;
|
||||||
|
@ -136,6 +140,62 @@ public class ServerRequest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636#section-4
|
||||||
|
public static AccessTokenResponse invokeAccessCodeToToken(KeycloakDeployment deployment, String code, String redirectUri, String sessionId, String codeVerifier) throws IOException, HttpFailure {
|
||||||
|
List<NameValuePair> formparams = new ArrayList<>();
|
||||||
|
redirectUri = stripOauthParametersFromRedirect(redirectUri);
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, "authorization_code"));
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.CODE, code));
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.REDIRECT_URI, redirectUri));
|
||||||
|
if (sessionId != null) {
|
||||||
|
formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_STATE, sessionId));
|
||||||
|
formparams.add(new BasicNameValuePair(AdapterConstants.CLIENT_SESSION_HOST, HostUtils.getHostName()));
|
||||||
|
}
|
||||||
|
// https://tools.ietf.org/html/rfc7636#section-4
|
||||||
|
if (codeVerifier != null) {
|
||||||
|
logger.debugf("add to POST parameters of Token Request, codeVerifier = %s", codeVerifier);
|
||||||
|
formparams.add(new BasicNameValuePair(OAuth2Constants.CODE_VERIFIER, codeVerifier));
|
||||||
|
} else {
|
||||||
|
logger.debug("add to POST parameters of Token Request without codeVerifier");
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpPost post = new HttpPost(deployment.getTokenUrl());
|
||||||
|
ClientCredentialsProviderUtils.setClientCredentials(deployment, post, formparams);
|
||||||
|
|
||||||
|
UrlEncodedFormEntity form = new UrlEncodedFormEntity(formparams, "UTF-8");
|
||||||
|
post.setEntity(form);
|
||||||
|
HttpResponse response = deployment.getClient().execute(post);
|
||||||
|
int status = response.getStatusLine().getStatusCode();
|
||||||
|
HttpEntity entity = response.getEntity();
|
||||||
|
if (status != 200) {
|
||||||
|
error(status, entity);
|
||||||
|
}
|
||||||
|
if (entity == null) {
|
||||||
|
throw new HttpFailure(status, null);
|
||||||
|
}
|
||||||
|
InputStream is = entity.getContent();
|
||||||
|
try {
|
||||||
|
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||||
|
int c;
|
||||||
|
while ((c = is.read()) != -1) {
|
||||||
|
os.write(c);
|
||||||
|
}
|
||||||
|
byte[] bytes = os.toByteArray();
|
||||||
|
String json = new String(bytes);
|
||||||
|
try {
|
||||||
|
return JsonSerialization.readValue(json, AccessTokenResponse.class);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new IOException(json, e);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
is.close();
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static AccessTokenResponse invokeRefresh(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure {
|
public static AccessTokenResponse invokeRefresh(KeycloakDeployment deployment, String refreshToken) throws IOException, HttpFailure {
|
||||||
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
||||||
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
|
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
|
||||||
|
|
|
@ -41,12 +41,60 @@ import java.io.InputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
* @author <a href="mailto:bill@burkecentral.com">Bill Burke</a>
|
||||||
* @version $Revision: 1 $
|
* @version $Revision: 1 $
|
||||||
*/
|
*/
|
||||||
public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636#section-4
|
||||||
|
private String codeVerifier;
|
||||||
|
private String codeChallenge;
|
||||||
|
private String codeChallengeMethod = OAuth2Constants.PKCE_METHOD_S256;
|
||||||
|
private static Logger logger = Logger.getLogger(ServletOAuthClient.class);
|
||||||
|
|
||||||
|
public static String generateSecret() {
|
||||||
|
return generateSecret(32);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String generateSecret(int bytes) {
|
||||||
|
byte[] buf = new byte[bytes];
|
||||||
|
new SecureRandom().nextBytes(buf);
|
||||||
|
return Base64Url.encode(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCodeVerifier() {
|
||||||
|
codeVerifier = generateSecret();
|
||||||
|
logger.debugf("Generated codeVerifier = %s", codeVerifier);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCodeChallenge() {
|
||||||
|
try {
|
||||||
|
if (codeChallengeMethod.equals(OAuth2Constants.PKCE_METHOD_S256)) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
codeChallenge = Base64Url.encode(sb.toString().getBytes());
|
||||||
|
} else {
|
||||||
|
codeChallenge = Base64Url.encode(codeVerifier.getBytes());
|
||||||
|
}
|
||||||
|
logger.debugf("Encode codeChallenge = %s, codeChallengeMethod = %s", codeChallenge, codeChallengeMethod);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.info("PKCE client side unknown hash algorithm");
|
||||||
|
codeChallenge = Base64Url.encode(codeVerifier.getBytes());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* closes client
|
* closes client
|
||||||
*/
|
*/
|
||||||
|
@ -57,8 +105,16 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
||||||
private AccessTokenResponse resolveBearerToken(HttpServletRequest request, String redirectUri, String code) throws IOException, ServerRequest.HttpFailure {
|
private AccessTokenResponse resolveBearerToken(HttpServletRequest request, String redirectUri, String code) throws IOException, ServerRequest.HttpFailure {
|
||||||
// Don't send sessionId in oauth clients for now
|
// Don't send sessionId in oauth clients for now
|
||||||
KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request);
|
KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request);
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636#section-4
|
||||||
|
if (codeVerifier != null) {
|
||||||
|
logger.debugf("Before sending Token Request, codeVerifier = %s", codeVerifier);
|
||||||
|
return ServerRequest.invokeAccessCodeToToken(resolvedDeployment, code, redirectUri, null, codeVerifier);
|
||||||
|
} else {
|
||||||
|
logger.debug("Before sending Token Request without codeVerifier");
|
||||||
return ServerRequest.invokeAccessCodeToToken(resolvedDeployment, code, redirectUri, null);
|
return ServerRequest.invokeAccessCodeToToken(resolvedDeployment, code, redirectUri, null);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the process of obtaining an access token by redirecting the browser
|
* Start the process of obtaining an access token by redirecting the browser
|
||||||
|
@ -94,6 +150,12 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
||||||
String authUrl = resolvedDeployment.getAuthUrl().clone().build().toString();
|
String authUrl = resolvedDeployment.getAuthUrl().clone().build().toString();
|
||||||
String scopeParam = TokenUtil.attachOIDCScope(scope);
|
String scopeParam = TokenUtil.attachOIDCScope(scope);
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636#section-4
|
||||||
|
if (resolvedDeployment.isPkce()) {
|
||||||
|
setCodeVerifier();
|
||||||
|
setCodeChallenge();
|
||||||
|
}
|
||||||
|
|
||||||
KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(authUrl)
|
KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(authUrl)
|
||||||
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
||||||
.queryParam(OAuth2Constants.CLIENT_ID, getClientId())
|
.queryParam(OAuth2Constants.CLIENT_ID, getClientId())
|
||||||
|
|
|
@ -78,6 +78,9 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
|
||||||
protected int publicKeyCacheTtl = 86400; // 1 day
|
protected int publicKeyCacheTtl = 86400; // 1 day
|
||||||
@JsonProperty("policy-enforcer")
|
@JsonProperty("policy-enforcer")
|
||||||
protected PolicyEnforcerConfig policyEnforcerConfig;
|
protected PolicyEnforcerConfig policyEnforcerConfig;
|
||||||
|
// https://tools.ietf.org/html/rfc7636
|
||||||
|
@JsonProperty("enable-pkce")
|
||||||
|
protected boolean pkce = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Proxy url to use for requests to the auth-server, configurable via the adapter config property {@code proxy-url}.
|
* The Proxy url to use for requests to the auth-server, configurable via the adapter config property {@code proxy-url}.
|
||||||
|
@ -244,4 +247,14 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
|
||||||
public void setPublicKeyCacheTtl(int publicKeyCacheTtl) {
|
public void setPublicKeyCacheTtl(int publicKeyCacheTtl) {
|
||||||
this.publicKeyCacheTtl = publicKeyCacheTtl;
|
this.publicKeyCacheTtl = publicKeyCacheTtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://tools.ietf.org/html/rfc7636
|
||||||
|
public boolean isPkce() {
|
||||||
|
return pkce;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPkce(boolean pkce) {
|
||||||
|
this.pkce = pkce;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,5 +5,6 @@
|
||||||
"ssl-required" : "external",
|
"ssl-required" : "external",
|
||||||
"credentials" : {
|
"credentials" : {
|
||||||
"secret": "password"
|
"secret": "password"
|
||||||
}
|
},
|
||||||
|
"enable-pkce" : true
|
||||||
}
|
}
|
Loading…
Reference in a new issue