diff --git a/docs/admin/auth-server/endpoints/client-registration.md b/docs/admin/auth-server/endpoints/client-registration.md index 6bb262c1fa6..5336924f890 100644 --- a/docs/admin/auth-server/endpoints/client-registration.md +++ b/docs/admin/auth-server/endpoints/client-registration.md @@ -171,6 +171,23 @@ parameters: point to JWKS URI. - `dcrSignatureValidationSoftwareStatementJwksClaim` - specifies claim name inside software statement. Value of claim should point to inlined JWKS. + - `trustedSsaIssuers` - map of trusted SSA issuers with configuration (e.g. automatically granted scopes). When empty - no issuers validation is performed. When not empty - AS forces validation and each SSA must match at least one entry from list. + - `automaticallyGrantedScopes` - automatically granted scopes for trusted issuer + ```json + { + "https://trusted.as.com": { + "automaticallyGrantedScopes": [ + "a", + "b" + ] + }, + "https://another-trusted.as.com": { + "automaticallyGrantedScopes": [ + "d" + ] + } + } + ``` Configure the AS using steps explained in the [link](#curl-commands-to-configure-jans-auth-server) @@ -501,9 +518,25 @@ When create entry in `dcrSsaValidationConfigs` configuration property : **Via other configuration properties** +* **trustedSsaIssuers** - map of trusted SSA issuers with configuration (e.g. automatically granted scopes). When empty - no issuers validation is performed. When not empty - AS forces validation and each SSA must match at least one entry from list. + * **automaticallyGrantedScopes** - automatically granted scopes for trusted issuer + ```json + { + "https://trusted.as.com": { + "automaticallyGrantedScopes": [ + "a", + "b" + ] + }, + "https://another-trusted.as.com": { + "automaticallyGrantedScopes": [ + "d" + ] + } + } + ``` * **dcrSignatureValidationJwks** - specifies JWKS for DCR's validations * **dcrSignatureValidationJwksUri** - specifies JWKS URI for DCR's validations -* **trustedSsaIssuers** - List of trusted SSA issuers. If MTLS private key is used to sign DCR JWT, certificate issuer is checked as well. * **dcrSignatureValidationSharedSecret** - if HMAC is used, this is the shared secret * **dcrSignatureValidationEnabled** - boolean value enables DCR signature validation. Default is false * **dcrSignatureValidationSoftwareStatementJwksURIClaim** - specifies claim name inside software statement. Value of claim should point to JWKS URI diff --git a/docs/admin/reference/json/properties/janssenauthserver-properties.md b/docs/admin/reference/json/properties/janssenauthserver-properties.md index f5acb26a999..1748bfc2f54 100644 --- a/docs/admin/reference/json/properties/janssenauthserver-properties.md +++ b/docs/admin/reference/json/properties/janssenauthserver-properties.md @@ -253,7 +253,7 @@ tags: | tokenEndpointAuthSigningAlgValuesSupported | A list of the JWS signing algorithms (alg values) supported by the Token Endpoint for the signature on the JWT used to authenticate the Client at the Token Endpoint for the private_key_jwt and client_secret_jwt authentication methods | [Details](#tokenendpointauthsigningalgvaluessupported) | | tokenRevocationEndpoint | The URL for the access_token or refresh_token revocation endpoint | [Details](#tokenrevocationendpoint) | | trustedClientEnabled | Boolean value specifying whether a client is trusted and no authorization is required | [Details](#trustedclientenabled) | -| trustedSsaIssuers | List of trusted SSA issuers. If MTLS private key is used to sign DCR JWT, certificate issuer is checked as well. | [Details](#trustedssaissuers) | +| trustedSsaIssuers | List of trusted SSA issuers with configuration (e.g. automatically granted scopes). | [Details](#trustedssaissuers) | | uiLocalesSupported | This list details the languages and scripts supported for the user interface | [Details](#uilocalessupported) | | umaAddScopesAutomatically | Add UMA scopes automatically if it is not registered yet | [Details](#umaaddscopesautomatically) | | umaConfigurationEndpoint | UMA Configuration endpoint URL | [Details](#umaconfigurationendpoint) | @@ -2466,7 +2466,7 @@ tags: ### trustedSsaIssuers -- Description: List of trusted SSA issuers. If MTLS private key is used to sign DCR JWT, certificate issuer is checked as well. +- Description: List of trusted SSA issuers with configuration (e.g. automatically granted scopes). - Required: No diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java index 4676339e9b7..6fa9f9d103f 100644 --- a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/AppConfiguration.java @@ -660,8 +660,8 @@ public class AppConfiguration implements Configuration { @DocProperty(description = "Boolean value indicating if DCR authorization allowed with MTLS", defaultValue = "false") private Boolean dcrAuthorizationWithMTLS = false; - @DocProperty(description = "List of trusted SSA issuers. If MTLS private key is used to sign DCR JWT, certificate issuer is checked as well.") - private List trustedSsaIssuers = new ArrayList<>(); + @DocProperty(description = "List of trusted SSA issuers with configuration (e.g. automatically granted scopes).") + private Map trustedSsaIssuers = new HashMap<>(); @DocProperty(description = "Cache in local memory cache attributes, scopes, clients and organization entry with expiration 60 seconds", defaultValue = "false") private Boolean useLocalCache = false; @@ -1300,12 +1300,12 @@ public void setDcrAuthorizationWithMTLS(Boolean dcrAuthorizationWithMTLS) { this.dcrAuthorizationWithMTLS = dcrAuthorizationWithMTLS; } - public List getTrustedSsaIssuers() { - if (trustedSsaIssuers == null) trustedSsaIssuers = new ArrayList<>(); + public Map getTrustedSsaIssuers() { + if (trustedSsaIssuers == null) trustedSsaIssuers = new HashMap<>(); return trustedSsaIssuers; } - public void setTrustedSsaIssuers(List trustedSsaIssuers) { + public void setTrustedSsaIssuers(Map trustedSsaIssuers) { this.trustedSsaIssuers = trustedSsaIssuers; } diff --git a/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/TrustedIssuerConfig.java b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/TrustedIssuerConfig.java new file mode 100644 index 00000000000..63dd4cae4a5 --- /dev/null +++ b/jans-auth-server/model/src/main/java/io/jans/as/model/configuration/TrustedIssuerConfig.java @@ -0,0 +1,34 @@ +package io.jans.as.model.configuration; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Yuriy Z + */ +@JsonIgnoreProperties(ignoreUnknown = true) +public class TrustedIssuerConfig implements Serializable { + + @JsonProperty("automaticallyGrantedScopes") + private List automaticallyGrantedScopes = new ArrayList<>(); + + public List getAutomaticallyGrantedScopes() { + if (automaticallyGrantedScopes == null) automaticallyGrantedScopes = new ArrayList<>(); + return automaticallyGrantedScopes; + } + + public void setAutomaticallyGrantedScopes(List automaticallyGrantedScopes) { + this.automaticallyGrantedScopes = automaticallyGrantedScopes; + } + + @Override + public String toString() { + return "TrustedIssuerConfig{" + + "automaticallyGrantedScopes=" + automaticallyGrantedScopes + + '}'; + } +} diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterValidator.java b/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterValidator.java index dacd65bcc6d..90d71ec88b2 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterValidator.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/register/ws/rs/RegisterValidator.java @@ -11,6 +11,7 @@ import io.jans.as.model.common.SoftwareStatementValidationType; import io.jans.as.model.common.SubjectType; import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.configuration.TrustedIssuerConfig; import io.jans.as.model.crypto.AbstractCryptoProvider; import io.jans.as.model.crypto.signature.AlgorithmFamily; import io.jans.as.model.crypto.signature.SignatureAlgorithm; @@ -18,7 +19,9 @@ import io.jans.as.model.exception.CryptoProviderException; import io.jans.as.model.exception.InvalidJwtException; import io.jans.as.model.jwt.Jwt; +import io.jans.as.model.jwt.JwtClaimName; import io.jans.as.model.register.RegisterErrorResponseType; +import io.jans.as.model.register.RegisterRequestParam; import io.jans.as.model.ssa.SsaValidationType; import io.jans.as.model.util.Pair; import io.jans.as.server.ciba.CIBARegisterParamsValidatorService; @@ -37,10 +40,17 @@ import jakarta.ws.rs.core.Response; import org.apache.commons.lang.StringUtils; import org.jetbrains.annotations.Nullable; +import org.json.JSONArray; import org.json.JSONObject; import org.slf4j.Logger; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + import static io.jans.as.model.register.RegisterRequestParam.SOFTWARE_STATEMENT; +import static io.jans.as.model.util.StringUtils.implode; import static org.apache.commons.lang3.BooleanUtils.isFalse; import static org.apache.commons.lang3.BooleanUtils.isTrue; @@ -214,6 +224,57 @@ private JSONObject getJwks(HttpServletRequest httpRequest, Jwt jwt, String jwksU } public JSONObject validateSoftwareStatement(HttpServletRequest httpServletRequest, JSONObject requestObject) { + final JSONObject jsonObject = validateSSA(httpServletRequest, requestObject); + TrustedIssuerConfig trustedIssuerConfig = validateIssuer(jsonObject); + applyTrustedIssuerConfig(trustedIssuerConfig, jsonObject); + return jsonObject; + } + + public void applyTrustedIssuerConfig(TrustedIssuerConfig trustedIssuerConfig, JSONObject jsonObject) { + if (trustedIssuerConfig == null) { + return; + } + + final List automaticallyGrantedScopes = trustedIssuerConfig.getAutomaticallyGrantedScopes(); + if (automaticallyGrantedScopes.isEmpty()) { + return; + } + + final Set scopes = new HashSet<>(automaticallyGrantedScopes); + final String scopeString = jsonObject.optString(RegisterRequestParam.SCOPE.toString()); + if (StringUtils.isNotBlank(scopeString)) { + scopes.addAll(io.jans.as.model.util.StringUtils.spaceSeparatedToList(scopeString)); + } + + final JSONArray scopeJsonArray = jsonObject.optJSONArray(RegisterRequestParam.SCOPE.toString()); + if (scopeJsonArray != null) { + scopes.addAll(io.jans.as.model.util.StringUtils.toList(scopeJsonArray)); + } + + jsonObject.putOpt(RegisterRequestParam.SCOPE.toString(), implode(scopes, " ")); + } + + public TrustedIssuerConfig validateIssuer(JSONObject jsonObject) { + final String issuer = jsonObject.optString(JwtClaimName.ISSUER); + if (StringUtils.isBlank(issuer)) { + log.trace("SSA does not contain 'iss' (issuer)."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_SOFTWARE_STATEMENT, "Failed to find 'iss' (issuer) in software statement"); + } + + final Map trustedSsaIssuers = appConfiguration.getTrustedSsaIssuers(); + if (trustedSsaIssuers.isEmpty()) { + return null; // nothing to check + } + + final TrustedIssuerConfig trustedIssuerConfig = trustedSsaIssuers.get(issuer); + if (trustedIssuerConfig == null) { + log.trace("SSA issuer is not added as trusted in 'trustedSsaIssuers' AS configuration."); + throw errorResponseFactory.createWebApplicationException(Response.Status.BAD_REQUEST, RegisterErrorResponseType.INVALID_SOFTWARE_STATEMENT, "Failed to validate 'iss' (issuer) of software statement."); + } + return trustedIssuerConfig; + } + + public JSONObject validateSSA(HttpServletRequest httpServletRequest, JSONObject requestObject) { if (!requestObject.has(SOFTWARE_STATEMENT.toString())) { return null; } diff --git a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/DynamicClientRegistrationContext.java b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/DynamicClientRegistrationContext.java index d0929cb0035..6522748509f 100644 --- a/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/DynamicClientRegistrationContext.java +++ b/jans-auth-server/server/src/main/java/io/jans/as/server/service/external/context/DynamicClientRegistrationContext.java @@ -9,6 +9,7 @@ import io.jans.as.client.RegisterRequest; import io.jans.as.common.model.registration.Client; import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.configuration.TrustedIssuerConfig; import io.jans.as.model.error.ErrorResponseFactory; import io.jans.as.model.error.IErrorType; import io.jans.as.model.jwt.Jwt; @@ -17,18 +18,19 @@ import io.jans.model.SimpleCustomProperty; import io.jans.model.custom.script.conf.CustomScriptConfiguration; import io.jans.service.cdi.util.CdiUtil; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import org.bouncycastle.asn1.x500.style.BCStyle; import org.json.JSONObject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.ws.rs.core.Response; import java.security.cert.X509Certificate; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; /** * @author Yuriy Zabrovarnyy @@ -130,7 +132,8 @@ public void validateSSA() { } public void validateIssuer() { - final List issuers = CdiUtil.bean(AppConfiguration.class).getTrustedSsaIssuers(); + final Map issuerConfigs = CdiUtil.bean(AppConfiguration.class).getTrustedSsaIssuers(); + final Set issuers = issuerConfigs.keySet(); if (issuers.isEmpty()) { // nothing to check return; } diff --git a/jans-auth-server/server/src/test/java/io/jans/as/server/register/ws/rs/RegisterValidatorTest.java b/jans-auth-server/server/src/test/java/io/jans/as/server/register/ws/rs/RegisterValidatorTest.java new file mode 100644 index 00000000000..6d893b1d656 --- /dev/null +++ b/jans-auth-server/server/src/test/java/io/jans/as/server/register/ws/rs/RegisterValidatorTest.java @@ -0,0 +1,136 @@ +package io.jans.as.server.register.ws.rs; + +import io.jans.as.model.configuration.AppConfiguration; +import io.jans.as.model.configuration.TrustedIssuerConfig; +import io.jans.as.model.crypto.AbstractCryptoProvider; +import io.jans.as.model.error.ErrorResponseFactory; +import io.jans.as.server.ciba.CIBARegisterParamsValidatorService; +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 io.jans.as.server.service.net.UriService; +import jakarta.ws.rs.WebApplicationException; +import org.json.JSONObject; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.Spy; +import org.mockito.testng.MockitoTestNGListener; +import org.slf4j.Logger; +import org.testng.annotations.Listeners; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +import static org.testng.AssertJUnit.assertEquals; + +/** + * @author Yuriy Z + */ +@Listeners(MockitoTestNGListener.class) +public class RegisterValidatorTest { + + @InjectMocks + @Spy + private RegisterValidator registerValidator; + + @Mock + private AppConfiguration appConfiguration; + + @Mock + private Logger log; + + @Mock + private ErrorResponseFactory errorResponseFactory; + + @Mock + private ExternalDynamicClientRegistrationService externalDynamicClientRegistrationService; + + @Mock + private AbstractCryptoProvider cryptoProvider; + + @Mock + private AuthorizationGrantList authorizationGrantList; + + @Mock + private CIBARegisterParamsValidatorService cibaRegisterParamsValidatorService; + + @Mock + private RegisterParamsValidator registerParamsValidator; + + @Mock + private SsaValidationConfigService ssaValidationConfigService; + + @Mock + private UriService uriService; + + @Test + public void validateIssuer_whenTrustedIssuersAreNotConfigured_shouldPass() { + when(appConfiguration.getTrustedSsaIssuers()).thenReturn(new HashMap<>()); + + final JSONObject ssa = createSsaPayload(); + registerValidator.validateIssuer(ssa); + } + + @Test(expectedExceptions = WebApplicationException.class) + public void validateIssuer_whenTrustedIssuersDoesNotMatch_shouldFail() { + final HashMap trustedIssuers = new HashMap<>(); + trustedIssuers.put("https://some.com", new TrustedIssuerConfig()); + + when(appConfiguration.getTrustedSsaIssuers()).thenReturn(trustedIssuers); + when(errorResponseFactory.createWebApplicationException(any(), any(), anyString())).thenReturn(new WebApplicationException()); + + final JSONObject ssa = createSsaPayload(); + registerValidator.validateIssuer(ssa); + } + + @Test + public void validateIssuer_whenTrustedIssuerMatch_shouldPass() { + final HashMap trustedIssuers = new HashMap<>(); + trustedIssuers.put("https://trusted.as.com", new TrustedIssuerConfig()); + + when(appConfiguration.getTrustedSsaIssuers()).thenReturn(trustedIssuers); + + final JSONObject ssa = createSsaPayload(); + registerValidator.validateIssuer(ssa); + } + + @Test + public void applyTrustedIssuerConfig_forNull_shouldPass() { + final JSONObject ssa = createSsaPayload(); + registerValidator.applyTrustedIssuerConfig(null, ssa); + } + + @Test + public void applyTrustedIssuerConfig_forTrustedConfigWithScopes_shouldApplyScopes() { + final TrustedIssuerConfig trustedIssuerConfig = new TrustedIssuerConfig(); + trustedIssuerConfig.setAutomaticallyGrantedScopes(Arrays.asList("b", "c")); + + final JSONObject ssa = createSsaPayload(); + registerValidator.applyTrustedIssuerConfig(trustedIssuerConfig, ssa); + + assertEquals("a b c", ssa.getString("scope")); + } + + @Test + public void applyTrustedIssuerConfig_forTrustedConfigWithoutScopes_shouldGetNoChangesForScopesInSsa() { + final TrustedIssuerConfig trustedIssuerConfig = new TrustedIssuerConfig(); + trustedIssuerConfig.setAutomaticallyGrantedScopes(new ArrayList<>()); + + final JSONObject ssa = createSsaPayload(); + registerValidator.applyTrustedIssuerConfig(trustedIssuerConfig, ssa); + + assertEquals("a b", ssa.getString("scope")); + } + + private JSONObject createSsaPayload() { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("iss", "https://trusted.as.com"); + jsonObject.put("scope", "a b"); + return jsonObject; + } +} diff --git a/jans-auth-server/server/src/test/resources/testng.xml b/jans-auth-server/server/src/test/resources/testng.xml index cb80a6e826e..88b3444348b 100644 --- a/jans-auth-server/server/src/test/resources/testng.xml +++ b/jans-auth-server/server/src/test/resources/testng.xml @@ -35,6 +35,7 @@ +