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

Introducing TLS certificate based token binding type #2240

Merged
Original file line number Diff line number Diff line change
Expand Up @@ -363,4 +363,11 @@ public class DiscoveryConstants {
* OPTIONAL. URL of the OpenID Connect token discovery endpoint
*/
public static final String WEBFINGER_ENDPOINT = "WebFinger_endpoint";

/**
* tls_client_certificate_bound_access_tokens
* OPTIONAL. Boolean value indicating server support for mutual-TLS client certificate-bound access tokens.
* If omitted, the default value is false.
*/
public static final String TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKEN = "tls_client_certificate_bound_access_tokens";
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public class OIDProviderConfigResponse {
private String[] codeChallengeMethodsSupported;
private String deviceAuthorizationEndpoint;
private String webFingerEndpoint;
private Boolean tlsClientCertificateBoundAccessTokens;

public String getIssuer() {
return issuer;
Expand Down Expand Up @@ -508,6 +509,11 @@ public void setWebFingerEndpoint(String webFingerEndpoint) {
this.webFingerEndpoint = webFingerEndpoint;
}

public void setTlsClientCertificateBoundAccessTokens(Boolean tlsClientCertificateBoundAccessTokens) {

this.tlsClientCertificateBoundAccessTokens = tlsClientCertificateBoundAccessTokens;
}

public Map<String, Object> getConfigMap() {
Map<String, Object> configMap = new HashMap<String, Object>();
configMap.put(DiscoveryConstants.ISSUER.toLowerCase(), this.issuer);
Expand Down Expand Up @@ -573,6 +579,8 @@ public Map<String, Object> getConfigMap() {
configMap.put(DiscoveryConstants.CODE_CHALLENGE_METHODS_SUPPORTED, this.codeChallengeMethodsSupported);
configMap.put(DiscoveryConstants.DEVICE_AUTHORIZATION_ENDPOINT, this.deviceAuthorizationEndpoint);
configMap.put(DiscoveryConstants.WEBFINGER_ENDPOINT.toLowerCase(), this.webFingerEndpoint);
configMap.put(DiscoveryConstants.TLS_CLIENT_CERTIFICATE_BOUND_ACCESS_TOKEN.toLowerCase(),
this.tlsClientCertificateBoundAccessTokens);
return configMap;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import org.wso2.carbon.identity.discovery.internal.OIDCDiscoveryDataHolder;
import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.oauth2.OAuth2Constants;
import org.wso2.carbon.identity.oauth2.util.OAuth2Util;

import java.net.URISyntaxException;
Expand Down Expand Up @@ -112,7 +113,7 @@ public OIDProviderConfigResponse buildOIDProviderConfig(OIDProviderRequest reque
providerConfig.setResponseTypesSupported(supportedResponseTypeNames.toArray(new
String[supportedResponseTypeNames.size()]));

providerConfig.setSubjectTypesSupported(new String[]{"public"});
providerConfig.setSubjectTypesSupported(new String[]{"public", "pairwise"});

providerConfig.setCheckSessionIframe(buildServiceUrl(IdentityConstants.OAuth.CHECK_SESSION,
IdentityUtil.getProperty(IdentityConstants.OAuth.OIDC_CHECK_SESSION_EP_URL)));
Expand Down Expand Up @@ -145,6 +146,8 @@ public OIDProviderConfigResponse buildOIDProviderConfig(OIDProviderRequest reque
providerConfig.setTokenEndpointAuthSigningAlgValuesSupported(
supportedTokenEndpointSigningAlgorithms.toArray(new String[0]));
providerConfig.setWebFingerEndpoint(OAuth2Util.OAuthURL.getOidcWebFingerEPUrl());
providerConfig.setTlsClientCertificateBoundAccessTokens(OAuth2Util.getSupportedTokenBindingTypes()
.contains(OAuth2Constants.TokenBinderType.CERTIFICATE_BASED_TOKEN_BINDER));
return providerConfig;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,17 @@ public final class OAuthConstants {

public static final String SECTOR_IDENTIFIER_URI = "sector_identifier_uri";
public static final String SUBJECT_TYPE = "subject_type";

public static final String CNF = "cnf";
public static final String MTLS_AUTH_HEADER = "MutualTLS.ClientCertificateHeader";
public static final String BEGIN_CERT = "-----BEGIN CERTIFICATE-----";
public static final String END_CERT = "-----END CERTIFICATE-----";
public static final String JAVAX_SERVLET_REQUEST_CERTIFICATE = "javax.servlet.request.X509Certificate";
public static final String CONFIG_NOT_FOUND = "CONFIG_NOT_FOUND";

public static final String ENABLE_TLS_CERT_TOKEN_BINDING = "OAuth.OpenIDConnect." +
ChinthakaJ98 marked this conversation as resolved.
Show resolved Hide resolved
"EnableTLSCertificateBoundAccessTokens";

/**
* Enum for OIDC supported subject types.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public final class IntrospectionResponse {
// OPTIONAL
// Access token binding reference.
public static final String BINDING_REFERENCE = "binding_ref";
public static final String CNF = "cnf";

public static final String AUT = "aut";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import org.json.JSONException;
import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand Down Expand Up @@ -242,6 +243,20 @@ public IntrospectionResponseBuilder setBindingReference(String bindingReference)
return this;
}

/**
* Set cnf value to be bound to the access token.
*
* @param cnfBindingValue Thumbprint of the TLS certificate passed in the request.
* @return IntrospectionResponseBuilder.
*/
public IntrospectionResponseBuilder setCnfBindingValue(String cnfBindingValue) {

if (StringUtils.isNotBlank(cnfBindingValue)) {
parameters.put(IntrospectionResponse.CNF, Collections.singletonMap("x5t#S256", cnfBindingValue));
}
return this;
}

/**
* @param errorCode Error Code
* @return IntrospectionResponseBuilder
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@
import org.wso2.carbon.identity.central.log.mgt.utils.LogConstants;
import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils;
import org.wso2.carbon.identity.core.handler.AbstractIdentityHandler;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.oauth.common.OAuthConstants;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.oauth2.IntrospectionDataProvider;
import org.wso2.carbon.identity.oauth2.OAuth2Constants;
import org.wso2.carbon.identity.oauth2.OAuth2TokenValidationService;
import org.wso2.carbon.identity.oauth2.dto.OAuth2IntrospectionResponseDTO;
import org.wso2.carbon.identity.oauth2.dto.OAuth2TokenValidationRequestDTO;
Expand Down Expand Up @@ -159,8 +161,14 @@ public Response introspect(@FormParam("token") String token, @FormParam("token_t
// Provide token binding related info which required to validate the token.
if (StringUtils.isNotBlank(introspectionResponse.getBindingType()) && StringUtils
.isNotBlank(introspectionResponse.getBindingReference())) {
respBuilder.setBindingType(introspectionResponse.getBindingType());
String bindingType = introspectionResponse.getBindingType();
respBuilder.setBindingType(bindingType);
respBuilder.setBindingReference(introspectionResponse.getBindingReference());
if (OAuth2Constants.TokenBinderType.CERTIFICATE_BASED_TOKEN_BINDER.equals(bindingType) &&
StringUtils.isNotBlank(introspectionResponse.getCnfBindingValue()) &&
Boolean.parseBoolean(IdentityUtil.getProperty(OAuthConstants.ENABLE_TLS_CERT_TOKEN_BINDING))) {
ChinthakaJ98 marked this conversation as resolved.
Show resolved Hide resolved
respBuilder.setCnfBindingValue(introspectionResponse.getCnfBindingValue());
}
}

// Retrieve list of registered IntrospectionDataProviders.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
*/
package org.wso2.carbon.identity.oauth.endpoint.introspection;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.json.JSONArray;
import org.json.JSONObject;
import org.mockito.Mock;
Expand Down Expand Up @@ -83,7 +84,8 @@ public Object[][] provideBuilderData() {
* This method does unit test for build response with values
*/
@Test(dataProvider = "provideBuilderData")
public void testResposeBuilderWithVal(boolean isActive, int notBefore, int expiration, boolean timesSetInJson) {
public void testResposeBuilderWithVal(boolean isActive, int notBefore, int expiration, boolean timesSetInJson)
throws Exception {

String idToken = "eyJhbGciOiJSUzI1NiJ9.eyJhdXRoX3RpbWUiOjE0NTIxNzAxNzYsImV4cCI6MTQ1MjE3Mzc3Niwic3ViI" +
"joidXNlQGNhcmJvbi5zdXBlciIsImF6cCI6IjF5TDFfZnpuekdZdXRYNWdCMDNMNnRYR3lqZ2EiLCJhdF9oYXNoI" +
Expand All @@ -109,6 +111,7 @@ public void testResposeBuilderWithVal(boolean isActive, int notBefore, int expir
introspectionResponseBuilder1.setErrorCode("Invalid input");
introspectionResponseBuilder1.setErrorDescription("error_discription");
introspectionResponseBuilder1.setAuthorizedUserType(OAuthConstants.UserType.APPLICATION_USER);
introspectionResponseBuilder1.setCnfBindingValue("R4Hj_0nNdIzVvPdCdsWlxNKm6a74cszp4Za4M1iE8P9");

JSONObject jsonObject = new JSONObject(introspectionResponseBuilder1.build());

Expand Down Expand Up @@ -136,6 +139,9 @@ public void testResposeBuilderWithVal(boolean isActive, int notBefore, int expir
"ERROR_DESCRIPTION messages are not equal");
assertEquals(jsonObject.get(IntrospectionResponse.AUT), "APPLICATION_USER",
"AUT values are not equal");
Map<String, Object> cnf = new ObjectMapper()
.readValue(jsonObject.get(IntrospectionResponse.CNF).toString(), HashMap.class);
assertEquals(cnf.get("x5t#S256"), "R4Hj_0nNdIzVvPdCdsWlxNKm6a74cszp4Za4M1iE8P9", "CNF value is not equal");
}

/**
Expand All @@ -161,6 +167,7 @@ public void testResposeBuilderWithoutVal() {
introspectionResponseBuilder2.setErrorCode("");
introspectionResponseBuilder2.setErrorDescription("");
introspectionResponseBuilder2.setAuthorizedUserType("");
introspectionResponseBuilder2.setCnfBindingValue("");

JSONObject jsonObject2 = new JSONObject(introspectionResponseBuilder2.build());
assertFalse(jsonObject2.has(IntrospectionResponse.EXP), "EXP already exists in the response builder");
Expand All @@ -180,6 +187,7 @@ public void testResposeBuilderWithoutVal() {
assertFalse(jsonObject2.has(IntrospectionResponse.Error.ERROR_DESCRIPTION),
"ERROR_DESCRIPTION already exists in the response builder");
assertFalse(jsonObject2.has(IntrospectionResponse.AUT), "AUT already exists in the response builder");
assertFalse(jsonObject2.has(IntrospectionResponse.CNF), "CNF value exists in the response builder");
}

@Test(dependsOnMethods = "testResposeBuilderWithVal")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,18 @@
import org.wso2.carbon.context.PrivilegedCarbonContext;
import org.wso2.carbon.identity.central.log.mgt.utils.LoggerUtils;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.oauth.common.OAuthConstants;
import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration;
import org.wso2.carbon.identity.oauth.tokenprocessor.TokenPersistenceProcessor;
import org.wso2.carbon.identity.oauth2.OAuth2Constants;
import org.wso2.carbon.identity.oauth2.OAuth2TokenValidationService;
import org.wso2.carbon.identity.oauth2.dto.OAuth2IntrospectionResponseDTO;
import org.wso2.carbon.identity.testutil.powermock.PowerMockIdentityBaseTest;

import java.nio.file.Paths;
import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.core.Response;

Expand All @@ -37,7 +41,7 @@
import static org.testng.AssertJUnit.assertEquals;

@PrepareForTest({PrivilegedCarbonContext.class, LoggerUtils.class, IdentityTenantUtil.class,
OAuthServerConfiguration.class, TokenPersistenceProcessor.class})
OAuthServerConfiguration.class, TokenPersistenceProcessor.class, IdentityUtil.class})
public class OAuth2IntrospectionEndpointTest extends PowerMockIdentityBaseTest {

@Mock
Expand Down Expand Up @@ -97,14 +101,27 @@ public void testTokenTypeHint(String tokenTypeHint, String expectedTokenType) th
mockedIntrospectionResponse.setTokenType(expectedTokenType);

when(mockedIntrospectionResponse.getTokenType()).thenReturn(expectedTokenType);
when(mockedIntrospectionResponse.getBindingType())
.thenReturn(OAuth2Constants.TokenBinderType.CERTIFICATE_BASED_TOKEN_BINDER);
when(mockedIntrospectionResponse.getBindingReference())
.thenReturn("test_reference_value");
when(mockedIntrospectionResponse.getCnfBindingValue())
.thenReturn("R4Hj_0nNdIzVvPdCdsWlxNKm6a74cszp4Za4M1iE8P9");
mockStatic(IdentityUtil.class);
when(IdentityUtil.getProperty(OAuthConstants.ENABLE_TLS_CERT_TOKEN_BINDING)).thenReturn("true");

Response response = oAuth2IntrospectionEndpoint.introspect(token, tokenTypeHint, requiredClaims);

HashMap<String, String> map = new Gson().fromJson((String) response.getEntity(), new TypeToken<HashMap<String,
String>>() {
HashMap<String, Object> map = new Gson().fromJson((String) response.getEntity(), new TypeToken<HashMap<String,
Object>>() {
}.getType());

assertEquals(map.get("token_type"), expectedTokenType);
assertEquals(map.get("binding_type"), OAuth2Constants.TokenBinderType.CERTIFICATE_BASED_TOKEN_BINDER);
assertEquals(map.get("binding_ref"), "test_reference_value");
assertEquals(((Map<String, String>) map.get("cnf")).get("x5t#S256"),
"R4Hj_0nNdIzVvPdCdsWlxNKm6a74cszp4Za4M1iE8P9");


}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static class TokenBinderType {

public static final String SSO_SESSION_BASED_TOKEN_BINDER = "sso-session";
public static final String COOKIE_BASED_TOKEN_BINDER = "cookie";
public static final String CERTIFICATE_BASED_TOKEN_BINDER = "certificate";

}
public static final String GROUPS = "groups";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ public class OAuth2IntrospectionResponseDTO {
* OPTIONAL. Token binding reference.
*/
private String bindingReference;
private String cnfBindingValue;

/**
* OPTIONAL. Authorized user type of the token. (APPLICATION or APPLICATION_USER)
Expand Down Expand Up @@ -293,6 +294,16 @@ public String getBindingReference() {
return bindingReference;
}

public void setCnfBindingValue(String cnfBindingValue) {

this.cnfBindingValue = cnfBindingValue;
}

public String getCnfBindingValue() {

return cnfBindingValue;
}

public void setBindingReference(String bindingReference) {

this.bindingReference = bindingReference;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import org.wso2.carbon.identity.consent.server.configs.mgt.services.ConsentServerConfigsManagementService;
import org.wso2.carbon.identity.core.SAMLSSOServiceProviderManager;
import org.wso2.carbon.identity.core.util.IdentityCoreInitializedEvent;
import org.wso2.carbon.identity.core.util.IdentityUtil;
import org.wso2.carbon.identity.event.handler.AbstractEventHandler;
import org.wso2.carbon.identity.event.services.IdentityEventService;
import org.wso2.carbon.identity.oauth.common.OAuthConstants;
Expand Down Expand Up @@ -76,6 +77,7 @@
import org.wso2.carbon.identity.oauth2.scopeservice.ScopeMetadataService;
import org.wso2.carbon.identity.oauth2.token.bindings.TokenBinder;
import org.wso2.carbon.identity.oauth2.token.bindings.handlers.TokenBindingExpiryEventHandler;
import org.wso2.carbon.identity.oauth2.token.bindings.impl.CertificateBasedTokenBinder;
import org.wso2.carbon.identity.oauth2.token.bindings.impl.CookieBasedTokenBinder;
import org.wso2.carbon.identity.oauth2.token.bindings.impl.DeviceFlowTokenBinder;
import org.wso2.carbon.identity.oauth2.token.bindings.impl.SSOSessionBasedTokenBinder;
Expand Down Expand Up @@ -251,6 +253,11 @@ protected void activate(ComponentContext context) {
bundleContext.registerService(TokenBinderInfo.class.getName(), deviceFlowTokenBinder, null);
}

if (Boolean.parseBoolean(IdentityUtil.getProperty(OAuthConstants.ENABLE_TLS_CERT_TOKEN_BINDING))) {
ChinthakaJ98 marked this conversation as resolved.
Show resolved Hide resolved
CertificateBasedTokenBinder certificateBasedTokenBinder = new CertificateBasedTokenBinder();
bundleContext.registerService(TokenBinderInfo.class.getName(), certificateBasedTokenBinder, null);
}

bundleContext.registerService(ResponseTypeRequestValidator.class.getName(),
new DeviceFlowResponseTypeRequestValidator(), null);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,9 @@
import org.wso2.carbon.identity.oauth.event.OAuthEventInterceptor;
import org.wso2.carbon.identity.oauth.internal.OAuthComponentServiceHolder;
import org.wso2.carbon.identity.oauth2.IDTokenValidationFailureException;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2ClientException;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.oauth2.OAuth2Constants;
import org.wso2.carbon.identity.oauth2.ResponseHeader;
import org.wso2.carbon.identity.oauth2.bean.OAuthClientAuthnContext;
import org.wso2.carbon.identity.oauth2.device.cache.DeviceAuthorizationGrantCache;
Expand Down Expand Up @@ -1058,6 +1060,11 @@ private void handleTokenBinding(OAuth2AccessTokenReqDTO tokenReqDTO, String gran

Optional<String> tokenBindingValueOptional = tokenBinder.getTokenBindingValue(tokenReqDTO);
if (!tokenBindingValueOptional.isPresent()) {
if (Boolean.parseBoolean(IdentityUtil.getProperty(OAuthConstants.ENABLE_TLS_CERT_TOKEN_BINDING)) &&
OAuth2Constants.TokenBinderType.CERTIFICATE_BASED_TOKEN_BINDER.equals(tokenBinder.getBindingType())) {
ChinthakaJ98 marked this conversation as resolved.
Show resolved Hide resolved
throw new IdentityOAuth2ClientException(OAuth2ErrorCodes.INVALID_REQUEST,
"TLS certificate not found in the request.");
}
throw new IdentityOAuth2Exception(
"Token binding reference cannot be retrieved form the token binder: " + tokenBinder
.getBindingType());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
import java.text.ParseException;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
Expand Down Expand Up @@ -821,8 +822,17 @@ private JWTClaimsSet handleTokenBinding(JWTClaimsSet.Builder jwtClaimsSetBuilder

if (tokReqMsgCtx != null && tokReqMsgCtx.getTokenBinding() != null) {
// Include token binding into the jwt token.
String bindingType = tokReqMsgCtx.getTokenBinding().getBindingType();
jwtClaimsSetBuilder.claim(TOKEN_BINDING_REF, tokReqMsgCtx.getTokenBinding().getBindingReference());
jwtClaimsSetBuilder.claim(TOKEN_BINDING_TYPE, tokReqMsgCtx.getTokenBinding().getBindingType());
jwtClaimsSetBuilder.claim(TOKEN_BINDING_TYPE, bindingType);
if (Boolean.parseBoolean(IdentityUtil.getProperty(OAuthConstants.ENABLE_TLS_CERT_TOKEN_BINDING)) &&
OAuth2Constants.TokenBinderType.CERTIFICATE_BASED_TOKEN_BINDER.equals(bindingType)) {
ChinthakaJ98 marked this conversation as resolved.
Show resolved Hide resolved
String cnf = tokReqMsgCtx.getTokenBinding().getBindingValue();
if (StringUtils.isNotBlank(cnf)) {
jwtClaimsSetBuilder.claim(OAuthConstants.CNF, Collections.singletonMap("x5t#S256",
ChinthakaJ98 marked this conversation as resolved.
Show resolved Hide resolved
tokReqMsgCtx.getTokenBinding().getBindingValue()));
}
}
}
return jwtClaimsSetBuilder.build();
}
Expand Down
Loading