Skip to content

Commit

Permalink
feat(jans-auth-server): draft for - improve dcr / ssa validation for …
Browse files Browse the repository at this point in the history
…dynamic registration #2980
  • Loading branch information
yuriyz committed Nov 29, 2022
1 parent b0034ec commit cd06cda
Show file tree
Hide file tree
Showing 7 changed files with 478 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
*/
public enum SoftwareStatementValidationType {
NONE("none"),
BUILTIN("builtin"),
JWKS("jwks"),
JWKS_URI("jwks_uri"),
SCRIPT("script");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.google.common.collect.Lists;

import io.jans.agama.model.EngineConfig;
import io.jans.as.model.common.*;
import io.jans.as.model.error.ErrorHandlingMethod;
import io.jans.as.model.jwk.KeySelectionStrategy;
import io.jans.as.model.ssa.SsaConfiguration;
import io.jans.as.model.ssa.SsaValidationConfig;
import io.jans.doc.annotation.DocProperty;

import java.util.ArrayList;
Expand Down Expand Up @@ -803,9 +803,17 @@ public class AppConfiguration implements Configuration {
@DocProperty(description = "Engine Config which offers an alternative way to build authentication flows in Janssen server")
private EngineConfig agamaConfiguration;

@DocProperty(description = "")
@DocProperty(description = "DCR SSA Validation configurations used to perform validation of SSA or DCR")
private List<SsaValidationConfig> dcrSsaValidationConfigs;

@DocProperty(description = "SSA Configuration")
private SsaConfiguration ssaConfiguration;

public List<SsaValidationConfig> getDcrSsaValidationConfigs() {
if (dcrSsaValidationConfigs == null) dcrSsaValidationConfigs = new ArrayList<>();
return dcrSsaValidationConfigs;
}

public Boolean getRequireRequestObjectEncryption() {
if (requireRequestObjectEncryption == null) requireRequestObjectEncryption = false;
return requireRequestObjectEncryption;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
package io.jans.as.model.ssa;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;

import java.util.ArrayList;
import java.util.List;

/**
* @author Yuriy Z
*/
@JsonIgnoreProperties(
ignoreUnknown = true
)
public class SsaValidationConfig {

private String id;
private SsaValidationType type;
private String displayName;
private String description;
private List<String> scopes;
private List<String> allowedClaims;
private String jwks;
private String jwksUri;
private List<String> issuers;
private String configurationEndpoint;
private String configurationEndpointClaim;
private String sharedSecret;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public SsaValidationType getType() {
return type;
}

public void setType(SsaValidationType type) {
this.type = type;
}

public String getDisplayName() {
return displayName;
}

public void setDisplayName(String displayName) {
this.displayName = displayName;
}

public String getDescription() {
return description;
}

public void setDescription(String description) {
this.description = description;
}

public List<String> getScopes() {
if (scopes == null) scopes = new ArrayList<>();
return scopes;
}

public void setScopes(List<String> scopes) {
this.scopes = scopes;
}

public List<String> getAllowedClaims() {
if (allowedClaims == null) allowedClaims = new ArrayList<>();
return allowedClaims;
}

public void setAllowedClaims(List<String> allowedClaims) {
this.allowedClaims = allowedClaims;
}

public String getJwks() {
return jwks;
}

public void setJwks(String jwks) {
this.jwks = jwks;
}

public String getJwksUri() {
return jwksUri;
}

public void setJwksUri(String jwksUri) {
this.jwksUri = jwksUri;
}

public List<String> getIssuers() {
if (issuers == null) issuers = new ArrayList<>();
return issuers;
}

public void setIssuers(List<String> issuers) {
this.issuers = issuers;
}

public String getConfigurationEndpoint() {
return configurationEndpoint;
}

public void setConfigurationEndpoint(String configurationEndpoint) {
this.configurationEndpoint = configurationEndpoint;
}

public String getConfigurationEndpointClaim() {
return configurationEndpointClaim;
}

public void setConfigurationEndpointClaim(String configurationEndpointClaim) {
this.configurationEndpointClaim = configurationEndpointClaim;
}

public String getSharedSecret() {
return sharedSecret;
}

public void setSharedSecret(String sharedSecret) {
this.sharedSecret = sharedSecret;
}

@Override
public String toString() {
return "SsaValidationConfig{" +
"id='" + id + '\'' +
", type=" + type +
", displayName='" + displayName + '\'' +
", description='" + description + '\'' +
", scopes=" + scopes +
", allowedClaims=" + allowedClaims +
", jwks='" + jwks + '\'' +
", jwksUri='" + jwksUri + '\'' +
", issuers=" + issuers +
", configurationEndpoint='" + configurationEndpoint + '\'' +
", configurationEndpointClaim='" + configurationEndpointClaim + '\'' +
", sharedSecret='" + sharedSecret + '\'' +
'}';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package io.jans.as.model.ssa;

/**
* @author Yuriy Z
*/
public enum SsaValidationType {
NONE("none"),
SSA("ssa"),
DCR("dcr");

private final String value;

SsaValidationType(String value) {
this.value = value;
}

public String getValue() {
return value;
}

public static SsaValidationType of(String value) {
for (SsaValidationType t : values()) {
if (t.value.equalsIgnoreCase(value)) {
return t;
}
}
return NONE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import io.jans.as.model.exception.InvalidJwtException;
import io.jans.as.model.jwt.Jwt;
import io.jans.as.model.register.RegisterErrorResponseType;
import io.jans.as.model.ssa.SsaValidationType;
import io.jans.as.model.util.JwtUtil;
import io.jans.as.model.util.Pair;
import io.jans.as.server.ciba.CIBARegisterParamsValidatorService;
Expand All @@ -27,18 +28,17 @@
import io.jans.as.server.model.common.AuthorizationGrantList;
import io.jans.as.server.model.registration.RegisterParamsValidator;
import io.jans.as.server.service.external.ExternalDynamicClientRegistrationService;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.Nullable;
import org.json.JSONObject;
import org.slf4j.Logger;

import jakarta.ejb.Stateless;
import jakarta.inject.Inject;
import jakarta.inject.Named;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.ws.rs.WebApplicationException;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.apache.commons.lang.StringUtils;
import org.jetbrains.annotations.Nullable;
import org.json.JSONObject;
import org.slf4j.Logger;

import static io.jans.as.model.register.RegisterRequestParam.SOFTWARE_STATEMENT;
import static org.apache.commons.lang3.BooleanUtils.isFalse;
Expand Down Expand Up @@ -75,6 +75,9 @@ public class RegisterValidator {
@Inject
private RegisterParamsValidator registerParamsValidator;

@Inject
private SsaValidationConfigService ssaValidationConfigService;

public void validateNotBlank(String input, String errorReason) {
if (StringUtils.isBlank(input)) {
log.trace("Failed to perform client action, reason: {}", errorReason);
Expand All @@ -90,23 +93,12 @@ public void validateRequestObject(String requestParams, JSONObject softwareState

final Jwt jwt = Jwt.parseOrThrow(requestParams);
final SignatureAlgorithm signatureAlgorithm = jwt.getHeader().getSignatureAlgorithm();
final SsaValidationConfigContext ssaContext = new SsaValidationConfigContext(jwt, SsaValidationType.DCR);

final boolean isHmac = AlgorithmFamily.HMAC.equals(signatureAlgorithm.getFamily());
if (isHmac) {
String hmacSecret = appConfiguration.getDcrSignatureValidationSharedSecret();
if (StringUtils.isBlank(hmacSecret)) {
hmacSecret = externalDynamicClientRegistrationService.getDcrHmacSecret(httpRequest, jwt);
}
if (StringUtils.isBlank(hmacSecret)) {
log.error("No hmacSecret provided in Dynamic Client Registration script (method getDcrHmacSecret didn't return actual secret). ");
throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_SOFTWARE_STATEMENT, "");
}

boolean validSignature = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), null, null, hmacSecret, signatureAlgorithm);
log.trace("Request object validation result: {}", validSignature);
if (!validSignature) {
throw new InvalidJwtException("Invalid cryptographic segment in the request object.");
}
validateRequestObjectHmac(httpRequest, ssaContext);
return;
}

String jwksUri = null;
Expand All @@ -126,16 +118,65 @@ public void validateRequestObject(String requestParams, JSONObject softwareState
jwt.getEncodedSignature(), jwt.getHeader().getKeyId(), jwks, null, signatureAlgorithm);

log.trace("Request object validation result: {}", validSignature);
if (!validSignature) {
throw new InvalidJwtException("Invalid cryptographic segment in the request object.");
if (validSignature) {
return;
}

if (validateRequestObjectSignatureWithSsaValidationConfigs(requestParams)) {
return;
}

throw new InvalidJwtException("Invalid request object.");
} catch (Exception e) {
if (validateRequestObjectSignatureWithSsaValidationConfigs(requestParams)) {
return;
}

final String msg = "Unable to validate request object JWT.";
log.error(msg, e);
throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_CLIENT_METADATA, msg);
}
}

private boolean validateRequestObjectSignatureWithSsaValidationConfigs(String requestParams) {
try {
final SsaValidationConfigContext ssaContext = new SsaValidationConfigContext(Jwt.parseOrThrow(requestParams), SsaValidationType.DCR);
final boolean valid = ssaValidationConfigService.hasValidSignature(ssaContext);
log.trace("Request object validation result for ssaValidationConfigs: {}", valid);
if (valid) {
log.trace("Request object successfully validated by ssaValidationConfig: {}", ssaContext.getSuccessfulConfig());
return true;
}
} catch (InvalidJwtException e) {
log.error("Unable to validate request object JWT.", e);
}
return false;
}

private void validateRequestObjectHmac(HttpServletRequest httpRequest, SsaValidationConfigContext ssaContext) throws CryptoProviderException, InvalidJwtException {
final Jwt jwt = ssaContext.getJwt();

if (ssaValidationConfigService.isHmacValid(ssaContext)) {
log.trace("Request object successfully validated by ssaValidationConfig: {}", ssaContext.getSuccessfulConfig());
return;
}
String hmacSecret = appConfiguration.getDcrSignatureValidationSharedSecret();
if (StringUtils.isBlank(hmacSecret)) {
hmacSecret = externalDynamicClientRegistrationService.getDcrHmacSecret(httpRequest, jwt);
}
if (StringUtils.isBlank(hmacSecret)) {
log.error("No hmacSecret provided in Dynamic Client Registration script (method getDcrHmacSecret didn't return actual secret). ");
throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_SOFTWARE_STATEMENT, "");
}

boolean validSignature = cryptoProvider.verifySignature(jwt.getSigningInput(), jwt.getEncodedSignature(), null, null, hmacSecret, jwt.getHeader().getSignatureAlgorithm());
log.trace("Request object validation result: {}", validSignature);
if (validSignature) {
return;
}
throw new InvalidJwtException("Invalid cryptographic segment in the request object.");
}

@Nullable
private String getJwksString(JSONObject softwareStatement) {
if (StringUtils.isNotBlank(appConfiguration.getDcrSignatureValidationSoftwareStatementJwksClaim())) {
Expand Down Expand Up @@ -179,6 +220,8 @@ public JSONObject validateSoftwareStatement(HttpServletRequest httpServletReques
final SignatureAlgorithm signatureAlgorithm = softwareStatement.getHeader().getSignatureAlgorithm();

final SoftwareStatementValidationType validationType = SoftwareStatementValidationType.fromString(appConfiguration.getSoftwareStatementValidationType());
printWarningIfNeeded(validationType);

if (validationType == SoftwareStatementValidationType.NONE) {
log.trace("software_statement validation was skipped due to `softwareStatementValidationType` configuration property set to none. (Not recommended.)");
return softwareStatement.getClaims().toJsonObject();
Expand All @@ -188,6 +231,10 @@ public JSONObject validateSoftwareStatement(HttpServletRequest httpServletReques
return validateSoftwareStatementForScript(httpServletRequest, requestObject, softwareStatement, signatureAlgorithm);
}

if (validationType == SoftwareStatementValidationType.BUILTIN) {
return ssaValidationConfigService.validateSsaForBuiltIn(softwareStatement);
}

if ((validationType == SoftwareStatementValidationType.JWKS_URI ||
validationType == SoftwareStatementValidationType.JWKS) &&
StringUtils.isBlank(appConfiguration.getSoftwareStatementValidationClaimName())) {
Expand Down Expand Up @@ -231,6 +278,13 @@ public JSONObject validateSoftwareStatement(HttpServletRequest httpServletReques
}
}

private void printWarningIfNeeded(SoftwareStatementValidationType validationType) {
if (validationType == SoftwareStatementValidationType.SCRIPT || validationType == SoftwareStatementValidationType.BUILTIN) {
return;
}
log.warn("It is strongly recommended to use SCRIPT or BUILTIN value for softwareStatementValidationType configuration property.");
}

@Nullable
private JSONObject validateSoftwareStatementForScript(HttpServletRequest httpServletRequest, JSONObject requestObject, Jwt softwareStatement, SignatureAlgorithm signatureAlgorithm) throws CryptoProviderException, InvalidJwtException {
if (!externalDynamicClientRegistrationService.isEnabled()) {
Expand Down
Loading

0 comments on commit cd06cda

Please sign in to comment.