diff --git a/FirebaseAuth/Sources/Swift/Auth/Auth.swift b/FirebaseAuth/Sources/Swift/Auth/Auth.swift index d198f5418f5..fcb4bc33636 100644 --- a/FirebaseAuth/Sources/Swift/Auth/Auth.swift +++ b/FirebaseAuth/Sources/Swift/Auth/Auth.swift @@ -2341,6 +2341,29 @@ extension Auth: AuthInterop { } #endif + // MARK: IDP Initiated SAML Sign In + + public func signInWithSamlIdp(ProviderId providerId: String, + SpAcsUrl spAcsUrl: String, + SamlResp samlResp: String) async throws -> AuthDataResult { + let samlRespBody = "SAMLResponse=\(samlResp)&providerId=\(providerId)" + let request = SignInWithSamlIdpRequest( + requestUri: spAcsUrl, + postBody: samlRespBody, + returnSecureToken: true, + requestConfiguration: requestConfiguration + ) + let response = try await backend.call(with: request) + let user = try await completeSignIn( + withAccessToken: response.idToken, + accessTokenExpirationDate: response.expirationDate, + refreshToken: response.refreshToken, + anonymous: false + ) + try await updateCurrentUser(user) + return AuthDataResult(withUser: user, additionalUserInfo: nil) + } + // MARK: Internal properties /// Allow tests to swap in an alternate mainBundle, including ObjC unit tests via CocoaPods. diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithSamlIdpRequest.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithSamlIdpRequest.swift new file mode 100644 index 00000000000..c85044cbc2b --- /dev/null +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithSamlIdpRequest.swift @@ -0,0 +1,55 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +final class SignInWithSamlIdpRequest: AuthRPCRequest { + typealias Response = SignInWithSamlIdpResponse + private let config: AuthRequestConfiguration + private let requestUri: String + private let postBody: String + private let returnSecureToken: Bool + + init(requestUri: String, + postBody: String, + returnSecureToken: Bool, + requestConfiguration: AuthRequestConfiguration) { + self.requestUri = requestUri + self.postBody = postBody + self.returnSecureToken = returnSecureToken + config = requestConfiguration + } + + func requestConfiguration() -> AuthRequestConfiguration { + return config + } + + func requestURL() -> URL { + var comps = URLComponents() + comps.scheme = "https" + comps.host = "identitytoolkit.googleapis.com" + comps.path = "/v1/accounts:signInWithIdp" + comps.queryItems = [URLQueryItem(name: "key", value: config.apiKey)] + return comps.url! + } + + var unencodedHTTPRequestBody: [String: AnyHashable]? { + let body: [String: AnyHashable] = [ + "requestUri": requestUri, + "postBody": postBody, + "returnSecureToken": returnSecureToken, + ] + return body + } +} diff --git a/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithSamlIdpResponse.swift b/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithSamlIdpResponse.swift new file mode 100644 index 00000000000..72ca328ba21 --- /dev/null +++ b/FirebaseAuth/Sources/Swift/Backend/RPC/SignInWithSamlIdpResponse.swift @@ -0,0 +1,46 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import Foundation + +struct SignInWithSamlIdpResponse: AuthRPCResponse { + /// The user raw access token. + let idToken: String + /// Refresh token for the authenticated user. + let refreshToken: String + /// The provider Identifier + let providerId: String + /// The email id of user + let email: String + /// The calculated date and time when the token expires. + let expirationDate: Date + + init(dictionary: [String: AnyHashable]) throws { + guard + let email = dictionary["email"] as? String, + let expiration = dictionary["expiresIn"] as? String, + let idToken = dictionary["idToken"] as? String, + let providerId = dictionary["providerId"] as? String, + let refreshToken = dictionary["refreshToken"] as? String + else { + throw AuthErrorUtils.unexpectedResponse(deserializedResponse: dictionary) + } + self.idToken = idToken + self.refreshToken = refreshToken + self.providerId = providerId + self.email = email + let expiresInSec = TimeInterval(expiration) + expirationDate = Date().addingTimeInterval(expiresInSec ?? 3600) + } +} diff --git a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SceneDelegate.swift b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SceneDelegate.swift index e7965767a8a..ed5a0cea2d3 100644 --- a/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SceneDelegate.swift +++ b/FirebaseAuth/Tests/SampleSwift/AuthenticationExample/SceneDelegate.swift @@ -50,10 +50,64 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { // Implementing this delegate method is needed when swizzling is disabled. // Without it, reCAPTCHA's login view controller will not dismiss. + // Without it, IdP Initiated SAML Sign In will not work. func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { for urlContext in URLContexts { let url = urlContext.url _ = Auth.auth().canHandle(url) + /// Handle IdP Initiated SAML deep link myapp://saml?resp= + if url.scheme?.lowercased() == "myapp", /// replace with your custom scheme + url.host?.lowercased() == "saml" { /// replace with your host + let spAcsUrl = + "https://iostemp-8a944.web.app/googleidp-saml/acs" /// replace with your SP ACS URL + if let rawQuery = url.query { + var respValue: String? + for pair in rawQuery.split(separator: "&", omittingEmptySubsequences: false) { + let parts = pair.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: false) + if parts.count == 2, parts[0] == "resp" { + respValue = String(parts[1]) + break + } + } + if let resp = respValue { + let alert = UIAlertController( + title: "SAML Sign In", + message: "Enter Provider ID", + preferredStyle: .alert + ) + alert.addTextField { tf in + tf.placeholder = "Provider ID" + tf.text = "saml.provider" + tf.autocapitalizationType = .none + tf.autocorrectionType = .no + } + alert.addAction(UIAlertAction(title: "Cancel", style: .cancel)) + alert.addAction(UIAlertAction(title: "OK", style: .default) { _ in + let providerId = alert.textFields?.first?.text? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + let requestUri = alert.textFields?.last?.text? + .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" + guard !providerId.isEmpty, !requestUri.isEmpty else { return } + Task { + do { + _ = try await AppManager.shared.auth().signInWithSamlIdp( + ProviderId: providerId, + SpAcsUrl: requestUri, + SamlResp: resp + ) + } catch { + print("IdP-initiated SAML sign-in failed with error:", error) + } + } + }) + var top = window?.rootViewController + while let presented = top?.presentedViewController { + top = presented + } + top?.present(alert, animated: true) + } + } + } } // URL not auth related; it should be handled separately. diff --git a/FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdpTests.swift b/FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdpTests.swift new file mode 100644 index 00000000000..5e8dc2c127c --- /dev/null +++ b/FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdpTests.swift @@ -0,0 +1,81 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if os(iOS) + + @testable import FirebaseAuth + import XCTest + + @available(iOS 15.0, macOS 12.0, tvOS 16.0, *) + class SignInWithSamlIdpTests: TestsBase { + func testSignInWithSamlFailureInvalidProvider() async throws { + try? await deleteCurrentUserAsync() + let invalidProvider = "saml.invalid" + let spAcsUrl = "https://example.com/saml-acs" + let samlResp = "samlResp" + do { + _ = try await Auth.auth().signInWithSamlIdp( + ProviderId: invalidProvider, + SpAcsUrl: spAcsUrl, + SamlResp: samlResp + ) + XCTFail("Expected failure for invalid provider ID") + } catch { + let ns = error as NSError + if let code = AuthErrorCode(rawValue: ns.code) { + XCTAssert([.operationNotAllowed].contains(code), + "Unexpected code: \(code)") + } else { + XCTFail("Unexpected error: \(error)") + } + let desc = (ns.userInfo[NSLocalizedDescriptionKey] as? String ?? "").uppercased() + XCTAssert( + desc.contains("THE IDENTITY PROVIDER CONFIGURATION IS NOT FOUND."), + "Expected backend invalid provider message, got: \(desc)" + ) + } + XCTAssertNil(Auth.auth().currentUser) + } + + func testSignInWithSamlFailureInvalidResponse() async throws { + try? await deleteCurrentUserAsync() + let providerId = "saml.googleidp" + let spAcsUrl = "https://example.com/saml-acs" + let invalidSamlResp = "invalid%25" + do { + _ = try await Auth.auth().signInWithSamlIdp( + ProviderId: providerId, + SpAcsUrl: spAcsUrl, + SamlResp: invalidSamlResp + ) + XCTFail("Expected failure for invalid SAMLResponse") + } catch { + let ns = error as NSError + if let code = AuthErrorCode(rawValue: ns.code) { + XCTAssert([.invalidCredential, .internalError].contains(code), + "Unexpected code: \(code)") + } else { + XCTFail("Unexpected error: \(error)") + } + let desc = (ns.userInfo[NSLocalizedDescriptionKey] as? String ?? "").uppercased() + XCTAssert( + desc.contains("UNABLE TO PARSE THE SAML TOKEN."), + "Expected backend invalid credential message, got: \(desc)" + ) + } + XCTAssertNil(Auth.auth().currentUser) + } + } + +#endif diff --git a/FirebaseAuth/Tests/Unit/AuthTests.swift b/FirebaseAuth/Tests/Unit/AuthTests.swift index 5ae1d522108..8e8fc2474f4 100644 --- a/FirebaseAuth/Tests/Unit/AuthTests.swift +++ b/FirebaseAuth/Tests/Unit/AuthTests.swift @@ -2287,6 +2287,152 @@ class AuthTests: RPCBaseTests { } #endif + // MARK: SAML IdP sign-in + + #if os(iOS) + + static let kSamlProviderId = "saml.idp" + static let kSamlAcsUrl = "https://example.com/saml-acs-url" + static let kSamlResponse = "BASE64_SAML_ASSERTION" + static let kBadSamlResponse = "MALFORMED_OR_TAMPERED_SAML" + + func testSignInWithSamlIdpSuccess() throws { + let expectation = self.expectation(description: #function) + setFakeGetAccountProvider() + setFakeSecureTokenService() + rpcIssuer.respondBlock = { + let req = try XCTUnwrap(self.rpcIssuer.request as? SignInWithSamlIdpRequest) + XCTAssertEqual(req.requestConfiguration().apiKey, AuthTests.kFakeAPIKey) + XCTAssertEqual( + req.unencodedHTTPRequestBody?["requestUri"] as? String, + AuthTests.kSamlAcsUrl + ) + XCTAssertTrue( + (req.unencodedHTTPRequestBody?["postBody"] as? String)?.contains( + AuthTests.kSamlProviderId + ) ?? false + ) + XCTAssertTrue(req.unencodedHTTPRequestBody?["returnSecureToken"] as? Bool ?? false) + return try self.rpcIssuer.respond(withJSON: [ + "idToken": RPCBaseTests.kFakeAccessToken, + "refreshToken": self.kRefreshToken, + "email": self.kEmail, + "providerId": AuthTests.kSamlProviderId, + "expiresIn": "3600", + ]) + } + try auth.signOut() + Task { + do { + let result = try await self.auth.signInWithSamlIdp( + ProviderId: AuthTests.kSamlProviderId, + SpAcsUrl: AuthTests.kSamlAcsUrl, + SamlResp: AuthTests.kSamlResponse + ) + XCTAssertEqual(result.user.email, self.kEmail) + XCTAssertEqual(result.user.refreshToken, self.kRefreshToken) + XCTAssertFalse(result.user.isAnonymous) + expectation.fulfill() + } catch { + XCTFail("Unexpected error: \(error)") + } + } + waitForExpectations(timeout: 5) + } + + func testSignInWithSamlIdpWithIncorrectUrl() throws { + let expectation = self.expectation(description: #function) + let kBadSamlAcsUrl = "https://example.com/saml-acs-incorrect-url" + rpcIssuer.respondBlock = { + let req = try XCTUnwrap(self.rpcIssuer.request as? SignInWithSamlIdpRequest) + XCTAssertEqual(req.requestConfiguration().apiKey, AuthTests.kFakeAPIKey) + let body = try XCTUnwrap(req.unencodedHTTPRequestBody) + XCTAssertEqual(body["requestUri"] as? String, kBadSamlAcsUrl) + return try self.rpcIssuer.respond(serverErrorMessage: "OPERATION_NOT_ALLOWED") + } + try auth.signOut() + Task { + do { + _ = try await self.auth.signInWithSamlIdp( + ProviderId: AuthTests.kSamlProviderId, + SpAcsUrl: kBadSamlAcsUrl, + SamlResp: AuthTests.kSamlResponse + ) + XCTFail("Expected OPERATION_NOT_ALLOWED") + } catch { + let ns = error as NSError + XCTAssertEqual(ns.code, AuthErrorCode.operationNotAllowed.rawValue) + expectation.fulfill() + } + } + waitForExpectations(timeout: 5) + XCTAssertNil(auth.currentUser) + } + + func testSignInWithSamlIdpFailureInvalidProviderId() throws { + let expectation = self.expectation(description: #function) + let badProvider = "saml.non-existent-idp" + rpcIssuer.respondBlock = { + let req = try XCTUnwrap(self.rpcIssuer.request as? SignInWithSamlIdpRequest) + XCTAssertEqual(req.requestConfiguration().apiKey, AuthTests.kFakeAPIKey) + let body = try XCTUnwrap(req.unencodedHTTPRequestBody) + let postBody = try XCTUnwrap(body["postBody"] as? String) + XCTAssertTrue(postBody.contains("providerId=\(badProvider)")) + return try self.rpcIssuer.respond(serverErrorMessage: "OPERATION_NOT_ALLOWED") + } + try auth.signOut() + Task { + do { + _ = try await self.auth.signInWithSamlIdp( + ProviderId: badProvider, // wrong providerId + SpAcsUrl: AuthTests.kSamlAcsUrl, + SamlResp: AuthTests.kSamlResponse + ) + XCTFail("Expected OPERATION_NOT_ALLOWED") + } catch { + let ns = error as NSError + XCTAssertEqual(ns.code, AuthErrorCode.operationNotAllowed.rawValue) + expectation.fulfill() + } + } + waitForExpectations(timeout: 5) + XCTAssertNil(auth.currentUser) + } + + func testSignInWithSamlIdpFailureInvalidPostBody() throws { + let expectation = self.expectation(description: #function) + rpcIssuer.respondBlock = { + let req = try XCTUnwrap(self.rpcIssuer.request as? SignInWithSamlIdpRequest) + XCTAssertEqual(req.requestConfiguration().apiKey, AuthTests.kFakeAPIKey) + let body = try XCTUnwrap(req.unencodedHTTPRequestBody) + XCTAssertEqual(body["requestUri"] as? String, AuthTests.kSamlAcsUrl) + let postBody = try XCTUnwrap(body["postBody"] as? String) + XCTAssertTrue(postBody.contains("SAMLResponse=\(AuthTests.kBadSamlResponse)")) + XCTAssertTrue(postBody.contains("providerId=\(AuthTests.kSamlProviderId)")) + XCTAssertTrue(body["returnSecureToken"] as? Bool ?? false) + return try self.rpcIssuer + .respond(underlyingErrorMessage: "INVALID_CREDENTIAL_OR_PROVIDER_ID") + } + try auth.signOut() + Task { + do { + _ = try await self.auth.signInWithSamlIdp( + ProviderId: AuthTests.kSamlProviderId, + SpAcsUrl: AuthTests.kSamlAcsUrl, + SamlResp: AuthTests.kBadSamlResponse + ) + XCTFail("Expected internalError but got success") + } catch { + let ns = error as NSError + XCTAssertEqual(ns.code, AuthErrorCode.internalError.rawValue) + expectation.fulfill() + } + } + waitForExpectations(timeout: 5) + XCTAssertNil(auth.currentUser) + } + #endif + // MARK: Application Delegate tests. #if os(iOS) diff --git a/FirebaseAuth/Tests/Unit/SignInWithSamlIdpRequestTests.swift b/FirebaseAuth/Tests/Unit/SignInWithSamlIdpRequestTests.swift new file mode 100644 index 00000000000..501dbf3a594 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/SignInWithSamlIdpRequestTests.swift @@ -0,0 +1,113 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@testable import FirebaseAuth +import FirebaseCore +import XCTest + +final class SignInWithSamlIdpRequestTests: XCTestCase { + let kAPIKey = "TEST_API_KEY" + let kAppID = "FAKE_APP_ID" + let kRequestUri = "https://example.web.app/sp-acs-url" + let kPostBody = "SAMLResponse=BASE64%2BSAFE&providerId=saml.provider" + let kComplexUri = "https://host/acs;param?p1=v1&p2=v2#frag" + let kRawPostBody = + "SAMLResponse=someResponse&providerId=saml.provider" + + var configuration: AuthRequestConfiguration! + + override func setUp() { + super.setUp() + configuration = AuthRequestConfiguration(apiKey: kAPIKey, appID: kAppID) + } + + override func tearDown() { + configuration = nil + super.tearDown() + } + + func testRequestURL() { + let request = SignInWithSamlIdpRequest( + requestUri: kRequestUri, + postBody: kPostBody, + returnSecureToken: true, + requestConfiguration: configuration + ) + + let url = request.requestURL() + XCTAssertEqual(url.scheme, "https") + XCTAssertEqual(url.host, "identitytoolkit.googleapis.com") + XCTAssertEqual(url.path, "/v1/accounts:signInWithIdp") + + let components = URLComponents(url: url, resolvingAgainstBaseURL: false) + XCTAssertEqual(components?.queryItems?.count, 1) + XCTAssertEqual(components?.queryItems?.first?.name, "key") + XCTAssertEqual(components?.queryItems?.first?.value, kAPIKey) + } + + func testRequestConfigurationPassed() { + let request = SignInWithSamlIdpRequest( + requestUri: kRequestUri, + postBody: kPostBody, + returnSecureToken: false, + requestConfiguration: configuration + ) + + let returned = request.requestConfiguration() + XCTAssertEqual(returned.apiKey, kAPIKey) + XCTAssertIdentical(returned.auth, configuration.auth) + } + + func testUnencodedHTTPRequestBody() { + let request = SignInWithSamlIdpRequest( + requestUri: kRequestUri, + postBody: kPostBody, + returnSecureToken: true, + requestConfiguration: configuration + ) + + guard let body = request.unencodedHTTPRequestBody else { + XCTFail("Body must not be nil") + return + } + + XCTAssertEqual(body.count, 3) + XCTAssertEqual(body["requestUri"] as? String, kRequestUri) + XCTAssertEqual(body["postBody"] as? String, kPostBody) + XCTAssertEqual(body["returnSecureToken"] as? Bool, true) + } + + func testUnencodedHTTPRequestPostBody() { + let request = SignInWithSamlIdpRequest( + requestUri: kRequestUri, + postBody: kRawPostBody, + returnSecureToken: true, + requestConfiguration: configuration + ) + + let body = request.unencodedHTTPRequestBody + XCTAssertEqual(body?["postBody"] as? String, kRawPostBody) + } + + func testUnencodedHTTPRequestBody_AllowsComplexRequestUri() throws { + let request = SignInWithSamlIdpRequest( + requestUri: kComplexUri, + postBody: kPostBody, + returnSecureToken: true, + requestConfiguration: configuration + ) + let body = try XCTUnwrap(request.unencodedHTTPRequestBody) + XCTAssertEqual(body["requestUri"] as? String, kComplexUri) + } +} diff --git a/FirebaseAuth/Tests/Unit/SignInWithSamlIdpResponseTests.swift b/FirebaseAuth/Tests/Unit/SignInWithSamlIdpResponseTests.swift new file mode 100644 index 00000000000..d3c6e2bb6d9 --- /dev/null +++ b/FirebaseAuth/Tests/Unit/SignInWithSamlIdpResponseTests.swift @@ -0,0 +1,85 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +@testable import FirebaseAuth +import XCTest + +final class SignInWithSamlIdpResponseTests: XCTestCase { + private func makeValidDictionary() -> [String: AnyHashable] { + return [ + "email": "user@example.com", + "expiresIn": "3600", + "idToken": "FAKE_ID_TOKEN", + "providerId": "saml.provider", + "refreshToken": "FAKE_REFRESH_TOKEN", + ] + } + + func testInitWithValidDictionaryAllRequiredFields() throws { + var dict = makeValidDictionary() + dict["email"] = "user1@example.com" + dict["idToken"] = "ID.TOKEN" + dict["providerId"] = "saml.myidp" + dict["refreshToken"] = "REFRESH.TOKEN" + let response = try SignInWithSamlIdpResponse(dictionary: dict) + XCTAssertEqual(response.email, "user1@example.com") + XCTAssertEqual(response.idToken, "ID.TOKEN") + XCTAssertEqual(response.providerId, "saml.myidp") + XCTAssertEqual(response.refreshToken, "REFRESH.TOKEN") + } + + func testInitMissingRequiredFields() { + struct Case { let name: String; let keyToRemove: String } + let cases: [Case] = [ + .init(name: "Missing email", keyToRemove: "email"), + .init(name: "Missing expiresIn", keyToRemove: "expiresIn"), + .init(name: "Missing idToken", keyToRemove: "idToken"), + .init(name: "Missing providerId", keyToRemove: "providerId"), + .init(name: "Missing refreshToken", keyToRemove: "refreshToken"), + ] + for c in cases { + var dict = makeValidDictionary() + dict.removeValue(forKey: c.keyToRemove) + XCTAssertThrowsError(try SignInWithSamlIdpResponse(dictionary: dict), c.name) { error in + let nsError = error as NSError + XCTAssertEqual(nsError.domain, AuthErrorDomain) + XCTAssertEqual(nsError.code, AuthErrorCode.internalError.rawValue) + } + } + } + + func testInitIncorrectFieldTypes() { + var dict = makeValidDictionary() + dict["expiresIn"] = 3600 + XCTAssertThrowsError(try SignInWithSamlIdpResponse(dictionary: dict)) { error in + let nsError = error as NSError + XCTAssertEqual(nsError.domain, AuthErrorDomain) + XCTAssertEqual(nsError.code, AuthErrorCode.internalError.rawValue) + } + dict = makeValidDictionary() + dict["idToken"] = 123 + XCTAssertThrowsError(try SignInWithSamlIdpResponse(dictionary: dict)) { error in + let nsError = error as NSError + XCTAssertEqual(nsError.domain, AuthErrorDomain) + XCTAssertEqual(nsError.code, AuthErrorCode.internalError.rawValue) + } + dict = makeValidDictionary() + dict["email"] = NSNull() + XCTAssertThrowsError(try SignInWithSamlIdpResponse(dictionary: dict)) { error in + let nsError = error as NSError + XCTAssertEqual(nsError.domain, AuthErrorDomain) + XCTAssertEqual(nsError.code, AuthErrorCode.internalError.rawValue) + } + } +}