diff --git a/EXAMPLES.md b/EXAMPLES.md index 24610b63..a0784dcb 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -993,7 +993,7 @@ Note that Organizations is currently only available to customers on our Enterpri ```kotlin WebAuthProvider.login(account) - .withOrganization(organizationId) + .withOrganization(organizationIdOrName) .start(this, callback) ``` diff --git a/auth0/src/main/java/com/auth0/android/provider/IdTokenVerifier.kt b/auth0/src/main/java/com/auth0/android/provider/IdTokenVerifier.kt index 4a49fabf..1c861d4c 100644 --- a/auth0/src/main/java/com/auth0/android/provider/IdTokenVerifier.kt +++ b/auth0/src/main/java/com/auth0/android/provider/IdTokenVerifier.kt @@ -59,13 +59,23 @@ internal class IdTokenVerifier { throw NonceClaimMismatchException(verifyOptions.nonce, nonceClaim) } } - if (verifyOptions.organization != null) { - val orgClaim = token.organizationId - if (TextUtils.isEmpty(orgClaim)) { - throw OrgClaimMissingException() - } - if (verifyOptions.organization != orgClaim) { - throw OrgClaimMismatchException(verifyOptions.organization, orgClaim) + verifyOptions.organization?.let {organizationInput -> + if(organizationInput.startsWith("org_")) { + val orgClaim = token.organizationId + if (TextUtils.isEmpty(orgClaim)) { + throw OrgClaimMissingException() + } + if (organizationInput != orgClaim) { + throw OrgClaimMismatchException(organizationInput, orgClaim) + } + } else { + val orgNameClaim = token.organizationName + if (TextUtils.isEmpty(orgNameClaim)) { + throw OrgNameClaimMissingException() + } + if (!organizationInput.equals(orgNameClaim, true)) { + throw OrgNameClaimMismatchException(organizationInput, orgNameClaim) + } } } if (audience.size > 1) { diff --git a/auth0/src/main/java/com/auth0/android/provider/TokenValidationExceptions.kt b/auth0/src/main/java/com/auth0/android/provider/TokenValidationExceptions.kt index 543988a0..3e412eec 100644 --- a/auth0/src/main/java/com/auth0/android/provider/TokenValidationExceptions.kt +++ b/auth0/src/main/java/com/auth0/android/provider/TokenValidationExceptions.kt @@ -229,6 +229,43 @@ public class OrgClaimMismatchException internal constructor(expected: String?, r } } +/** + * This Exception is thrown when Organization Name (org_name) claim is missing in the ID Token + */ +public class OrgNameClaimMissingException internal constructor() : TokenValidationException(MESSAGE) { + private companion object { + private const val MESSAGE = "Organization Name (org_name) claim must be a string present in the ID token" + } + + /** + * To avoid backward compatibility issue, we still have the toString conversion similar to the + * old [TokenValidationException] that was thrown + */ + override fun toString(): String { + return "${this.javaClass.superclass.name}: $message" + } +} + +/** + * This Exception is thrown when the Organization Name (org_name) claim found in the ID token is not the + * one that was expected + */ +public class OrgNameClaimMismatchException internal constructor(expected: String?, received: String?) : + TokenValidationException(message(expected, received)) { + private companion object { + private fun message(expected: String?, received: String?): String = + "Organization Name (org_name) claim mismatch in the ID token; expected \"$expected\", found \"$received\"" + } + + /** + * To avoid backward compatibility issue, we still have the toString conversion similar to the + * old [TokenValidationException] that was thrown + */ + override fun toString(): String { + return "${this.javaClass.superclass.name}: $message" + } +} + /** * This Exception is thrown when Authorized Party (azp) claim is missing in the ID Token */ diff --git a/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt b/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt index 0c8f97f4..f7488f73 100644 --- a/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt +++ b/auth0/src/main/java/com/auth0/android/request/internal/Jwt.kt @@ -23,6 +23,7 @@ internal class Jwt(rawToken: String) { val issuer: String? val nonce: String? val organizationId: String? + val organizationName: String? val issuedAt: Date? val expiresAt: Date? val authorizedParty: String? @@ -46,6 +47,7 @@ internal class Jwt(rawToken: String) { issuer = decodedPayload["iss"] as String? nonce = decodedPayload["nonce"] as String? organizationId = decodedPayload["org_id"] as String? + organizationName = decodedPayload["org_name"] as String? issuedAt = (decodedPayload["iat"] as? Double)?.let { Date(it.toLong() * 1000) } expiresAt = (decodedPayload["exp"] as? Double)?.let { Date(it.toLong() * 1000) } authorizedParty = decodedPayload["azp"] as String? diff --git a/auth0/src/test/java/com/auth0/android/provider/IdTokenVerifierTest.java b/auth0/src/test/java/com/auth0/android/provider/IdTokenVerifierTest.java index 3fdd4a32..a8fc6a7f 100644 --- a/auth0/src/test/java/com/auth0/android/provider/IdTokenVerifierTest.java +++ b/auth0/src/test/java/com/auth0/android/provider/IdTokenVerifierTest.java @@ -18,7 +18,8 @@ import static com.auth0.android.provider.JwtTestUtils.EXPECTED_AUDIENCE_ARRAY; import static com.auth0.android.provider.JwtTestUtils.EXPECTED_ISSUER; import static com.auth0.android.provider.JwtTestUtils.EXPECTED_NONCE; -import static com.auth0.android.provider.JwtTestUtils.EXPECTED_ORGANIZATION; +import static com.auth0.android.provider.JwtTestUtils.EXPECTED_ORGANIZATION_ID; +import static com.auth0.android.provider.JwtTestUtils.EXPECTED_ORGANIZATION_NAME; import static com.auth0.android.provider.JwtTestUtils.FIXED_CLOCK_CURRENT_TIME_MS; import static com.auth0.android.provider.JwtTestUtils.createJWTBody; import static com.auth0.android.provider.JwtTestUtils.createTestJWT; @@ -52,7 +53,7 @@ public void setUp() { } @Test - public void shouldPassAllClaimsVerification() throws Exception { + public void shouldPassAllClaimsVerificationWithOrgId() throws Exception { long clock = FIXED_CLOCK_CURRENT_TIME_MS / 1000; long authTime = clock - 1; @@ -62,12 +63,33 @@ public void shouldPassAllClaimsVerification() throws Exception { jwtBody.put("azp", EXPECTED_AUDIENCE); jwtBody.put("auth_time", authTime); jwtBody.put("nonce", EXPECTED_NONCE); - jwtBody.put("org_id", EXPECTED_ORGANIZATION); + jwtBody.put("org_id", EXPECTED_ORGANIZATION_ID); String token = createTestJWT("none", jwtBody); Jwt jwt = new Jwt(token); options.setNonce(EXPECTED_NONCE); - options.setOrganization(EXPECTED_ORGANIZATION); + options.setOrganization(EXPECTED_ORGANIZATION_ID); + options.setMaxAge(60 * 2); + idTokenVerifier.verify(jwt, options, true); + } + + @Test + public void shouldPassAllClaimsVerificationWithOrgName() throws Exception { + long clock = FIXED_CLOCK_CURRENT_TIME_MS / 1000; + long authTime = clock - 1; + + Map jwtBody = createJWTBody(); + //Overrides + jwtBody.put("aud", EXPECTED_AUDIENCE_ARRAY); + jwtBody.put("azp", EXPECTED_AUDIENCE); + jwtBody.put("auth_time", authTime); + jwtBody.put("nonce", EXPECTED_NONCE); + jwtBody.put("org_name", EXPECTED_ORGANIZATION_NAME); + + String token = createTestJWT("none", jwtBody); + Jwt jwt = new Jwt(token); + options.setNonce(EXPECTED_NONCE); + options.setOrganization(EXPECTED_ORGANIZATION_NAME); options.setMaxAge(60 * 2); idTokenVerifier.verify(jwt, options, true); } @@ -236,6 +258,62 @@ public void shouldFailWhenNonceClaimIsRequiredAndHasUnexpectedValue() { assertEquals(message, e.getMessage()); } + @Test + public void shouldNotFailWhenOrganizationNameClaimIsMissingButNotRequired() throws Exception { + Map jwtBody = createJWTBody("org_name"); + String token = createTestJWT("none", jwtBody); + Jwt jwt = new Jwt(token); + idTokenVerifier.verify(jwt, options, true); + } + + @Test + public void shouldFailWhenOrganizationNameClaimIsMissingAndRequired() { + String message = "Organization Name (org_name) claim must be a string present in the ID token"; + Exception e = Assert.assertThrows(message, OrgNameClaimMissingException.class, () -> { + Map jwtBody = createJWTBody("org_name"); + String token = createTestJWT("none", jwtBody); + Jwt jwt = new Jwt(token); + options.setOrganization(EXPECTED_ORGANIZATION_NAME); + idTokenVerifier.verify(jwt, options, true); + }); + assertEquals("com.auth0.android.provider.TokenValidationException: " + message, e.toString()); + assertEquals(message, e.getMessage()); + } + + @Test + public void shouldFailWhenOrganizationNameClaimIsRequiredAndHasUnexpectedValue() { + String message = "Organization Name (org_name) claim mismatch in the ID token; expected \"__test_org_name__\", found \"--invalid--\""; + Exception e = Assert.assertThrows(message, OrgNameClaimMismatchException.class, () -> { + Map jwtBody = createJWTBody(); + jwtBody.put("org_name", "--invalid--"); + String token = createTestJWT("none", jwtBody); + Jwt jwt = new Jwt(token); + options.setOrganization(EXPECTED_ORGANIZATION_NAME); + idTokenVerifier.verify(jwt, options, true); + }); + assertEquals("com.auth0.android.provider.TokenValidationException: " + message, e.toString()); + assertEquals(message, e.getMessage()); + } + @Test + public void shouldNotFailWhenOrganizationNameClaimIsRequiredAndHasSameValue() throws Exception { + Map jwtBody = createJWTBody(); + jwtBody.put("org_name", EXPECTED_ORGANIZATION_NAME); + String token = createTestJWT("none", jwtBody); + Jwt jwt = new Jwt(token); + options.setOrganization(EXPECTED_ORGANIZATION_NAME); + idTokenVerifier.verify(jwt, options, true); + } + + @Test + public void shouldNotFailWhenOrganizationNameClaimIsRequiredAndHasSameValueInDifferentCase() throws Exception { + Map jwtBody = createJWTBody(); + jwtBody.put("org_name", "__tESt_OrG_nAme__"); + String token = createTestJWT("none", jwtBody); + Jwt jwt = new Jwt(token); + options.setOrganization(EXPECTED_ORGANIZATION_NAME); + idTokenVerifier.verify(jwt, options, true); + } + @Test public void shouldNotFailWhenOrganizationIdClaimIsMissingButNotRequired() throws Exception { Map jwtBody = createJWTBody("org_id"); @@ -251,7 +329,7 @@ public void shouldFailWhenOrganizationIdClaimIsMissingAndRequired() { Map jwtBody = createJWTBody("org_id"); String token = createTestJWT("none", jwtBody); Jwt jwt = new Jwt(token); - options.setOrganization(EXPECTED_ORGANIZATION); + options.setOrganization(EXPECTED_ORGANIZATION_ID); idTokenVerifier.verify(jwt, options, true); }); assertEquals("com.auth0.android.provider.TokenValidationException: " + message, e.toString()); @@ -260,13 +338,13 @@ public void shouldFailWhenOrganizationIdClaimIsMissingAndRequired() { @Test public void shouldFailWhenOrganizationIdClaimIsRequiredAndHasUnexpectedValue() { - String message = "Organization Id (org_id) claim mismatch in the ID token; expected \"__test_org_id__\", found \"--invalid--\""; + String message = "Organization Id (org_id) claim mismatch in the ID token; expected \"org___test_org_id__\", found \"--invalid--\""; Exception e = Assert.assertThrows(message, OrgClaimMismatchException.class, () -> { Map jwtBody = createJWTBody(); jwtBody.put("org_id", "--invalid--"); String token = createTestJWT("none", jwtBody); Jwt jwt = new Jwt(token); - options.setOrganization(EXPECTED_ORGANIZATION); + options.setOrganization(EXPECTED_ORGANIZATION_ID); idTokenVerifier.verify(jwt, options, true); }); assertEquals("com.auth0.android.provider.TokenValidationException: " + message, e.toString()); diff --git a/auth0/src/test/java/com/auth0/android/provider/JwtTestUtils.java b/auth0/src/test/java/com/auth0/android/provider/JwtTestUtils.java index ee3872b0..9d1f017b 100644 --- a/auth0/src/test/java/com/auth0/android/provider/JwtTestUtils.java +++ b/auth0/src/test/java/com/auth0/android/provider/JwtTestUtils.java @@ -27,7 +27,8 @@ public class JwtTestUtils { static final String[] EXPECTED_AUDIENCE_ARRAY = new String[]{"__test_client_id__", "__test_other_client_id__"}; static final String EXPECTED_AUDIENCE = "__test_client_id__"; static final String EXPECTED_NONCE = "__test_nonce__"; - static final String EXPECTED_ORGANIZATION = "__test_org_id__"; + static final String EXPECTED_ORGANIZATION_ID = "org___test_org_id__"; + static final String EXPECTED_ORGANIZATION_NAME = "__test_org_name__"; static final Object EXPECTED_SUBJECT = "__test_subject__"; private static final String RSA_PRIVATE_KEY = "src/test/resources/rsa_private.pem";