KEYCLOAK-2604 Proof Key for Code Exchange by OAuth Public Clients - RFC

7636 - Client Side Implementation
This commit is contained in:
Takashi Norimatsu 2017-02-03 12:02:54 +09:00
parent 88bfa563df
commit fe5fe4c968
6 changed files with 156 additions and 2 deletions

View file

@ -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;
}
}

View file

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

View file

@ -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));

View file

@ -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())

View file

@ -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;
}
}

View file

@ -5,5 +5,6 @@
"ssl-required" : "external",
"credentials" : {
"secret": "password"
}
},
"enable-pkce" : true
}