diff --git a/core-security/src/main/java/org/opendevstack/apiservice/core/security/flow/validator/ClientCredentialFlowValidator.java b/core-security/src/main/java/org/opendevstack/apiservice/core/security/flow/validator/ClientCredentialFlowValidator.java index 6a5082d..e4cb17a 100644 --- a/core-security/src/main/java/org/opendevstack/apiservice/core/security/flow/validator/ClientCredentialFlowValidator.java +++ b/core-security/src/main/java/org/opendevstack/apiservice/core/security/flow/validator/ClientCredentialFlowValidator.java @@ -1,6 +1,7 @@ package org.opendevstack.apiservice.core.security.flow.validator; import org.opendevstack.apiservice.core.contracts.auth.AuthType; +import org.opendevstack.apiservice.core.security.client.credentials.ClientCredentialsTokenProperties; import org.opendevstack.apiservice.core.security.flow.AuthFlowValidator; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.stereotype.Component; @@ -8,6 +9,12 @@ @Component public class ClientCredentialFlowValidator implements AuthFlowValidator { + private final ClientCredentialsTokenProperties clientCredentialsTokenProperties; + + public ClientCredentialFlowValidator(ClientCredentialsTokenProperties clientCredentialsTokenProperties) { + this.clientCredentialsTokenProperties = clientCredentialsTokenProperties; + } + @Override public AuthType getSupportedFlow() { return AuthType.CLIENT_CREDENTIALS; @@ -27,6 +34,12 @@ public boolean validate(Jwt jwt) { return false; } + // The audience must contain the configured client-id (app.security.client-credentials.client-id) + String configuredClientId = clientCredentialsTokenProperties.getClientId(); + if (configuredClientId == null || configuredClientId.isBlank() || !aud.toString().contains(configuredClientId)) { + return false; + } + String scp = jwt.getClaimAsString("scp"); if (scp != null && !scp.isBlank()) { return false; diff --git a/core-security/src/test/java/org/opendevstack/apiservice/core/security/flow/validator/ClientCredentialFlowValidatorTest.java b/core-security/src/test/java/org/opendevstack/apiservice/core/security/flow/validator/ClientCredentialFlowValidatorTest.java index 1cc0b3c..0d67700 100644 --- a/core-security/src/test/java/org/opendevstack/apiservice/core/security/flow/validator/ClientCredentialFlowValidatorTest.java +++ b/core-security/src/test/java/org/opendevstack/apiservice/core/security/flow/validator/ClientCredentialFlowValidatorTest.java @@ -2,6 +2,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.opendevstack.apiservice.core.security.client.credentials.ClientCredentialsTokenProperties; import org.springframework.security.oauth2.jwt.Jwt; import java.time.Instant; @@ -12,11 +13,15 @@ class ClientCredentialFlowValidatorTest { + private static final String CONFIGURED_CLIENT_ID = "my-configured-client-id"; + private ClientCredentialFlowValidator validator; @BeforeEach void setUp() { - validator = new ClientCredentialFlowValidator(); + ClientCredentialsTokenProperties properties = new ClientCredentialsTokenProperties(); + properties.setClientId(CONFIGURED_CLIENT_ID); + validator = new ClientCredentialFlowValidator(properties); } // --- v1 token (appid) --- @@ -25,7 +30,7 @@ void setUp() { void validate_v1Token_valid() { Jwt jwt = buildJwt(Map.of( "appid", "client-id-v1", - "aud", "api://my-app", + "aud", "api://" + CONFIGURED_CLIENT_ID, "oid", "same-as-sub" ), "same-as-sub"); assertTrue(validator.validate(jwt)); @@ -35,7 +40,7 @@ void validate_v1Token_valid() { void validate_v1Token_withScp_invalid() { Jwt jwt = buildJwt(Map.of( "appid", "client-id-v1", - "aud", "api://my-app", + "aud", "api://" + CONFIGURED_CLIENT_ID, "scp", "api.read", "oid", "same-as-sub" ), "same-as-sub"); @@ -46,7 +51,7 @@ void validate_v1Token_withScp_invalid() { void validate_v1Token_withUpn_invalid() { Jwt jwt = buildJwt(Map.of( "appid", "client-id-v1", - "aud", "api://my-app", + "aud", "api://" + CONFIGURED_CLIENT_ID, "upn", "user@example.com", "oid", "same-as-sub" ), "same-as-sub"); @@ -57,7 +62,7 @@ void validate_v1Token_withUpn_invalid() { void validate_v1Token_subNotEqualOid_invalid() { Jwt jwt = buildJwt(Map.of( "appid", "client-id-v1", - "aud", "api://my-app", + "aud", "api://" + CONFIGURED_CLIENT_ID, "oid", "different-oid" ), "subject"); assertFalse(validator.validate(jwt)); @@ -69,7 +74,7 @@ void validate_v1Token_subNotEqualOid_invalid() { void validate_v2Token_valid() { Jwt jwt = buildJwt(Map.of( "azp", "client-id-v2", - "aud", "api://my-app", + "aud", "api://" + CONFIGURED_CLIENT_ID, "oid", "same-as-sub" ), "same-as-sub"); assertTrue(validator.validate(jwt)); @@ -79,7 +84,7 @@ void validate_v2Token_valid() { void validate_v2Token_withScp_invalid() { Jwt jwt = buildJwt(Map.of( "azp", "client-id-v2", - "aud", "api://my-app", + "aud", "api://" + CONFIGURED_CLIENT_ID, "scp", "api.read", "oid", "same-as-sub" ), "same-as-sub"); @@ -90,7 +95,7 @@ void validate_v2Token_withScp_invalid() { void validate_v2Token_withPreferredUsername_invalid() { Jwt jwt = buildJwt(Map.of( "azp", "client-id-v2", - "aud", "api://my-app", + "aud", "api://" + CONFIGURED_CLIENT_ID, "preferred_username", "user@example.com", "oid", "same-as-sub" ), "same-as-sub"); @@ -101,7 +106,7 @@ void validate_v2Token_withPreferredUsername_invalid() { void validate_v2Token_subNotEqualOid_invalid() { Jwt jwt = buildJwt(Map.of( "azp", "client-id-v2", - "aud", "api://my-app", + "aud", "api://" + CONFIGURED_CLIENT_ID, "oid", "different-oid" ), "subject"); assertFalse(validator.validate(jwt)); @@ -112,7 +117,7 @@ void validate_v2Token_subNotEqualOid_invalid() { @Test void validate_noClientIdClaim_invalid() { Jwt jwt = buildJwt(Map.of( - "aud", "api://my-app", + "aud", "api://" + CONFIGURED_CLIENT_ID, "oid", "same-as-sub" ), "same-as-sub"); assertFalse(validator.validate(jwt)); @@ -127,6 +132,18 @@ void validate_missingAud_invalid() { assertFalse(validator.validate(jwt)); } + // --- audience validation --- + + @Test + void validate_audDoesNotContainConfiguredClientId_invalid() { + Jwt jwt = buildJwt(Map.of( + "appid", "client-id-v1", + "aud", "some-other-client-id", + "oid", "same-as-sub" + ), "same-as-sub"); + assertFalse(validator.validate(jwt)); + } + private Jwt buildJwt(Map extraClaims, String subject) { return Jwt.withTokenValue("token") .header("alg", "RS256")