From 567cbdc06a6ae0958e813b4a99aed3867a73aadb Mon Sep 17 00:00:00 2001 From: Drew McLean Date: Mon, 11 Nov 2019 12:36:33 -0500 Subject: [PATCH 1/5] Add support for extended profile using SIWA token exchange --- Auth0/Authentication.swift | 29 +++++++++++++++++----- Auth0Tests/AuthenticationSpec.swift | 38 +++++++++++++++++++++++------ Cartfile.resolved | 4 +-- 3 files changed, 55 insertions(+), 16 deletions(-) diff --git a/Auth0/Authentication.swift b/Auth0/Authentication.swift index cf633292..584e5f14 100644 --- a/Auth0/Authentication.swift +++ b/Auth0/Authentication.swift @@ -911,21 +911,38 @@ public extension Authentication { ``` Auth0 .authentication(clientId: clientId, domain: "samples.auth0.com") - .tokenExchange(withAppleAuthorizationCode: authCode, scope: "openid profile offline_access", audience: "https://myapi.com/api) + .tokenExchange(withAppleAuthorizationCode: authCode, fullName: credentials.fullName, scope: "openid profile offline_access", audience: "https://myapi.com/api) .start { print($0) } ``` - parameter authCode: Authorization Code retrieved from Apple Authorization - parameter scope: requested scope value when authenticating the user. By default is 'openid profile offline_access' - parameter audience: API Identifier that the client is requesting access to + - parameter fullName: The full name property returned with the Apple ID Credentials - returns: a request that will yield Auth0 user's credentials */ - func tokenExchange(withAppleAuthorizationCode authCode: String, scope: String? = nil, audience: String? = nil) -> Request { - var parameters = [ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", - "subject_token": authCode, - "subject_token_type": "http://auth0.com/oauth/token-type/apple-authz-code", - "scope": scope ?? "openid profile offline_access"] + func tokenExchange(withAppleAuthorizationCode authCode: String, scope: String? = nil, audience: String? = nil, fullName: PersonNameComponents? = nil) -> Request { + + var parameters: [String: String] = + [ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "subject_token": authCode, + "subject_token_type": "http://auth0.com/oauth/token-type/apple-authz-code", + "scope": scope ?? "openid profile offline_access" ] + if fullName != nil { + if let jsonData = try? String( + data: JSONSerialization.data( + withJSONObject: [ + "name": [ + "firstName": fullName?.givenName, + "lastName": fullName?.familyName + ] + ], + options: []), + encoding: String.Encoding.utf8) { + parameters["user_profile"] = jsonData + } + } parameters["audience"] = audience return self.tokenExchange(withParameters: parameters) } diff --git a/Auth0Tests/AuthenticationSpec.swift b/Auth0Tests/AuthenticationSpec.swift index 062f66ed..4e311d83 100644 --- a/Auth0Tests/AuthenticationSpec.swift +++ b/Auth0Tests/AuthenticationSpec.swift @@ -215,11 +215,10 @@ class AuthenticationSpec: QuickSpec { } } - + // MARK:- Token Exchange describe("token exchange") { - beforeEach { stub(condition: isToken(Domain) && hasAtLeast([ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", @@ -227,14 +226,14 @@ class AuthenticationSpec: QuickSpec { "subject_token_type": "http://auth0.com/oauth/token-type/apple-authz-code", "scope": "openid profile offline_access" ])) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "Token Exchange Apple Success" - + stub(condition: isToken(Domain) && hasAtLeast([ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": "VALIDCODE", "subject_token_type": "http://auth0.com/oauth/token-type/apple-authz-code", "scope": "openid email" ])) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "Token Exchange Apple Success with custom scope" - + stub(condition: isToken(Domain) && hasAtLeast([ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": "VALIDCODE", @@ -242,6 +241,14 @@ class AuthenticationSpec: QuickSpec { "scope": "openid email", "audience": "https://myapi.com/api" ])) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "Token Exchange Apple Success with custom scope and audience" + + stub(condition: isToken(Domain) && hasAtLeast([ + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "subject_token": "VALIDNAMECODE", + "subject_token_type": "http://auth0.com/oauth/token-type/apple-authz-code"]) && + (hasAtLeast(["user_profile": "{\"name\":{\"lastName\":\"Smith\",\"firstName\":\"John\"}}" ]) || hasAtLeast(["user_profile": "{\"name\":{\"firstName\":\"John\",\"lastName\":\"Smith\"}}" ])) + ) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "Token Exchange Apple Success with user profile" + } it("should exchange apple auth code for credentials") { @@ -253,7 +260,7 @@ class AuthenticationSpec: QuickSpec { } } } - + it("should exchange apple auth code and fail") { waitUntil(timeout: Timeout) { done in auth.tokenExchange(withAppleAuthorizationCode: "INVALIDCODE") @@ -263,7 +270,7 @@ class AuthenticationSpec: QuickSpec { } } } - + it("should exchange apple auth code for credentials with custom scope") { waitUntil(timeout: Timeout) { done in auth.tokenExchange(withAppleAuthorizationCode: "VALIDCODE", scope: "openid email") @@ -273,7 +280,7 @@ class AuthenticationSpec: QuickSpec { } } } - + it("should exchange apple auth code for credentials with custom scope and audience") { waitUntil(timeout: Timeout) { done in auth.tokenExchange(withAppleAuthorizationCode: "VALIDCODE", scope: "openid email", audience: "https://myapi.com/api") @@ -283,7 +290,22 @@ class AuthenticationSpec: QuickSpec { } } } - + + it("should exchange apple auth code for credentials with fullName") { + var fullName = PersonNameComponents() + fullName.givenName = "John" + fullName.familyName = "Smith" + fullName.middleName = "Ignored" + + waitUntil(timeout: Timeout) { done in + auth.tokenExchange(withAppleAuthorizationCode: "VALIDNAMECODE", + fullName: fullName) + .start { result in + expect(result).to(haveCredentials()) + done() + } + } + } } describe("revoke refresh token") { diff --git a/Cartfile.resolved b/Cartfile.resolved index dc291396..aa1d7687 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,4 @@ github "AliSoftware/OHHTTPStubs" "8.0.0" -github "Quick/Nimble" "v8.0.2" -github "Quick/Quick" "v2.1.0" +github "Quick/Nimble" "v8.0.4" +github "Quick/Quick" "v2.2.0" github "auth0/SimpleKeychain" "0.9.0" From 7a6733ec878205fa93b4aee7ee957edbae5ed09e Mon Sep 17 00:00:00 2001 From: Drew McLean Date: Mon, 11 Nov 2019 15:38:14 -0500 Subject: [PATCH 2/5] Fix indenting/whitespace --- Auth0/Authentication.swift | 1 - Auth0Tests/AuthenticationSpec.swift | 20 +++++++++++--------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/Auth0/Authentication.swift b/Auth0/Authentication.swift index 584e5f14..0cc87163 100644 --- a/Auth0/Authentication.swift +++ b/Auth0/Authentication.swift @@ -923,7 +923,6 @@ public extension Authentication { - returns: a request that will yield Auth0 user's credentials */ func tokenExchange(withAppleAuthorizationCode authCode: String, scope: String? = nil, audience: String? = nil, fullName: PersonNameComponents? = nil) -> Request { - var parameters: [String: String] = [ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": authCode, diff --git a/Auth0Tests/AuthenticationSpec.swift b/Auth0Tests/AuthenticationSpec.swift index 4e311d83..95c724dd 100644 --- a/Auth0Tests/AuthenticationSpec.swift +++ b/Auth0Tests/AuthenticationSpec.swift @@ -215,10 +215,11 @@ class AuthenticationSpec: QuickSpec { } } - + // MARK:- Token Exchange describe("token exchange") { + beforeEach { stub(condition: isToken(Domain) && hasAtLeast([ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", @@ -226,14 +227,14 @@ class AuthenticationSpec: QuickSpec { "subject_token_type": "http://auth0.com/oauth/token-type/apple-authz-code", "scope": "openid profile offline_access" ])) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "Token Exchange Apple Success" - + stub(condition: isToken(Domain) && hasAtLeast([ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": "VALIDCODE", "subject_token_type": "http://auth0.com/oauth/token-type/apple-authz-code", "scope": "openid email" ])) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "Token Exchange Apple Success with custom scope" - + stub(condition: isToken(Domain) && hasAtLeast([ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": "VALIDCODE", @@ -241,7 +242,7 @@ class AuthenticationSpec: QuickSpec { "scope": "openid email", "audience": "https://myapi.com/api" ])) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "Token Exchange Apple Success with custom scope and audience" - + stub(condition: isToken(Domain) && hasAtLeast([ "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", "subject_token": "VALIDNAMECODE", @@ -260,7 +261,7 @@ class AuthenticationSpec: QuickSpec { } } } - + it("should exchange apple auth code and fail") { waitUntil(timeout: Timeout) { done in auth.tokenExchange(withAppleAuthorizationCode: "INVALIDCODE") @@ -270,7 +271,7 @@ class AuthenticationSpec: QuickSpec { } } } - + it("should exchange apple auth code for credentials with custom scope") { waitUntil(timeout: Timeout) { done in auth.tokenExchange(withAppleAuthorizationCode: "VALIDCODE", scope: "openid email") @@ -280,7 +281,7 @@ class AuthenticationSpec: QuickSpec { } } } - + it("should exchange apple auth code for credentials with custom scope and audience") { waitUntil(timeout: Timeout) { done in auth.tokenExchange(withAppleAuthorizationCode: "VALIDCODE", scope: "openid email", audience: "https://myapi.com/api") @@ -290,7 +291,7 @@ class AuthenticationSpec: QuickSpec { } } } - + it("should exchange apple auth code for credentials with fullName") { var fullName = PersonNameComponents() fullName.givenName = "John" @@ -306,8 +307,9 @@ class AuthenticationSpec: QuickSpec { } } } - } + } + describe("revoke refresh token") { let refreshToken = UUID().uuidString.replacingOccurrences(of: "-", with: "") From cb9abbce09811dada37528a739063dbb815f6d45 Mon Sep 17 00:00:00 2001 From: Drew McLean Date: Mon, 11 Nov 2019 15:39:39 -0500 Subject: [PATCH 3/5] Revert accidental Cartfile.resolved changes --- Cartfile.resolved | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cartfile.resolved b/Cartfile.resolved index aa1d7687..dc291396 100644 --- a/Cartfile.resolved +++ b/Cartfile.resolved @@ -1,4 +1,4 @@ github "AliSoftware/OHHTTPStubs" "8.0.0" -github "Quick/Nimble" "v8.0.4" -github "Quick/Quick" "v2.2.0" +github "Quick/Nimble" "v8.0.2" +github "Quick/Quick" "v2.1.0" github "auth0/SimpleKeychain" "0.9.0" From 71128a535c0713fabd98ed827aa9f7e22464111f Mon Sep 17 00:00:00 2001 From: Drew McLean Date: Mon, 11 Nov 2019 20:28:51 -0500 Subject: [PATCH 4/5] Reorder SIWA parameters, and stop sending missing values --- Auth0/Authentication.swift | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Auth0/Authentication.swift b/Auth0/Authentication.swift index 0cc87163..2e832324 100644 --- a/Auth0/Authentication.swift +++ b/Auth0/Authentication.swift @@ -911,7 +911,7 @@ public extension Authentication { ``` Auth0 .authentication(clientId: clientId, domain: "samples.auth0.com") - .tokenExchange(withAppleAuthorizationCode: authCode, fullName: credentials.fullName, scope: "openid profile offline_access", audience: "https://myapi.com/api) + .tokenExchange(withAppleAuthorizationCode: authCode, scope: "openid profile offline_access", audience: "https://myapi.com/api, fullName: credentials.fullName) .start { print($0) } ``` @@ -928,17 +928,19 @@ public extension Authentication { "subject_token": authCode, "subject_token_type": "http://auth0.com/oauth/token-type/apple-authz-code", "scope": scope ?? "openid profile offline_access" ] - if fullName != nil { - if let jsonData = try? String( - data: JSONSerialization.data( - withJSONObject: [ - "name": [ - "firstName": fullName?.givenName, - "lastName": fullName?.familyName - ] - ], - options: []), - encoding: String.Encoding.utf8) { + if let fullName = fullName { + let name = [ + "firstName": fullName.givenName, + "lastName": fullName.familyName + ].filter { $0.value != nil }.mapValues { $0! } + if !name.isEmpty, let jsonData = try? String( + data: JSONSerialization.data( + withJSONObject: [ + "name": name + ], + options: []), + encoding: String.Encoding.utf8 + ) { parameters["user_profile"] = jsonData } } From 341cf65e632b290bfb8e9de27252ea3e1825860e Mon Sep 17 00:00:00 2001 From: Drew McLean Date: Tue, 12 Nov 2019 11:35:20 -0500 Subject: [PATCH 5/5] Expand test cases for SIWA --- Auth0Tests/AuthenticationSpec.swift | 50 +++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/Auth0Tests/AuthenticationSpec.swift b/Auth0Tests/AuthenticationSpec.swift index 95c724dd..2e488724 100644 --- a/Auth0Tests/AuthenticationSpec.swift +++ b/Auth0Tests/AuthenticationSpec.swift @@ -249,7 +249,21 @@ class AuthenticationSpec: QuickSpec { "subject_token_type": "http://auth0.com/oauth/token-type/apple-authz-code"]) && (hasAtLeast(["user_profile": "{\"name\":{\"lastName\":\"Smith\",\"firstName\":\"John\"}}" ]) || hasAtLeast(["user_profile": "{\"name\":{\"firstName\":\"John\",\"lastName\":\"Smith\"}}" ])) ) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "Token Exchange Apple Success with user profile" - + + stub(condition: isToken(Domain) && hasAtLeast([ + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "subject_token": "VALIDPARTIALNAMECODE", + "subject_token_type": "http://auth0.com/oauth/token-type/apple-authz-code", + "user_profile": "{\"name\":{\"firstName\":\"John\"}}" + ])) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "Token Exchange Apple Success with partial user profile" + + stub(condition: isToken(Domain) && hasAtLeast([ + "grant_type": "urn:ietf:params:oauth:grant-type:token-exchange", + "subject_token": "VALIDMISSINGNAMECODE", + "subject_token_type": "http://auth0.com/oauth/token-type/apple-authz-code"]) && + hasNoneOf(["user_profile"]) + ) { _ in return authResponse(accessToken: AccessToken, idToken: IdToken) }.name = "Token Exchange Apple Success with missing user profile" + } it("should exchange apple auth code for credentials") { @@ -307,7 +321,39 @@ class AuthenticationSpec: QuickSpec { } } } - + + it("should exchange apple auth code for credentials with partial fullName") { + var fullName = PersonNameComponents() + fullName.givenName = "John" + fullName.familyName = nil + fullName.middleName = "Ignored" + + waitUntil(timeout: Timeout) { done in + auth.tokenExchange(withAppleAuthorizationCode: "VALIDPARTIALNAMECODE", + fullName: fullName) + .start { result in + expect(result).to(haveCredentials()) + done() + } + } + } + + it("should exchange apple auth code for credentials with missing fullName") { + var fullName = PersonNameComponents() + fullName.givenName = nil + fullName.familyName = nil + fullName.middleName = nil + + waitUntil(timeout: Timeout) { done in + auth.tokenExchange(withAppleAuthorizationCode: "VALIDMISSINGNAMECODE", + fullName: fullName) + .start { result in + expect(result).to(haveCredentials()) + done() + } + } + } + } describe("revoke refresh token") {