From 5b6e4450b5f81fc50322b811ce8f918152488250 Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Fri, 5 Sep 2025 02:57:13 +0530 Subject: [PATCH 1/2] Sample App Implementation for Idp Initiated SAML Sign-In --- .../AuthenticationExample/SceneDelegate.swift | 54 ++++++++++++ .../SwiftApiTests/SignInWithSamlIdp.swift | 84 +++++++++++++++++++ 2 files changed, 138 insertions(+) create mode 100644 FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdp.swift 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/SignInWithSamlIdp.swift b/FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdp.swift new file mode 100644 index 00000000000..1418d0804ef --- /dev/null +++ b/FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdp.swift @@ -0,0 +1,84 @@ +/* + * Copyright 2025 Google + * + * 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 SamlSignInIntegrationTests: 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 From ee47804ec2d654ce592407726bab863489bc3447 Mon Sep 17 00:00:00 2001 From: Srushti Vaidya Date: Fri, 5 Sep 2025 03:22:51 +0530 Subject: [PATCH 2/2] lint fix --- ...Idp.swift => SignInWithSamlIdpTests.swift} | 31 +++++++++---------- 1 file changed, 14 insertions(+), 17 deletions(-) rename FirebaseAuth/Tests/SampleSwift/SwiftApiTests/{SignInWithSamlIdp.swift => SignInWithSamlIdpTests.swift} (79%) diff --git a/FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdp.swift b/FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdpTests.swift similarity index 79% rename from FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdp.swift rename to FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdpTests.swift index 1418d0804ef..5e8dc2c127c 100644 --- a/FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdp.swift +++ b/FirebaseAuth/Tests/SampleSwift/SwiftApiTests/SignInWithSamlIdpTests.swift @@ -1,18 +1,16 @@ -/* - * Copyright 2025 Google - * - * 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. - */ +// 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) @@ -20,7 +18,7 @@ import XCTest @available(iOS 15.0, macOS 12.0, tvOS 16.0, *) - class SamlSignInIntegrationTests: TestsBase { + class SignInWithSamlIdpTests: TestsBase { func testSignInWithSamlFailureInvalidProvider() async throws { try? await deleteCurrentUserAsync() let invalidProvider = "saml.invalid" @@ -55,7 +53,6 @@ let providerId = "saml.googleidp" let spAcsUrl = "https://example.com/saml-acs" let invalidSamlResp = "invalid%25" - do { _ = try await Auth.auth().signInWithSamlIdp( ProviderId: providerId,