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
@@ -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
@@ -80,6 +80,7 @@ public class OIDProviderConfigResponse {
private String[] codeChallengeMethodsSupported;
private String deviceAuthorizationEndpoint;
private String webFingerEndpoint;
private Boolean tlsClientCertificateBoundAccessTokens;

public String getIssuer() {
return issuer;
@@ -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);
@@ -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
@@ -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;
@@ -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)));
@@ -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
@@ -114,6 +114,18 @@ 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 X5T_S256 = "x5t#S256";

public static final String ENABLE_TLS_CERT_BOUND_ACCESS_TOKENS_VIA_BINDING_TYPE = "OAuth.OpenIDConnect." +
"EnableTLSCertificateBoundAccessTokensViaBindingType";

/**
* Enum for OIDC supported subject types.
*/
Original file line number Diff line number Diff line change
@@ -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";

Original file line number Diff line number Diff line change
@@ -20,8 +20,10 @@
import org.apache.commons.lang.StringUtils;
import org.apache.oltu.oauth2.common.utils.JSONUtils;
import org.json.JSONException;
import org.wso2.carbon.identity.oauth.common.OAuthConstants;
import org.wso2.carbon.identity.oauth.config.OAuthServerConfiguration;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -242,6 +244,21 @@ 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(OAuthConstants.X5T_S256, cnfBindingValue));
}
return this;
}

/**
* @param errorCode Error Code
* @return IntrospectionResponseBuilder
Original file line number Diff line number Diff line change
@@ -28,6 +28,7 @@
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;
@@ -159,8 +160,13 @@ 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())) {
respBuilder.setCnfBindingValue(introspectionResponse.getCnfBindingValue());
}
}

// Retrieve list of registered IntrospectionDataProviders.
Original file line number Diff line number Diff line change
@@ -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;
@@ -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" +
@@ -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());

@@ -136,6 +139,10 @@ 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(OAuthConstants.X5T_S256), "R4Hj_0nNdIzVvPdCdsWlxNKm6a74cszp4Za4M1iE8P9",
"CNF value is not equal");
}

/**
@@ -161,6 +168,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");
@@ -180,6 +188,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")
Original file line number Diff line number Diff line change
@@ -16,14 +16,17 @@
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.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;

@@ -97,14 +100,24 @@ 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");

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(OAuthConstants.CNF))
.get(OAuthConstants.X5T_S256), "R4Hj_0nNdIzVvPdCdsWlxNKm6a74cszp4Za4M1iE8P9");

}

Original file line number Diff line number Diff line change
@@ -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";
Original file line number Diff line number Diff line change
@@ -1642,6 +1642,12 @@ public class SQLQueries {
public static final String GET_SHARED_APP_ID = "SELECT SHARED_APP_ID FROM SP_SHARED_APP WHERE " +
"OWNER_ORG_ID = ? AND MAIN_APP_ID = ? AND SHARED_ORG_ID = ? ";

public static final String RETRIEVE_TOKEN_BINDING_BY_REFRESH_TOKEN =
"SELECT BINDING.TOKEN_BINDING_TYPE, BINDING.TOKEN_BINDING_VALUE, BINDING.TOKEN_BINDING_REF " +
"FROM IDN_OAUTH2_ACCESS_TOKEN TOKEN LEFT JOIN IDN_OAUTH2_TOKEN_BINDING BINDING ON " +
"TOKEN.TOKEN_ID=BINDING.TOKEN_ID WHERE TOKEN.REFRESH_TOKEN = ? " +
"AND BINDING.TOKEN_BINDING_TYPE = ?";

private SQLQueries() {

}
Original file line number Diff line number Diff line change
@@ -76,4 +76,18 @@ default Optional<TokenBinding> getTokenBindingByBindingRef(String tokenId, Strin
* @throws IdentityOAuth2Exception in case of failure.
*/
void deleteTokenBinding(String tokenId) throws IdentityOAuth2Exception;

/**
* Obtain token binding from refresh token.
*
* @param refreshToken Refresh token sent in the request.
* @param isTokenHashingEnabled Whether token hashing is enabled.
* @return Optional token binding for the refresh token.
* @throws IdentityOAuth2Exception An exception is thrown if an error occurs while obtaining the token binding.
*/
default Optional<TokenBinding> getBindingFromRefreshToken(String refreshToken, boolean isTokenHashingEnabled)
throws IdentityOAuth2Exception {

return Optional.empty();
}
}
Original file line number Diff line number Diff line change
@@ -20,8 +20,12 @@

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.wso2.carbon.database.utils.jdbc.JdbcTemplate;
import org.wso2.carbon.database.utils.jdbc.exceptions.DataAccessException;
import org.wso2.carbon.identity.core.util.IdentityDatabaseUtil;
import org.wso2.carbon.identity.core.util.IdentityTenantUtil;
import org.wso2.carbon.identity.oauth.tokenprocessor.HashingPersistenceProcessor;
import org.wso2.carbon.identity.oauth.tokenprocessor.TokenPersistenceProcessor;
import org.wso2.carbon.identity.oauth2.IdentityOAuth2Exception;
import org.wso2.carbon.identity.oauth2.token.bindings.TokenBinding;
import org.wso2.carbon.utils.multitenancy.MultitenantConstants;
@@ -30,9 +34,12 @@
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;

import static org.wso2.carbon.identity.oauth2.OAuth2Constants.TokenBinderType.CERTIFICATE_BASED_TOKEN_BINDER;
import static org.wso2.carbon.identity.oauth2.dao.SQLQueries.DELETE_TOKEN_BINDING_BY_TOKEN_ID;
import static org.wso2.carbon.identity.oauth2.dao.SQLQueries.RETRIEVE_TOKEN_BINDING_BY_REFRESH_TOKEN;
import static org.wso2.carbon.identity.oauth2.dao.SQLQueries.RETRIEVE_TOKEN_BINDING_BY_TOKEN_ID;
import static org.wso2.carbon.identity.oauth2.dao.SQLQueries.RETRIEVE_TOKEN_BINDING_BY_TOKEN_ID_AND_BINDING_REF;
import static org.wso2.carbon.identity.oauth2.dao.SQLQueries.RETRIEVE_TOKEN_BINDING_REF_EXISTS;
@@ -163,4 +170,37 @@ public void deleteTokenBinding(String tokenId) throws IdentityOAuth2Exception {
throw new IdentityOAuth2Exception("Failed to get token binding for the token id: " + tokenId, e);
}
}

@Override
public Optional<TokenBinding> getBindingFromRefreshToken(String refreshToken, boolean isTokenHashingEnabled)
throws IdentityOAuth2Exception {

TokenPersistenceProcessor hashingPersistenceProcessor = new HashingPersistenceProcessor();
JdbcTemplate jdbcTemplate = new JdbcTemplate(IdentityDatabaseUtil.getDataSource());
if (isTokenHashingEnabled) {
refreshToken = hashingPersistenceProcessor.getProcessedRefreshToken(refreshToken);
}
try {
String finalRefreshToken = refreshToken;
List<TokenBinding> tokenBindingList = jdbcTemplate.executeQuery(RETRIEVE_TOKEN_BINDING_BY_REFRESH_TOKEN,
(resultSet, rowNumber) -> {
TokenBinding tokenBinding = new TokenBinding();
tokenBinding.setBindingType(resultSet.getString(1));
tokenBinding.setBindingValue(resultSet.getString(2));
tokenBinding.setBindingReference(resultSet.getString(3));

return tokenBinding;
},
preparedStatement -> {
preparedStatement.setString(1, finalRefreshToken);
preparedStatement.setString(2, CERTIFICATE_BASED_TOKEN_BINDER);
});

return tokenBindingList.isEmpty() ? null : Optional.ofNullable(tokenBindingList.get(0));
} catch (DataAccessException e) {
String error = String.format("Error obtaining token binding type using refresh token: %s.",
refreshToken);
throw new IdentityOAuth2Exception(error, e);
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
@@ -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;
Loading