Skip to content

Commit

Permalink
Merge pull request #38 from gooddata/cbon-lx-614-bug-fix-and-log-enha…
Browse files Browse the repository at this point in the history
…ncement

fix: proper azure b2c endpoint validation
  • Loading branch information
chrisbonilla95 authored Nov 1, 2024
2 parents e6b1190 + d3a4e95 commit 1a05a40
Show file tree
Hide file tree
Showing 2 changed files with 75 additions and 37 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -160,27 +160,35 @@ private fun dexClientRegistration(
private fun handleAzureB2CClientRegistration(
issuerLocation: String
): ClientRegistration.Builder {
val uri = buildMetadataUri(issuerLocation)
val configuration = retrieveOidcConfiguration(uri)
val issuerUri = URI.create(issuerLocation)
val metadataUri = buildMetadataUri(issuerUri)
val configuration = retrieveOidcConfiguration(metadataUri)

return if (isValidAzureB2CMetadata(configuration, uri)) {
val validationResult = validateAzureB2CMetadata(configuration, issuerUri)
return if (validationResult.isValid) {
fromOidcConfiguration(configuration)
} else {
val mismatches = validationResult.mismatchedEndpoints.entries.joinToString(separator = "\n") {
"${it.key}: ${it.value}"
}
throw ResponseStatusException(
HttpStatus.UNAUTHORIZED,
"Authorization failed for given issuer \"$issuerLocation\". Metadata endpoints do not match."
"""
Authorization failed for the given issuer "$issuerLocation".
Metadata endpoints do not match the configured issuer location. Mismatched endpoints:
$mismatches
""".trimIndent()
)
}
}

/**
* Builds metadata retrieval URI based on the provided [issuerLocation].
* Builds metadata retrieval URI based on the provided [issuer].
*
* @param issuerLocation The issuer location URL as a string.
* @param issuer The issuer location URI.
* @return The constructed [URI] for metadata retrieval.
*/
internal fun buildMetadataUri(issuerLocation: String): URI {
val issuer = URI.create(issuerLocation)
internal fun buildMetadataUri(issuer: URI): URI {
return UriComponentsBuilder.fromUri(issuer)
.replacePath(issuer.path + OAuthConstants.OIDC_METADATA_PATH)
.build(Collections.emptyMap<String, String>())
Expand All @@ -202,27 +210,58 @@ internal fun retrieveOidcConfiguration(uri: URI): Map<String, Any> {
)
}

/**
* Result of validating endpoint URLs in the metadata against the configured issuer location.
*
* @param isValid `true` if all endpoint URLs in the metadata match the configured issuer location; `false` otherwise.
* @param mismatchedEndpoints A map of endpoint names to their actual URLs for endpoints that do not match the
* configured issuer location.
*/
data class MetadataValidationResult(
val isValid: Boolean,
val mismatchedEndpoints: Map<String, String>
)

/**
* As the issuer in metadata returned from Azure B2C provider is not the same as the configured issuer location,
* we must instead validate that the endpoint URLs in the metadata start with the configured issuer location.
*
* @param configuration The OIDC configuration metadata.
* @param uri The issuer location URI to validate against.
* @return `true` if all endpoint URLs in the metadata match the configured issuer location; `false` otherwise.
* @return A MetadataValidationResult containing whether all endpoint URLs match the configured issuer location,
* and a map of any mismatched endpoints with their actual values.
*/
internal fun isValidAzureB2CMetadata(
internal fun validateAzureB2CMetadata(
configuration: Map<String, Any>,
uri: URI
): Boolean {
): MetadataValidationResult {
val metadata = parse(configuration, OIDCProviderMetadata::parse)
val issuerASCIIString = uri.toASCIIString()
return listOf(
metadata.authorizationEndpointURI,
metadata.tokenEndpointURI,
metadata.endSessionEndpointURI,
metadata.jwkSetURI,
metadata.userInfoEndpointURI
).all { it.toASCIIString().startsWith(issuerASCIIString) }
val unversionedIssuer = uri.toASCIIString().removeVersionSegment()

val endpoints = mapOf(
"authorizationEndpointURI" to metadata.authorizationEndpointURI,
"tokenEndpointURI" to metadata.tokenEndpointURI,
"endSessionEndpointURI" to metadata.endSessionEndpointURI,
"jwkSetURI" to metadata.jwkSetURI,
"userInfoEndpointURI" to metadata.userInfoEndpointURI
)

val mismatchedEndpoints = endpoints.filterValues {
it.toASCIIString().startsWith(prefix = unversionedIssuer, ignoreCase = true).not()
}.mapValues { it.value.toASCIIString() }

return MetadataValidationResult(
isValid = mismatchedEndpoints.isEmpty(),
mismatchedEndpoints = mismatchedEndpoints
)
}

/**
* Remove version segment from the issuer location URL
*/
internal fun String.removeVersionSegment(): String {
val regex = Regex("""/v\d+(\.\d+)*""")
return this.replace(regex, "")
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@

package com.gooddata.oauth2.server

import com.fasterxml.jackson.databind.ObjectMapper
import com.github.tomakehurst.wiremock.WireMockServer
import com.github.tomakehurst.wiremock.client.WireMock
import com.github.tomakehurst.wiremock.core.WireMockConfiguration
Expand Down Expand Up @@ -62,8 +61,6 @@ internal class AuthenticationUtilsTest {

lateinit var clientRegistrationBuilderCache: ClientRegistrationBuilderCache

lateinit var objectMapper: ObjectMapper

@BeforeEach
internal fun setUp() {
properties = HostBasedClientRegistrationRepositoryProperties("http://remote", "http://localhost")
Expand Down Expand Up @@ -321,17 +318,18 @@ internal class AuthenticationUtilsTest {
}

@Test
fun `isValidAzureB2CMetadata returns true for valid metadata`() {
fun `validateAzureB2CMetadata returns true for valid metadata`() {
val uri = URI.create(AZURE_B2C_ISSUER)

assertTrue(isValidAzureB2CMetadata(VALID_AZURE_B2C_OIDC_CONFIG, uri))
assertTrue(validateAzureB2CMetadata(VALID_AZURE_B2C_OIDC_CONFIG, uri).isValid)
}

@Test
fun `isValidAzureB2CMetadata returns false for invalid metadata`() {
fun `validateAzureB2CMetadata returns false for invalid metadata`() {
val uri = URI.create(AZURE_B2C_ISSUER)

assertFalse(isValidAzureB2CMetadata(INVALID_AZURE_B2C_OIDC_CONFIG, uri))
val validationResult = validateAzureB2CMetadata(INVALID_AZURE_B2C_OIDC_CONFIG, uri)
assertFalse(validationResult.isValid)
// The [INVALID_AZURE_B2C_OIDC_CONFIG] has 5 mismatched endpoints
assertEquals(5, validationResult.mismatchedEndpoints.size)
}

private fun mockOidcIssuer(): String {
Expand All @@ -351,6 +349,7 @@ internal class AuthenticationUtilsTest {
private const val OIDC_CONFIG_PATH = "/.well-known/openid-configuration"
private const val USER_ID = "userId"
private const val AZURE_B2C_ISSUER = "https://tenant.b2clogin.com/tenant.onmicrosoft.com/policy/v2.0"
private val UNVERSIONED_AZURE_B2C_ISSUER = AZURE_B2C_ISSUER.removeVersionSegment()
private val ORGANIZATION = Organization(ORGANIZATION_ID)
private val wireMockServer = WireMockServer(WireMockConfiguration().dynamicPort()).apply {
start()
Expand Down Expand Up @@ -512,11 +511,11 @@ internal class AuthenticationUtilsTest {

private val VALID_AZURE_B2C_OIDC_CONFIG: Map<String, Any> = mapOf(
"issuer" to "https://some-microsoft-issuer.com/someGuid/v2.0/",
"authorization_endpoint" to "${AZURE_B2C_ISSUER}/oauth2/v1/authorize",
"token_endpoint" to "${AZURE_B2C_ISSUER}/oauth2/v1/token",
"userinfo_endpoint" to "${AZURE_B2C_ISSUER}/oauth2/v1/userinfo",
"registration_endpoint" to "${AZURE_B2C_ISSUER}/oauth2/v1/clients",
"jwks_uri" to "${AZURE_B2C_ISSUER}/oauth2/v1/keys",
"authorization_endpoint" to "${UNVERSIONED_AZURE_B2C_ISSUER}/oauth2/v1/authorize",
"token_endpoint" to "${UNVERSIONED_AZURE_B2C_ISSUER}/oauth2/v1/token",
"userinfo_endpoint" to "${UNVERSIONED_AZURE_B2C_ISSUER}/oauth2/v1/userinfo",
"registration_endpoint" to "${UNVERSIONED_AZURE_B2C_ISSUER}/oauth2/v1/clients",
"jwks_uri" to "${UNVERSIONED_AZURE_B2C_ISSUER}/oauth2/v1/keys",
"response_types_supported" to listOf(
"code",
"id_token",
Expand Down Expand Up @@ -590,23 +589,23 @@ internal class AuthenticationUtilsTest {
"c_hash"
),
"code_challenge_methods_supported" to listOf("S256"),
"introspection_endpoint" to "${AZURE_B2C_ISSUER}/oauth2/v1/introspect",
"introspection_endpoint" to "${UNVERSIONED_AZURE_B2C_ISSUER}/oauth2/v1/introspect",
"introspection_endpoint_auth_methods_supported" to listOf(
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"none"
),
"revocation_endpoint" to "${AZURE_B2C_ISSUER}/oauth2/v1/revoke",
"revocation_endpoint" to "${UNVERSIONED_AZURE_B2C_ISSUER}/oauth2/v1/revoke",
"revocation_endpoint_auth_methods_supported" to listOf(
"client_secret_basic",
"client_secret_post",
"client_secret_jwt",
"private_key_jwt",
"none"
),
"end_session_endpoint" to "${AZURE_B2C_ISSUER}/oauth2/v1/logout",
"end_session_endpoint" to "${UNVERSIONED_AZURE_B2C_ISSUER}/oauth2/v1/logout",
"request_parameter_supported" to true,
"request_uri_parameter_supported" to true,
"request_object_signing_alg_values_supported" to listOf(
Expand All @@ -620,7 +619,7 @@ internal class AuthenticationUtilsTest {
"ES384",
"ES512"
),
"device_authorization_endpoint" to "${AZURE_B2C_ISSUER}/oauth2/v1/device/authorize"
"device_authorization_endpoint" to "${UNVERSIONED_AZURE_B2C_ISSUER}/oauth2/v1/device/authorize"
)

private val INVALID_AZURE_B2C_OIDC_CONFIG: Map<String, Any> = mapOf(
Expand Down

0 comments on commit 1a05a40

Please sign in to comment.