From 7a3271463ce20b8477890abcd03c1f09215b1853 Mon Sep 17 00:00:00 2001 From: Ricardo Koch Date: Tue, 19 May 2026 15:54:02 -0500 Subject: [PATCH 1/2] =?UTF-8?q?Fix=20#254=20=E2=80=94=20User-Agent=20heade?= =?UTF-8?q?r=20missing=20in=20Release=20builds?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SDKVersion.userAgent is populated as a side effect of evaluating the lazy 'static let' registration in each Version+*.swift. Every dereference site is currently spelled 'assert(SDKVersion.X != nil)' — but Swift's assert(_:) takes its condition as @autoclosure, which the optimizer strips in -O builds. In Release builds the lazies never fire, SDKVersion._userAgent stays the empty string, and every outgoing request goes out with an empty User-Agent header. Replace the 19 'assert(SDKVersion.X != nil)' triggers across 13 files with '_ = SDKVersion.X' so the lazy registration fires regardless of build configuration. Same registration mechanism — only the trigger moves out of an @autoclosure. Adds Tests/AuthFoundationTests/SDKVersionRegistrationTests.swift to pin the contract. Verified to fail in 'swift test -c release' before this change and pass after. --- .../AuthFoundation/Migration/Migration.swift | 2 +- .../AuthFoundation/OAuth2/OAuth2Client.swift | 2 +- .../User Management/Credential.swift | 12 +++---- Sources/BrowserSignin/BrowserSignin.swift | 2 +- .../AuthorizationCodeFlow.swift | 2 +- .../DeviceAuthorizationFlow.swift | 2 +- .../Authentication/JWTAuthorizationFlow.swift | 2 +- .../Authentication/ResourceOwnerFlow.swift | 2 +- .../Authentication/SessionTokenFlow.swift | 2 +- .../Authentication/TokenExchangeFlow.swift | 2 +- .../OAuth2Auth/Logout/SessionLogoutFlow.swift | 2 +- Sources/OktaDirectAuth/DirectAuthFlow.swift | 4 +-- Sources/OktaIdxAuth/InteractionCodeFlow.swift | 2 +- .../SDKVersionRegistrationTests.swift | 36 +++++++++++++++++++ 14 files changed, 55 insertions(+), 19 deletions(-) create mode 100644 Tests/AuthFoundationTests/SDKVersionRegistrationTests.swift diff --git a/Sources/AuthFoundation/Migration/Migration.swift b/Sources/AuthFoundation/Migration/Migration.swift index 96c600603..4ddb8f29f 100644 --- a/Sources/AuthFoundation/Migration/Migration.swift +++ b/Sources/AuthFoundation/Migration/Migration.swift @@ -43,7 +43,7 @@ public final class Migration { nonisolated(unsafe) private(set) var registeredMigrators: [any SDKVersionMigrator] init(migrators: [any SDKVersionMigrator]? = nil) { - assert(SDKVersion.authFoundation != nil) + _ = SDKVersion.authFoundation self.registeredMigrators = migrators ?? Self.defaultMigrators } diff --git a/Sources/AuthFoundation/OAuth2/OAuth2Client.swift b/Sources/AuthFoundation/OAuth2/OAuth2Client.swift index 3b04b83a4..29baf077d 100644 --- a/Sources/AuthFoundation/OAuth2/OAuth2Client.swift +++ b/Sources/AuthFoundation/OAuth2/OAuth2Client.swift @@ -115,7 +115,7 @@ public final class OAuth2Client: UsesDelegateCollection { /// - configuration: The pre-formed configuration for this client. /// - session: Optional URLSession to use for network requests. public init(_ configuration: Configuration, session: (any URLSessionProtocol)? = nil) { - assert(SDKVersion.authFoundation != nil) + _ = SDKVersion.authFoundation // Ensure the time coordinator is properly initialized _ = Date.coordinator diff --git a/Sources/AuthFoundation/User Management/Credential.swift b/Sources/AuthFoundation/User Management/Credential.swift index 026c7cc9e..158dfbc3a 100644 --- a/Sources/AuthFoundation/User Management/Credential.swift +++ b/Sources/AuthFoundation/User Management/Credential.swift @@ -29,14 +29,14 @@ public final class Credential: Equatable, OAuth2ClientDelegate { /// This can be used as a convenience to store a user's token within storage, and to access the user in a safe way. If the user's token isn't stored, this will automatically store the token for later use. public static var `default`: Credential? { get { - assert(SDKVersion.authFoundation != nil) + _ = SDKVersion.authFoundation return CredentialActor.sync { TaskData.coordinator.default } } set { - assert(SDKVersion.authFoundation != nil) + _ = SDKVersion.authFoundation CredentialActor.sync { TaskData.coordinator.default = newValue @@ -46,7 +46,7 @@ public final class Credential: Equatable, OAuth2ClientDelegate { /// Lists all users currently stored within the user's application. public static var allIDs: [String] { - assert(SDKVersion.authFoundation != nil) + _ = SDKVersion.authFoundation return CredentialActor.sync { TaskData.coordinator.allIDs @@ -72,7 +72,7 @@ public final class Credential: Equatable, OAuth2ClientDelegate { /// - authenticationContext: Optional `LAContext` to use when retrieving credentials, on systems that support it. /// - Returns: Credential matching the ID. public static func with(id: String, prompt: String? = nil, authenticationContext: (any TokenAuthenticationContext)? = nil) throws -> Credential? { - assert(SDKVersion.authFoundation != nil) + _ = SDKVersion.authFoundation return try CredentialActor.sync { try TaskData.coordinator.with(id: id, @@ -100,7 +100,7 @@ public final class Credential: Equatable, OAuth2ClientDelegate { /// - authenticationContext: Optional `LAContext` to use when retrieving credentials, on systems that support it. /// - Returns: Collection of credentials that matches the given expression. public static func find(where expression: @Sendable @escaping (Token.Metadata) -> Bool, prompt: String? = nil, authenticationContext: (any TokenAuthenticationContext)? = nil) throws -> [Credential] { - assert(SDKVersion.authFoundation != nil) + _ = SDKVersion.authFoundation return try CredentialActor.sync { try TaskData.coordinator.find(where: expression, @@ -127,7 +127,7 @@ public final class Credential: Equatable, OAuth2ClientDelegate { tags: [String: String] = [:], security options: [Security] = Security.standard ) throws -> Credential { - assert(SDKVersion.authFoundation != nil) + _ = SDKVersion.authFoundation return try CredentialActor.sync { try TaskData.coordinator.store(token: token, tags: tags, security: options) diff --git a/Sources/BrowserSignin/BrowserSignin.swift b/Sources/BrowserSignin/BrowserSignin.swift index 998c6d2eb..9b1ed64dc 100644 --- a/Sources/BrowserSignin/BrowserSignin.swift +++ b/Sources/BrowserSignin/BrowserSignin.swift @@ -311,7 +311,7 @@ public final class BrowserSignin { /// - loginFlow: Authorization code flow instance for signing in to this client. /// - logoutFlow: Session sign out flow to use when signing out from this client. public init(loginFlow: AuthorizationCodeFlow, logoutFlow: SessionLogoutFlow?) { - assert(SDKVersion.browserSignin != nil) + _ = SDKVersion.browserSignin self.signInFlow = loginFlow self.signOutFlow = logoutFlow diff --git a/Sources/OAuth2Auth/Authentication/AuthorizationCodeFlow.swift b/Sources/OAuth2Auth/Authentication/AuthorizationCodeFlow.swift index d03bc28c9..90bfd4b08 100644 --- a/Sources/OAuth2Auth/Authentication/AuthorizationCodeFlow.swift +++ b/Sources/OAuth2Auth/Authentication/AuthorizationCodeFlow.swift @@ -145,7 +145,7 @@ public actor AuthorizationCodeFlow: AuthenticationFlow { init(verifiedClient client: OAuth2Client, additionalParameters: [String: any APIRequestArgument]? = nil) { - assert(SDKVersion.oauth2 != nil) + _ = SDKVersion.oauth2 self.client = client self.additionalParameters = additionalParameters diff --git a/Sources/OAuth2Auth/Authentication/DeviceAuthorizationFlow.swift b/Sources/OAuth2Auth/Authentication/DeviceAuthorizationFlow.swift index 3c374ff1b..46e390071 100644 --- a/Sources/OAuth2Auth/Authentication/DeviceAuthorizationFlow.swift +++ b/Sources/OAuth2Auth/Authentication/DeviceAuthorizationFlow.swift @@ -121,7 +121,7 @@ public actor DeviceAuthorizationFlow: AuthenticationFlow { /// - additionalParameters: Optional additional query string parameters you would like to supply to the authorization server. public init(client: OAuth2Client, additionalParameters: [String: any APIRequestArgument]? = nil) { - assert(SDKVersion.oauth2 != nil) + _ = SDKVersion.oauth2 self.client = client self.additionalParameters = additionalParameters diff --git a/Sources/OAuth2Auth/Authentication/JWTAuthorizationFlow.swift b/Sources/OAuth2Auth/Authentication/JWTAuthorizationFlow.swift index 4f8871205..db81f92a1 100644 --- a/Sources/OAuth2Auth/Authentication/JWTAuthorizationFlow.swift +++ b/Sources/OAuth2Auth/Authentication/JWTAuthorizationFlow.swift @@ -76,7 +76,7 @@ public actor JWTAuthorizationFlow: AuthenticationFlow { public init(client: OAuth2Client, additionalParameters: [String: any APIRequestArgument]? = nil) { - assert(SDKVersion.oauth2 != nil) + _ = SDKVersion.oauth2 self.client = client self.additionalParameters = additionalParameters diff --git a/Sources/OAuth2Auth/Authentication/ResourceOwnerFlow.swift b/Sources/OAuth2Auth/Authentication/ResourceOwnerFlow.swift index 5fb502000..e34d52eee 100644 --- a/Sources/OAuth2Auth/Authentication/ResourceOwnerFlow.swift +++ b/Sources/OAuth2Auth/Authentication/ResourceOwnerFlow.swift @@ -79,7 +79,7 @@ public actor ResourceOwnerFlow: AuthenticationFlow { public init(client: OAuth2Client, additionalParameters: [String: any APIRequestArgument]? = nil) { - assert(SDKVersion.oauth2 != nil) + _ = SDKVersion.oauth2 self.client = client self.additionalParameters = additionalParameters diff --git a/Sources/OAuth2Auth/Authentication/SessionTokenFlow.swift b/Sources/OAuth2Auth/Authentication/SessionTokenFlow.swift index 260b21328..5a17bb582 100644 --- a/Sources/OAuth2Auth/Authentication/SessionTokenFlow.swift +++ b/Sources/OAuth2Auth/Authentication/SessionTokenFlow.swift @@ -87,7 +87,7 @@ public actor SessionTokenFlow: AuthenticationFlow { throw OAuth2Error.redirectUriRequired } - assert(SDKVersion.oauth2 != nil) + _ = SDKVersion.oauth2 self.client = client self.additionalParameters = additionalParameters diff --git a/Sources/OAuth2Auth/Authentication/TokenExchangeFlow.swift b/Sources/OAuth2Auth/Authentication/TokenExchangeFlow.swift index 213f15106..adba95bd2 100644 --- a/Sources/OAuth2Auth/Authentication/TokenExchangeFlow.swift +++ b/Sources/OAuth2Auth/Authentication/TokenExchangeFlow.swift @@ -102,7 +102,7 @@ public actor TokenExchangeFlow: AuthenticationFlow { public init(client: OAuth2Client, additionalParameters: [String: any APIRequestArgument]? = nil) { - assert(SDKVersion.oauth2 != nil) + _ = SDKVersion.oauth2 self.client = client self.additionalParameters = additionalParameters diff --git a/Sources/OAuth2Auth/Logout/SessionLogoutFlow.swift b/Sources/OAuth2Auth/Logout/SessionLogoutFlow.swift index 1070b39a9..4793d1b53 100644 --- a/Sources/OAuth2Auth/Logout/SessionLogoutFlow.swift +++ b/Sources/OAuth2Auth/Logout/SessionLogoutFlow.swift @@ -104,7 +104,7 @@ public actor SessionLogoutFlow: LogoutFlow { public init(client: OAuth2Client, additionalParameters: [String: any APIRequestArgument]? = nil) { - assert(SDKVersion.oauth2 != nil) + _ = SDKVersion.oauth2 self.client = client self.additionalParameters = additionalParameters diff --git a/Sources/OktaDirectAuth/DirectAuthFlow.swift b/Sources/OktaDirectAuth/DirectAuthFlow.swift index 7d0013f7f..5a7bb5668 100644 --- a/Sources/OktaDirectAuth/DirectAuthFlow.swift +++ b/Sources/OktaDirectAuth/DirectAuthFlow.swift @@ -369,7 +369,7 @@ public actor DirectAuthenticationFlow: AuthenticationFlow { supportedGrants grantTypes: [GrantType] = .directAuth, additionalParameters: [String: any APIRequestArgument]? = nil) { - assert(SDKVersion.directAuth != nil) + _ = SDKVersion.directAuth self.client = client self.supportedGrantTypes = grantTypes @@ -379,7 +379,7 @@ public actor DirectAuthenticationFlow: AuthenticationFlow { } public init(client: OAuth2Client, additionalParameters: [String: any APIRequestArgument]?) throws { - assert(SDKVersion.directAuth != nil) + _ = SDKVersion.directAuth self.client = client self.supportedGrantTypes = .directAuth diff --git a/Sources/OktaIdxAuth/InteractionCodeFlow.swift b/Sources/OktaIdxAuth/InteractionCodeFlow.swift index 6acabed9c..296b9e18f 100644 --- a/Sources/OktaIdxAuth/InteractionCodeFlow.swift +++ b/Sources/OktaIdxAuth/InteractionCodeFlow.swift @@ -101,7 +101,7 @@ public actor InteractionCodeFlow: AuthenticationFlow { init(verifiedClient client: OAuth2Client, additionalParameters: [String: any APIRequestArgument]? = nil) { - assert(SDKVersion.oktaIdx != nil) + _ = SDKVersion.oktaIdx self.client = client self.additionalParameters = additionalParameters diff --git a/Tests/AuthFoundationTests/SDKVersionRegistrationTests.swift b/Tests/AuthFoundationTests/SDKVersionRegistrationTests.swift new file mode 100644 index 000000000..0664587a4 --- /dev/null +++ b/Tests/AuthFoundationTests/SDKVersionRegistrationTests.swift @@ -0,0 +1,36 @@ +// +// Copyright (c) 2026-Present, Okta, Inc. and/or its affiliates. All rights reserved. +// The Okta software accompanied by this notice is provided pursuant to the Apache License, Version 2.0 (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 XCTest +@testable import AuthFoundation + +final class SDKVersionRegistrationTests: XCTestCase { + /// Regression test for #254 — User-Agent header missing in Release builds. + /// + /// `SDKVersion.userAgent` is populated as a side effect of evaluating the + /// lazy `static let` registration in each `Version+*.swift`. That evaluation + /// must run in every build configuration. This test asserts the contract + /// after constructing an `OAuth2Client`. + func testOAuth2ClientInitPopulatesUserAgent() throws { + let client = OAuth2Client( + issuerURL: try XCTUnwrap(URL(string: "https://example.okta.com/oauth2/default")), + clientId: "test-client-id", + scope: ["openid"] + ) + _ = client + + XCTAssertFalse(SDKVersion.userAgent.isEmpty, + "User-Agent must be populated after OAuth2Client init in any build configuration.") + XCTAssertTrue(SDKVersion.userAgent.contains("okta-authfoundation-swift/"), + "User-Agent must include the AuthFoundation SDK marker; got '\(SDKVersion.userAgent)'.") + } +} From ab266ef27ce0111454534f6d32f66e3a6314be84 Mon Sep 17 00:00:00 2001 From: Alex Nachbaur Date: Thu, 2 Jul 2026 11:51:17 -0700 Subject: [PATCH 2/2] Trim regression-test doc comment to match repo style --- Tests/AuthFoundationTests/SDKVersionRegistrationTests.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Tests/AuthFoundationTests/SDKVersionRegistrationTests.swift b/Tests/AuthFoundationTests/SDKVersionRegistrationTests.swift index 0664587a4..64e9affa1 100644 --- a/Tests/AuthFoundationTests/SDKVersionRegistrationTests.swift +++ b/Tests/AuthFoundationTests/SDKVersionRegistrationTests.swift @@ -14,12 +14,6 @@ import XCTest @testable import AuthFoundation final class SDKVersionRegistrationTests: XCTestCase { - /// Regression test for #254 — User-Agent header missing in Release builds. - /// - /// `SDKVersion.userAgent` is populated as a side effect of evaluating the - /// lazy `static let` registration in each `Version+*.swift`. That evaluation - /// must run in every build configuration. This test asserts the contract - /// after constructing an `OAuth2Client`. func testOAuth2ClientInitPopulatesUserAgent() throws { let client = OAuth2Client( issuerURL: try XCTUnwrap(URL(string: "https://example.okta.com/oauth2/default")),