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;
|
||||
private PolicyEnforcer policyEnforcer;
|
||||
|
||||
// https://tools.ietf.org/html/rfc7636
|
||||
protected boolean pkce = false;
|
||||
|
||||
public KeycloakDeployment() {
|
||||
}
|
||||
|
||||
|
@ -414,4 +417,14 @@ public class KeycloakDeployment {
|
|||
public PolicyEnforcer getPolicyEnforcer() {
|
||||
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());
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc7636
|
||||
if (adapterConfig.isPkce()) {
|
||||
deployment.setPkce(true);
|
||||
}
|
||||
|
||||
deployment.setBearerOnly(adapterConfig.isBearerOnly());
|
||||
deployment.setAutodetectBearerOnly(adapterConfig.isAutodetectBearerOnly());
|
||||
deployment.setEnableBasicAuth(adapterConfig.isEnableBasicAuth());
|
||||
|
|
|
@ -33,6 +33,8 @@ import org.keycloak.constants.AdapterConstants;
|
|||
import org.keycloak.representations.AccessTokenResponse;
|
||||
import org.keycloak.util.JsonSerialization;
|
||||
|
||||
import org.jboss.logging.Logger;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
@ -46,6 +48,8 @@ import java.util.List;
|
|||
*/
|
||||
public class ServerRequest {
|
||||
|
||||
private static Logger logger = Logger.getLogger(ServerRequest.class);
|
||||
|
||||
public static class HttpFailure extends Exception {
|
||||
private int status;
|
||||
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 {
|
||||
List<NameValuePair> formparams = new ArrayList<NameValuePair>();
|
||||
formparams.add(new BasicNameValuePair(OAuth2Constants.GRANT_TYPE, OAuth2Constants.REFRESH_TOKEN));
|
||||
|
|
|
@ -41,12 +41,60 @@ import java.io.InputStream;
|
|||
import java.net.URI;
|
||||
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>
|
||||
* @version $Revision: 1 $
|
||||
*/
|
||||
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
|
||||
*/
|
||||
|
@ -57,7 +105,15 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
|||
private AccessTokenResponse resolveBearerToken(HttpServletRequest request, String redirectUri, String code) throws IOException, ServerRequest.HttpFailure {
|
||||
// Don't send sessionId in oauth clients for now
|
||||
KeycloakDeployment resolvedDeployment = resolveDeployment(getDeployment(), request);
|
||||
return ServerRequest.invokeAccessCodeToToken(resolvedDeployment, code, redirectUri, null);
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -94,6 +150,12 @@ public class ServletOAuthClient extends KeycloakDeploymentDelegateOAuthClient {
|
|||
String authUrl = resolvedDeployment.getAuthUrl().clone().build().toString();
|
||||
String scopeParam = TokenUtil.attachOIDCScope(scope);
|
||||
|
||||
// https://tools.ietf.org/html/rfc7636#section-4
|
||||
if (resolvedDeployment.isPkce()) {
|
||||
setCodeVerifier();
|
||||
setCodeChallenge();
|
||||
}
|
||||
|
||||
KeycloakUriBuilder uriBuilder = KeycloakUriBuilder.fromUri(authUrl)
|
||||
.queryParam(OAuth2Constants.RESPONSE_TYPE, OAuth2Constants.CODE)
|
||||
.queryParam(OAuth2Constants.CLIENT_ID, getClientId())
|
||||
|
|
|
@ -78,6 +78,9 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
|
|||
protected int publicKeyCacheTtl = 86400; // 1 day
|
||||
@JsonProperty("policy-enforcer")
|
||||
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}.
|
||||
|
@ -244,4 +247,14 @@ public class AdapterConfig extends BaseAdapterConfig implements AdapterHttpClien
|
|||
public void setPublicKeyCacheTtl(int 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",
|
||||
"credentials" : {
|
||||
"secret": "password"
|
||||
}
|
||||
},
|
||||
"enable-pkce" : true
|
||||
}
|
Loading…
Reference in a new issue