Skip to content

Commit ea9223a

Browse files
authored
sample app changes with updated logic (#14960) _final
1 parent 4ce3a4d commit ea9223a

File tree

8 files changed

+256
-355
lines changed

8 files changed

+256
-355
lines changed

ExchangeTokensRequestTests.swift

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
// Copyright 2023 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
import Foundation
16+
import XCTest
17+
18+
@testable import FirebaseAuth
19+
import FirebaseCore
20+
21+
/// @class ExchangeTokenRequestTests
22+
/// @brief Tests for the @c ExchangeTokenRequest struct.
23+
@available(iOS 13, *)
24+
class ExchangeTokenRequestTests: XCTestCase {
25+
// MARK: - Constants for Testing
26+
27+
let kAPIKey = "test-api-key"
28+
let kProjectID = "test-project-id"
29+
let kLocation = "asia-northeast1"
30+
let kTenantID = "test-tenant-id-123"
31+
let kCustomToken = "a-very-long-and-secure-oidc-token-string"
32+
let kIdpConfigId = "oidc.my-test-provider"
33+
34+
let kProductionHost = "identityplatform.googleapis.com"
35+
let kStagingHost = "staging-identityplatform.sandbox.googleapis.com"
36+
37+
// MARK: - Test Cases
38+
39+
func testProductionURLIsCorrectlyConstructed() {
40+
let (auth, app) = createTestAuthInstance(
41+
projectID: kProjectID,
42+
location: kLocation,
43+
tenantId: kTenantID
44+
)
45+
_ = app
46+
47+
let request = ExchangeTokenRequest(
48+
customToken: kCustomToken,
49+
idpConfigID: kIdpConfigId,
50+
config: auth.requestConfiguration,
51+
useStaging: false
52+
)
53+
54+
let expectedHost = "\(kLocation)-\(kProductionHost)"
55+
let expectedURL = "https://\(expectedHost)/v2alpha/projects/\(kProjectID)" +
56+
"/locations/\(kLocation)/tenants/\(kTenantID)/idpConfigs/\(kIdpConfigId):exchangeOidcToken?key=\(kAPIKey)"
57+
58+
XCTAssertEqual(request.requestURL().absoluteString, expectedURL)
59+
}
60+
61+
func testProductionURLIsCorrectlyConstructedForGlobalLocation() {
62+
let (auth, app) = createTestAuthInstance(
63+
projectID: kProjectID,
64+
location: "prod-global",
65+
tenantId: kTenantID
66+
)
67+
_ = app
68+
69+
let request = ExchangeTokenRequest(
70+
customToken: kCustomToken,
71+
idpConfigID: kIdpConfigId,
72+
config: auth.requestConfiguration,
73+
useStaging: false
74+
)
75+
76+
let expectedHost = kProductionHost
77+
let expectedURL = "https://\(expectedHost)/v2alpha/projects/\(kProjectID)" +
78+
"/locations/prod-global/tenants/\(kTenantID)/idpConfigs/\(kIdpConfigId):exchangeOidcToken?key=\(kAPIKey)"
79+
80+
XCTAssertEqual(request.requestURL().absoluteString, expectedURL)
81+
}
82+
83+
func testStagingURLIsCorrectlyConstructed() {
84+
let (auth, app) = createTestAuthInstance(
85+
projectID: kProjectID,
86+
location: kLocation,
87+
tenantId: kTenantID
88+
)
89+
_ = app
90+
91+
let request = ExchangeTokenRequest(
92+
customToken: kCustomToken,
93+
idpConfigID: kIdpConfigId,
94+
config: auth.requestConfiguration,
95+
useStaging: true
96+
)
97+
98+
let expectedHost = "\(kLocation)-\(kStagingHost)"
99+
let expectedURL = "https://\(expectedHost)/v2alpha/projects/\(kProjectID)" +
100+
"/locations/\(kLocation)/tenants/\(kTenantID)/idpConfigs/\(kIdpConfigId):exchangeOidcToken?key=\(kAPIKey)"
101+
102+
XCTAssertEqual(request.requestURL().absoluteString, expectedURL)
103+
}
104+
105+
func testUnencodedHTTPBodyIsCorrect() {
106+
let (auth, app) = createTestAuthInstance(
107+
projectID: kProjectID,
108+
location: kLocation,
109+
tenantId: kTenantID
110+
)
111+
_ = app
112+
113+
let request = ExchangeTokenRequest(
114+
customToken: kCustomToken,
115+
idpConfigID: kIdpConfigId,
116+
config: auth.requestConfiguration
117+
)
118+
119+
let body = request.unencodedHTTPRequestBody
120+
XCTAssertNotNil(body)
121+
XCTAssertEqual(body?.count, 1)
122+
XCTAssertEqual(body?["custom_token"] as? String, kCustomToken)
123+
}
124+
125+
// MARK: - Helper Function
126+
127+
private func createTestAuthInstance(projectID: String?, location: String?,
128+
tenantId: String?) -> (auth: Auth, app: FirebaseApp) {
129+
let appName = "TestApp-\(UUID().uuidString)"
130+
let options = FirebaseOptions(
131+
googleAppID: "1:1234567890:ios:abcdef123456",
132+
gcmSenderID: "1234567890"
133+
)
134+
options.apiKey = kAPIKey
135+
if let projectID = projectID {
136+
options.projectID = projectID
137+
}
138+
139+
if FirebaseApp.app(name: appName) != nil {
140+
FirebaseApp.app(name: appName)?.delete { _ in }
141+
}
142+
let app = FirebaseApp(instanceWithName: appName, options: options)
143+
144+
let auth = Auth(app: app)
145+
auth.app = app
146+
auth.requestConfiguration.location = location
147+
auth.requestConfiguration.tenantId = tenantId
148+
149+
return (auth, app)
150+
}
151+
}

FirebaseAuth/Sources/Swift/Auth/Auth.swift

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2478,6 +2478,7 @@ public extension Auth {
24782478
/// - completion: A closure that gets called with either an `AuthTokenResult` or an `Error`.
24792479
func exchangeToken(customToken: String,
24802480
idpConfigId: String,
2481+
useStaging: Bool = false,
24812482
completion: @escaping (FirebaseToken?, Error?) -> Void) {
24822483
// Ensure R-GCIP is configured with location and tenant ID
24832484
guard let _ = requestConfiguration.location,
@@ -2493,7 +2494,8 @@ public extension Auth {
24932494
let request = ExchangeTokenRequest(
24942495
customToken: customToken,
24952496
idpConfigID: idpConfigId,
2496-
config: requestConfiguration
2497+
config: requestConfiguration,
2498+
useStaging: true
24972499
)
24982500
Task {
24992501
do {
@@ -2523,7 +2525,8 @@ public extension Auth {
25232525
/// - Returns: An `AuthTokenResult` containing the Firebase ID token and its expiration details.
25242526
/// - Throws: An error if R-GCIP is not configured, if the network call fails,
25252527
/// or if the token parsing fails.
2526-
func exchangeToken(customToken: String, idpConfigId: String) async throws -> FirebaseToken {
2528+
func exchangeToken(customToken: String, idpConfigId: String,
2529+
useStaging: Bool = false) async throws -> FirebaseToken {
25272530
// Ensure R-GCIP is configured with location and tenant ID
25282531
guard let _ = requestConfiguration.location,
25292532
let _ = requestConfiguration.tenantId
@@ -2533,7 +2536,8 @@ public extension Auth {
25332536
let request = ExchangeTokenRequest(
25342537
customToken: customToken,
25352538
idpConfigID: idpConfigId,
2536-
config: requestConfiguration
2539+
config: requestConfiguration,
2540+
useStaging: true
25372541
)
25382542
do {
25392543
let response = try await backend.call(with: request)

FirebaseAuth/Sources/Swift/Backend/IdentityToolkitRequest.swift

Lines changed: 2 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,10 @@ private nonisolated(unsafe) var gAPIHost = "www.googleapis.com"
2525

2626
private let kFirebaseAuthAPIHost = "www.googleapis.com"
2727
private let kIdentityPlatformAPIHost = "identitytoolkit.googleapis.com"
28-
private let kRegionalGCIPAPIHost = "identityplatform.googleapis.com" // Regional R-GCIP v2 hosts
2928

3029
private let kFirebaseAuthStagingAPIHost = "staging-www.sandbox.googleapis.com"
3130
private let kIdentityPlatformStagingAPIHost =
3231
"staging-identitytoolkit.sandbox.googleapis.com"
33-
private let kRegionalGCIPStagingAPIHost =
34-
"staging-identityplatform.sandbox.googleapis.com" // Regional R-GCIP v2 hosts
3532

3633
/// Represents a request to an identity toolkit endpoint routing either to legacy GCIP or
3734
/// regionalized R-GCIP
@@ -78,25 +75,8 @@ class IdentityToolkitRequest {
7875
let apiHostAndPathPrefix: String
7976
let urlString: String
8077
let emulatorHostAndPort = _requestConfiguration.emulatorHostAndPort
81-
/// R-GCIP
82-
if let location = _requestConfiguration.location,
83-
let tenant = _requestConfiguration.tenantId, // Use tenantId from requestConfiguration
84-
!location.isEmpty,
85-
!tenant.isEmpty {
86-
let projectID = _requestConfiguration.auth?.app?.options.projectID
87-
if useStaging {
88-
apiProtocol = kHttpsProtocol
89-
apiHostAndPathPrefix = kRegionalGCIPStagingAPIHost
90-
} else {
91-
apiProtocol = kHttpsProtocol
92-
apiHostAndPathPrefix = kRegionalGCIPAPIHost
93-
}
94-
urlString =
95-
"\(apiProtocol)//\(apiHostAndPathPrefix)/v2alpha/projects/\(projectID ?? "projectID")"
96-
+ "/locations/\(location)/tenants/\(tenant)/idpConfigs:\(endpoint)?key=\(apiKey)"
97-
}
98-
// legacy gcip existing logic
99-
else if useIdentityPlatform {
78+
// legacy gcip logic
79+
if useIdentityPlatform {
10080
if let emulatorHostAndPort = emulatorHostAndPort {
10181
apiProtocol = kHttpProtocol
10282
apiHostAndPathPrefix = "\(emulatorHostAndPort)/\(kIdentityPlatformAPIHost)"

FirebaseAuth/Sources/Swift/Backend/RPC/ExchangeTokenRequest.swift

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@
1414

1515
import Foundation
1616

17+
private let kRegionalGCIPAPIHost = "identityplatform.googleapis.com"
18+
private let kRegionalGCIPStagingAPIHost = "staging-identityplatform.sandbox.googleapis.com"
19+
1720
/// A request to exchange a third-party OIDC token for a Firebase STS token.
1821
///
1922
/// This structure encapsulates the parameters required to make an API request
@@ -34,38 +37,16 @@ struct ExchangeTokenRequest: AuthRPCRequest {
3437
/// The configuration for the request, holding API key, tenant, etc.
3538
let config: AuthRequestConfiguration
3639

37-
var path: String {
38-
guard let location = config.location,
39-
let tenant = config.tenantId,
40-
let project = config.auth?.app?.options.projectID
41-
else {
42-
fatalError(
43-
"exchangeOidcToken requires `auth.location` & `auth.tenantID`"
44-
)
45-
}
46-
let _: String
47-
if location == "prod-global" {
48-
_ = "identityplatform.googleapis.com"
49-
} else {
50-
_ = "\(location)-identityplatform.googleapis.com"
51-
}
52-
53-
return "/v2alpha/projects/\(project)/locations/\(location)" +
54-
"/tenants/\(tenant)/idpConfigs/\(idpConfigID):exchangeOidcToken"
55-
}
40+
let useStaging: Bool
5641

57-
/// Initializes a new `ExchangeTokenRequest` instance.
58-
///
59-
/// - Parameters:
60-
/// - idpConfigID: The identifier of the OIDC provider configuration.
61-
/// - idToken: The third-party OIDC token to exchange.
62-
/// - config: The configuration for the request.
6342
init(customToken: String,
6443
idpConfigID: String,
65-
config: AuthRequestConfiguration) {
44+
config: AuthRequestConfiguration,
45+
useStaging: Bool = false) {
6646
self.idpConfigID = idpConfigID
6747
self.customToken = customToken
6848
self.config = config
49+
self.useStaging = useStaging
6950
}
7051

7152
/// The unencoded HTTP request body for the API.
@@ -80,20 +61,30 @@ struct ExchangeTokenRequest: AuthRPCRequest {
8061
func requestURL() -> URL {
8162
guard let location = config.location,
8263
let tenant = config.tenantId,
83-
let project = config.auth?.app?.options.projectID
64+
let projectId = config.auth?.app?.options.projectID
8465
else {
8566
fatalError(
86-
"exchangeOidcToken requires `auth.useIdentityPlatform`, `auth.location`, `auth.tenantID` & `projectID`"
67+
"exchangeOidcToken requires `location`, `tenantID` & `projectID` to be configured."
8768
)
8869
}
8970
let host: String
90-
if location == "prod-global" {
91-
host = "identityplatform.googleapis.com"
71+
if useStaging {
72+
if location == "prod-global" {
73+
host = kRegionalGCIPStagingAPIHost
74+
} else {
75+
host = "\(location)-\(kRegionalGCIPStagingAPIHost)"
76+
}
9277
} else {
93-
host = "\(location)-identityplatform.googleapis.com"
78+
if location == "prod-global" {
79+
host = kRegionalGCIPAPIHost
80+
} else {
81+
host = "\(location)-\(kRegionalGCIPAPIHost)"
82+
}
9483
}
95-
let path = "/v2/projects/$\(project)/locations/$\(location)" +
96-
"/tenants/$\(tenant)/idpConfigs/$\(idpConfigID):exchangeOidcToken"
84+
85+
let path = "/v2alpha/projects/\(projectId)/locations/\(location)" +
86+
"/tenants/\(tenant)/idpConfigs/\(idpConfigID):exchangeOidcToken"
87+
9788
guard let url = URL(string: "https://\(host)\(path)?key=\(config.apiKey)") else {
9889
fatalError("Failed to create URL for exchangeOidcToken")
9990
}

FirebaseAuth/Tests/SampleSwift/AuthenticationExample/AppManager.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ class AppManager {
2525
private var otherApp: FirebaseApp
2626
var app: FirebaseApp
2727

28+
let tenantConfig = TenantConfig(tenantId: "tenantId", location: "us-east1")
29+
2830
func auth() -> Auth {
29-
return Auth.auth(app: app)
31+
return Auth.auth(app: app, tenantConfig: tenantConfig)
3032
}
3133

3234
private init() {

FirebaseAuth/Tests/SampleSwift/AuthenticationExample/Models/AuthMenu.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ enum AuthMenu: String {
5353
case phoneEnroll
5454
case totpEnroll
5555
case multifactorUnenroll
56+
case exchangeToken
5657

5758
// More intuitively named getter for `rawValue`.
5859
var id: String { rawValue }
@@ -139,6 +140,9 @@ enum AuthMenu: String {
139140
return "TOTP Enroll"
140141
case .multifactorUnenroll:
141142
return "Multifactor unenroll"
143+
// R-GCIP Exchange Token
144+
case .exchangeToken:
145+
return "Exchange Token"
142146
}
143147
}
144148

@@ -220,6 +224,8 @@ enum AuthMenu: String {
220224
self = .totpEnroll
221225
case "Multifactor unenroll":
222226
self = .multifactorUnenroll
227+
case "Exchange Token":
228+
self = .exchangeToken
223229
default:
224230
return nil
225231
}
@@ -354,9 +360,17 @@ class AuthMenuData: DataSourceProvidable {
354360
return Section(headerDescription: header, items: items)
355361
}
356362

363+
static var exchangeTokenSection: Section {
364+
let header = "Exchange Token [Regionalized]"
365+
let items: [Item] = [
366+
Item(title: AuthMenu.exchangeToken.name),
367+
]
368+
return Section(headerDescription: header, items: items)
369+
}
370+
357371
static let sections: [Section] =
358372
[settingsSection, providerSection, emailPasswordSection, otherSection, recaptchaSection,
359-
customAuthDomainSection, appSection, oobSection, multifactorSection]
373+
customAuthDomainSection, appSection, oobSection, multifactorSection, exchangeTokenSection]
360374

361375
static var authLinkSections: [Section] {
362376
let allItems = [providerSection, emailPasswordSection, otherSection].flatMap { $0.items }

0 commit comments

Comments
 (0)