Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added MTLS external validation support #21

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ public class OIDCConfigurationRepresentation {
@JsonProperty("tls_client_certificate_bound_access_tokens")
private Boolean tlsClientCertificateBoundAccessTokens;

@JsonProperty("tls_client_certificate_extended_verification")
DmitryMishchuk marked this conversation as resolved.
Show resolved Hide resolved
private Boolean tlsClientCertificateExtendedValidation;

@JsonProperty("tls_client_certificate_extended_verification_impl")
private String tlsClientCertificateExtendedValidationImplementation;

@JsonProperty("revocation_endpoint")
private String revocationEndpoint;

Expand Down Expand Up @@ -445,4 +451,20 @@ public Map<String, Object> getOtherClaims() {
public void setOtherClaims(String name, Object value) {
otherClaims.put(name, value);
}

public Boolean getTlsClientCertificateExtendedValidation() {
return tlsClientCertificateExtendedValidation;
}

public void setTlsClientCertificateExtendedValidation(Boolean tlsClientCertificateExtendedValidation) {
this.tlsClientCertificateExtendedValidation = tlsClientCertificateExtendedValidation;
}

public String getTlsClientCertificateExtendedValidationImplementation() {
return tlsClientCertificateExtendedValidationImplementation;
}

public void setTlsClientCertificateExtendedValidationImplementation(String tlsClientCertificateExtendedValidationImplementation) {
this.tlsClientCertificateExtendedValidationImplementation = tlsClientCertificateExtendedValidationImplementation;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ public class OIDCClientRepresentation {
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-6.5
private Boolean tls_client_certificate_bound_access_tokens;

private Boolean tls_client_certificate_extended_validation;
DmitryMishchuk marked this conversation as resolved.
Show resolved Hide resolved
private String tls_client_certificate_extended_validation_impl;

private String tls_client_auth_subject_dn;

// OIDC Session Management
Expand Down Expand Up @@ -487,4 +490,19 @@ public void setTlsClientAuthSubjectDn(String tls_client_auth_subject_dn) {
this.tls_client_auth_subject_dn = tls_client_auth_subject_dn;
}

public Boolean getTls_client_certificate_extended_validation() {
return tls_client_certificate_extended_validation;
}

public void setTls_client_certificate_extended_validation(Boolean tls_client_certificate_extended_validation) {
this.tls_client_certificate_extended_validation = tls_client_certificate_extended_validation;
}

public String getTls_client_certificate_extended_validation_impl() {
return tls_client_certificate_extended_validation_impl;
}

public void setTls_client_certificate_extended_validation_impl(String tls_client_certificate_extended_validation_impl) {
this.tls_client_certificate_extended_validation_impl = tls_client_certificate_extended_validation_impl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package org.keycloak.mtls;
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure whether this provider is in server-spi project.

AFAIK, the providers in server-spi are fixed and never change their interfaces.
IMO, it might be appropriate that this provider be in server-spi-private.

It might be better to ask Keycloak development team about this point.


import org.keycloak.provider.Provider;

import java.security.cert.X509Certificate;
import java.util.Map;

public interface MtlsExtendedValidationProvider extends Provider {

Map<String, String> parseAdditionalFields(X509Certificate[] certs);

void performAdditionalValidation(X509Certificate[] certs);
tnorimat marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.keycloak.mtls;

import org.keycloak.provider.ProviderFactory;

public interface MtlsExtendedValidationProviderFactory extends ProviderFactory<MtlsExtendedValidationProvider> {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package org.keycloak.mtls;

import org.keycloak.provider.Provider;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.provider.Spi;

public class MtlsExtendedValidationSpi implements Spi {
@Override
public boolean isInternal() {
return false;
tnorimat marked this conversation as resolved.
Show resolved Hide resolved
}

@Override
public String getName() {
return "tls-client-extended-validation-impl";
}

@Override
public Class<? extends Provider> getProviderClass() {
return MtlsExtendedValidationProvider.class;
}

@Override
public Class<? extends ProviderFactory> getProviderFactoryClass() {
return MtlsExtendedValidationProviderFactory.class;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ org.keycloak.locale.LocaleUpdaterSPI
org.keycloak.storage.UserStorageProviderSpi
org.keycloak.theme.ThemeResourceSpi
org.keycloak.theme.ThemeSelectorSpi
org.keycloak.urls.HostnameSpi
org.keycloak.urls.HostnameSpi
org.keycloak.mtls.MtlsExtendedValidationSpi
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
package org.keycloak.authentication.authenticators.client;

import org.keycloak.OAuth2Constants;
import org.keycloak.authentication.AuthenticationFlowContext;
import org.keycloak.authentication.AuthenticationFlowError;
import org.keycloak.authentication.ClientAuthenticationFlowContext;
import org.keycloak.models.AuthenticationExecutionModel;
import org.keycloak.models.ClientModel;
import org.keycloak.mtls.MtlsExtendedValidationProvider;
import org.keycloak.protocol.oidc.OIDCLoginProtocol;
import org.keycloak.provider.ProviderConfigProperty;
import org.keycloak.provider.ProviderFactory;
import org.keycloak.services.ServicesLogger;
import org.keycloak.services.x509.X509ClientCertificateLookup;

import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import java.nio.file.ProviderNotFoundException;
import java.security.GeneralSecurityException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
Expand Down Expand Up @@ -127,6 +131,17 @@ public void authenticateClient(ClientAuthenticationFlowContext context) {
logger.debug("[X509ClientCertificateAuthenticator:authenticate] Matched " + matchedCertificate.get() + " certificate.");
}

boolean mtlsExtendedValidationEnabled = Boolean.parseBoolean(client.getAttribute("tls.client.certificate.extended.validation"));
DmitryMishchuk marked this conversation as resolved.
Show resolved Hide resolved
MtlsExtendedValidationProvider mtlsExtendedValidationProvider = getMtlsExtendedValidationProvider(mtlsExtendedValidationEnabled, context, client);
DmitryMishchuk marked this conversation as resolved.
Show resolved Hide resolved

if (mtlsExtendedValidationEnabled) {
Map<String,String> additionalDetails = mtlsExtendedValidationProvider.parseAdditionalFields(certs);
additionalDetails.forEach((k,v)->{
context.getEvent().detail(k, v);
});
mtlsExtendedValidationProvider.performAdditionalValidation(certs);
DmitryMishchuk marked this conversation as resolved.
Show resolved Hide resolved
}

context.success();
}

Expand Down Expand Up @@ -179,4 +194,17 @@ public List<ProviderConfigProperty> getConfigProperties() {
public String getId() {
return PROVIDER_ID;
}

private MtlsExtendedValidationProvider getMtlsExtendedValidationProvider(boolean mtlsExtendedValidationEnabled, ClientAuthenticationFlowContext context, ClientModel client) {
if (!mtlsExtendedValidationEnabled) {
return null;
} else {
String requiredProviderId = client.getAttribute("tls.client.certificate.extended.validation.impl");
DmitryMishchuk marked this conversation as resolved.
Show resolved Hide resolved
ProviderFactory<MtlsExtendedValidationProvider> factory = context.getSession().getKeycloakSessionFactory()
.getProviderFactoriesStream(MtlsExtendedValidationProvider.class)
.filter(f -> f.getId().equals(requiredProviderId)).findFirst()
.orElseThrow(() -> new ProviderNotFoundException("MTLS Extended validation Provider Not Found! Please specify your provider in your Clients Advanced Settings!"));
return factory.create(context.getSession());
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,24 @@ public void setUseMtlsHoKToken(boolean useUtlsHokToken) {
setAttribute(OIDCConfigAttributes.USE_MTLS_HOK_TOKEN, val);
}

public boolean isUseMtlsExtendedValidation() {
DmitryMishchuk marked this conversation as resolved.
Show resolved Hide resolved
String useMtlsExtendedValidation = getAttribute(OIDCConfigAttributes.USE_MTLS_EXTENDED_VALIDATION);
return Boolean.parseBoolean(useMtlsExtendedValidation);
}

public void setUseMtlsExtendedValidation(boolean useMtlsExtendedValidation) {
String val = String.valueOf(useMtlsExtendedValidation);
setAttribute(OIDCConfigAttributes.USE_MTLS_EXTENDED_VALIDATION, val);
}

public String getMtlsExtendedValidationImpl(){
return getAttribute(OIDCConfigAttributes.USE_MTLS_EXTENDED_VALIDATION_IMPL);
}

public void setMtlsExtendedValidationImpl(String mtlsExtendedValidationImpl){
setAttribute(OIDCConfigAttributes.USE_MTLS_EXTENDED_VALIDATION, mtlsExtendedValidationImpl);
}

/**
* If true, then Client Credentials Grant generates refresh token and creates user session. This is not per specs, so it is false by default
* For the details @see https://tools.ietf.org/html/rfc6749#section-4.4.3
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ public final class OIDCConfigAttributes {

public static final String USE_MTLS_HOK_TOKEN = "tls.client.certificate.bound.access.tokens";

public static final String USE_MTLS_EXTENDED_VALIDATION = "tls.client.certificate.extended.validation";

public static final String USE_MTLS_EXTENDED_VALIDATION_IMPL = "tls.client.certificate.extended.validation.impl";

public static final String ID_TOKEN_SIGNED_RESPONSE_ALG = "id.token.signed.response.alg";

public static final String ID_TOKEN_ENCRYPTED_RESPONSE_ALG = "id.token.encrypted.response.alg";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ public static ClientRepresentation toInternal(KeycloakSession session, OIDCClien
else configWrapper.setUseMtlsHoKToken(false);
}

boolean tlsClientCertificateExtendedValidation = configWrapper.isUseMtlsHokToken()&&clientOIDC.getTls_client_certificate_extended_validation();
configWrapper.setUseMtlsExtendedValidation(tlsClientCertificateExtendedValidation);

String tlsClientCertificateExtendedValidationImpl = clientOIDC.getTls_client_certificate_extended_validation_impl();
configWrapper.setMtlsExtendedValidationImpl(tlsClientCertificateExtendedValidationImpl);

if (clientOIDC.getTlsClientAuthSubjectDn() != null) {
configWrapper.setTlsClientAuthSubjectDn(clientOIDC.getTlsClientAuthSubjectDn());
}
Expand Down Expand Up @@ -246,6 +252,10 @@ public static OIDCClientRepresentation toExternalResponse(KeycloakSession sessio
} else {
response.setTlsClientCertificateBoundAccessTokens(Boolean.FALSE);
}

response.setTls_client_certificate_extended_validation(config.isUseMtlsExtendedValidation());
response.setTls_client_certificate_extended_validation_impl(config.getMtlsExtendedValidationImpl());

if (config.getTlsClientAuthSubjectDn() != null) {
response.setTlsClientAuthSubjectDn(config.getTlsClientAuthSubjectDn());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1767,6 +1767,11 @@ 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=This 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.
tls-client-certificate-extended-validation=OAuth 2.0 Mutual TLS Certificate Extended Validation Enabled
tls-client-certificate-extended-validation.tooltip=This enables support from OAuth 2.0 Mutual TLS Certificate Extended Validation, which means that keycloak will perform additional validation and parameters extraction through one of custom extensions.
tls-client-certificate-extended-validation-impl=OAuth 2.0 Mutual TLS Certificate Extended Validation Implementation
tls-client-certificate-extended-validation-impl.tooltip=Select your OAuth 2.0 Mutual TLS Certificate Extended Validation Implementation here

subjectdn=Subject DN
subjectdn-tooltip=A regular expression for validating Subject DN in the Client Certificate. Use "(.*?)(?:$)" to match all kind of expressions.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1111,6 +1111,9 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
// KEYCLOAK-6771 Certificate Bound Token
// https://tools.ietf.org/html/draft-ietf-oauth-mtls-08#section-3
$scope.tlsClientCertificateBoundAccessTokens = false;
$scope.tlsClientCertificateExtendedValidation = false;
$scope.tlsClientCertificateExtendedValidationImpl = client.attributes['tls.client.certificate.extended.validation.impl'];
$scope.tlsClientCertificateExtendedValidationImpls = serverInfo.listProviderIds('tls-client-extended-validation-impl');

$scope.accessTokenLifespan = TimeUnit2.asUnit(client.attributes['access.token.lifespan']);
$scope.samlAssertionLifespan = TimeUnit2.asUnit(client.attributes['saml.assertion.lifespan']);
Expand Down Expand Up @@ -1289,6 +1292,16 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
}
}

if ($scope.client.attributes["tls.client.certificate.extended.validation"]) {
$scope.tlsClientCertificateExtendedValidation = $scope.client.attributes["tls.client.certificate.extended.validation"] === "true";
}

if ($scope.client.attributes['tls.client.certificate.extended.validation.impl']==null){
$scope.tlsClientCertificateExtendedValidationImpl = '';
} else {
$scope.tlsClientCertificateExtendedValidationImpl = $scope.client.attributes['tls.client.certificate.extended.validation.impl'];
}

var useRefreshToken = $scope.client.attributes["client_credentials.use_refresh_token"];
if (useRefreshToken === "true") {
$scope.useRefreshTokenForClientCredentialsGrant = true;
Expand Down Expand Up @@ -1447,6 +1460,10 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
$scope.clientEdit.attributes['pkce.code.challenge.method'] = $scope.pkceCodeChallengeMethod;
};

$scope.changeMtlsExtendedValidationImpl = function() {
$scope.clientEdit.attributes['tls.client.certificate.extended.validation.impl'] = $scope.tlsClientCertificateExtendedValidationImpl;
};

$scope.$watch(function() {
return $location.path();
}, function() {
Expand Down Expand Up @@ -1672,6 +1689,15 @@ module.controller('ClientDetailCtrl', function($scope, realm, client, flows, $ro
$scope.clientEdit.attributes["tls.client.certificate.bound.access.tokens"] = "false";
}

$scope.clientEdit.attributes["tls.client.certificate.extended.validation"] = $scope.tlsClientCertificateExtendedValidation && $scope.tlsClientCertificateBoundAccessTokens === true
? "true"
: "false";


$scope.clientEdit.attributes["tls.client.certificate.extended.validation.impl"] = $scope.tlsClientCertificateExtendedValidationImpl === 'none' || !$scope.tlsClientCertificateExtendedValidation
? null
: $scope.tlsClientCertificateExtendedValidationImpl;

// KEYCLOAK-9551 Client Credentials Grant generates refresh token
// https://tools.ietf.org/html/rfc6749#section-4.4.3
if ($scope.useRefreshTokenForClientCredentialsGrant === true) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -670,6 +670,28 @@
<kc-tooltip>{{:: 'tls-client-certificate-bound-access-tokens.tooltip' | translate}}</kc-tooltip>
</div>

<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect' && tlsClientCertificateBoundAccessTokens">
<label class="col-md-2 control-label" for="tlsClientCertificateExtendedValidation">{{:: 'tls-client-certificate-extended-validation' | translate}}</label>
<div class="col-sm-6">
<input ng-model="tlsClientCertificateExtendedValidation" ng-click="switchChange()" name="tlsClientCertificateExtendedValidation" id="tlsClientCertificateExtendedValidation" onoffswitch on-text="{{:: 'onText' | translate}}" off-text="{{:: 'offText' | translate}}"/>
</div>
<kc-tooltip>{{:: 'tls-client-certificate-extended-validation.tooltip' | translate}}</kc-tooltip>
</div>

<div class="form-group" data-ng-show="tlsClientCertificateExtendedValidation">
<label class="col-md-2 control-label" for="tlsClientCertificateExtendedValidationImpl">{{:: 'tls-client-certificate-extended-validation-impl' | translate}}</label>
<div class="col-sm-6">
<div>
<select class="form-control" id="tlsClientCertificateExtendedValidationImpl"
ng-model="tlsClientCertificateExtendedValidationImpl"
ng-change="changeMtlsExtendedValidationImpl()"
ng-options="validationImpl for validationImpl in tlsClientCertificateExtendedValidationImpls">
</select>
</div>
</div>
<kc-tooltip>{{:: 'tls-client-certificate-extended-validation-impl.tooltip' | translate}}</kc-tooltip>
</div>

<div class="form-group clearfix block" data-ng-show="protocol == 'openid-connect'">
<label class="col-md-2 control-label" for="changePkceCodeChallengeMethod">{{:: 'pkce-code-challenge-method' | translate}}</label>
<div class="col-sm-6">
Expand Down