Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
com.nimbusds.openid.connect.provider.spi.reg.statement.SoftwareStatementVerifier Maven / Gradle / Ivy
package com.nimbusds.openid.connect.provider.spi.reg.statement;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.EnumSet;
import java.util.Properties;
import java.util.Set;
import com.jayway.jsonpath.Option;
import com.jayway.jsonpath.spi.json.JsonOrgJsonProvider;
import com.jayway.jsonpath.spi.json.JsonProvider;
import com.jayway.jsonpath.spi.mapper.JsonOrgMappingProvider;
import com.jayway.jsonpath.spi.mapper.MappingProvider;
import net.jcip.annotations.ThreadSafe;
import net.minidev.json.JSONObject;
import com.nimbusds.common.contenttype.ContentType;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.RemoteKeySourceException;
import com.nimbusds.jose.jwk.source.RemoteJWKSet;
import com.nimbusds.jose.proc.BadJOSEException;
import com.nimbusds.jose.proc.JWSVerificationKeySelector;
import com.nimbusds.jose.util.DefaultResourceRetriever;
import com.nimbusds.jwt.JWTClaimsSet;
import com.nimbusds.jwt.SignedJWT;
import com.nimbusds.jwt.proc.DefaultJWTClaimsVerifier;
import com.nimbusds.jwt.proc.DefaultJWTProcessor;
import com.nimbusds.oauth2.sdk.OAuth2Error;
import com.nimbusds.oauth2.sdk.ParseException;
import com.nimbusds.oauth2.sdk.Scope;
import com.nimbusds.oauth2.sdk.client.RegistrationError;
import com.nimbusds.oauth2.sdk.http.HTTPRequest;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import com.nimbusds.oauth2.sdk.util.JSONObjectUtils;
import com.nimbusds.oauth2.sdk.util.StringUtils;
import com.nimbusds.openid.connect.provider.spi.InitContext;
import com.nimbusds.openid.connect.provider.spi.reg.InterceptorContext;
import com.nimbusds.openid.connect.provider.spi.reg.RegistrationInterceptor;
import com.nimbusds.openid.connect.provider.spi.reg.WrappedHTTPResponseException;
import com.nimbusds.openid.connect.sdk.rp.OIDCClientMetadata;
import com.nimbusds.openid.connect.sdk.rp.statement.InvalidSoftwareStatementException;
import com.nimbusds.openid.connect.sdk.rp.statement.SoftwareStatementProcessor;
/**
* Software statement verifier.
*/
@ThreadSafe
public class SoftwareStatementVerifier implements RegistrationInterceptor {
private static final URL DUMMY_URL;
static {
try {
DUMMY_URL = new URL("http:///");
} catch (MalformedURLException e) {
throw new RuntimeException(e);
}
}
/**
* The verifier configuration.
*/
private Configuration config;
/**
* The configured software statements processor, {@code null} if
* not enabled.
*/
private SoftwareStatementProcessor> statementProcessor;
/**
* Optional processor for signed JWT requests, where the JWK set URL
* is statically configured, {@code null} if not enabled.
*/
private DefaultJWTProcessor> requestJWTProcessorWithStaticJWKSetURL;
/**
* Optional processor for signed JWT requests, where the JWK set URL is
* specified by a claim in the software statement, {@code null} if not
* enabled.
*/
private DefaultJWTProcessor requestJWTProcessorWithStatementReferencedJWKSetURL;
/**
* Loads the configuration.
*
* @param initContext The initialisation context. Must not be
* {@code null}.
*
* @return The configuration.
*
* @throws IOException If loading failed.
*/
private static Configuration loadConfiguration(final InitContext initContext)
throws IOException {
var props = new Properties();
var inputStream = initContext.getResourceAsStream(Configuration.FILE_PATH);
if (inputStream != null) {
props.load(inputStream);
}
return new Configuration(props);
}
@Override
public void init(final InitContext initContext)
throws Exception {
config = loadConfiguration(initContext);
config.log();
if (! config.enable) {
return;
}
com.jayway.jsonpath.Configuration.setDefaults(new com.jayway.jsonpath.Configuration.Defaults() {
private final JsonProvider jsonProvider = new JsonOrgJsonProvider();
private final MappingProvider mappingProvider = new JsonOrgMappingProvider();
@Override
public JsonProvider jsonProvider() {
return jsonProvider;
}
@Override
public MappingProvider mappingProvider() {
return mappingProvider;
}
@Override
public Set options() {
return EnumSet.noneOf(Option.class);
}
});
statementProcessor = new SoftwareStatementProcessor<>(
config.issuer,
false,
config.jwsAlgorithms,
config.issuerJWKSetURL,
config.httpConnectTimeout,
config.httpReadTimeout,
100_000);
if (config.requestType.equals(RequestType.JWT)) {
// The registration requests will be encoded into signed JWTs
var resourceRetriever = new DefaultResourceRetriever(config.httpConnectTimeout, config.httpReadTimeout);
if (config.requestJWT_jwkSetSource.getStaticURL() != null) {
// Static JWK set URL
requestJWTProcessorWithStaticJWKSetURL = new DefaultJWTProcessor<>();
requestJWTProcessorWithStaticJWKSetURL.setJWSKeySelector(
new JWSVerificationKeySelector<>(
config.requestJWT_jwsAlgorithms,
new RemoteJWKSet<>(
config.requestJWT_jwkSetSource.getStaticURL(),
resourceRetriever)));
requestJWTProcessorWithStaticJWKSetURL.setJWTClaimsSetVerifier(
new DefaultJWTClaimsVerifier<>(
null,
config.requestJWT_requiredClaims));
} else if (config.requestJWT_jwkSetSource.getURLClaimName() != null) {
// JWK set URL found in a software statement claim
requestJWTProcessorWithStatementReferencedJWKSetURL = new DefaultJWTProcessor<>();
requestJWTProcessorWithStatementReferencedJWKSetURL.setJWSKeySelector(
new SoftwareStatementBasedKeySelector(
config.requestJWT_jwkSetSource.getURLClaimName(),
resourceRetriever));
requestJWTProcessorWithStatementReferencedJWKSetURL.setJWTClaimsSetVerifier(
new DefaultJWTClaimsVerifier<>(
null,
config.requestJWT_requiredClaims));
} else {
throw new IllegalStateException();
}
}
}
/**
* Returns the software statement verifier configuration.
*
* @return The configuration, {@code null} if not configured yet.
*/
public Configuration getConfiguration() {
return config;
}
@Override
public boolean isEnabled() {
return config.enable;
}
/**
* Logs a pass-through reason for the specified HTTP request.
*
* @param originalHTTPRequest The original HTTP request.
* @param reason The reason to log.
*
* @return The original HTTP request.
*/
HTTPRequest passThrough(final HTTPRequest originalHTTPRequest, final String reason) {
// Pass the request unmodified. If the Connect2id
// server isn't configured for open registration and an
// initial registration token isn't provided the
// request will be rejected downstream.
Loggers.REGISTRATION.info("[SSV0109] Original HTTP request passed through: {}", reason);
return originalHTTPRequest;
}
/**
* Applies the configured JSON object transforms to merged client
* metadata.
*
* @param clientMetadata The client metadata.
*
* @return The client metadata, with any transforms applied.
*/
OIDCClientMetadata applyTransforms(final OIDCClientMetadata clientMetadata) {
final var jsonObject = clientMetadata.toJSONObject();
// Order matters - allow members to be renamed before
// potentially moving them into the "data" container!
for (var memberName: config.transforms_remove) {
JSONObjectTransforms.remove(jsonObject, memberName);
}
for (var en: config.transforms_rename.entrySet()) {
JSONObjectTransforms.rename(jsonObject, en.getKey(), en.getValue());
}
for (var memberName: config.transforms_moveIntoData) {
JSONObjectTransforms.moveIntoData(jsonObject, memberName);
}
try {
return OIDCClientMetadata.parse(jsonObject);
} catch (ParseException e) {
var msg = "Error reconstructing client metadata after applying JSON transforms: " + e.getMessage();
Loggers.REGISTRATION.error("[SSV0122] {}", msg);
throw new RuntimeException(msg, e);
}
}
@Override
public HTTPRequest interceptPostRequest(final HTTPRequest httpRequest,
final InterceptorContext interceptorCtx)
throws WrappedHTTPResponseException {
if (! isEnabled()) {
return httpRequest;
}
if (config.clientX509Certificate_require) {
if (httpRequest.getClientX509Certificate() == null) {
return passThrough(httpRequest, "Client X.509 certificate missing");
}
Loggers.REGISTRATION.info("[SSV0113] Received client X.509 certificate: iss={} sub={}",
httpRequest.getClientX509Certificate().getIssuerDN(),
httpRequest.getClientX509Certificate().getSubjectDN());
if (config.clientX509Certificate_rootDN != null) {
var clientCertRootDN = httpRequest.getClientX509CertificateRootDN();
if (! config.clientX509Certificate_rootDN.equalsIgnoreCase(clientCertRootDN)) {
return passThrough(httpRequest, "Client X.509 certificate doesn't have required root DN: " + clientCertRootDN);
}
}
}
final JSONObject requestJSONObject;
SignedJWT requestJWT = null;
if (config.requestType.equals(RequestType.JWT)) {
// The software statement must be within a registration
// request that is a signed JWT
if (
ContentType.APPLICATION_JSON.equals(httpRequest.getEntityContentType()) // explicit application/json
|| StringUtils.isBlank(httpRequest.getQuery()) // empty entity body
|| httpRequest.getQuery().startsWith("{") // entity body looks like JSON object
) {
return passThrough(httpRequest, "No registration request JWT found");
}
try {
requestJWT = SignedJWT.parse(httpRequest.getQuery());
requestJSONObject = JSONObjectUtils.toJSONObject(requestJWT.getJWTClaimsSet());
} catch (java.text.ParseException e) {
Loggers.REGISTRATION.info("[SSV0112] Invalid registration JWT: {}", e.getMessage());
throw new WrappedHTTPResponseException(
e.getMessage(),
OAuth2Error.INVALID_REQUEST
.setHTTPStatusCode(HTTPResponse.SC_BAD_REQUEST)
.setDescription("The request must be a signed JWT")
.toHTTPResponse());
}
// Verify the request JWT audience if required
if (config.requestJWT_requiredClaims.contains("aud")) {
var assertedAud = requestJSONObject.get("aud");
if (! interceptorCtx.getIssuer().getValue().equals(assertedAud)) {
var msg = "Invalid registration JWT: Audience not accepted: " + (assertedAud != null ? assertedAud : "Missing claim");
Loggers.REGISTRATION.info("[SSV0119] {}", msg);
throw new WrappedHTTPResponseException(
msg,
OAuth2Error.INVALID_REQUEST
.setHTTPStatusCode(HTTPResponse.SC_BAD_REQUEST)
.setDescription(msg)
.toHTTPResponse());
}
}
// If the request JWT is verified with a dynamic JWK set specified by a
// software statement URL claim, the signature validation must be postponed
// until the statement is verified
if (requestJWTProcessorWithStaticJWKSetURL != null) {
// The request JWT is validated with a statically configured JWK set
try {
requestJWTProcessorWithStaticJWKSetURL.process(requestJWT, null);
} catch (BadJOSEException e) {
var msg = "Invalid registration JWT: " + e.getMessage();
Loggers.REGISTRATION.info("[SSV0116] {}", msg);
throw new WrappedHTTPResponseException(
e.getMessage(),
OAuth2Error.INVALID_REQUEST
.setHTTPStatusCode(HTTPResponse.SC_BAD_REQUEST)
.setDescription(msg)
.toHTTPResponse());
} catch (RemoteKeySourceException e) {
var msg = "Registration JWT validation failed: " + e.getMessage();
Loggers.REGISTRATION.info("[SSV0121] {}", msg);
throw new WrappedHTTPResponseException(
msg,
OAuth2Error.INVALID_REQUEST
.setHTTPStatusCode(HTTPResponse.SC_BAD_REQUEST)
.setDescription(msg)
.toHTTPResponse());
} catch (JOSEException e) {
Loggers.REGISTRATION.error("[SSV0117] {}", e.getMessage(), e);
throw new RuntimeException(e);
}
}
} else {
try {
requestJSONObject = httpRequest.getQueryAsJSONObject();
} catch (ParseException e) {
Loggers.REGISTRATION.info("[SSV0114] Invalid registration request: {}", e.getMessage());
throw new WrappedHTTPResponseException(
e.getMessage(),
OAuth2Error.INVALID_REQUEST
.setHTTPStatusCode(HTTPResponse.SC_BAD_REQUEST)
.setDescription("Invalid JSON: " + e.getMessage())
.toHTTPResponse());
}
}
OIDCClientMetadata clientMetadata;
try {
clientMetadata = OIDCClientMetadata.parse(requestJSONObject);
} catch (ParseException e) {
Loggers.REGISTRATION.info("[SSV0104] Invalid client metadata: {}", e.getMessage());
throw new WrappedHTTPResponseException(e.getMessage(), e.getErrorObject().toHTTPResponse());
}
Loggers.REGISTRATION.debug("[SSV0106] Received client metadata: {}", clientMetadata::toString);
var softwareStatement = clientMetadata.getSoftwareStatement();
if (softwareStatement == null) {
// No software statement found
if (requestJWT != null) {
var msg = "Missing required software statement in JWT";
Loggers.REGISTRATION.info("[SSV0115] {}", msg);
throw new WrappedHTTPResponseException(
msg,
OAuth2Error.INVALID_REQUEST
.setDescription(msg)
.toHTTPResponse());
}
return passThrough(httpRequest, "No software statement found");
}
JWTClaimsSet softwareStatementClaims;
try {
softwareStatementClaims = softwareStatement.getJWTClaimsSet();
} catch (java.text.ParseException e) {
Loggers.REGISTRATION.info("[SSV0107] Invalid software statement JWT claims set: {}", e.getMessage());
String msg = "Invalid software statement JWT claims set";
throw new WrappedHTTPResponseException(
msg,
RegistrationError.INVALID_SOFTWARE_STATEMENT
.setDescription(msg)
.toHTTPResponse());
}
for (var requiredClaim: config.additionalRequiredClaims) {
if (! softwareStatementClaims.getClaims().containsKey(requiredClaim)) {
String msg = "Missing required software statement JWT claim: " + requiredClaim;
Loggers.REGISTRATION.info("[SSV0108] {}", msg);
throw new WrappedHTTPResponseException(
msg,
RegistrationError.INVALID_SOFTWARE_STATEMENT
.setDescription(msg)
.toHTTPResponse());
}
}
// Save the scope requested for registration before scrubbing it
Scope requestedTopLevelScope = clientMetadata.getScope();
// Scrub scope and all custom fields from top-level metadata before merging
// as these can have special meaning in a authorised request
clientMetadata.setScope(null);
clientMetadata.setCustomFields(new JSONObject());
// Verify the software statement signature
OIDCClientMetadata mergedClientMetadata;
try {
mergedClientMetadata = statementProcessor.process(clientMetadata);
} catch (InvalidSoftwareStatementException e) {
Loggers.REGISTRATION.info("[SSV0102] Invalid software statement: {}", e.getMessage());
throw new WrappedHTTPResponseException(e.getMessage(), e.getErrorObject().toHTTPResponse());
} catch (RemoteKeySourceException e) {
var msg = "Software statement JWT validation failed: " + e.getMessage();
Loggers.REGISTRATION.info("[SSV0120] {}", msg);
throw new WrappedHTTPResponseException(
msg,
RegistrationError.INVALID_SOFTWARE_STATEMENT
.setDescription(msg)
.toHTTPResponse());
} catch (JOSEException e) {
Loggers.REGISTRATION.info("[SSV0103] Internal JOSE error: {}", e.getMessage());
throw new RuntimeException(e.getMessage(), e);
}
if (requestJWTProcessorWithStatementReferencedJWKSetURL != null) {
// Complete the request JWT verification using the JWK
// set URL from the verified software statement
try {
requestJWTProcessorWithStatementReferencedJWKSetURL.process(
requestJWT,
new SoftwareStatementContext(softwareStatementClaims));
} catch (BadJOSEException e) {
var msg = "Invalid registration JWT: " + e.getMessage();
Loggers.REGISTRATION.info("[SSV0116] {}", msg);
throw new WrappedHTTPResponseException(
e.getMessage(),
OAuth2Error.INVALID_REQUEST
.setHTTPStatusCode(HTTPResponse.SC_BAD_REQUEST)
.setDescription(msg)
.toHTTPResponse());
} catch (RemoteKeySourceException e) {
var msg = "Registration JWT validation failed: " + e.getMessage();
Loggers.REGISTRATION.info("[SSV0121] {}", msg);
throw new WrappedHTTPResponseException(
msg,
OAuth2Error.INVALID_REQUEST
.setHTTPStatusCode(HTTPResponse.SC_BAD_REQUEST)
.setDescription(msg)
.toHTTPResponse());
} catch (JOSEException e) {
Loggers.REGISTRATION.error("[SSV0117] {}", e.getMessage(), e);
throw new RuntimeException(e);
}
}
// Find out if any of the requested top-level scope values are allowed
var mergedScope = new Scope();
Scope allowedTopLevelScope = ScopeRule.filter(
requestedTopLevelScope,
config.scopeRules.values(),
com.nimbusds.jose.util.JSONObjectUtils.toJSONString(softwareStatementClaims.toJSONObject())
);
mergedScope.addAll(allowedTopLevelScope);
Scope softwareStatementScope = mergedClientMetadata.getScope();
if (softwareStatementScope != null) {
mergedScope.addAll(softwareStatementScope);
}
if (! mergedScope.isEmpty()) {
mergedClientMetadata.setScope(mergedScope);
}
// Apply optional transforms
var finalClientMetadata = applyTransforms(mergedClientMetadata);
var rewrittenHTTPRequest = new HTTPRequest(HTTPRequest.Method.POST, DUMMY_URL);
rewrittenHTTPRequest.setAuthorization(config.registrationAccessToken.toAuthorizationHeader());
rewrittenHTTPRequest.setEntityContentType(ContentType.APPLICATION_JSON);
var finalClientMetadataString = finalClientMetadata.toString();
rewrittenHTTPRequest.setQuery(finalClientMetadataString);
Loggers.REGISTRATION.info("[SSV0100] Applied verified software statement to client metadata:{}",
createSoftwareStatementClaimsLogString(softwareStatementClaims));
Loggers.REGISTRATION.debug("[SSV0105] Final client metadata: {}", finalClientMetadataString);
return rewrittenHTTPRequest;
}
/**
* Creates a string to log the configured software statement claims.
*
* @param softwareStatementClaims The software statement claims.
*
* @return The string to log.
*/
private String createSoftwareStatementClaimsLogString(final JWTClaimsSet softwareStatementClaims) {
StringBuilder sb = new StringBuilder();
for (var claimName: config.logClaims) {
sb.append(" ");
sb.append(claimName).append("=").append(softwareStatementClaims.getClaim(claimName));
}
return sb.toString();
}
@Override
public void shutdown() {
Loggers.MAIN.info("[SSV0199] Shutting down ...");
}
}