KEYCLOAK-6771 Holder of Key mechanism
OAuth 2.0 Mutual TLS Client Authentication and Certificate Bound Access Tokens
This commit is contained in:
parent
f8919f8baa
commit
c586c63533
24 changed files with 1397 additions and 198 deletions
|
@ -244,4 +244,30 @@ public class AccessToken extends IDToken {
|
||||||
public void setAuthorization(Authorization authorization) {
|
public void setAuthorization(Authorization authorization) {
|
||||||
this.authorization = authorization;
|
this.authorization = authorization;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3.1
|
||||||
|
public static class CertConf {
|
||||||
|
@JsonProperty("x5t#S256")
|
||||||
|
protected String certThumbprint;
|
||||||
|
|
||||||
|
public String getCertThumbprint() {
|
||||||
|
return certThumbprint;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCertThumbprint(String certThumbprint) {
|
||||||
|
this.certThumbprint = certThumbprint;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonProperty("cnf")
|
||||||
|
protected CertConf certConf;
|
||||||
|
|
||||||
|
public CertConf getCertConf() {
|
||||||
|
return certConf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCertConf(CertConf certConf) {
|
||||||
|
this.certConf = certConf;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -96,6 +96,10 @@ public class OIDCClientRepresentation {
|
||||||
|
|
||||||
private List<String> request_uris;
|
private List<String> request_uris;
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.5
|
||||||
|
private Boolean tls_client_certificate_bound_access_tokens;
|
||||||
|
|
||||||
// OIDC Session Management
|
// OIDC Session Management
|
||||||
private List<String> post_logout_redirect_uris;
|
private List<String> post_logout_redirect_uris;
|
||||||
|
|
||||||
|
@ -433,4 +437,13 @@ public class OIDCClientRepresentation {
|
||||||
this.registration_access_token = registrationAccessToken;
|
this.registration_access_token = registrationAccessToken;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.5
|
||||||
|
public Boolean getTlsClientCertificateBoundAccessTokens() {
|
||||||
|
return tls_client_certificate_bound_access_tokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTlsClientCertificateBoundAccessTokens(Boolean tls_client_certificate_bound_access_tokens) {
|
||||||
|
this.tls_client_certificate_bound_access_tokens = tls_client_certificate_bound_access_tokens;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,6 +43,10 @@ public class OIDCAdvancedConfigWrapper {
|
||||||
|
|
||||||
private static final String EXCLUDE_SESSION_STATE_FROM_AUTH_RESPONSE = "exclude.session.state.from.auth.response";
|
private static final String EXCLUDE_SESSION_STATE_FROM_AUTH_RESPONSE = "exclude.session.state.from.auth.response";
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.5
|
||||||
|
private static final String USE_MTLS_HOK_TOKEN = "tls.client.certificate.bound.access.tokens";
|
||||||
|
|
||||||
private final ClientModel clientModel;
|
private final ClientModel clientModel;
|
||||||
private final ClientRepresentation clientRep;
|
private final ClientRepresentation clientRep;
|
||||||
|
|
||||||
|
@ -121,6 +125,18 @@ public class OIDCAdvancedConfigWrapper {
|
||||||
setAttribute(EXCLUDE_SESSION_STATE_FROM_AUTH_RESPONSE, val);
|
setAttribute(EXCLUDE_SESSION_STATE_FROM_AUTH_RESPONSE, val);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.5
|
||||||
|
public boolean isUseMtlsHokToken() {
|
||||||
|
String useUtlsHokToken = getAttribute(USE_MTLS_HOK_TOKEN);
|
||||||
|
return Boolean.parseBoolean(useUtlsHokToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUseMtlsHoKToken(boolean useUtlsHokToken) {
|
||||||
|
String val = String.valueOf(useUtlsHokToken);
|
||||||
|
setAttribute(USE_MTLS_HOK_TOKEN, val);
|
||||||
|
}
|
||||||
|
|
||||||
private String getAttribute(String attrKey) {
|
private String getAttribute(String attrKey) {
|
||||||
if (clientModel != null) {
|
if (clientModel != null) {
|
||||||
return clientModel.getAttribute(attrKey);
|
return clientModel.getAttribute(attrKey);
|
||||||
|
|
|
@ -119,6 +119,10 @@ public class OIDCWellKnownProvider implements WellKnownProvider {
|
||||||
// KEYCLOAK-7451 OAuth Authorization Server Metadata for Proof Key for Code Exchange
|
// KEYCLOAK-7451 OAuth Authorization Server Metadata for Proof Key for Code Exchange
|
||||||
config.setCodeChallengeMethodsSupported(DEFAULT_CODE_CHALLENGE_METHODS_SUPPORTED);
|
config.setCodeChallengeMethodsSupported(DEFAULT_CODE_CHALLENGE_METHODS_SUPPORTED);
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.2
|
||||||
|
config.setTlsClientCertificateBoundAccessTokens(true);
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
package org.keycloak.protocol.oidc;
|
package org.keycloak.protocol.oidc;
|
||||||
|
|
||||||
import org.jboss.logging.Logger;
|
import org.jboss.logging.Logger;
|
||||||
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
import org.keycloak.cluster.ClusterProvider;
|
import org.keycloak.cluster.ClusterProvider;
|
||||||
import org.keycloak.common.ClientConnection;
|
import org.keycloak.common.ClientConnection;
|
||||||
import org.keycloak.OAuth2Constants;
|
import org.keycloak.OAuth2Constants;
|
||||||
|
@ -61,6 +62,7 @@ import org.keycloak.services.managers.AuthenticationSessionManager;
|
||||||
import org.keycloak.services.managers.ClientSessionCode;
|
import org.keycloak.services.managers.ClientSessionCode;
|
||||||
import org.keycloak.services.managers.UserSessionCrossDCManager;
|
import org.keycloak.services.managers.UserSessionCrossDCManager;
|
||||||
import org.keycloak.services.managers.UserSessionManager;
|
import org.keycloak.services.managers.UserSessionManager;
|
||||||
|
import org.keycloak.services.util.MtlsHoKTokenUtil;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.util.TokenUtil;
|
import org.keycloak.util.TokenUtil;
|
||||||
import org.keycloak.common.util.Time;
|
import org.keycloak.common.util.Time;
|
||||||
|
@ -238,8 +240,8 @@ public class TokenManager {
|
||||||
|
|
||||||
|
|
||||||
public RefreshResult refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient,
|
public RefreshResult refreshAccessToken(KeycloakSession session, UriInfo uriInfo, ClientConnection connection, RealmModel realm, ClientModel authorizedClient,
|
||||||
String encodedRefreshToken, EventBuilder event, HttpHeaders headers) throws OAuthErrorException {
|
String encodedRefreshToken, EventBuilder event, HttpHeaders headers, HttpRequest request) throws OAuthErrorException {
|
||||||
RefreshToken refreshToken = verifyRefreshToken(session, realm, encodedRefreshToken);
|
RefreshToken refreshToken = verifyRefreshToken(session, realm, authorizedClient, request, encodedRefreshToken, true);
|
||||||
|
|
||||||
event.user(refreshToken.getSubject()).session(refreshToken.getSessionState())
|
event.user(refreshToken.getSubject()).session(refreshToken.getSessionState())
|
||||||
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
|
.detail(Details.REFRESH_TOKEN_ID, refreshToken.getId())
|
||||||
|
@ -265,6 +267,15 @@ public class TokenManager {
|
||||||
.accessToken(validation.newToken)
|
.accessToken(validation.newToken)
|
||||||
.generateRefreshToken();
|
.generateRefreshToken();
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3.1
|
||||||
|
// bind refreshed access and refresh token with Client Certificate
|
||||||
|
AccessToken.CertConf certConf = refreshToken.getCertConf();
|
||||||
|
if (certConf != null) {
|
||||||
|
responseBuilder.getAccessToken().setCertConf(certConf);
|
||||||
|
responseBuilder.getRefreshToken().setCertConf(certConf);
|
||||||
|
}
|
||||||
|
|
||||||
String scopeParam = validation.clientSession.getNote(OAuth2Constants.SCOPE);
|
String scopeParam = validation.clientSession.getNote(OAuth2Constants.SCOPE);
|
||||||
if (TokenUtil.isOIDCRequest(scopeParam)) {
|
if (TokenUtil.isOIDCRequest(scopeParam)) {
|
||||||
responseBuilder.generateIDToken();
|
responseBuilder.generateIDToken();
|
||||||
|
@ -307,6 +318,10 @@ public class TokenManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public RefreshToken verifyRefreshToken(KeycloakSession session, RealmModel realm, String encodedRefreshToken, boolean checkExpiration) throws OAuthErrorException {
|
public RefreshToken verifyRefreshToken(KeycloakSession session, RealmModel realm, String encodedRefreshToken, boolean checkExpiration) throws OAuthErrorException {
|
||||||
|
return verifyRefreshToken(session, realm, null, null, encodedRefreshToken, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RefreshToken verifyRefreshToken(KeycloakSession session, RealmModel realm, ClientModel client, HttpRequest request, String encodedRefreshToken, boolean checkExpiration) throws OAuthErrorException {
|
||||||
try {
|
try {
|
||||||
RefreshToken refreshToken = toRefreshToken(session, realm, encodedRefreshToken);
|
RefreshToken refreshToken = toRefreshToken(session, realm, encodedRefreshToken);
|
||||||
|
|
||||||
|
@ -324,6 +339,13 @@ public class TokenManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
if (client != null && OIDCAdvancedConfigWrapper.fromClientModel(client).isUseMtlsHokToken()) {
|
||||||
|
if (!MtlsHoKTokenUtil.verifyTokenBindingWithClientCertificate(refreshToken, request)) {
|
||||||
|
throw new OAuthErrorException(OAuthErrorException.UNAUTHORIZED_CLIENT, MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return refreshToken;
|
return refreshToken;
|
||||||
} catch (JWSInputException e) {
|
} catch (JWSInputException e) {
|
||||||
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e);
|
throw new OAuthErrorException(OAuthErrorException.INVALID_GRANT, "Invalid refresh token", e);
|
||||||
|
|
|
@ -43,6 +43,7 @@ import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.managers.UserSessionManager;
|
import org.keycloak.services.managers.UserSessionManager;
|
||||||
import org.keycloak.services.messages.Messages;
|
import org.keycloak.services.messages.Messages;
|
||||||
import org.keycloak.services.resources.Cors;
|
import org.keycloak.services.resources.Cors;
|
||||||
|
import org.keycloak.services.util.MtlsHoKTokenUtil;
|
||||||
import org.keycloak.util.TokenUtil;
|
import org.keycloak.util.TokenUtil;
|
||||||
|
|
||||||
import javax.ws.rs.Consumes;
|
import javax.ws.rs.Consumes;
|
||||||
|
@ -180,8 +181,11 @@ public class LogoutEndpoint {
|
||||||
event.error(Errors.INVALID_TOKEN);
|
event.error(Errors.INVALID_TOKEN);
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST);
|
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "No refresh token", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RefreshToken token = null;
|
||||||
try {
|
try {
|
||||||
RefreshToken token = tokenManager.verifyRefreshToken(session, realm, refreshToken, false);
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
token = tokenManager.verifyRefreshToken(session, realm, client, request, refreshToken, false);
|
||||||
|
|
||||||
boolean offline = TokenUtil.TOKEN_TYPE_OFFLINE.equals(token.getType());
|
boolean offline = TokenUtil.TOKEN_TYPE_OFFLINE.equals(token.getType());
|
||||||
|
|
||||||
|
@ -197,9 +201,16 @@ public class LogoutEndpoint {
|
||||||
logout(userSessionModel, offline);
|
logout(userSessionModel, offline);
|
||||||
}
|
}
|
||||||
} catch (OAuthErrorException e) {
|
} catch (OAuthErrorException e) {
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
if (MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC.equals(e.getDescription())) {
|
||||||
|
event.error(Errors.NOT_ALLOWED);
|
||||||
|
throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.UNAUTHORIZED);
|
||||||
|
} else {
|
||||||
event.error(Errors.INVALID_TOKEN);
|
event.error(Errors.INVALID_TOKEN);
|
||||||
throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.BAD_REQUEST);
|
throw new ErrorResponseException(e.getError(), e.getDescription(), Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Cors.add(request, Response.noContent()).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
|
return Cors.add(request, Response.noContent()).auth().allowedOrigins(uriInfo, client).allowedMethods("POST").exposedHeaders(Cors.ACCESS_CONTROL_ALLOW_METHODS).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,7 @@ import org.keycloak.models.RealmModel;
|
||||||
import org.keycloak.models.UserModel;
|
import org.keycloak.models.UserModel;
|
||||||
import org.keycloak.models.UserSessionModel;
|
import org.keycloak.models.UserSessionModel;
|
||||||
import org.keycloak.models.utils.AuthenticationFlowResolver;
|
import org.keycloak.models.utils.AuthenticationFlowResolver;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.TokenManager;
|
import org.keycloak.protocol.oidc.TokenManager;
|
||||||
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
||||||
|
@ -79,10 +80,10 @@ import org.keycloak.services.resources.Cors;
|
||||||
import org.keycloak.services.resources.IdentityBrokerService;
|
import org.keycloak.services.resources.IdentityBrokerService;
|
||||||
import org.keycloak.services.resources.admin.AdminAuth;
|
import org.keycloak.services.resources.admin.AdminAuth;
|
||||||
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
import org.keycloak.services.resources.admin.permissions.AdminPermissions;
|
||||||
|
import org.keycloak.services.util.MtlsHoKTokenUtil;
|
||||||
import org.keycloak.services.validation.Validation;
|
import org.keycloak.services.validation.Validation;
|
||||||
import org.keycloak.sessions.AuthenticationSessionModel;
|
import org.keycloak.sessions.AuthenticationSessionModel;
|
||||||
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
import org.keycloak.sessions.RootAuthenticationSessionModel;
|
||||||
import org.keycloak.util.JsonSerialization;
|
|
||||||
import org.keycloak.util.TokenUtil;
|
import org.keycloak.util.TokenUtil;
|
||||||
import org.keycloak.utils.ProfileHelper;
|
import org.keycloak.utils.ProfileHelper;
|
||||||
|
|
||||||
|
@ -404,6 +405,19 @@ public class TokenEndpoint {
|
||||||
.accessToken(token)
|
.accessToken(token)
|
||||||
.generateRefreshToken();
|
.generateRefreshToken();
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
|
||||||
|
if (OIDCAdvancedConfigWrapper.fromClientModel(client).isUseMtlsHokToken()) {
|
||||||
|
AccessToken.CertConf certConf = MtlsHoKTokenUtil.bindTokenWithClientCertificate(request);
|
||||||
|
if (certConf != null) {
|
||||||
|
responseBuilder.getAccessToken().setCertConf(certConf);
|
||||||
|
responseBuilder.getRefreshToken().setCertConf(certConf);
|
||||||
|
} else {
|
||||||
|
event.error(Errors.INVALID_REQUEST);
|
||||||
|
throw new CorsErrorResponseException(cors, OAuthErrorException.INVALID_REQUEST, "Client Certification missing for MTLS HoK Token Binding", Response.Status.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE);
|
String scopeParam = clientSession.getNote(OAuth2Constants.SCOPE);
|
||||||
if (TokenUtil.isOIDCRequest(scopeParam)) {
|
if (TokenUtil.isOIDCRequest(scopeParam)) {
|
||||||
responseBuilder.generateIDToken();
|
responseBuilder.generateIDToken();
|
||||||
|
@ -424,7 +438,8 @@ public class TokenEndpoint {
|
||||||
|
|
||||||
AccessTokenResponse res;
|
AccessTokenResponse res;
|
||||||
try {
|
try {
|
||||||
TokenManager.RefreshResult result = tokenManager.refreshAccessToken(session, uriInfo, clientConnection, realm, client, refreshToken, event, headers);
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
TokenManager.RefreshResult result = tokenManager.refreshAccessToken(session, uriInfo, clientConnection, realm, client, refreshToken, event, headers, request);
|
||||||
res = result.getResponse();
|
res = result.getResponse();
|
||||||
|
|
||||||
if (!result.isOfflineToken()) {
|
if (!result.isOfflineToken()) {
|
||||||
|
@ -436,9 +451,15 @@ public class TokenEndpoint {
|
||||||
|
|
||||||
} catch (OAuthErrorException e) {
|
} catch (OAuthErrorException e) {
|
||||||
logger.trace(e.getMessage(), e);
|
logger.trace(e.getMessage(), e);
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
if (MtlsHoKTokenUtil.CERT_VERIFY_ERROR_DESC.equals(e.getDescription())) {
|
||||||
|
event.error(Errors.NOT_ALLOWED);
|
||||||
|
throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.UNAUTHORIZED);
|
||||||
|
} else {
|
||||||
event.error(Errors.INVALID_TOKEN);
|
event.error(Errors.INVALID_TOKEN);
|
||||||
throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.BAD_REQUEST);
|
throw new CorsErrorResponseException(cors, e.getError(), e.getDescription(), Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
event.success();
|
event.success();
|
||||||
|
|
||||||
|
|
|
@ -44,6 +44,7 @@ import org.keycloak.services.managers.AppAuthManager;
|
||||||
import org.keycloak.services.managers.AuthenticationManager;
|
import org.keycloak.services.managers.AuthenticationManager;
|
||||||
import org.keycloak.services.managers.UserSessionCrossDCManager;
|
import org.keycloak.services.managers.UserSessionCrossDCManager;
|
||||||
import org.keycloak.services.resources.Cors;
|
import org.keycloak.services.resources.Cors;
|
||||||
|
import org.keycloak.services.util.MtlsHoKTokenUtil;
|
||||||
import org.keycloak.utils.MediaType;
|
import org.keycloak.utils.MediaType;
|
||||||
|
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
|
@ -161,6 +162,15 @@ public class UserInfoEndpoint {
|
||||||
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User not found", Response.Status.BAD_REQUEST);
|
throw new ErrorResponseException(OAuthErrorException.INVALID_REQUEST, "User not found", Response.Status.BAD_REQUEST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
|
||||||
|
if (OIDCAdvancedConfigWrapper.fromClientModel(clientModel).isUseMtlsHokToken()) {
|
||||||
|
if (!MtlsHoKTokenUtil.verifyTokenBindingWithClientCertificate(token, request)) {
|
||||||
|
event.error(Errors.NOT_ALLOWED);
|
||||||
|
throw new ErrorResponseException(OAuthErrorException.UNAUTHORIZED_CLIENT, "Client certificate missing, or its thumbprint and one in the refresh token did NOT match", Response.Status.UNAUTHORIZED);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
event.user(userModel)
|
event.user(userModel)
|
||||||
.detail(Details.USERNAME, userModel.getUsername());
|
.detail(Details.USERNAME, userModel.getUsername());
|
||||||
|
|
||||||
|
|
|
@ -107,6 +107,11 @@ public class OIDCConfigurationRepresentation {
|
||||||
@JsonProperty("code_challenge_methods_supported")
|
@JsonProperty("code_challenge_methods_supported")
|
||||||
private List<String> codeChallengeMethodsSupported;
|
private List<String> codeChallengeMethodsSupported;
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.2
|
||||||
|
@JsonProperty("tls_client_certificate_bound_access_tokens")
|
||||||
|
private Boolean tlsClientCertificateBoundAccessTokens;
|
||||||
|
|
||||||
protected Map<String, Object> otherClaims = new HashMap<String, Object>();
|
protected Map<String, Object> otherClaims = new HashMap<String, Object>();
|
||||||
|
|
||||||
public String getIssuer() {
|
public String getIssuer() {
|
||||||
|
@ -305,10 +310,21 @@ public class OIDCConfigurationRepresentation {
|
||||||
public List<String> getCodeChallengeMethodsSupported() {
|
public List<String> getCodeChallengeMethodsSupported() {
|
||||||
return codeChallengeMethodsSupported;
|
return codeChallengeMethodsSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setCodeChallengeMethodsSupported(List<String> codeChallengeMethodsSupported) {
|
public void setCodeChallengeMethodsSupported(List<String> codeChallengeMethodsSupported) {
|
||||||
this.codeChallengeMethodsSupported = codeChallengeMethodsSupported;
|
this.codeChallengeMethodsSupported = codeChallengeMethodsSupported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.2
|
||||||
|
public Boolean getTlsClientCertificateBoundAccessTokens() {
|
||||||
|
return tlsClientCertificateBoundAccessTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setTlsClientCertificateBoundAccessTokens(Boolean tlsClientCertificateBoundAccessTokens) {
|
||||||
|
this.tlsClientCertificateBoundAccessTokens = tlsClientCertificateBoundAccessTokens;
|
||||||
|
}
|
||||||
|
|
||||||
@JsonAnyGetter
|
@JsonAnyGetter
|
||||||
public Map<String, Object> getOtherClaims() {
|
public Map<String, Object> getOtherClaims() {
|
||||||
return otherClaims;
|
return otherClaims;
|
||||||
|
|
|
@ -32,7 +32,6 @@ import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
|
||||||
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
|
import org.keycloak.protocol.oidc.mappers.PairwiseSubMapperHelper;
|
||||||
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
import org.keycloak.protocol.oidc.utils.AuthorizeClientUtil;
|
||||||
import org.keycloak.protocol.oidc.utils.JWKSHttpUtils;
|
|
||||||
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||||
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
|
import org.keycloak.protocol.oidc.utils.PairwiseSubMapperUtils;
|
||||||
import org.keycloak.protocol.oidc.utils.SubjectType;
|
import org.keycloak.protocol.oidc.utils.SubjectType;
|
||||||
|
@ -44,7 +43,6 @@ import org.keycloak.services.clientregistration.ClientRegistrationException;
|
||||||
import org.keycloak.services.util.CertificateInfoHelper;
|
import org.keycloak.services.util.CertificateInfoHelper;
|
||||||
import org.keycloak.util.JWKSUtils;
|
import org.keycloak.util.JWKSUtils;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.security.PublicKey;
|
import java.security.PublicKey;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
@ -115,6 +113,14 @@ public class DescriptionConverter {
|
||||||
configWrapper.setRequestObjectSignatureAlg(algorithm);
|
configWrapper.setRequestObjectSignatureAlg(algorithm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.5
|
||||||
|
Boolean tlsClientCertificateBoundAccessTokens = clientOIDC.getTlsClientCertificateBoundAccessTokens();
|
||||||
|
if (tlsClientCertificateBoundAccessTokens != null) {
|
||||||
|
if (tlsClientCertificateBoundAccessTokens.booleanValue()) configWrapper.setUseMtlsHoKToken(true);
|
||||||
|
else configWrapper.setUseMtlsHoKToken(false);
|
||||||
|
}
|
||||||
|
|
||||||
return client;
|
return client;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -188,6 +194,13 @@ public class DescriptionConverter {
|
||||||
if (config.isUseJwksUrl()) {
|
if (config.isUseJwksUrl()) {
|
||||||
response.setJwksUri(config.getJwksUrl());
|
response.setJwksUri(config.getJwksUrl());
|
||||||
}
|
}
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.5
|
||||||
|
if (config.isUseMtlsHokToken()) {
|
||||||
|
response.setTlsClientCertificateBoundAccessTokens(Boolean.TRUE);
|
||||||
|
} else {
|
||||||
|
response.setTlsClientCertificateBoundAccessTokens(Boolean.FALSE);
|
||||||
|
}
|
||||||
|
|
||||||
List<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client);
|
List<ProtocolMapperRepresentation> foundPairwiseMappers = PairwiseSubMapperUtils.getPairwiseSubMappers(client);
|
||||||
SubjectType subjectType = foundPairwiseMappers.isEmpty() ? SubjectType.PUBLIC : SubjectType.PAIRWISE;
|
SubjectType subjectType = foundPairwiseMappers.isEmpty() ? SubjectType.PUBLIC : SubjectType.PAIRWISE;
|
||||||
|
|
|
@ -0,0 +1,142 @@
|
||||||
|
package org.keycloak.services.util;
|
||||||
|
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
|
||||||
|
import org.jboss.logging.Logger;
|
||||||
|
import org.jboss.resteasy.spi.HttpRequest;
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
import org.keycloak.jose.jws.JWSInputException;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.RefreshToken;
|
||||||
|
|
||||||
|
public class MtlsHoKTokenUtil {
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3.1
|
||||||
|
|
||||||
|
// retrieve client certificate exchanged in TLS handshake
|
||||||
|
// https://docs.oracle.com/javaee/6/api/javax/servlet/ServletRequest.html#getAttribute(java.lang.String)
|
||||||
|
private static final String JAVAX_SERVLET_REQUEST_X509_CERTIFICATE = "javax.servlet.request.X509Certificate";
|
||||||
|
|
||||||
|
protected static final Logger logger = Logger.getLogger(MtlsHoKTokenUtil.class);
|
||||||
|
|
||||||
|
private static final String DIGEST_ALG = "SHA-256";
|
||||||
|
|
||||||
|
public static final String CERT_VERIFY_ERROR_DESC = "Client certificate missing, or its thumbprint and one in the refresh token did NOT match";
|
||||||
|
|
||||||
|
|
||||||
|
public static AccessToken.CertConf bindTokenWithClientCertificate(HttpRequest request) {
|
||||||
|
X509Certificate[] certs = (X509Certificate[]) request.getAttribute(JAVAX_SERVLET_REQUEST_X509_CERTIFICATE);
|
||||||
|
|
||||||
|
if (certs == null || certs.length < 1) {
|
||||||
|
logger.warnf("no client certificate available.");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String DERX509Base64UrlEncoded = null;
|
||||||
|
try {
|
||||||
|
// On Certificate Chain, first entry is considered to be client certificate.
|
||||||
|
DERX509Base64UrlEncoded = getCertificateThumbprintInSHA256DERX509Base64UrlEncoded(certs[0]);
|
||||||
|
if (logger.isTraceEnabled()) dumpCertInfo(certs);
|
||||||
|
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||||
|
// give up issuing MTLS HoK Token
|
||||||
|
logger.warnf("give up issuing hok token. %s", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
AccessToken.CertConf certConf = new AccessToken.CertConf();
|
||||||
|
certConf.setCertThumbprint(DERX509Base64UrlEncoded);
|
||||||
|
return certConf;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean verifyTokenBindingWithClientCertificate(AccessToken token, HttpRequest request) {
|
||||||
|
X509Certificate[] certs = (X509Certificate[]) request.getAttribute(JAVAX_SERVLET_REQUEST_X509_CERTIFICATE);
|
||||||
|
|
||||||
|
if (token == null) {
|
||||||
|
logger.warnf("token is null");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bearer Token, not MTLS HoK Token
|
||||||
|
if (token.getCertConf() == null) {
|
||||||
|
logger.warnf("bearer token received instead of hok token.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// HoK Token, but no Client Certificate available
|
||||||
|
if (certs == null || certs.length < 1) {
|
||||||
|
logger.warnf("missing client certificate.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String DERX509Base64UrlEncoded = null;
|
||||||
|
String x5ts256 = token.getCertConf().getCertThumbprint();
|
||||||
|
logger.tracef("hok token cnf-x5t#s256 = %s", x5ts256);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// On Certificate Chain, first entry is considered to be client certificate.
|
||||||
|
DERX509Base64UrlEncoded = getCertificateThumbprintInSHA256DERX509Base64UrlEncoded(certs[0]);
|
||||||
|
if (logger.isTraceEnabled()) dumpCertInfo(certs);
|
||||||
|
} catch (NoSuchAlgorithmException | CertificateEncodingException e) {
|
||||||
|
logger.warnf("client certificate exception. %s", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MessageDigest.isEqual(x5ts256.getBytes(), DERX509Base64UrlEncoded.getBytes())) {
|
||||||
|
logger.warnf("certificate's thumbprint and one in the refresh token did not match.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean verifyTokenBindingWithClientCertificate(String refreshToken, HttpRequest request) {
|
||||||
|
JWSInput jws = null;
|
||||||
|
RefreshToken rt = null;
|
||||||
|
|
||||||
|
try {
|
||||||
|
jws = new JWSInput(refreshToken);
|
||||||
|
rt = jws.readJsonContent(RefreshToken.class);
|
||||||
|
} catch (JWSInputException e) {
|
||||||
|
logger.warnf("refresh token JWS Input Exception. %s", e);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return verifyTokenBindingWithClientCertificate(rt, request);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String getCertificateThumbprintInSHA256DERX509Base64UrlEncoded (X509Certificate cert) throws NoSuchAlgorithmException, CertificateEncodingException {
|
||||||
|
// need to calculate over DER encoding of the X.509 certificate
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3.1
|
||||||
|
// in order to do that, call getEncoded()
|
||||||
|
// https://docs.oracle.com/javase/8/docs/api/java/security/cert/Certificate.html#getEncoded--
|
||||||
|
byte[] DERX509Hash = cert.getEncoded();
|
||||||
|
MessageDigest md = MessageDigest.getInstance(DIGEST_ALG);
|
||||||
|
md.update(DERX509Hash);
|
||||||
|
String DERX509Base64UrlEncoded = Base64Url.encode(md.digest());
|
||||||
|
return DERX509Base64UrlEncoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void dumpCertInfo(X509Certificate[] certs) throws CertificateEncodingException {
|
||||||
|
logger.tracef(":: Try Holder of Key Token");
|
||||||
|
logger.tracef(":: # of x509 Client Certificate in Certificate Chain = %d", certs.length);
|
||||||
|
for (int i = 0; i < certs.length; i++) {
|
||||||
|
logger.tracef(":: certs[%d] Raw Bytes Counts of first x509 Client Certificate in Certificate Chain = %d", i, certs[i].toString().length());
|
||||||
|
logger.tracef(":: certs[%d] Raw Bytes String of first x509 Client Certificate in Certificate Chain = %s", i, certs[i].toString());
|
||||||
|
logger.tracef(":: certs[%d] DER Dump Bytes of first x509 Client Certificate in Certificate Chain = %d", i, certs[i].getEncoded().length);
|
||||||
|
String DERX509Base64UrlEncoded = null;
|
||||||
|
try {
|
||||||
|
DERX509Base64UrlEncoded = getCertificateThumbprintInSHA256DERX509Base64UrlEncoded(certs[i]);
|
||||||
|
} catch (Exception e) {}
|
||||||
|
logger.tracef(":: certs[%d] Base64URL Encoded SHA-256 Hash of DER formatted first x509 Client Certificate in Certificate Chain = %s", i, DERX509Base64UrlEncoded);
|
||||||
|
logger.tracef(":: certs[%d] DER Dump Bytes of first x509 Client Certificate TBScertificate in Certificate Chain = %d", i, certs[i].getTBSCertificate().length);
|
||||||
|
logger.tracef(":: certs[%d] Signature Algorithm of first x509 Client Certificate in Certificate Chain = %s", i, certs[i].getSigAlgName());
|
||||||
|
logger.tracef(":: certs[%d] Certfication Type of first x509 Client Certificate in Certificate Chain = %s", i, certs[i].getType());
|
||||||
|
logger.tracef(":: certs[%d] Issuer DN of first x509 Client Certificate in Certificate Chain = %s", i, certs[i].getIssuerDN().getName());
|
||||||
|
logger.tracef(":: certs[%d] Subject DN of first x509 Client Certificate in Certificate Chain = %s", i, certs[i].getSubjectDN().getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -370,6 +370,17 @@ To run the X.509 client certificate authentication tests:
|
||||||
-Dbrowser=phantomjs \
|
-Dbrowser=phantomjs \
|
||||||
"-Dtest=*.x509.*"
|
"-Dtest=*.x509.*"
|
||||||
|
|
||||||
|
## Run Mutual TLS Client Certificate Bound Access Tokens tests
|
||||||
|
|
||||||
|
To run the Mutual TLS Client Certificate Bound Access Tokens tests:
|
||||||
|
|
||||||
|
mvn -f testsuite/integration-arquillian/pom.xml \
|
||||||
|
clean install \
|
||||||
|
-Pauth-server-wildfly \
|
||||||
|
-Dauth.server.ssl.required \
|
||||||
|
-Dbrowser=phantomjs \
|
||||||
|
-Dtest=org.keycloak.testsuite.hok.HoKTest
|
||||||
|
|
||||||
## Cluster tests
|
## Cluster tests
|
||||||
|
|
||||||
Cluster tests use 2 backend servers (Keycloak on Wildfly/EAP) and 1 frontend loadbalancer server node. Invalidation tests don't use loadbalancer.
|
Cluster tests use 2 backend servers (Keycloak on Wildfly/EAP) and 1 frontend loadbalancer server node. Invalidation tests don't use loadbalancer.
|
||||||
|
|
Binary file not shown.
|
@ -243,6 +243,8 @@
|
||||||
<include>client.key</include>
|
<include>client.key</include>
|
||||||
<include>intermediate-ca.crl</include>
|
<include>intermediate-ca.crl</include>
|
||||||
<include>empty.crl</include>
|
<include>empty.crl</include>
|
||||||
|
<!-- KEYCLOAK-6771 Certificate Bound Token -->
|
||||||
|
<include>other_client.jks</include>
|
||||||
</includes>
|
</includes>
|
||||||
</resource>
|
</resource>
|
||||||
<resource>
|
<resource>
|
||||||
|
|
|
@ -43,6 +43,8 @@
|
||||||
<exclude.crossdc>**/crossdc/**/*Test.java</exclude.crossdc>
|
<exclude.crossdc>**/crossdc/**/*Test.java</exclude.crossdc>
|
||||||
<!-- exclude x509 tests by default, enabled by 'ssl' profile -->
|
<!-- exclude x509 tests by default, enabled by 'ssl' profile -->
|
||||||
<exclude.x509>**/x509/*Test.java</exclude.x509>
|
<exclude.x509>**/x509/*Test.java</exclude.x509>
|
||||||
|
<!-- KEYCLOAK-6771 exclude Mutual TLS Holder of Key Token x509 tests by default, enabled by 'ssl' profile -->
|
||||||
|
<exclude.HoK>**/hok/**/*Test.java</exclude.HoK>
|
||||||
<!-- exclude undertow adapter tests. They can be added by -Dtest=org.keycloak.testsuite.adapter.undertow.**.*Test -->
|
<!-- exclude undertow adapter tests. They can be added by -Dtest=org.keycloak.testsuite.adapter.undertow.**.*Test -->
|
||||||
<exclude.undertow.adapter>**/adapter/undertow/**/*Test.java</exclude.undertow.adapter>
|
<exclude.undertow.adapter>**/adapter/undertow/**/*Test.java</exclude.undertow.adapter>
|
||||||
</properties>
|
</properties>
|
||||||
|
@ -160,6 +162,7 @@
|
||||||
<exclude>${exclude.crossdc}</exclude>
|
<exclude>${exclude.crossdc}</exclude>
|
||||||
<exclude>${exclude.undertow.adapter}</exclude>
|
<exclude>${exclude.undertow.adapter}</exclude>
|
||||||
<exclude>${exclude.x509}</exclude>
|
<exclude>${exclude.x509}</exclude>
|
||||||
|
<exclude>${exclude.HoK}</exclude>
|
||||||
</excludes>
|
</excludes>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
|
|
@ -0,0 +1,138 @@
|
||||||
|
package org.keycloak.testsuite.util;
|
||||||
|
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.KeyStoreException;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.security.cert.CertificateEncodingException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.Enumeration;
|
||||||
|
|
||||||
|
import javax.ws.rs.client.Client;
|
||||||
|
import javax.ws.rs.client.ClientBuilder;
|
||||||
|
import javax.ws.rs.client.WebTarget;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.apache.http.impl.client.HttpClientBuilder;
|
||||||
|
import org.keycloak.common.util.Base64Url;
|
||||||
|
import org.keycloak.common.util.KeystoreUtil;
|
||||||
|
|
||||||
|
public class HoKTokenUtils {
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
|
||||||
|
|
||||||
|
public static final String DEFAULT_KEYSTOREPATH = System.getProperty("client.certificate.keystore");
|
||||||
|
public static final String DEFAULT_KEYSTOREPASSWORD = System.getProperty("client.certificate.keystore.passphrase");
|
||||||
|
public static final String DEFAULT_TRUSTSTOREPATH = System.getProperty("client.truststore");
|
||||||
|
public static final String DEFAULT_TRUSTSTOREPASSWORD = System.getProperty("client.truststore.passphrase");
|
||||||
|
|
||||||
|
public static final String OTHER_KEYSTOREPATH = System.getProperty("hok.client.certificate.keystore");
|
||||||
|
public static final String OTHER_KEYSTOREPASSWORD = System.getProperty("hok.client.certificate.keystore.passphrase");
|
||||||
|
|
||||||
|
public static CloseableHttpClient newCloseableHttpClientWithDefaultKeyStoreAndTrustStore() {
|
||||||
|
return newCloseableHttpClient(DEFAULT_KEYSTOREPATH, DEFAULT_KEYSTOREPASSWORD, DEFAULT_TRUSTSTOREPATH, DEFAULT_TRUSTSTOREPASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CloseableHttpClient newCloseableHttpClientWithOtherKeyStoreAndTrustStore() {
|
||||||
|
return newCloseableHttpClient(OTHER_KEYSTOREPATH, OTHER_KEYSTOREPASSWORD, DEFAULT_TRUSTSTOREPATH, DEFAULT_TRUSTSTOREPASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CloseableHttpClient newCloseableHttpClientWithoutKeyStoreAndTrustStore() {
|
||||||
|
return newCloseableHttpClient(null, null, null, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CloseableHttpClient newCloseableHttpClient(String keyStorePath, String keyStorePassword, String trustStorePath, String trustStorePassword) {
|
||||||
|
|
||||||
|
KeyStore keystore = null;
|
||||||
|
// Load the keystore file
|
||||||
|
if (keyStorePath != null) {
|
||||||
|
try {
|
||||||
|
keystore = KeystoreUtil.loadKeyStore(keyStorePath, keyStorePassword);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// load the trustore
|
||||||
|
KeyStore truststore = null;
|
||||||
|
if (trustStorePath != null) {
|
||||||
|
try {
|
||||||
|
truststore = KeystoreUtil.loadKeyStore(trustStorePath, trustStorePassword);
|
||||||
|
} catch(Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (keystore != null || truststore != null)
|
||||||
|
return (CloseableHttpClient) new org.keycloak.adapters.HttpClientBuilder()
|
||||||
|
.keyStore(keystore, keyStorePassword)
|
||||||
|
.trustStore(truststore)
|
||||||
|
.hostnameVerification(org.keycloak.adapters.HttpClientBuilder.HostnameVerificationPolicy.ANY)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
return HttpClientBuilder.create().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getThumbprintFromDefaultClientCert() throws KeyStoreException, CertificateEncodingException {
|
||||||
|
return getThumbprintFromClientCert(DEFAULT_KEYSTOREPATH, DEFAULT_KEYSTOREPASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getThumbprintFromOtherClientCert() throws KeyStoreException, CertificateEncodingException {
|
||||||
|
return getThumbprintFromClientCert(OTHER_KEYSTOREPATH, OTHER_KEYSTOREPASSWORD);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getThumbprintFromClientCert(String keyStorePath, String keyStorePassword) throws KeyStoreException, CertificateEncodingException {
|
||||||
|
KeyStore keystore = null;
|
||||||
|
// load the keystore containing the client certificate - keystore type is probably jks or pkcs12
|
||||||
|
try {
|
||||||
|
keystore = KeystoreUtil.loadKeyStore(keyStorePath, keyStorePassword);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
Enumeration<String> es = keystore.aliases();
|
||||||
|
String alias = null;
|
||||||
|
while(es.hasMoreElements()) {
|
||||||
|
alias = es.nextElement();
|
||||||
|
}
|
||||||
|
X509Certificate cert = (X509Certificate) keystore.getCertificate(alias);
|
||||||
|
String digestAlg = "SHA-256";
|
||||||
|
byte[] DERX509Hash = cert.getEncoded();
|
||||||
|
String DERX509Base64UrlEncoded = null;
|
||||||
|
try {
|
||||||
|
MessageDigest md = MessageDigest.getInstance(digestAlg);
|
||||||
|
md.update(DERX509Hash);
|
||||||
|
DERX509Base64UrlEncoded = Base64Url.encode(md.digest());
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
return DERX509Base64UrlEncoded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Response executeUserInfoRequestInGetMethod(String accessToken, boolean isKeystoreUsed, String keystorePath, String keystorePassward) {
|
||||||
|
ClientBuilder clientBuilder = ClientBuilder.newBuilder();
|
||||||
|
KeyStore keystore = null;
|
||||||
|
// Load the keystore file
|
||||||
|
if(isKeystoreUsed) {
|
||||||
|
try {
|
||||||
|
if (keystorePath != null) {
|
||||||
|
keystore = KeystoreUtil.loadKeyStore(keystorePath, keystorePassward);
|
||||||
|
clientBuilder.keyStore(keystore, keystorePassward);
|
||||||
|
} else {
|
||||||
|
keystore = KeystoreUtil.loadKeyStore(DEFAULT_KEYSTOREPATH, DEFAULT_KEYSTOREPASSWORD);
|
||||||
|
clientBuilder.keyStore(keystore, DEFAULT_KEYSTOREPASSWORD);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Client client = clientBuilder.build();
|
||||||
|
WebTarget userInfoTarget = null;
|
||||||
|
try {
|
||||||
|
userInfoTarget = UserInfoClientUtil.getUserInfoWebTarget(client);
|
||||||
|
} finally {
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
return userInfoTarget.request().header(HttpHeaders.AUTHORIZATION, "bearer " + accessToken).get();
|
||||||
|
}
|
||||||
|
}
|
|
@ -279,8 +279,17 @@ public class OAuthClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
public AccessTokenResponse doAccessTokenRequest(String code, String password) {
|
public AccessTokenResponse doAccessTokenRequest(String code, String password) {
|
||||||
try (CloseableHttpClient client = newCloseableHttpClient()) {
|
try (CloseableHttpClient client = newCloseableHttpClient()) {
|
||||||
|
return doAccessTokenRequest(code, password, client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
public AccessTokenResponse doAccessTokenRequest(String code, String password, CloseableHttpClient client) {
|
||||||
HttpPost post = new HttpPost(getAccessTokenUrl());
|
HttpPost post = new HttpPost(getAccessTokenUrl());
|
||||||
|
|
||||||
List<NameValuePair> parameters = new LinkedList<>();
|
List<NameValuePair> parameters = new LinkedList<>();
|
||||||
|
@ -323,21 +332,19 @@ public class OAuthClient {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Failed to retrieve access token", e);
|
throw new RuntimeException("Failed to retrieve access token", e);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
public String introspectTokenWithClientCredential(String clientId, String clientSecret, String tokenType, String tokenToIntrospect) {
|
||||||
|
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
|
||||||
|
return introspectTokenWithClientCredential(clientId, clientSecret, tokenType, tokenToIntrospect, client);
|
||||||
} catch (IOException ioe) {
|
} catch (IOException ioe) {
|
||||||
throw new RuntimeException(ioe);
|
throw new RuntimeException(ioe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public String introspectAccessTokenWithClientCredential(String clientId, String clientSecret, String tokenToIntrospect) {
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
return introspectTokenWithClientCredential(clientId, clientSecret, "access_token", tokenToIntrospect);
|
public String introspectTokenWithClientCredential(String clientId, String clientSecret, String tokenType, String tokenToIntrospect, CloseableHttpClient client) {
|
||||||
}
|
|
||||||
|
|
||||||
public String introspectRefreshTokenWithClientCredential(String clientId, String clientSecret, String tokenToIntrospect) {
|
|
||||||
return introspectTokenWithClientCredential(clientId, clientSecret, "refresh_token", tokenToIntrospect);
|
|
||||||
}
|
|
||||||
|
|
||||||
public String introspectTokenWithClientCredential(String clientId, String clientSecret, String tokenType, String tokenToIntrospect) {
|
|
||||||
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
|
|
||||||
HttpPost post = new HttpPost(getTokenIntrospectionUrl());
|
HttpPost post = new HttpPost(getTokenIntrospectionUrl());
|
||||||
|
|
||||||
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
|
String authorization = BasicAuthHelper.createHeader(clientId, clientSecret);
|
||||||
|
@ -366,9 +373,14 @@ public class OAuthClient {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Failed to retrieve access token", e);
|
throw new RuntimeException("Failed to retrieve access token", e);
|
||||||
}
|
}
|
||||||
} catch (IOException ioe) {
|
|
||||||
throw new RuntimeException(ioe);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String introspectAccessTokenWithClientCredential(String clientId, String clientSecret, String tokenToIntrospect) {
|
||||||
|
return introspectTokenWithClientCredential(clientId, clientSecret, "access_token", tokenToIntrospect);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String introspectRefreshTokenWithClientCredential(String clientId, String clientSecret, String tokenToIntrospect) {
|
||||||
|
return introspectTokenWithClientCredential(clientId, clientSecret, "refresh_token", tokenToIntrospect);
|
||||||
}
|
}
|
||||||
|
|
||||||
public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username, String password) throws Exception {
|
public AccessTokenResponse doGrantAccessTokenRequest(String clientSecret, String username, String password) throws Exception {
|
||||||
|
@ -532,9 +544,17 @@ public class OAuthClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
public CloseableHttpResponse doLogout(String refreshToken, String clientSecret) throws IOException {
|
public CloseableHttpResponse doLogout(String refreshToken, String clientSecret) {
|
||||||
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
|
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
|
||||||
|
return doLogout(refreshToken, clientSecret, client);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
public CloseableHttpResponse doLogout(String refreshToken, String clientSecret, CloseableHttpClient client) throws IOException {
|
||||||
HttpPost post = new HttpPost(getLogoutUrl().build());
|
HttpPost post = new HttpPost(getLogoutUrl().build());
|
||||||
|
|
||||||
List<NameValuePair> parameters = new LinkedList<>();
|
List<NameValuePair> parameters = new LinkedList<>();
|
||||||
|
@ -558,10 +578,18 @@ public class OAuthClient {
|
||||||
|
|
||||||
return client.execute(post);
|
return client.execute(post);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
public AccessTokenResponse doRefreshTokenRequest(String refreshToken, String password) {
|
public AccessTokenResponse doRefreshTokenRequest(String refreshToken, String password) {
|
||||||
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
|
try (CloseableHttpClient client = HttpClientBuilder.create().build()) {
|
||||||
|
return doRefreshTokenRequest(refreshToken, password, client);
|
||||||
|
} catch (IOException ex) {
|
||||||
|
throw new RuntimeException(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
public AccessTokenResponse doRefreshTokenRequest(String refreshToken, String password, CloseableHttpClient client) {
|
||||||
HttpPost post = new HttpPost(getRefreshTokenUrl());
|
HttpPost post = new HttpPost(getRefreshTokenUrl());
|
||||||
|
|
||||||
List<NameValuePair> parameters = new LinkedList<>();
|
List<NameValuePair> parameters = new LinkedList<>();
|
||||||
|
@ -600,9 +628,6 @@ public class OAuthClient {
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
throw new RuntimeException("Failed to retrieve access token", e);
|
throw new RuntimeException("Failed to retrieve access token", e);
|
||||||
}
|
}
|
||||||
} catch (IOException ex) {
|
|
||||||
throw new RuntimeException(ex);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void closeClient(CloseableHttpClient client) {
|
public void closeClient(CloseableHttpClient client) {
|
||||||
|
@ -1098,5 +1123,4 @@ public class OAuthClient {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -222,6 +222,46 @@ public class OIDCClientRegistrationTest extends AbstractClientRegistrationTest {
|
||||||
Assert.assertFalse(kcClientRep.isPublicClient());
|
Assert.assertFalse(kcClientRep.isPublicClient());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.5
|
||||||
|
@Test
|
||||||
|
public void testMtlsHoKTokenEnabled() throws Exception {
|
||||||
|
// create (no specification)
|
||||||
|
OIDCClientRepresentation clientRep = createRep();
|
||||||
|
|
||||||
|
OIDCClientRepresentation response = reg.oidc().create(clientRep);
|
||||||
|
Assert.assertEquals(Boolean.FALSE, response.getTlsClientCertificateBoundAccessTokens());
|
||||||
|
Assert.assertNotNull(response.getClientSecret());
|
||||||
|
|
||||||
|
// Test Keycloak representation
|
||||||
|
ClientRepresentation kcClient = getClient(response.getClientId());
|
||||||
|
OIDCAdvancedConfigWrapper config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
|
||||||
|
assertTrue(!config.isUseMtlsHokToken());
|
||||||
|
|
||||||
|
// update (true)
|
||||||
|
reg.auth(Auth.token(response));
|
||||||
|
response.setTlsClientCertificateBoundAccessTokens(Boolean.TRUE);
|
||||||
|
OIDCClientRepresentation updated = reg.oidc().update(response);
|
||||||
|
assertTrue(updated.getTlsClientCertificateBoundAccessTokens().booleanValue());
|
||||||
|
|
||||||
|
// Test Keycloak representation
|
||||||
|
kcClient = getClient(updated.getClientId());
|
||||||
|
config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
|
||||||
|
assertTrue(config.isUseMtlsHokToken());
|
||||||
|
|
||||||
|
// update (false)
|
||||||
|
reg.auth(Auth.token(updated));
|
||||||
|
updated.setTlsClientCertificateBoundAccessTokens(Boolean.FALSE);
|
||||||
|
OIDCClientRepresentation reUpdated = reg.oidc().update(updated);
|
||||||
|
assertTrue(!reUpdated.getTlsClientCertificateBoundAccessTokens().booleanValue());
|
||||||
|
|
||||||
|
// Test Keycloak representation
|
||||||
|
kcClient = getClient(reUpdated.getClientId());
|
||||||
|
config = OIDCAdvancedConfigWrapper.fromClientRepresentation(kcClient);
|
||||||
|
assertTrue(!config.isUseMtlsHokToken());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
private ClientRepresentation getKeycloakClient(String clientId) {
|
private ClientRepresentation getKeycloakClient(String clientId) {
|
||||||
return ApiUtil.findClientByClientId(adminClient.realms().realm(REALM_NAME), clientId).toRepresentation();
|
return ApiUtil.findClientByClientId(adminClient.realms().realm(REALM_NAME), clientId).toRepresentation();
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,663 @@
|
||||||
|
package org.keycloak.testsuite.hok;
|
||||||
|
|
||||||
|
import static org.hamcrest.Matchers.allOf;
|
||||||
|
import static org.hamcrest.Matchers.greaterThanOrEqualTo;
|
||||||
|
import static org.hamcrest.Matchers.lessThanOrEqualTo;
|
||||||
|
import static org.junit.Assert.assertEquals;
|
||||||
|
import static org.junit.Assert.assertNotNull;
|
||||||
|
import static org.junit.Assert.assertNull;
|
||||||
|
import static org.junit.Assert.assertThat;
|
||||||
|
import static org.junit.Assert.assertTrue;
|
||||||
|
import static org.keycloak.testsuite.admin.ApiUtil.findUserByUsername;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URI;
|
||||||
|
import java.security.KeyStore;
|
||||||
|
import java.security.MessageDigest;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import javax.ws.rs.client.Client;
|
||||||
|
import javax.ws.rs.client.ClientBuilder;
|
||||||
|
import javax.ws.rs.client.WebTarget;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
|
import javax.ws.rs.core.Response;
|
||||||
|
import javax.ws.rs.core.Response.Status;
|
||||||
|
|
||||||
|
import org.apache.http.client.methods.CloseableHttpResponse;
|
||||||
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
import org.hamcrest.Matchers;
|
||||||
|
import org.jboss.arquillian.drone.api.annotation.Drone;
|
||||||
|
import org.junit.Assert;
|
||||||
|
import org.junit.Before;
|
||||||
|
import org.junit.Rule;
|
||||||
|
import org.junit.Test;
|
||||||
|
import org.keycloak.OAuth2Constants;
|
||||||
|
import org.keycloak.OAuthErrorException;
|
||||||
|
import org.keycloak.admin.client.resource.ClientResource;
|
||||||
|
import org.keycloak.common.util.KeystoreUtil;
|
||||||
|
import org.keycloak.events.Details;
|
||||||
|
import org.keycloak.events.EventType;
|
||||||
|
import org.keycloak.jose.jws.Algorithm;
|
||||||
|
import org.keycloak.jose.jws.JWSHeader;
|
||||||
|
import org.keycloak.jose.jws.JWSInput;
|
||||||
|
import org.keycloak.jose.jws.JWSInputException;
|
||||||
|
import org.keycloak.jose.jws.crypto.HashProvider;
|
||||||
|
import org.keycloak.protocol.oidc.OIDCAdvancedConfigWrapper;
|
||||||
|
import org.keycloak.protocol.oidc.utils.OIDCResponseType;
|
||||||
|
import org.keycloak.representations.AccessToken;
|
||||||
|
import org.keycloak.representations.IDToken;
|
||||||
|
import org.keycloak.representations.RefreshToken;
|
||||||
|
import org.keycloak.representations.idm.ClientRepresentation;
|
||||||
|
import org.keycloak.representations.idm.CredentialRepresentation;
|
||||||
|
import org.keycloak.representations.idm.EventRepresentation;
|
||||||
|
import org.keycloak.representations.idm.RealmRepresentation;
|
||||||
|
import org.keycloak.representations.idm.UserRepresentation;
|
||||||
|
import org.keycloak.representations.oidc.TokenMetadataRepresentation;
|
||||||
|
import org.keycloak.testsuite.AbstractKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.AbstractTestRealmKeycloakTest;
|
||||||
|
import org.keycloak.testsuite.AssertEvents;
|
||||||
|
import org.keycloak.testsuite.admin.ApiUtil;
|
||||||
|
import org.keycloak.testsuite.drone.Different;
|
||||||
|
import org.keycloak.testsuite.util.ClientManager;
|
||||||
|
import org.keycloak.testsuite.util.HoKTokenUtils;
|
||||||
|
import org.keycloak.testsuite.util.KeycloakModelUtils;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient;
|
||||||
|
import org.keycloak.testsuite.util.UserInfoClientUtil;
|
||||||
|
import org.keycloak.testsuite.util.OAuthClient.AccessTokenResponse;
|
||||||
|
import org.keycloak.util.JsonSerialization;
|
||||||
|
import org.openqa.selenium.WebDriver;
|
||||||
|
|
||||||
|
|
||||||
|
public class HoKTest extends AbstractTestRealmKeycloakTest {
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
|
||||||
|
|
||||||
|
@Drone
|
||||||
|
@Different
|
||||||
|
protected WebDriver driver2;
|
||||||
|
|
||||||
|
private static final List<String> CLIENT_LIST = Arrays.asList("test-app", "named-test-app");
|
||||||
|
|
||||||
|
public static class HoKAssertEvents extends AssertEvents {
|
||||||
|
|
||||||
|
public HoKAssertEvents(AbstractKeycloakTest ctx) {
|
||||||
|
super(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
private final String defaultRedirectUri = "https://localhost:8543/auth/realms/master/app/auth";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ExpectedEvent expectLogin() {
|
||||||
|
return expect(EventType.LOGIN)
|
||||||
|
.detail(Details.CODE_ID, isCodeId())
|
||||||
|
//.detail(Details.USERNAME, DEFAULT_USERNAME)
|
||||||
|
//.detail(Details.AUTH_METHOD, OIDCLoginProtocol.LOGIN_PROTOCOL)
|
||||||
|
//.detail(Details.AUTH_TYPE, AuthorizationEndpoint.CODE_AUTH_TYPE)
|
||||||
|
.detail(Details.REDIRECT_URI, defaultRedirectUri)
|
||||||
|
.detail(Details.CONSENT, Details.CONSENT_VALUE_NO_CONSENT_REQUIRED)
|
||||||
|
.session(isUUID());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Rule
|
||||||
|
public HoKAssertEvents events = new HoKAssertEvents(this);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void configureTestRealm(RealmRepresentation testRealm) {
|
||||||
|
// override due to effects caused by enabling TLS
|
||||||
|
for (String clientId : CLIENT_LIST) addRedirectUrlForTls(testRealm, clientId);
|
||||||
|
|
||||||
|
// for token introspection
|
||||||
|
configTestRealmForTokenIntrospection(testRealm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addRedirectUrlForTls(RealmRepresentation testRealm, String clientId) {
|
||||||
|
for (ClientRepresentation client : testRealm.getClients()) {
|
||||||
|
if (client.getClientId().equals(clientId)) {
|
||||||
|
URI baseUri = URI.create(client.getRedirectUris().get(0));
|
||||||
|
URI redir = URI.create("https://localhost:" + System.getProperty("app.server.https.port", "8543") + baseUri.getRawPath());
|
||||||
|
client.getRedirectUris().add(redir.toString());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void configTestRealmForTokenIntrospection(RealmRepresentation testRealm) {
|
||||||
|
ClientRepresentation confApp = KeycloakModelUtils.createClient(testRealm, "confidential-cli");
|
||||||
|
confApp.setSecret("secret1");
|
||||||
|
confApp.setServiceAccountsEnabled(Boolean.TRUE);
|
||||||
|
|
||||||
|
ClientRepresentation pubApp = KeycloakModelUtils.createClient(testRealm, "public-cli");
|
||||||
|
pubApp.setPublicClient(Boolean.TRUE);
|
||||||
|
|
||||||
|
UserRepresentation user = new UserRepresentation();
|
||||||
|
user.setUsername("no-permissions");
|
||||||
|
CredentialRepresentation credential = new CredentialRepresentation();
|
||||||
|
credential.setType("password");
|
||||||
|
credential.setValue("password");
|
||||||
|
List<CredentialRepresentation> creds = new ArrayList<>();
|
||||||
|
creds.add(credential);
|
||||||
|
user.setCredentials(creds);
|
||||||
|
user.setEnabled(Boolean.TRUE);
|
||||||
|
List<String> realmRoles = new ArrayList<>();
|
||||||
|
realmRoles.add("user");
|
||||||
|
user.setRealmRoles(realmRoles);
|
||||||
|
testRealm.getUsers().add(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
// enable HoK Token as default
|
||||||
|
@Before
|
||||||
|
public void enableHoKToken() {
|
||||||
|
// Enable MTLS HoK Token
|
||||||
|
for (String clientId : CLIENT_LIST) enableHoKToken(clientId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void enableHoKToken(String clientId) {
|
||||||
|
// Enable MTLS HoK Token
|
||||||
|
ClientResource clientResource = ApiUtil.findClientByClientId(adminClient.realm("test"), clientId);
|
||||||
|
ClientRepresentation clientRep = clientResource.toRepresentation();
|
||||||
|
OIDCAdvancedConfigWrapper.fromClientRepresentation(clientRep).setUseMtlsHoKToken(true);
|
||||||
|
clientResource.update(clientRep);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Authorization Code Flow
|
||||||
|
// Bind HoK Token
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void accessTokenRequestWithClientCertificate() throws Exception {
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
AccessTokenResponse response;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
response = oauth.doAccessTokenRequest(code, "password", client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success Pattern
|
||||||
|
expectSuccessfulResponseFromTokenEndpoint(sessionId, codeId, response);
|
||||||
|
verifyHoKTokenDefaultCertThumbPrint(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void accessTokenRequestWithoutClientCertificate() throws Exception {
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
AccessTokenResponse response;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
|
||||||
|
response = oauth.doAccessTokenRequest(code, "password", client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error Pattern
|
||||||
|
assertEquals(400, response.getStatusCode());
|
||||||
|
assertEquals(OAuthErrorException.INVALID_REQUEST, response.getError());
|
||||||
|
assertEquals("Client Certification missing for MTLS HoK Token Binding", response.getErrorDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expectSuccessfulResponseFromTokenEndpoint(String sessionId, String codeId, AccessTokenResponse response) throws Exception {
|
||||||
|
assertEquals(200, response.getStatusCode());
|
||||||
|
|
||||||
|
Assert.assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
|
||||||
|
Assert.assertThat(response.getRefreshExpiresIn(), allOf(greaterThanOrEqualTo(1750), lessThanOrEqualTo(1800)));
|
||||||
|
|
||||||
|
assertEquals("bearer", response.getTokenType());
|
||||||
|
|
||||||
|
String expectedKid = oauth.doCertsRequest("test").getKeys()[0].getKeyId();
|
||||||
|
|
||||||
|
JWSHeader header = new JWSInput(response.getAccessToken()).getHeader();
|
||||||
|
assertEquals("RS256", header.getAlgorithm().name());
|
||||||
|
assertEquals("JWT", header.getType());
|
||||||
|
assertEquals(expectedKid, header.getKeyId());
|
||||||
|
assertNull(header.getContentType());
|
||||||
|
|
||||||
|
header = new JWSInput(response.getIdToken()).getHeader();
|
||||||
|
assertEquals("RS256", header.getAlgorithm().name());
|
||||||
|
assertEquals("JWT", header.getType());
|
||||||
|
assertEquals(expectedKid, header.getKeyId());
|
||||||
|
assertNull(header.getContentType());
|
||||||
|
|
||||||
|
header = new JWSInput(response.getRefreshToken()).getHeader();
|
||||||
|
assertEquals("RS256", header.getAlgorithm().name());
|
||||||
|
assertEquals("JWT", header.getType());
|
||||||
|
assertEquals(expectedKid, header.getKeyId());
|
||||||
|
assertNull(header.getContentType());
|
||||||
|
|
||||||
|
AccessToken token = oauth.verifyToken(response.getAccessToken());
|
||||||
|
|
||||||
|
assertEquals(findUserByUsername(adminClient.realm("test"), "test-user@localhost").getId(), token.getSubject());
|
||||||
|
Assert.assertNotEquals("test-user@localhost", token.getSubject());
|
||||||
|
|
||||||
|
assertEquals(sessionId, token.getSessionState());
|
||||||
|
|
||||||
|
assertEquals(1, token.getRealmAccess().getRoles().size());
|
||||||
|
assertTrue(token.getRealmAccess().isUserInRole("user"));
|
||||||
|
|
||||||
|
assertEquals(1, token.getResourceAccess(oauth.getClientId()).getRoles().size());
|
||||||
|
assertTrue(token.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user"));
|
||||||
|
|
||||||
|
EventRepresentation event = events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||||
|
assertEquals(token.getId(), event.getDetails().get(Details.TOKEN_ID));
|
||||||
|
assertEquals(oauth.verifyRefreshToken(response.getRefreshToken()).getId(), event.getDetails().get(Details.REFRESH_TOKEN_ID));
|
||||||
|
assertEquals(sessionId, token.getSessionState());
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify HoK Token - Token Refresh
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void refreshTokenRequestByHoKRefreshTokenByOtherClient() throws Exception {
|
||||||
|
// first client user login
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
AccessTokenResponse tokenResponse = null;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
tokenResponse = oauth.doAccessTokenRequest(code, "password", client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
verifyHoKTokenDefaultCertThumbPrint(tokenResponse);
|
||||||
|
String refreshTokenString = tokenResponse.getRefreshToken();
|
||||||
|
|
||||||
|
// second client user login
|
||||||
|
OAuthClient oauth2 = new OAuthClient();
|
||||||
|
oauth2.init(adminClient, driver2);
|
||||||
|
oauth2.doLogin("john-doh@localhost", "password");
|
||||||
|
String code2 = oauth2.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
AccessTokenResponse tokenResponse2 = null;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithOtherKeyStoreAndTrustStore()) {
|
||||||
|
tokenResponse2 = oauth2.doAccessTokenRequest(code2, "password", client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
verifyHoKTokenOtherCertThumbPrint(tokenResponse2);
|
||||||
|
|
||||||
|
// token refresh by second client by first client's refresh token
|
||||||
|
AccessTokenResponse response = null;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithOtherKeyStoreAndTrustStore()) {
|
||||||
|
response = oauth2.doRefreshTokenRequest(refreshTokenString, "password", client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error Pattern
|
||||||
|
assertEquals(401, response.getStatusCode());
|
||||||
|
assertEquals(OAuthErrorException.UNAUTHORIZED_CLIENT, response.getError());
|
||||||
|
assertEquals("Client certificate missing, or its thumbprint and one in the refresh token did NOT match", response.getErrorDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void refreshTokenRequestByHoKRefreshTokenWithClientCertificate() throws Exception {
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
AccessTokenResponse tokenResponse = null;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
tokenResponse = oauth.doAccessTokenRequest(code, "password", client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
verifyHoKTokenDefaultCertThumbPrint(tokenResponse);
|
||||||
|
AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
|
||||||
|
String refreshTokenString = tokenResponse.getRefreshToken();
|
||||||
|
RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString);
|
||||||
|
EventRepresentation tokenEvent = events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||||
|
|
||||||
|
Assert.assertNotNull(refreshTokenString);
|
||||||
|
assertEquals("bearer", tokenResponse.getTokenType());
|
||||||
|
Assert.assertThat(token.getExpiration() - getCurrentTime(), allOf(greaterThanOrEqualTo(200), lessThanOrEqualTo(350)));
|
||||||
|
int actual = refreshToken.getExpiration() - getCurrentTime();
|
||||||
|
Assert.assertThat(actual, allOf(greaterThanOrEqualTo(1799), lessThanOrEqualTo(1800)));
|
||||||
|
assertEquals(sessionId, refreshToken.getSessionState());
|
||||||
|
|
||||||
|
setTimeOffset(2);
|
||||||
|
|
||||||
|
AccessTokenResponse response = null;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
response = oauth.doRefreshTokenRequest(refreshTokenString, "password", client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success Pattern
|
||||||
|
expectSuccessfulResponseFromTokenEndpoint(response, sessionId, token, refreshToken, tokenEvent);
|
||||||
|
verifyHoKTokenDefaultCertThumbPrint(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void refreshTokenRequestByRefreshTokenWithoutClientCertificate() throws Exception {
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
AccessTokenResponse tokenResponse = null;
|
||||||
|
tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
|
||||||
|
verifyHoKTokenDefaultCertThumbPrint(tokenResponse);
|
||||||
|
AccessToken token = oauth.verifyToken(tokenResponse.getAccessToken());
|
||||||
|
String refreshTokenString = tokenResponse.getRefreshToken();
|
||||||
|
RefreshToken refreshToken = oauth.verifyRefreshToken(refreshTokenString);
|
||||||
|
|
||||||
|
Assert.assertNotNull(refreshTokenString);
|
||||||
|
assertEquals("bearer", tokenResponse.getTokenType());
|
||||||
|
Assert.assertThat(token.getExpiration() - getCurrentTime(), allOf(greaterThanOrEqualTo(200), lessThanOrEqualTo(350)));
|
||||||
|
int actual = refreshToken.getExpiration() - getCurrentTime();
|
||||||
|
Assert.assertThat(actual, allOf(greaterThanOrEqualTo(1799), lessThanOrEqualTo(1800)));
|
||||||
|
assertEquals(sessionId, refreshToken.getSessionState());
|
||||||
|
|
||||||
|
setTimeOffset(2);
|
||||||
|
|
||||||
|
AccessTokenResponse response = null;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
|
||||||
|
response = oauth.doRefreshTokenRequest(refreshTokenString, "password", client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error Pattern
|
||||||
|
assertEquals(401, response.getStatusCode());
|
||||||
|
assertEquals(OAuthErrorException.UNAUTHORIZED_CLIENT, response.getError());
|
||||||
|
assertEquals("Client certificate missing, or its thumbprint and one in the refresh token did NOT match", response.getErrorDescription());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expectSuccessfulResponseFromTokenEndpoint(AccessTokenResponse response, String sessionId, AccessToken token, RefreshToken refreshToken, EventRepresentation tokenEvent) {
|
||||||
|
expectSuccessfulResponseFromTokenEndpoint(oauth, "test-user@localhost", response, sessionId, token, refreshToken, tokenEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void expectSuccessfulResponseFromTokenEndpoint(OAuthClient oauth, String username, AccessTokenResponse response, String sessionId, AccessToken token, RefreshToken refreshToken, EventRepresentation tokenEvent) {
|
||||||
|
AccessToken refreshedToken = oauth.verifyToken(response.getAccessToken());
|
||||||
|
RefreshToken refreshedRefreshToken = oauth.verifyRefreshToken(response.getRefreshToken());
|
||||||
|
if (refreshedToken.getCertConf() != null) {
|
||||||
|
log.warnf("refreshed access token's cnf-x5t#256 = %s", refreshedToken.getCertConf().getCertThumbprint());
|
||||||
|
log.warnf("refreshed refresh token's cnf-x5t#256 = %s", refreshedRefreshToken.getCertConf().getCertThumbprint());
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(200, response.getStatusCode());
|
||||||
|
|
||||||
|
assertEquals(sessionId, refreshedToken.getSessionState());
|
||||||
|
assertEquals(sessionId, refreshedRefreshToken.getSessionState());
|
||||||
|
|
||||||
|
Assert.assertThat(response.getExpiresIn(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
|
||||||
|
Assert.assertThat(refreshedToken.getExpiration() - getCurrentTime(), allOf(greaterThanOrEqualTo(250), lessThanOrEqualTo(300)));
|
||||||
|
|
||||||
|
Assert.assertThat(refreshedToken.getExpiration() - token.getExpiration(), allOf(greaterThanOrEqualTo(1), lessThanOrEqualTo(10)));
|
||||||
|
Assert.assertThat(refreshedRefreshToken.getExpiration() - refreshToken.getExpiration(), allOf(greaterThanOrEqualTo(1), lessThanOrEqualTo(10)));
|
||||||
|
|
||||||
|
Assert.assertNotEquals(token.getId(), refreshedToken.getId());
|
||||||
|
Assert.assertNotEquals(refreshToken.getId(), refreshedRefreshToken.getId());
|
||||||
|
|
||||||
|
assertEquals("bearer", response.getTokenType());
|
||||||
|
|
||||||
|
assertEquals(findUserByUsername(adminClient.realm("test"), username).getId(), refreshedToken.getSubject());
|
||||||
|
Assert.assertNotEquals(username, refreshedToken.getSubject());
|
||||||
|
|
||||||
|
assertEquals(1, refreshedToken.getRealmAccess().getRoles().size());
|
||||||
|
Assert.assertTrue(refreshedToken.getRealmAccess().isUserInRole("user"));
|
||||||
|
|
||||||
|
assertEquals(1, refreshedToken.getResourceAccess(oauth.getClientId()).getRoles().size());
|
||||||
|
Assert.assertTrue(refreshedToken.getResourceAccess(oauth.getClientId()).isUserInRole("customer-user"));
|
||||||
|
|
||||||
|
EventRepresentation refreshEvent = events.expectRefresh(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), sessionId).user(AssertEvents.isUUID()).assertEvent();
|
||||||
|
Assert.assertNotEquals(tokenEvent.getDetails().get(Details.TOKEN_ID), refreshEvent.getDetails().get(Details.TOKEN_ID));
|
||||||
|
Assert.assertNotEquals(tokenEvent.getDetails().get(Details.REFRESH_TOKEN_ID), refreshEvent.getDetails().get(Details.UPDATED_REFRESH_TOKEN_ID));
|
||||||
|
|
||||||
|
setTimeOffset(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify HoK Token - Get UserInfo
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getUserInfoByHoKAccessTokenWithClientCertificate() throws Exception {
|
||||||
|
// get an access token
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
AccessTokenResponse tokenResponse = null;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
tokenResponse = oauth.doAccessTokenRequest(code, "password", client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
verifyHoKTokenDefaultCertThumbPrint(tokenResponse);
|
||||||
|
events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||||
|
|
||||||
|
// execute the access token to get UserInfo with token binded client certificate in mutual authentication TLS
|
||||||
|
ClientBuilder clientBuilder = ClientBuilder.newBuilder();
|
||||||
|
KeyStore keystore = null;
|
||||||
|
keystore = KeystoreUtil.loadKeyStore(HoKTokenUtils.DEFAULT_KEYSTOREPATH, HoKTokenUtils.DEFAULT_KEYSTOREPASSWORD);
|
||||||
|
clientBuilder.keyStore(keystore, HoKTokenUtils.DEFAULT_KEYSTOREPASSWORD);
|
||||||
|
Client client = clientBuilder.build();
|
||||||
|
WebTarget userInfoTarget = null;
|
||||||
|
Response response = null;
|
||||||
|
try {
|
||||||
|
userInfoTarget = UserInfoClientUtil.getUserInfoWebTarget(client);
|
||||||
|
response = userInfoTarget.request().header(HttpHeaders.AUTHORIZATION, "bearer " + tokenResponse.getAccessToken()).get();
|
||||||
|
testSuccessfulUserInfoResponse(response);
|
||||||
|
} finally {
|
||||||
|
response.close();
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void getUserInfoByHoKAccessTokenWithoutClientCertificate() throws Exception {
|
||||||
|
// get an access token
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||||
|
String sessionId = loginEvent.getSessionId();
|
||||||
|
String codeId = loginEvent.getDetails().get(Details.CODE_ID);
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
|
||||||
|
AccessTokenResponse tokenResponse = null;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
tokenResponse = oauth.doAccessTokenRequest(code, "password", client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
verifyHoKTokenDefaultCertThumbPrint(tokenResponse);
|
||||||
|
events.expectCodeToToken(codeId, sessionId).assertEvent();
|
||||||
|
|
||||||
|
// execute the access token to get UserInfo without token binded client certificate in mutual authentication TLS
|
||||||
|
ClientBuilder clientBuilder = ClientBuilder.newBuilder();
|
||||||
|
Client client = clientBuilder.build();
|
||||||
|
WebTarget userInfoTarget = null;
|
||||||
|
Response response = null;
|
||||||
|
try {
|
||||||
|
userInfoTarget = UserInfoClientUtil.getUserInfoWebTarget(client);
|
||||||
|
response = userInfoTarget.request().header(HttpHeaders.AUTHORIZATION, "bearer " + tokenResponse.getAccessToken()).get();
|
||||||
|
assertEquals(401, response.getStatus());
|
||||||
|
} finally {
|
||||||
|
response.close();
|
||||||
|
client.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void testSuccessfulUserInfoResponse(Response response) {
|
||||||
|
events.expect(EventType.USER_INFO_REQUEST)
|
||||||
|
.session(Matchers.notNullValue(String.class))
|
||||||
|
.detail(Details.AUTH_METHOD, Details.VALIDATE_ACCESS_TOKEN)
|
||||||
|
.detail(Details.USERNAME, "test-user@localhost")
|
||||||
|
.detail(Details.SIGNATURE_REQUIRED, "false")
|
||||||
|
.assertEvent();
|
||||||
|
UserInfoClientUtil.testSuccessfulUserInfoResponse(response, "test-user@localhost", "test-user@localhost");
|
||||||
|
}
|
||||||
|
|
||||||
|
// verify HoK Token - Back Channel Logout
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void postLogoutByHoKRefreshTokenWithClientCertificate() throws Exception {
|
||||||
|
String refreshTokenString = execPreProcessPostLogout();
|
||||||
|
|
||||||
|
CloseableHttpResponse response = null;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
response = oauth.doLogout(refreshTokenString, "password", client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success Pattern
|
||||||
|
assertThat(response, org.keycloak.testsuite.util.Matchers.statusCodeIsHC(Status.NO_CONTENT));
|
||||||
|
assertNotNull(testingClient.testApp().getAdminLogoutAction());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void postLogoutByHoKRefreshTokenWithoutClientCertificate() throws Exception {
|
||||||
|
String refreshTokenString = execPreProcessPostLogout();
|
||||||
|
|
||||||
|
CloseableHttpResponse response = null;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
|
||||||
|
response = oauth.doLogout(refreshTokenString, "password", client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
// Error Pattern
|
||||||
|
assertEquals(401, response.getStatusLine().getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String execPreProcessPostLogout() throws Exception {
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
oauth.clientSessionState("client-session");
|
||||||
|
AccessTokenResponse tokenResponse = oauth.doAccessTokenRequest(code, "password");
|
||||||
|
verifyHoKTokenDefaultCertThumbPrint(tokenResponse);
|
||||||
|
|
||||||
|
return tokenResponse.getRefreshToken();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hybrid Code Flow : response_type = code id_token
|
||||||
|
// Bind HoK Token
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void accessTokenRequestWithClientCertificateInHybridFlowWithCodeIDToken() throws Exception {
|
||||||
|
String nonce = "ckw938gnspa93dj";
|
||||||
|
ClientManager.realm(adminClient.realm("test")).clientId("test-app").standardFlow(true).implicitFlow(true);
|
||||||
|
oauth.clientId("test-app");
|
||||||
|
oauth.responseType(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN);
|
||||||
|
oauth.nonce(nonce);
|
||||||
|
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||||
|
OAuthClient.AuthorizationEndpointResponse authzResponse = new OAuthClient.AuthorizationEndpointResponse(oauth, true);
|
||||||
|
Assert.assertNotNull(authzResponse.getSessionState());
|
||||||
|
List<IDToken> idTokens = testAuthzResponseAndRetrieveIDTokens(authzResponse, loginEvent);
|
||||||
|
for (IDToken idToken : idTokens) {
|
||||||
|
Assert.assertEquals(nonce, idToken.getNonce());
|
||||||
|
Assert.assertEquals(authzResponse.getSessionState(), idToken.getSessionState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected List<IDToken> testAuthzResponseAndRetrieveIDTokens(OAuthClient.AuthorizationEndpointResponse authzResponse, EventRepresentation loginEvent) {
|
||||||
|
Assert.assertEquals(OIDCResponseType.CODE + " " + OIDCResponseType.ID_TOKEN, loginEvent.getDetails().get(Details.RESPONSE_TYPE));
|
||||||
|
|
||||||
|
// IDToken from the authorization response
|
||||||
|
Assert.assertNull(authzResponse.getAccessToken());
|
||||||
|
String idTokenStr = authzResponse.getIdToken();
|
||||||
|
IDToken idToken = oauth.verifyIDToken(idTokenStr);
|
||||||
|
|
||||||
|
// Validate "c_hash"
|
||||||
|
Assert.assertNull(idToken.getAccessTokenHash());
|
||||||
|
Assert.assertNotNull(idToken.getCodeHash());
|
||||||
|
Assert.assertEquals(idToken.getCodeHash(), HashProvider.oidcHash(Algorithm.RS256, authzResponse.getCode()));
|
||||||
|
|
||||||
|
// IDToken exchanged for the code
|
||||||
|
IDToken idToken2 = sendTokenRequestAndGetIDToken(loginEvent);
|
||||||
|
|
||||||
|
return Arrays.asList(idToken, idToken2);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testIntrospectHoKAccessToken() throws Exception {
|
||||||
|
// get an access token with client certificate in mutual authenticate TLS
|
||||||
|
// mimic Client
|
||||||
|
oauth.doLogin("test-user@localhost", "password");
|
||||||
|
String code = oauth.getCurrentQuery().get(OAuth2Constants.CODE);
|
||||||
|
EventRepresentation loginEvent = events.expectLogin().assertEvent();
|
||||||
|
AccessTokenResponse accessTokenResponse = null;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithDefaultKeyStoreAndTrustStore()) {
|
||||||
|
accessTokenResponse = oauth.doAccessTokenRequest(code, "password", client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do token introspection
|
||||||
|
// mimic Resource Server
|
||||||
|
String tokenResponse;
|
||||||
|
try (CloseableHttpClient client = HoKTokenUtils.newCloseableHttpClientWithoutKeyStoreAndTrustStore()) {
|
||||||
|
tokenResponse = oauth.introspectTokenWithClientCredential("confidential-cli", "secret1", "access_token", accessTokenResponse.getAccessToken(), client);
|
||||||
|
} catch (IOException ioe) {
|
||||||
|
throw new RuntimeException(ioe);
|
||||||
|
}
|
||||||
|
TokenMetadataRepresentation rep = JsonSerialization.readValue(tokenResponse, TokenMetadataRepresentation.class);
|
||||||
|
JWSInput jws = new JWSInput(accessTokenResponse.getAccessToken());
|
||||||
|
AccessToken at = jws.readJsonContent(AccessToken.class);
|
||||||
|
jws = new JWSInput(accessTokenResponse.getRefreshToken());
|
||||||
|
RefreshToken rt = jws.readJsonContent(RefreshToken.class);
|
||||||
|
String certThumprintFromAccessToken = at.getCertConf().getCertThumbprint();
|
||||||
|
String certThumprintFromRefreshToken = rt.getCertConf().getCertThumbprint();
|
||||||
|
String certThumprintFromTokenIntrospection = rep.getCertConf().getCertThumbprint();
|
||||||
|
String certThumprintFromBoundClientCertificate = HoKTokenUtils.getThumbprintFromDefaultClientCert();
|
||||||
|
|
||||||
|
assertTrue(rep.isActive());
|
||||||
|
assertEquals("test-user@localhost", rep.getUserName());
|
||||||
|
assertEquals("test-app", rep.getClientId());
|
||||||
|
assertEquals(loginEvent.getUserId(), rep.getSubject());
|
||||||
|
|
||||||
|
assertEquals(certThumprintFromTokenIntrospection, certThumprintFromBoundClientCertificate);
|
||||||
|
assertEquals(certThumprintFromBoundClientCertificate, certThumprintFromAccessToken);
|
||||||
|
assertEquals(certThumprintFromAccessToken, certThumprintFromRefreshToken);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void verifyHoKTokenDefaultCertThumbPrint(AccessTokenResponse response) throws Exception {
|
||||||
|
verifyHoKTokenCertThumbPrint(response, HoKTokenUtils.getThumbprintFromDefaultClientCert());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyHoKTokenOtherCertThumbPrint(AccessTokenResponse response) throws Exception {
|
||||||
|
verifyHoKTokenCertThumbPrint(response, HoKTokenUtils.getThumbprintFromOtherClientCert());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifyHoKTokenCertThumbPrint(AccessTokenResponse response, String certThumbPrint) {
|
||||||
|
JWSInput jws = null;
|
||||||
|
AccessToken at = null;
|
||||||
|
try {
|
||||||
|
jws = new JWSInput(response.getAccessToken());
|
||||||
|
at = jws.readJsonContent(AccessToken.class);
|
||||||
|
} catch (JWSInputException e) {
|
||||||
|
Assert.fail(e.toString());
|
||||||
|
}
|
||||||
|
assertTrue(MessageDigest.isEqual(certThumbPrint.getBytes(), at.getCertConf().getCertThumbprint().getBytes()));
|
||||||
|
|
||||||
|
RefreshToken rt = null;
|
||||||
|
try {
|
||||||
|
jws = new JWSInput(response.getRefreshToken());
|
||||||
|
rt = jws.readJsonContent(RefreshToken.class);
|
||||||
|
} catch (JWSInputException e) {
|
||||||
|
Assert.fail(e.toString());
|
||||||
|
}
|
||||||
|
assertTrue(MessageDigest.isEqual(certThumbPrint.getBytes(), rt.getCertConf().getCertThumbprint().getBytes()));
|
||||||
|
}
|
||||||
|
}
|
|
@ -123,6 +123,11 @@ public class OIDCWellKnownProviderTest extends AbstractKeycloakTest {
|
||||||
// KEYCLOAK-7451 OAuth Authorization Server Metadata for Proof Key for Code Exchange
|
// KEYCLOAK-7451 OAuth Authorization Server Metadata for Proof Key for Code Exchange
|
||||||
// PKCE support
|
// PKCE support
|
||||||
Assert.assertNames(oidcConfig.getCodeChallengeMethodsSupported(), OAuth2Constants.PKCE_METHOD_PLAIN, OAuth2Constants.PKCE_METHOD_S256);
|
Assert.assertNames(oidcConfig.getCodeChallengeMethodsSupported(), OAuth2Constants.PKCE_METHOD_PLAIN, OAuth2Constants.PKCE_METHOD_S256);
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.2
|
||||||
|
Assert.assertTrue(oidcConfig.getTlsClientCertificateBoundAccessTokens());
|
||||||
|
|
||||||
} finally {
|
} finally {
|
||||||
client.close();
|
client.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -131,6 +131,10 @@
|
||||||
<client.key.file>${auth.server.config.dir}/client.key</client.key.file>
|
<client.key.file>${auth.server.config.dir}/client.key</client.key.file>
|
||||||
<client.key.passphrase>secret</client.key.passphrase>
|
<client.key.passphrase>secret</client.key.passphrase>
|
||||||
|
|
||||||
|
<!-- KEYCLOAK-6771 Certificate Bound Token -->
|
||||||
|
<hok.client.certificate.keystore>${auth.server.config.dir}/other_client.jks</hok.client.certificate.keystore>
|
||||||
|
<hok.client.certificate.keystore.passphrase>secret</hok.client.certificate.keystore.passphrase>
|
||||||
|
|
||||||
<auth.server.ocsp.responder.enabled>false</auth.server.ocsp.responder.enabled>
|
<auth.server.ocsp.responder.enabled>false</auth.server.ocsp.responder.enabled>
|
||||||
<keycloak.x509cert.lookup.provider>default</keycloak.x509cert.lookup.provider>
|
<keycloak.x509cert.lookup.provider>default</keycloak.x509cert.lookup.provider>
|
||||||
</properties>
|
</properties>
|
||||||
|
@ -304,6 +308,10 @@
|
||||||
<client.key.file>${client.key.file}</client.key.file>
|
<client.key.file>${client.key.file}</client.key.file>
|
||||||
<client.key.passphrase>${client.key.passphrase}</client.key.passphrase>
|
<client.key.passphrase>${client.key.passphrase}</client.key.passphrase>
|
||||||
|
|
||||||
|
<!-- KEYCLOAK-6771 Certificate Bound Token -->
|
||||||
|
<hok.client.certificate.keystore>${hok.client.certificate.keystore}</hok.client.certificate.keystore>
|
||||||
|
<hok.client.certificate.keystore.passphrase>${hok.client.certificate.keystore.passphrase}</hok.client.certificate.keystore.passphrase>
|
||||||
|
|
||||||
<auth.server.ocsp.responder.enabled>${auth.server.ocsp.responder.enabled}</auth.server.ocsp.responder.enabled>
|
<auth.server.ocsp.responder.enabled>${auth.server.ocsp.responder.enabled}</auth.server.ocsp.responder.enabled>
|
||||||
|
|
||||||
<!--cache server properties-->
|
<!--cache server properties-->
|
||||||
|
|
|
@ -608,27 +608,22 @@ gitlab-application-secret=Application Secret
|
||||||
gitlab.application-id.tooltip=Application Id for the application you created in your GitLab Applications account menu
|
gitlab.application-id.tooltip=Application Id for the application you created in your GitLab Applications account menu
|
||||||
gitlab.application-secret.tooltip=Secret for the application that you created in your GitLab Applications account menu
|
gitlab.application-secret.tooltip=Secret for the application that you created in your GitLab Applications account menu
|
||||||
gitlab.default-scopes.tooltip=Scopes to ask for on login. Will always ask for openid. Additionally adds api if you do not specify anything.
|
gitlab.default-scopes.tooltip=Scopes to ask for on login. Will always ask for openid. Additionally adds api if you do not specify anything.
|
||||||
|
|
||||||
bitbucket-consumer-key=Consumer Key
|
bitbucket-consumer-key=Consumer Key
|
||||||
bitbucket-consumer-secret=Consumer Secret
|
bitbucket-consumer-secret=Consumer Secret
|
||||||
bitbucket.key.tooltip=Bitbucket OAuth Consumer Key
|
bitbucket.key.tooltip=Bitbucket OAuth Consumer Key
|
||||||
bitbucket.secret.tooltip=Bitbucket OAuth Consumer Secret
|
bitbucket.secret.tooltip=Bitbucket OAuth Consumer Secret
|
||||||
bitbucket.default-scopes.tooltip=Scopes to ask for on login. If you do not specify anything, scope defaults to 'email'.
|
bitbucket.default-scopes.tooltip=Scopes to ask for on login. If you do not specify anything, scope defaults to 'email'.
|
||||||
|
|
||||||
# User federation
|
# User federation
|
||||||
sync-ldap-roles-to-keycloak=Sync LDAP Roles To Keycloak
|
sync-ldap-roles-to-keycloak=Sync LDAP Roles To Keycloak
|
||||||
sync-keycloak-roles-to-ldap=Sync Keycloak Roles To LDAP
|
sync-keycloak-roles-to-ldap=Sync Keycloak Roles To LDAP
|
||||||
sync-ldap-groups-to-keycloak=Sync LDAP Groups To Keycloak
|
sync-ldap-groups-to-keycloak=Sync LDAP Groups To Keycloak
|
||||||
sync-keycloak-groups-to-ldap=Sync Keycloak Groups To LDAP
|
sync-keycloak-groups-to-ldap=Sync Keycloak Groups To LDAP
|
||||||
|
|
||||||
realms=Realms
|
realms=Realms
|
||||||
realm=Realm
|
realm=Realm
|
||||||
|
|
||||||
identity-provider-mappers=Identity Provider Mappers
|
identity-provider-mappers=Identity Provider Mappers
|
||||||
create-identity-provider-mapper=Create Identity Provider Mapper
|
create-identity-provider-mapper=Create Identity Provider Mapper
|
||||||
add-identity-provider-mapper=Add Identity Provider Mapper
|
add-identity-provider-mapper=Add Identity Provider Mapper
|
||||||
client.description.tooltip=Specifies description of the client. For example 'My Client for TimeSheets'. Supports keys for localized values as well. For example\: ${my_client_description}
|
client.description.tooltip=Specifies description of the client. For example 'My Client for TimeSheets'. Supports keys for localized values as well. For example\: ${my_client_description}
|
||||||
|
|
||||||
expires=Expires
|
expires=Expires
|
||||||
expiration=Expiration
|
expiration=Expiration
|
||||||
expiration.tooltip=Specifies how long the token should be valid
|
expiration.tooltip=Specifies how long the token should be valid
|
||||||
|
@ -645,7 +640,6 @@ continue=Continue
|
||||||
initial-access-token.confirm.title=Copy Initial Access Token
|
initial-access-token.confirm.title=Copy Initial Access Token
|
||||||
initial-access-token.confirm.text=Please copy and paste the initial access token before confirming as it can't be retrieved later
|
initial-access-token.confirm.text=Please copy and paste the initial access token before confirming as it can't be retrieved later
|
||||||
no-initial-access-available=No Initial Access Tokens available
|
no-initial-access-available=No Initial Access Tokens available
|
||||||
|
|
||||||
client-reg-policies=Client Registration Policies
|
client-reg-policies=Client Registration Policies
|
||||||
client-reg-policy.name.tooltip=Display Name of the policy
|
client-reg-policy.name.tooltip=Display Name of the policy
|
||||||
anonymous-policies=Anonymous Access Policies
|
anonymous-policies=Anonymous Access Policies
|
||||||
|
@ -812,14 +806,12 @@ ldap-attribute-name-for-uuid=LDAP attribute name for UUID
|
||||||
uuid-ldap-attribute.tooltip=Name of LDAP attribute, which is used as unique object identifier (UUID) for objects in LDAP. For many LDAP server vendors it's 'entryUUID' however some are different. For example for Active directory it should be 'objectGUID'. If your LDAP server really doesn't support the notion of UUID, you can use any other attribute, which is supposed to be unique among LDAP users in tree. For example 'uid' or 'entryDN'.
|
uuid-ldap-attribute.tooltip=Name of LDAP attribute, which is used as unique object identifier (UUID) for objects in LDAP. For many LDAP server vendors it's 'entryUUID' however some are different. For example for Active directory it should be 'objectGUID'. If your LDAP server really doesn't support the notion of UUID, you can use any other attribute, which is supposed to be unique among LDAP users in tree. For example 'uid' or 'entryDN'.
|
||||||
user-object-classes=User Object Classes
|
user-object-classes=User Object Classes
|
||||||
ldap-user-object-classes.placeholder=LDAP User Object Classes (div. by comma)
|
ldap-user-object-classes.placeholder=LDAP User Object Classes (div. by comma)
|
||||||
|
|
||||||
ldap-connection-url=LDAP connection URL
|
ldap-connection-url=LDAP connection URL
|
||||||
ldap-users-dn=LDAP Users DN
|
ldap-users-dn=LDAP Users DN
|
||||||
ldap-bind-dn=LDAP Bind DN
|
ldap-bind-dn=LDAP Bind DN
|
||||||
ldap-bind-credentials=LDAP Bind Credentials
|
ldap-bind-credentials=LDAP Bind Credentials
|
||||||
ldap-filter=LDAP Filter
|
ldap-filter=LDAP Filter
|
||||||
ldap.user-object-classes.tooltip=All values of LDAP objectClass attribute for users in LDAP divided by comma. For example: 'inetOrgPerson, organizationalPerson' . Newly created Keycloak users will be written to LDAP with all those object classes and existing LDAP user records are found just if they contain all those object classes.
|
ldap.user-object-classes.tooltip=All values of LDAP objectClass attribute for users in LDAP divided by comma. For example: 'inetOrgPerson, organizationalPerson' . Newly created Keycloak users will be written to LDAP with all those object classes and existing LDAP user records are found just if they contain all those object classes.
|
||||||
|
|
||||||
connection-url=Connection URL
|
connection-url=Connection URL
|
||||||
ldap.connection-url.tooltip=Connection URL to your LDAP server
|
ldap.connection-url.tooltip=Connection URL to your LDAP server
|
||||||
test-connection=Test connection
|
test-connection=Test connection
|
||||||
|
@ -893,7 +885,6 @@ identity-provider-user-id.tooltip=Unique ID of the user on the Identity Provider
|
||||||
identity-provider-username=Identity Provider Username
|
identity-provider-username=Identity Provider Username
|
||||||
identity-provider-username.tooltip=Username on the Identity Provider side
|
identity-provider-username.tooltip=Username on the Identity Provider side
|
||||||
pagination=Pagination
|
pagination=Pagination
|
||||||
|
|
||||||
browser-flow=Browser Flow
|
browser-flow=Browser Flow
|
||||||
browser-flow.tooltip=Select the flow you want to use for browser authentication.
|
browser-flow.tooltip=Select the flow you want to use for browser authentication.
|
||||||
registration-flow=Registration Flow
|
registration-flow=Registration Flow
|
||||||
|
@ -976,7 +967,6 @@ default-groups=Default Groups
|
||||||
groups.default-groups.tooltip=Set of groups that new users will automatically join.
|
groups.default-groups.tooltip=Set of groups that new users will automatically join.
|
||||||
cut=Cut
|
cut=Cut
|
||||||
paste=Paste
|
paste=Paste
|
||||||
|
|
||||||
create-group=Create group
|
create-group=Create group
|
||||||
create-authenticator-execution=Create Authenticator Execution
|
create-authenticator-execution=Create Authenticator Execution
|
||||||
create-form-action-execution=Create Form Action Execution
|
create-form-action-execution=Create Form Action Execution
|
||||||
|
@ -1137,7 +1127,6 @@ saved-types=Saved Types
|
||||||
clear-admin-events=Clear admin events
|
clear-admin-events=Clear admin events
|
||||||
clear-changes=Clear changes
|
clear-changes=Clear changes
|
||||||
error=Error
|
error=Error
|
||||||
|
|
||||||
# Authz
|
# Authz
|
||||||
# Authz Common
|
# Authz Common
|
||||||
authz-authorization=Authorization
|
authz-authorization=Authorization
|
||||||
|
@ -1175,22 +1164,17 @@ authz-show-details=Show Details
|
||||||
authz-hide-details=Hide Details
|
authz-hide-details=Hide Details
|
||||||
authz-associated-permissions=Associated Permissions
|
authz-associated-permissions=Associated Permissions
|
||||||
authz-no-permission-associated=No permissions associated
|
authz-no-permission-associated=No permissions associated
|
||||||
|
|
||||||
# Authz Settings
|
# Authz Settings
|
||||||
authz-import-config.tooltip=Import a JSON file containing authorization settings for this resource server.
|
authz-import-config.tooltip=Import a JSON file containing authorization settings for this resource server.
|
||||||
|
|
||||||
authz-policy-enforcement-mode=Policy Enforcement Mode
|
authz-policy-enforcement-mode=Policy Enforcement Mode
|
||||||
authz-policy-enforcement-mode.tooltip=The policy enforcement mode dictates how policies are enforced when evaluating authorization requests. 'Enforcing' means requests are denied by default even when there is no policy associated with a given resource. 'Permissive' means requests are allowed even when there is no policy associated with a given resource. 'Disabled' completely disables the evaluation of policies and allows access to any resource.
|
authz-policy-enforcement-mode.tooltip=The policy enforcement mode dictates how policies are enforced when evaluating authorization requests. 'Enforcing' means requests are denied by default even when there is no policy associated with a given resource. 'Permissive' means requests are allowed even when there is no policy associated with a given resource. 'Disabled' completely disables the evaluation of policies and allows access to any resource.
|
||||||
authz-policy-enforcement-mode-enforcing=Enforcing
|
authz-policy-enforcement-mode-enforcing=Enforcing
|
||||||
authz-policy-enforcement-mode-permissive=Permissive
|
authz-policy-enforcement-mode-permissive=Permissive
|
||||||
authz-policy-enforcement-mode-disabled=Disabled
|
authz-policy-enforcement-mode-disabled=Disabled
|
||||||
|
|
||||||
authz-remote-resource-management=Remote Resource Management
|
authz-remote-resource-management=Remote Resource Management
|
||||||
authz-remote-resource-management.tooltip=Should resources be managed remotely by the resource server? If false, resources can be managed only from this admin console.
|
authz-remote-resource-management.tooltip=Should resources be managed remotely by the resource server? If false, resources can be managed only from this admin console.
|
||||||
|
|
||||||
authz-export-settings=Export Settings
|
authz-export-settings=Export Settings
|
||||||
authz-export-settings.tooltip=Export and download all authorization settings for this resource server.
|
authz-export-settings.tooltip=Export and download all authorization settings for this resource server.
|
||||||
|
|
||||||
# Authz Resource List
|
# Authz Resource List
|
||||||
authz-no-resources-available=No resources available.
|
authz-no-resources-available=No resources available.
|
||||||
authz-no-scopes-assigned=No scopes assigned.
|
authz-no-scopes-assigned=No scopes assigned.
|
||||||
|
@ -1199,7 +1183,6 @@ authz-no-uri-defined=No URI defined.
|
||||||
authz-no-permission-assigned=No permission assigned.
|
authz-no-permission-assigned=No permission assigned.
|
||||||
authz-no-policy-assigned=No policy assigned.
|
authz-no-policy-assigned=No policy assigned.
|
||||||
authz-create-permission=Create Permission
|
authz-create-permission=Create Permission
|
||||||
|
|
||||||
# Authz Resource Detail
|
# Authz Resource Detail
|
||||||
authz-add-resource=Add Resource
|
authz-add-resource=Add Resource
|
||||||
authz-resource-name.tooltip=A unique name for this resource. The name can be used to uniquely identify a resource, useful when querying for a specific resource.
|
authz-resource-name.tooltip=A unique name for this resource. The name can be used to uniquely identify a resource, useful when querying for a specific resource.
|
||||||
|
@ -1215,15 +1198,12 @@ authz-resource-user-managed-access-enabled.tooltip=If enabled this access to thi
|
||||||
# Authz Scope List
|
# Authz Scope List
|
||||||
authz-add-scope=Add Scope
|
authz-add-scope=Add Scope
|
||||||
authz-no-scopes-available=No scopes available.
|
authz-no-scopes-available=No scopes available.
|
||||||
|
|
||||||
# Authz Scope Detail
|
# Authz Scope Detail
|
||||||
authz-scope-name.tooltip=A unique name for this scope. The name can be used to uniquely identify a scope, useful when querying for a specific scope.
|
authz-scope-name.tooltip=A unique name for this scope. The name can be used to uniquely identify a scope, useful when querying for a specific scope.
|
||||||
|
|
||||||
# Authz Policy List
|
# Authz Policy List
|
||||||
authz-all-types=All types
|
authz-all-types=All types
|
||||||
authz-create-policy=Create Policy
|
authz-create-policy=Create Policy
|
||||||
authz-no-policies-available=No policies available.
|
authz-no-policies-available=No policies available.
|
||||||
|
|
||||||
# Authz Policy Detail
|
# Authz Policy Detail
|
||||||
authz-policy-name.tooltip=The name of this policy.
|
authz-policy-name.tooltip=The name of this policy.
|
||||||
authz-policy-description.tooltip=A description for this policy.
|
authz-policy-description.tooltip=A description for this policy.
|
||||||
|
@ -1240,24 +1220,20 @@ authz-policy-decision-strategy-unanimous=Unanimous
|
||||||
authz-policy-decision-strategy-consensus=Consensus
|
authz-policy-decision-strategy-consensus=Consensus
|
||||||
authz-select-a-policy=Select existing policy
|
authz-select-a-policy=Select existing policy
|
||||||
authz-no-policies-assigned=No policies assigned.
|
authz-no-policies-assigned=No policies assigned.
|
||||||
|
|
||||||
# Authz Role Policy Detail
|
# Authz Role Policy Detail
|
||||||
authz-add-role-policy=Add Role Policy
|
authz-add-role-policy=Add Role Policy
|
||||||
authz-no-roles-assigned=No roles assigned.
|
authz-no-roles-assigned=No roles assigned.
|
||||||
authz-policy-role-realm-roles.tooltip=Specifies the *realm* roles allowed by this policy.
|
authz-policy-role-realm-roles.tooltip=Specifies the *realm* roles allowed by this policy.
|
||||||
authz-policy-role-clients.tooltip=Selects a client in order to filter the client roles that can be applied to this policy.
|
authz-policy-role-clients.tooltip=Selects a client in order to filter the client roles that can be applied to this policy.
|
||||||
authz-policy-role-client-roles.tooltip=Specifies the client roles allowed by this policy.
|
authz-policy-role-client-roles.tooltip=Specifies the client roles allowed by this policy.
|
||||||
|
|
||||||
# Authz User Policy Detail
|
# Authz User Policy Detail
|
||||||
authz-add-user-policy=Add User Policy
|
authz-add-user-policy=Add User Policy
|
||||||
authz-no-users-assigned=No users assigned.
|
authz-no-users-assigned=No users assigned.
|
||||||
authz-policy-user-users.tooltip=Specifies which user(s) are allowed by this policy.
|
authz-policy-user-users.tooltip=Specifies which user(s) are allowed by this policy.
|
||||||
|
|
||||||
# Authz Client Policy Detail
|
# Authz Client Policy Detail
|
||||||
authz-add-client-policy=Add Client Policy
|
authz-add-client-policy=Add Client Policy
|
||||||
authz-no-clients-assigned=No clients assigned.
|
authz-no-clients-assigned=No clients assigned.
|
||||||
authz-policy-client-clients.tooltip=Specifies which client(s) are allowed by this policy.
|
authz-policy-client-clients.tooltip=Specifies which client(s) are allowed by this policy.
|
||||||
|
|
||||||
# Authz Time Policy Detail
|
# Authz Time Policy Detail
|
||||||
authz-add-time-policy=Add Time Policy
|
authz-add-time-policy=Add Time Policy
|
||||||
authz-policy-time-not-before.tooltip=Defines the time before which the policy MUST NOT be granted. Only granted if current date/time is after or equal to this value.
|
authz-policy-time-not-before.tooltip=Defines the time before which the policy MUST NOT be granted. Only granted if current date/time is after or equal to this value.
|
||||||
|
@ -1273,7 +1249,6 @@ authz-policy-time-hour=Hour
|
||||||
authz-policy-time-hour.tooltip=Defines the hour which the policy MUST be granted. You can also provide a range by filling the second field. In this case, permission is granted only if current hour is between or equal to the two values you provided.
|
authz-policy-time-hour.tooltip=Defines the hour which the policy MUST be granted. You can also provide a range by filling the second field. In this case, permission is granted only if current hour is between or equal to the two values you provided.
|
||||||
authz-policy-time-minute=Minute
|
authz-policy-time-minute=Minute
|
||||||
authz-policy-time-minute.tooltip=Defines the minute which the policy MUST be granted. You can also provide a range by filling the second field. In this case, permission is granted only if current minute is between or equal to the two values you provided.
|
authz-policy-time-minute.tooltip=Defines the minute which the policy MUST be granted. You can also provide a range by filling the second field. In this case, permission is granted only if current minute is between or equal to the two values you provided.
|
||||||
|
|
||||||
# Authz Drools Policy Detail
|
# Authz Drools Policy Detail
|
||||||
authz-add-drools-policy=Add Rules Policy
|
authz-add-drools-policy=Add Rules Policy
|
||||||
authz-policy-drools-maven-artifact-resolve=Resolve
|
authz-policy-drools-maven-artifact-resolve=Resolve
|
||||||
|
@ -1285,17 +1260,13 @@ authz-policy-drools-session=Session
|
||||||
authz-policy-drools-session.tooltip=The session used by this policy. The session provides all the rules to evaluate when processing the policy.
|
authz-policy-drools-session.tooltip=The session used by this policy. The session provides all the rules to evaluate when processing the policy.
|
||||||
authz-policy-drools-update-period=Update Period
|
authz-policy-drools-update-period=Update Period
|
||||||
authz-policy-drools-update-period.tooltip=Specifies an interval for scanning for artifact updates.
|
authz-policy-drools-update-period.tooltip=Specifies an interval for scanning for artifact updates.
|
||||||
|
|
||||||
# Authz JS Policy Detail
|
# Authz JS Policy Detail
|
||||||
authz-add-js-policy=Add JavaScript Policy
|
authz-add-js-policy=Add JavaScript Policy
|
||||||
authz-policy-js-code=Code
|
authz-policy-js-code=Code
|
||||||
authz-policy-js-code.tooltip=The JavaScript code providing the conditions for this policy.
|
authz-policy-js-code.tooltip=The JavaScript code providing the conditions for this policy.
|
||||||
|
|
||||||
|
|
||||||
# Authz Aggregated Policy Detail
|
# Authz Aggregated Policy Detail
|
||||||
authz-aggregated=Aggregated
|
authz-aggregated=Aggregated
|
||||||
authz-add-aggregated-policy=Add Aggregated Policy
|
authz-add-aggregated-policy=Add Aggregated Policy
|
||||||
|
|
||||||
# Authz Group Policy Detail
|
# Authz Group Policy Detail
|
||||||
authz-add-group-policy=Add Group Policy
|
authz-add-group-policy=Add Group Policy
|
||||||
authz-no-groups-assigned=No groups assigned.
|
authz-no-groups-assigned=No groups assigned.
|
||||||
|
@ -1453,3 +1424,10 @@ map-roles-authz-users-scope-description=Policies that decide if admin can map ro
|
||||||
user-impersonated-authz-users-scope-description=Policies that decide which users can be impersonated. These policies are applied to the user being impersonated.
|
user-impersonated-authz-users-scope-description=Policies that decide which users can be impersonated. These policies are applied to the user being impersonated.
|
||||||
manage-membership-authz-group-scope-description=Policies that decide if admin can add or remove users from this group
|
manage-membership-authz-group-scope-description=Policies that decide if admin can add or remove users from this group
|
||||||
manage-members-authz-group-scope-description=Policies that decide if an admin can manage the members of this group
|
manage-members-authz-group-scope-description=Policies that decide if an admin can manage the members of this group
|
||||||
|
|
||||||
|
# KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
# https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
|
||||||
|
advanced-client-settings=Advanced Settings
|
||||||
|
advanced-client-settings.tooltip=Expand this section to configure advanced settings of this client
|
||||||
|
tls-client-certificate-bound-access-tokens=OAuth 2.0 Mutual TLS Certificate Bound Access Tokens Enabled
|
||||||
|
tls-client-certificate-bound-access-tokens.tooltip=TThis enables support for OAuth 2.0 Mutual TLS Certificate Bound Access Tokens, which means that keycloak bind an access token and a refresh token with a X.509 certificate of a token requesting client exchanged in mutual TLS between keycloak's Token Endpoint and this client. These tokens can be treated as Holder-of-Key tokens instead of bearer tokens.
|
|
@ -920,6 +920,9 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
|
||||||
$scope.disableAuthorizationTab = !client.authorizationServicesEnabled;
|
$scope.disableAuthorizationTab = !client.authorizationServicesEnabled;
|
||||||
$scope.disableServiceAccountRolesTab = !client.serviceAccountsEnabled;
|
$scope.disableServiceAccountRolesTab = !client.serviceAccountsEnabled;
|
||||||
$scope.disableCredentialsTab = client.publicClient;
|
$scope.disableCredentialsTab = client.publicClient;
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
|
||||||
|
$scope.tlsClientCertificateBoundAccessTokens = false;
|
||||||
|
|
||||||
if(client.origin) {
|
if(client.origin) {
|
||||||
if ($scope.access.viewRealm) {
|
if ($scope.access.viewRealm) {
|
||||||
|
@ -1068,6 +1071,16 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
|
||||||
$scope.excludeSessionStateFromAuthResponse = false;
|
$scope.excludeSessionStateFromAuthResponse = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
|
||||||
|
if ($scope.client.attributes["tls.client.certificate.bound.access.tokens"]) {
|
||||||
|
if ($scope.client.attributes["tls.client.certificate.bound.access.tokens"] == "true") {
|
||||||
|
$scope.tlsClientCertificateBoundAccessTokens = true;
|
||||||
|
} else {
|
||||||
|
$scope.tlsClientCertificateBoundAccessTokens = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$scope.create) {
|
if (!$scope.create) {
|
||||||
|
@ -1309,6 +1322,14 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, templates,
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KEYCLOAK-6771 Certificate Bound Token
|
||||||
|
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
|
||||||
|
if ($scope.tlsClientCertificateBoundAccessTokens == true) {
|
||||||
|
$scope.clientEdit.attributes["tls.client.certificate.bound.access.tokens"] = "true";
|
||||||
|
} else {
|
||||||
|
$scope.clientEdit.attributes["tls.client.certificate.bound.access.tokens"] = "false";
|
||||||
|
}
|
||||||
|
|
||||||
$scope.clientEdit.protocol = $scope.protocol;
|
$scope.clientEdit.protocol = $scope.protocol;
|
||||||
$scope.clientEdit.attributes['saml.signature.algorithm'] = $scope.signatureAlgorithm;
|
$scope.clientEdit.attributes['saml.signature.algorithm'] = $scope.signatureAlgorithm;
|
||||||
$scope.clientEdit.attributes['saml_name_id_format'] = $scope.nameIdFormat;
|
$scope.clientEdit.attributes['saml_name_id_format'] = $scope.nameIdFormat;
|
||||||
|
|
|
@ -435,6 +435,18 @@
|
||||||
</div>
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<!-- KEYCLOAK-6771 Certificate Bound Token https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3 -->
|
||||||
|
<fieldset data-ng-show="protocol == 'openid-connect'">
|
||||||
|
<legend collapsed><span class="text">{{:: 'advanced-client-settings' | translate}}</span> <kc-tooltip>{{:: 'advanced-client-settings.tooltip' | translate}}</kc-tooltip></legend>
|
||||||
|
<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
|
||||||
|
<label class="col-md-2 control-label" for="tlsClientCertificateBoundAccessTokens">{{:: 'tls-client-certificate-bound-access-tokens' | translate}}</label>
|
||||||
|
<div class="col-sm-6">
|
||||||
|
<input ng-model="tlsClientCertificateBoundAccessTokens" ng-click="switchChange()" name="tlsClientCertificateBoundAccessTokens" id="tlsClientCertificateBoundAccessTokens" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
|
||||||
|
</div>
|
||||||
|
<kc-tooltip>{{:: 'tls-client-certificate-bound-access-tokens.tooltip' | translate}}</kc-tooltip>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend collapsed><span class="text">{{:: 'client-flow-bindings' | translate}}</span> <kc-tooltip>{{:: 'client-flow-bindings.tooltip' | translate}}</kc-tooltip></legend>
|
<legend collapsed><span class="text">{{:: 'client-flow-bindings' | translate}}</span> <kc-tooltip>{{:: 'client-flow-bindings.tooltip' | translate}}</kc-tooltip></legend>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
Loading…
Reference in a new issue