From b08d18077f37bb72ba2d6432d56975f58358a977 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Tue, 11 Jul 2023 23:32:47 +0100 Subject: [PATCH 1/5] Add support for validating the `org_name` claim --- Auth0/ClaimValidators.swift | 43 +++++++++++++--- Auth0/IDTokenValidator.swift | 6 ++- Auth0Tests/ClaimValidatorsSpec.swift | 72 ++++++++++++++++++++------ Auth0Tests/Generators.swift | 20 +++++--- Auth0Tests/IDTokenValidatorSpec.swift | 73 +++++++++++++++++++++++++-- 5 files changed, 179 insertions(+), 35 deletions(-) diff --git a/Auth0/ClaimValidators.swift b/Auth0/ClaimValidators.swift index 89dddf98..28227b4a 100644 --- a/Auth0/ClaimValidators.swift +++ b/Auth0/ClaimValidators.swift @@ -249,7 +249,7 @@ struct IDTokenAuthTimeValidator: JWTValidator { } } -struct IDTokenOrgIdValidator: JWTValidator { +struct IDTokenOrgIDValidator: JWTValidator { enum ValidationError: Auth0Error { case missingOrgId case mismatchedOrgId(actual: String, expected: String) @@ -263,16 +263,45 @@ struct IDTokenOrgIdValidator: JWTValidator { } } - private let expectedOrganization: String + private let expectedOrgID: String - init(organization: String) { - self.expectedOrganization = organization + init(orgID: String) { + self.expectedOrgID = orgID } func validate(_ jwt: JWT) -> Auth0Error? { - guard let actualOrganization = jwt.claim(name: "org_id").string else { return ValidationError.missingOrgId } - guard actualOrganization == expectedOrganization else { - return ValidationError.mismatchedOrgId(actual: actualOrganization, expected: expectedOrganization) + guard let actualOrgID = jwt.claim(name: "org_id").string else { return ValidationError.missingOrgId } + guard actualOrgID == expectedOrgID else { + return ValidationError.mismatchedOrgId(actual: actualOrgID, expected: expectedOrgID) + } + return nil + } +} + +struct IDTokenOrgNameValidator: JWTValidator { + enum ValidationError: Auth0Error { + case missingOrgName + case mismatchedOrgName(actual: String, expected: String) + + var debugDescription: String { + switch self { + case .missingOrgName: return "Organization Name (org_name) claim must be a string present in the ID token" + case .mismatchedOrgName(let actual, let expected): + return "Organization Name (org_name) claim value mismatch in the ID token; expected (\(expected)), found (\(actual))" + } + } + } + + private let expectedOrgName: String + + init(orgName: String) { + self.expectedOrgName = orgName + } + + func validate(_ jwt: JWT) -> Auth0Error? { + guard let actualOrgName = jwt.claim(name: "org_name").string else { return ValidationError.missingOrgName } + guard actualOrgName == expectedOrgName else { + return ValidationError.mismatchedOrgName(actual: actualOrgName, expected: expectedOrgName) } return nil } diff --git a/Auth0/IDTokenValidator.swift b/Auth0/IDTokenValidator.swift index b5c5e635..b5ae54fe 100644 --- a/Auth0/IDTokenValidator.swift +++ b/Auth0/IDTokenValidator.swift @@ -67,7 +67,11 @@ func validate(idToken: String, claimValidators.append(IDTokenAuthTimeValidator(leeway: context.leeway, maxAge: maxAge)) } if let organization = context.organization { - claimValidators.append(IDTokenOrgIdValidator(organization: organization)) + if organization.starts(with: "org_") { + claimValidators.append(IDTokenOrgIDValidator(orgID: organization)) + } else { + claimValidators.append(IDTokenOrgNameValidator(orgName: organization)) + } } let validator = IDTokenValidator(signatureValidator: signatureValidator ?? IDTokenSignatureValidator(context: context), claimsValidator: claimsValidator ?? IDTokenClaimsValidator(validators: claimValidators), diff --git a/Auth0Tests/ClaimValidatorsSpec.swift b/Auth0Tests/ClaimValidatorsSpec.swift index 04989741..b5c1aefb 100644 --- a/Auth0Tests/ClaimValidatorsSpec.swift +++ b/Auth0Tests/ClaimValidatorsSpec.swift @@ -414,26 +414,26 @@ class ClaimValidatorsSpec: IDTokenValidatorBaseSpec { } - describe("organization validation") { + describe("organization id validation") { - var organizationValidator: IDTokenOrgIdValidator! - let expectedOrganization = "abc1234" + var orgIDValidator: IDTokenOrgIDValidator! + let expectedOrgID = "org_abc1234" beforeEach { - organizationValidator = IDTokenOrgIdValidator(organization: expectedOrganization) + orgIDValidator = IDTokenOrgIDValidator(orgID: expectedOrgID) } context("missing org_id") { it("should return nil if org_id is present") { - let jwt = generateJWT(organization: expectedOrganization) + let jwt = generateJWT(orgID: expectedOrgID) - expect(organizationValidator.validate(jwt)).to(beNil()) + expect(orgIDValidator.validate(jwt)).to(beNil()) } it("should return an error if org_id is missing") { - let jwt = generateJWT(organization: nil) - let expectedError = IDTokenOrgIdValidator.ValidationError.missingOrgId - let result = organizationValidator.validate(jwt) + let jwt = generateJWT(orgID: nil) + let expectedError = IDTokenOrgIDValidator.ValidationError.missingOrgId + let result = orgIDValidator.validate(jwt) expect(result).to(matchError(expectedError)) expect(result?.localizedDescription).to(equal(expectedError.localizedDescription)) @@ -441,12 +441,53 @@ class ClaimValidatorsSpec: IDTokenValidatorBaseSpec { } context("mismatched org_id") { - it("should return an error if org_id does not match the request organization") { - let organization = "xyz6789" - let jwt = generateJWT(organization: organization) - let expectedError = IDTokenOrgIdValidator.ValidationError.mismatchedOrgId(actual: organization, - expected: expectedOrganization) - let result = organizationValidator.validate(jwt) + it("should return an error if org_id does not match the request organization id") { + let orgID = "org_xyz6789" + let jwt = generateJWT(orgID: orgID) + let expectedError = IDTokenOrgIDValidator.ValidationError.mismatchedOrgId(actual: orgID, + expected: expectedOrgID) + let result = orgIDValidator.validate(jwt) + + expect(result).to(matchError(expectedError)) + expect(result?.localizedDescription).to(equal(expectedError.localizedDescription)) + } + } + + } + + describe("organization name validation") { + + var orgNameValidator: IDTokenOrgNameValidator! + let expectedOrgName = "abc1234" + + beforeEach { + orgNameValidator = IDTokenOrgNameValidator(orgName: expectedOrgName) + } + + context("missing org_name") { + it("should return nil if org_name is present") { + let jwt = generateJWT(orgName: expectedOrgName) + + expect(orgNameValidator.validate(jwt)).to(beNil()) + } + + it("should return an error if org_name is missing") { + let jwt = generateJWT(orgName: nil) + let expectedError = IDTokenOrgNameValidator.ValidationError.missingOrgName + let result = orgNameValidator.validate(jwt) + + expect(result).to(matchError(expectedError)) + expect(result?.localizedDescription).to(equal(expectedError.localizedDescription)) + } + } + + context("mismatched org_name") { + it("should return an error if org_name does not match the request organization name") { + let orgName = "xyz6789" + let jwt = generateJWT(orgName: orgName) + let expectedError = IDTokenOrgNameValidator.ValidationError.mismatchedOrgName(actual: orgName, + expected: expectedOrgName) + let result = orgNameValidator.validate(jwt) expect(result).to(matchError(expectedError)) expect(result?.localizedDescription).to(equal(expectedError.localizedDescription)) @@ -454,7 +495,6 @@ class ClaimValidatorsSpec: IDTokenValidatorBaseSpec { } } - } } diff --git a/Auth0Tests/Generators.swift b/Auth0Tests/Generators.swift index 1cbbea61..37af582e 100644 --- a/Auth0Tests/Generators.swift +++ b/Auth0Tests/Generators.swift @@ -45,7 +45,8 @@ private func generateJWTPayload(iss: String?, nonce: String?, maxAge: Int?, authTime: Date?, - organization: String?) -> String { + orgID: String?, + orgName: String?) -> String { var bodyDict: [String: Any] = [:] if let iss = iss { @@ -84,10 +85,14 @@ private func generateJWTPayload(iss: String?, bodyDict["nonce"] = nonce } - if let organization = organization { - bodyDict["org_id"] = organization + if let orgID = orgID { + bodyDict["org_id"] = orgID } - + + if let orgName = orgName { + bodyDict["org_name"] = orgName + } + return encodeJWTPart(from: bodyDict) } @@ -102,7 +107,8 @@ func generateJWT(alg: String = JWTAlgorithm.rs256.rawValue, nonce: String? = "a1b2c3d4e5", maxAge: Int? = nil, authTime: Date? = nil, - organization: String? = nil, + orgID: String? = nil, + orgName: String? = nil, signature: String? = nil) -> JWT { let header = generateJWTHeader(alg: alg, kid: kid) let body = generateJWTPayload(iss: iss, @@ -114,7 +120,8 @@ func generateJWT(alg: String = JWTAlgorithm.rs256.rawValue, nonce: nonce, maxAge: maxAge, authTime: authTime, - organization: organization) + orgID: orgID, + orgName: orgName) let signableParts = "\(header).\(body)" var signaturePart = "" @@ -128,7 +135,6 @@ func generateJWT(alg: String = JWTAlgorithm.rs256.rawValue, signaturePart = (data! as Data).a0_encodeBase64URLSafe()! } - return try! decode(jwt: "\(signableParts).\(signaturePart)") } diff --git a/Auth0Tests/IDTokenValidatorSpec.swift b/Auth0Tests/IDTokenValidatorSpec.swift index 6e47aa01..151f4e08 100644 --- a/Auth0Tests/IDTokenValidatorSpec.swift +++ b/Auth0Tests/IDTokenValidatorSpec.swift @@ -216,16 +216,16 @@ class IDTokenValidatorSpec: IDTokenValidatorBaseSpec { } } - it("should validate a token with an organization") { - let organization = "abc1234" - let jwt = generateJWT(aud: aud, azp: nil, nonce: nil, maxAge: nil, authTime: nil, organization: organization) + it("should validate a token with an organization ID") { + let orgID = "org_abc1234" + let jwt = generateJWT(aud: aud, azp: nil, nonce: nil, maxAge: nil, authTime: nil, orgID: orgID) let context = IDTokenValidatorContext(issuer: validatorContext.issuer, audience: aud[0], jwksRequest: validatorContext.jwksRequest, leeway: validatorContext.leeway, maxAge: nil, nonce: nil, - organization: organization) + organization: orgID) await waitUntil { done in validate(idToken: jwt.string, @@ -236,6 +236,71 @@ class IDTokenValidatorSpec: IDTokenValidatorBaseSpec { } } } + + it("should validate a token with an organization name") { + let orgName = "abc1234" + let jwt = generateJWT(aud: aud, azp: nil, nonce: nil, maxAge: nil, authTime: nil, orgName: orgName) + let context = IDTokenValidatorContext(issuer: validatorContext.issuer, + audience: aud[0], + jwksRequest: validatorContext.jwksRequest, + leeway: validatorContext.leeway, + maxAge: nil, + nonce: nil, + organization: orgName) + + await waitUntil { done in + validate(idToken: jwt.string, + with: context, + signatureValidator: mockSignatureValidator) { error in + expect(error).to(beNil()) + done() + } + } + } + + it("should expect an organization ID instead of an organization name") { + let orgID = "org_abc1234" + let jwt = generateJWT(aud: aud, azp: nil, nonce: nil, maxAge: nil, authTime: nil, orgName: orgID) + let context = IDTokenValidatorContext(issuer: validatorContext.issuer, + audience: aud[0], + jwksRequest: validatorContext.jwksRequest, + leeway: validatorContext.leeway, + maxAge: nil, + nonce: nil, + organization: orgID) + let expectedError = IDTokenOrgIDValidator.ValidationError.missingOrgId + + await waitUntil { done in + validate(idToken: jwt.string, + with: context, + signatureValidator: mockSignatureValidator) { error in + expect(error).to(matchError(expectedError)) + done() + } + } + } + + it("should expect an organization name instead of an organization ID") { + let orgName = "abc1234" + let jwt = generateJWT(aud: aud, azp: nil, nonce: nil, maxAge: nil, authTime: nil, orgID: orgName) + let context = IDTokenValidatorContext(issuer: validatorContext.issuer, + audience: aud[0], + jwksRequest: validatorContext.jwksRequest, + leeway: validatorContext.leeway, + maxAge: nil, + nonce: nil, + organization: orgName) + let expectedError = IDTokenOrgNameValidator.ValidationError.missingOrgName + + await waitUntil { done in + validate(idToken: jwt.string, + with: context, + signatureValidator: mockSignatureValidator) { error in + expect(error).to(matchError(expectedError)) + done() + } + } + } } } From a0782bbfe889aad6871314997501f96801557f01 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 12 Jul 2023 11:19:55 +0100 Subject: [PATCH 2/5] Use case insensitive compare --- Auth0/ClaimValidators.swift | 2 +- Auth0Tests/ClaimValidatorsSpec.swift | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/Auth0/ClaimValidators.swift b/Auth0/ClaimValidators.swift index 28227b4a..f0ac6178 100644 --- a/Auth0/ClaimValidators.swift +++ b/Auth0/ClaimValidators.swift @@ -300,7 +300,7 @@ struct IDTokenOrgNameValidator: JWTValidator { func validate(_ jwt: JWT) -> Auth0Error? { guard let actualOrgName = jwt.claim(name: "org_name").string else { return ValidationError.missingOrgName } - guard actualOrgName == expectedOrgName else { + guard actualOrgName.caseInsensitiveCompare(expectedOrgName) == .orderedSame else { return ValidationError.mismatchedOrgName(actual: actualOrgName, expected: expectedOrgName) } return nil diff --git a/Auth0Tests/ClaimValidatorsSpec.swift b/Auth0Tests/ClaimValidatorsSpec.swift index b5c1aefb..ac027e20 100644 --- a/Auth0Tests/ClaimValidatorsSpec.swift +++ b/Auth0Tests/ClaimValidatorsSpec.swift @@ -494,6 +494,17 @@ class ClaimValidatorsSpec: IDTokenValidatorBaseSpec { } } + it("should perform a case insensitive compare") { + let orgName = "aBc1234" + let expectedOrgName = "AbC1234" + orgNameValidator = IDTokenOrgNameValidator(orgName: expectedOrgName) + let jwt = generateJWT(orgName: orgName) + let expectedError = IDTokenOrgNameValidator.ValidationError.mismatchedOrgName(actual: orgName, + expected: expectedOrgName) + + expect(orgNameValidator.validate(jwt)).to(beNil()) + } + } } From 7d8cd5968dc293ef15fa6ca4feb53aaf5bdac7a7 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 12 Jul 2023 11:21:46 +0100 Subject: [PATCH 3/5] Remove unused const --- Auth0Tests/ClaimValidatorsSpec.swift | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Auth0Tests/ClaimValidatorsSpec.swift b/Auth0Tests/ClaimValidatorsSpec.swift index ac027e20..6d53f0ff 100644 --- a/Auth0Tests/ClaimValidatorsSpec.swift +++ b/Auth0Tests/ClaimValidatorsSpec.swift @@ -497,10 +497,8 @@ class ClaimValidatorsSpec: IDTokenValidatorBaseSpec { it("should perform a case insensitive compare") { let orgName = "aBc1234" let expectedOrgName = "AbC1234" - orgNameValidator = IDTokenOrgNameValidator(orgName: expectedOrgName) let jwt = generateJWT(orgName: orgName) - let expectedError = IDTokenOrgNameValidator.ValidationError.mismatchedOrgName(actual: orgName, - expected: expectedOrgName) + orgNameValidator = IDTokenOrgNameValidator(orgName: expectedOrgName) expect(orgNameValidator.validate(jwt)).to(beNil()) } From e7263c3509029117b6d9e52745da914ce570280f Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 12 Jul 2023 17:54:41 +0100 Subject: [PATCH 4/5] Update EXAMPLES.md --- EXAMPLES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/EXAMPLES.md b/EXAMPLES.md index 4d62737e..8e28a392 100644 --- a/EXAMPLES.md +++ b/EXAMPLES.md @@ -1212,7 +1212,7 @@ Auth0 ```swift Auth0 .webAuth() - .organization("YOUR_AUTH0_ORGANIZATION_ID") + .organization("YOUR_AUTH0_ORGANIZATION_NAME_OR_ID") .start { result in switch result { case .success(let credentials): @@ -1230,7 +1230,7 @@ Auth0 do { let credentials = try await Auth0 .webAuth() - .organization("YOUR_AUTH0_ORGANIZATION_ID") + .organization("YOUR_AUTH0_ORGANIZATION_NAME_OR_ID") .start() print("Obtained credentials: \(credentials)") } catch { @@ -1245,7 +1245,7 @@ do { ```swift Auth0 .webAuth() - .organization("YOUR_AUTH0_ORGANIZATION_ID") + .organization("YOUR_AUTH0_ORGANIZATION_NAME_OR_ID") .start() .sink(receiveCompletion: { completion in if case .failure(let error) = completion { From 94f12a4b297c126c1c9e2ad61e1ec5b92f445b13 Mon Sep 17 00:00:00 2001 From: Rita Zerrizuela Date: Wed, 12 Jul 2023 17:56:16 +0100 Subject: [PATCH 5/5] Remove extra blank space --- Auth0/ClaimValidators.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Auth0/ClaimValidators.swift b/Auth0/ClaimValidators.swift index f0ac6178..65b5ebd2 100644 --- a/Auth0/ClaimValidators.swift +++ b/Auth0/ClaimValidators.swift @@ -300,7 +300,7 @@ struct IDTokenOrgNameValidator: JWTValidator { func validate(_ jwt: JWT) -> Auth0Error? { guard let actualOrgName = jwt.claim(name: "org_name").string else { return ValidationError.missingOrgName } - guard actualOrgName.caseInsensitiveCompare(expectedOrgName) == .orderedSame else { + guard actualOrgName.caseInsensitiveCompare(expectedOrgName) == .orderedSame else { return ValidationError.mismatchedOrgName(actual: actualOrgName, expected: expectedOrgName) } return nil