From 644528f358c039285909601826fd89aa94ac31e6 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:56:06 -0700 Subject: [PATCH 01/14] chore(example): commit empty SwiftUI SPM sample scaffold Scaffold for Examples/Example_Swift-SPM: Xcode project (bundle ID net.openid.appauth.Example, no Example_Extension target), asset catalog, and placeholder ExampleApp.swift/ContentView.swift generated by Xcode. Orchestration will fill these in with AppAuth integration. --- .../Example.xcodeproj/project.pbxproj | 333 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../Example/Assets.xcassets/Contents.json | 6 + .../Example/ContentView.swift | 24 ++ .../Example/ExampleApp.swift | 17 + 7 files changed, 433 insertions(+) create mode 100644 Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj create mode 100644 Examples/Example_Swift-SPM/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 Examples/Example_Swift-SPM/Example/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 Examples/Example_Swift-SPM/Example/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 Examples/Example_Swift-SPM/Example/Assets.xcassets/Contents.json create mode 100644 Examples/Example_Swift-SPM/Example/ContentView.swift create mode 100644 Examples/Example_Swift-SPM/Example/ExampleApp.swift diff --git a/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj b/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj new file mode 100644 index 000000000..84a0c1702 --- /dev/null +++ b/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj @@ -0,0 +1,333 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + 09BB0B542F91B157004C0D4B /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 09BB0B562F91B157004C0D4B /* Example */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Example; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 09BB0B512F91B157004C0D4B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 09BB0B4B2F91B157004C0D4B = { + isa = PBXGroup; + children = ( + 09BB0B562F91B157004C0D4B /* Example */, + 09BB0B552F91B157004C0D4B /* Products */, + ); + sourceTree = ""; + }; + 09BB0B552F91B157004C0D4B /* Products */ = { + isa = PBXGroup; + children = ( + 09BB0B542F91B157004C0D4B /* Example.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 09BB0B532F91B157004C0D4B /* Example */ = { + isa = PBXNativeTarget; + buildConfigurationList = 09BB0B5F2F91B158004C0D4B /* Build configuration list for PBXNativeTarget "Example" */; + buildPhases = ( + 09BB0B502F91B157004C0D4B /* Sources */, + 09BB0B512F91B157004C0D4B /* Frameworks */, + 09BB0B522F91B157004C0D4B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 09BB0B562F91B157004C0D4B /* Example */, + ); + name = Example; + packageProductDependencies = ( + ); + productName = Example; + productReference = 09BB0B542F91B157004C0D4B /* Example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 09BB0B4C2F91B157004C0D4B /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2620; + LastUpgradeCheck = 2620; + TargetAttributes = { + 09BB0B532F91B157004C0D4B = { + CreatedOnToolsVersion = 26.2; + }; + }; + }; + buildConfigurationList = 09BB0B4F2F91B157004C0D4B /* Build configuration list for PBXProject "Example" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 09BB0B4B2F91B157004C0D4B; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 09BB0B552F91B157004C0D4B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 09BB0B532F91B157004C0D4B /* Example */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 09BB0B522F91B157004C0D4B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 09BB0B502F91B157004C0D4B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 09BB0B5D2F91B158004C0D4B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 09BB0B5E2F91B158004C0D4B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 09BB0B602F91B158004C0D4B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.openid.appauth.Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 09BB0B612F91B158004C0D4B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = net.openid.appauth.Example; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 09BB0B4F2F91B157004C0D4B /* Build configuration list for PBXProject "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 09BB0B5D2F91B158004C0D4B /* Debug */, + 09BB0B5E2F91B158004C0D4B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 09BB0B5F2F91B158004C0D4B /* Build configuration list for PBXNativeTarget "Example" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 09BB0B602F91B158004C0D4B /* Debug */, + 09BB0B612F91B158004C0D4B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 09BB0B4C2F91B157004C0D4B /* Project object */; +} diff --git a/Examples/Example_Swift-SPM/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/Examples/Example_Swift-SPM/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 000000000..919434a62 --- /dev/null +++ b/Examples/Example_Swift-SPM/Example.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/Examples/Example_Swift-SPM/Example/Assets.xcassets/AccentColor.colorset/Contents.json b/Examples/Example_Swift-SPM/Example/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 000000000..eb8789700 --- /dev/null +++ b/Examples/Example_Swift-SPM/Example/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Example_Swift-SPM/Example/Assets.xcassets/AppIcon.appiconset/Contents.json b/Examples/Example_Swift-SPM/Example/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 000000000..230588010 --- /dev/null +++ b/Examples/Example_Swift-SPM/Example/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Example_Swift-SPM/Example/Assets.xcassets/Contents.json b/Examples/Example_Swift-SPM/Example/Assets.xcassets/Contents.json new file mode 100644 index 000000000..73c00596a --- /dev/null +++ b/Examples/Example_Swift-SPM/Example/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Examples/Example_Swift-SPM/Example/ContentView.swift b/Examples/Example_Swift-SPM/Example/ContentView.swift new file mode 100644 index 000000000..376fefe03 --- /dev/null +++ b/Examples/Example_Swift-SPM/Example/ContentView.swift @@ -0,0 +1,24 @@ +// +// ContentView.swift +// Example +// +// Created by Worthing ~ on 4/16/26. +// + +import SwiftUI + +struct ContentView: View { + var body: some View { + VStack { + Image(systemName: "globe") + .imageScale(.large) + .foregroundStyle(.tint) + Text("Hello, world!") + } + .padding() + } +} + +#Preview { + ContentView() +} diff --git a/Examples/Example_Swift-SPM/Example/ExampleApp.swift b/Examples/Example_Swift-SPM/Example/ExampleApp.swift new file mode 100644 index 000000000..4a4ed1539 --- /dev/null +++ b/Examples/Example_Swift-SPM/Example/ExampleApp.swift @@ -0,0 +1,17 @@ +// +// ExampleApp.swift +// Example +// +// Created by Worthing ~ on 4/16/26. +// + +import SwiftUI + +@main +struct ExampleApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} From 15586050ae895e678c4143b6973ff0f4a3adf5b0 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:58:35 -0700 Subject: [PATCH 02/14] g-orchestrated: add AppDelegate for SwiftUI SPM sample --- .../Example/AppDelegate.swift | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Examples/Example_Swift-SPM/Example/AppDelegate.swift diff --git a/Examples/Example_Swift-SPM/Example/AppDelegate.swift b/Examples/Example_Swift-SPM/Example/AppDelegate.swift new file mode 100644 index 000000000..a1838b2cd --- /dev/null +++ b/Examples/Example_Swift-SPM/Example/AppDelegate.swift @@ -0,0 +1,34 @@ +// +// AppDelegate.swift +// +// Copyright (c) 2026 The AppAuth Authors. +// +// 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 UIKit +import AppAuth + +class AppDelegate: NSObject, UIApplicationDelegate { + + var currentAuthorizationFlow: OIDExternalUserAgentSession? + + func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool { + if let authorizationFlow = self.currentAuthorizationFlow, authorizationFlow.resumeExternalUserAgentFlow(with: url) { + self.currentAuthorizationFlow = nil + return true + } + + return false + } +} From 3f1b3e6f78521622e6c16e2064d509da3d75fb22 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 16 Apr 2026 17:59:44 -0700 Subject: [PATCH 03/14] g-orchestrated: build SwiftUI view with auth/exchange/userinfo/clear buttons and log panel --- .../Example/ContentView.swift | 98 ++++++++++++++++--- .../Example/ExampleApp.swift | 22 ++++- 2 files changed, 106 insertions(+), 14 deletions(-) diff --git a/Examples/Example_Swift-SPM/Example/ContentView.swift b/Examples/Example_Swift-SPM/Example/ContentView.swift index 376fefe03..7ddcbcac4 100644 --- a/Examples/Example_Swift-SPM/Example/ContentView.swift +++ b/Examples/Example_Swift-SPM/Example/ContentView.swift @@ -1,24 +1,98 @@ // // ContentView.swift -// Example // -// Created by Worthing ~ on 4/16/26. +// Copyright (c) 2026 The AppAuth Authors. +// +// 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 SwiftUI struct ContentView: View { + @EnvironmentObject var authManager: AuthManager + var body: some View { - VStack { - Image(systemName: "globe") - .imageScale(.large) - .foregroundStyle(.tint) - Text("Hello, world!") + NavigationStack { + VStack(spacing: 12) { + HStack { + Button(authManager.hasAuthState ? "Re-Auth (Auto)" : "Auto") { + authManager.authWithAutoCodeExchange() + } + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + + Button(authManager.hasAuthState ? "Re-Auth (Manual)" : "Manual") { + authManager.authNoCodeExchange() + } + .buttonStyle(.borderedProminent) + .frame(maxWidth: .infinity) + } + + HStack { + Button("Code Exchange") { + authManager.codeExchange() + } + .buttonStyle(.borderedProminent) + .disabled(!authManager.hasAuthorizationCode) + .frame(maxWidth: .infinity) + + Button("User Info") { + authManager.userinfo() + } + .buttonStyle(.borderedProminent) + .disabled(!authManager.isAuthorized) + .frame(maxWidth: .infinity) + } + + ScrollViewReader { proxy in + ScrollView { + Text(authManager.logText) + .frame(maxWidth: .infinity, alignment: .leading) + .font(.system(.caption, design: .monospaced)) + .textSelection(.enabled) + .padding(8) + .id("bottom") + } + .overlay( + RoundedRectangle(cornerRadius: 6) + .stroke(Color.secondary.opacity(0.4)) + ) + .onChange(of: authManager.logText) { _ in + proxy.scrollTo("bottom", anchor: .bottom) + } + } + } + .padding() + .navigationTitle("AppAuth Example") + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Menu { + Button(role: .destructive) { + authManager.clearAuthState() + } label: { + Text("Clear OAuth State") + } + + Button { + authManager.clearLogs() + } label: { + Text("Clear Logs") + } + } label: { + Image(systemName: "trash") + } + } + } } - .padding() } } - -#Preview { - ContentView() -} diff --git a/Examples/Example_Swift-SPM/Example/ExampleApp.swift b/Examples/Example_Swift-SPM/Example/ExampleApp.swift index 4a4ed1539..462e81fa2 100644 --- a/Examples/Example_Swift-SPM/Example/ExampleApp.swift +++ b/Examples/Example_Swift-SPM/Example/ExampleApp.swift @@ -1,17 +1,35 @@ // // ExampleApp.swift -// Example // -// Created by Worthing ~ on 4/16/26. +// Copyright (c) 2026 The AppAuth Authors. +// +// 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 SwiftUI @main struct ExampleApp: App { + @UIApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate + @StateObject private var authManager = AuthManager() + var body: some Scene { WindowGroup { ContentView() + .environmentObject(authManager) + .onAppear { + authManager.appDelegate = appDelegate + } } } } From 5e3602fa45e55583102481dec6c64ddf4ee641ab Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:00:12 -0700 Subject: [PATCH 04/14] g-orchestrated: add AuthManager porting Carthage auth flow to ObservableObject --- .../Example/AuthManager.swift | 424 ++++++++++++++++++ 1 file changed, 424 insertions(+) create mode 100644 Examples/Example_Swift-SPM/Example/AuthManager.swift diff --git a/Examples/Example_Swift-SPM/Example/AuthManager.swift b/Examples/Example_Swift-SPM/Example/AuthManager.swift new file mode 100644 index 000000000..8cf5cbc88 --- /dev/null +++ b/Examples/Example_Swift-SPM/Example/AuthManager.swift @@ -0,0 +1,424 @@ +// +// AuthManager.swift +// +// Copyright (c) 2026 The AppAuth Authors. +// +// 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 AppAuth +import SwiftUI +import UIKit + +typealias PostRegistrationCallback = (OIDServiceConfiguration?, OIDRegistrationResponse?) -> Void + +let kIssuer: String = "https://issuer.example.com" +let kClientID: String? = "YOUR_CLIENT_ID" +let kRedirectURI: String = "com.example.app:/oauth2redirect/example-provider" +let kAppAuthExampleAuthStateKey: String = "authState" + +final class AuthManager: NSObject, ObservableObject { + @Published private(set) var authState: OIDAuthState? + @Published private(set) var logText: String = "" + + var isAuthorized: Bool { authState?.isAuthorized ?? false } + var hasAuthorizationCode: Bool { authState?.lastAuthorizationResponse.authorizationCode != nil && authState?.lastTokenResponse == nil } + var hasAuthState: Bool { authState != nil } + + weak var appDelegate: AppDelegate? + + override init() { + super.init() + self.loadState() + } + + // MARK: Public Methods + + func authWithAutoCodeExchange() { + guard let issuer = URL(string: kIssuer) else { + self.logMessage("Error creating URL for : \(kIssuer)") + return + } + + self.logMessage("Fetching configuration for issuer: \(issuer)") + + // discovers endpoints + OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { configuration, error in + guard let config = configuration else { + self.logMessage("Error retrieving discovery document: \(error?.localizedDescription ?? "DEFAULT_ERROR")") + self.setAuthState(nil) + return + } + + self.logMessage("Got configuration: \(config)") + + if let clientId = kClientID { + self.doAuthWithAutoCodeExchange(configuration: config, clientID: clientId, clientSecret: nil) + } else { + self.doClientRegistration(configuration: config) { configuration, response in + guard let configuration = configuration, let clientID = response?.clientID else { + self.logMessage("Error retrieving configuration OR clientID") + return + } + + self.doAuthWithAutoCodeExchange(configuration: configuration, + clientID: clientID, + clientSecret: response?.clientSecret) + } + } + } + } + + func authNoCodeExchange() { + guard let issuer = URL(string: kIssuer) else { + self.logMessage("Error creating URL for : \(kIssuer)") + return + } + + self.logMessage("Fetching configuration for issuer: \(issuer)") + + OIDAuthorizationService.discoverConfiguration(forIssuer: issuer) { configuration, error in + if let error = error { + self.logMessage("Error retrieving discovery document: \(error.localizedDescription)") + return + } + + guard let configuration = configuration else { + self.logMessage("Error retrieving discovery document. Error & Configuration both are NIL!") + return + } + + self.logMessage("Got configuration: \(configuration)") + + if let clientId = kClientID { + self.doAuthWithoutCodeExchange(configuration: configuration, clientID: clientId, clientSecret: nil) + } else { + self.doClientRegistration(configuration: configuration) { configuration, response in + guard let configuration = configuration, let response = response else { + return + } + + self.doAuthWithoutCodeExchange(configuration: configuration, + clientID: response.clientID, + clientSecret: response.clientSecret) + } + } + } + } + + func codeExchange() { + guard let tokenExchangeRequest = self.authState?.lastAuthorizationResponse.tokenExchangeRequest() else { + self.logMessage("Error creating authorization code exchange request") + return + } + + self.logMessage("Performing authorization code exchange with request \(tokenExchangeRequest)") + + OIDAuthorizationService.perform(tokenExchangeRequest) { response, error in + if let tokenResponse = response { + self.logMessage("Received token response with accessToken: \(tokenResponse.accessToken ?? "DEFAULT_TOKEN")") + } else { + self.logMessage("Token exchange error: \(error?.localizedDescription ?? "DEFAULT_ERROR")") + } + self.authState?.update(with: response, error: error) + } + } + + func userinfo() { + guard let userinfoEndpoint = self.authState?.lastAuthorizationResponse.request.configuration.discoveryDocument?.userinfoEndpoint else { + self.logMessage("Userinfo endpoint not declared in discovery document") + return + } + + self.logMessage("Performing userinfo request") + + let currentAccessToken: String? = self.authState?.lastTokenResponse?.accessToken + + self.authState?.performAction() { (accessToken, idToken, error) in + if error != nil { + self.logMessage("Error fetching fresh tokens: \(error?.localizedDescription ?? "ERROR")") + return + } + + guard let accessToken = accessToken else { + self.logMessage("Error getting accessToken") + return + } + + if currentAccessToken != accessToken { + self.logMessage("Access token was refreshed automatically (\(currentAccessToken ?? "CURRENT_ACCESS_TOKEN") to \(accessToken))") + } else { + self.logMessage("Access token was fresh and not updated \(accessToken)") + } + + var urlRequest = URLRequest(url: userinfoEndpoint) + urlRequest.allHTTPHeaderFields = ["Authorization":"Bearer \(accessToken)"] + + let task = URLSession.shared.dataTask(with: urlRequest) { data, response, error in + DispatchQueue.main.async { + guard error == nil else { + self.logMessage("HTTP request failed \(error?.localizedDescription ?? "ERROR")") + return + } + + guard let response = response as? HTTPURLResponse else { + self.logMessage("Non-HTTP response") + return + } + + guard let data = data else { + self.logMessage("HTTP response data is empty") + return + } + + var json: [AnyHashable: Any]? + + do { + json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] + } catch { + self.logMessage("JSON Serialization Error") + } + + if response.statusCode != 200 { + // server replied with an error + let responseText: String? = String(data: data, encoding: String.Encoding.utf8) + + if response.statusCode == 401 { + // "401 Unauthorized" generally indicates there is an issue with the authorization + // grant. Puts OIDAuthState into an error state. + let oauthError = OIDErrorUtilities.resourceServerAuthorizationError(withCode: 0, + errorResponse: json, + underlyingError: error) + self.authState?.update(withAuthorizationError: oauthError) + self.logMessage("Authorization Error (\(oauthError)). Response: \(responseText ?? "RESPONSE_TEXT")") + } else { + self.logMessage("HTTP: \(response.statusCode), Response: \(responseText ?? "RESPONSE_TEXT")") + } + + return + } + + if let json = json { + self.logMessage("Success: \(json)") + } + } + } + + task.resume() + } + } + + func clearAuthState() { + setAuthState(nil) + } + + func clearLogs() { + DispatchQueue.main.async { + self.logText = "" + } + } + + // MARK: Private Methods + + private func doClientRegistration(configuration: OIDServiceConfiguration, callback: @escaping PostRegistrationCallback) { + guard let redirectURI = URL(string: kRedirectURI) else { + self.logMessage("Error creating URL for : \(kRedirectURI)") + return + } + + let request: OIDRegistrationRequest = OIDRegistrationRequest(configuration: configuration, + redirectURIs: [redirectURI], + responseTypes: nil, + grantTypes: nil, + subjectType: nil, + tokenEndpointAuthMethod: "client_secret_post", + additionalParameters: nil, + additionalHeaders: nil) + + // performs registration request + self.logMessage("Initiating registration request") + + OIDAuthorizationService.perform(request) { response, error in + if let regResponse = response { + self.setAuthState(OIDAuthState(registrationResponse: regResponse)) + self.logMessage("Got registration response: \(regResponse)") + callback(configuration, regResponse) + } else { + self.logMessage("Registration error: \(error?.localizedDescription ?? "DEFAULT_ERROR")") + self.setAuthState(nil) + } + } + } + + private func doAuthWithAutoCodeExchange(configuration: OIDServiceConfiguration, clientID: String, clientSecret: String?) { + guard let redirectURI = URL(string: kRedirectURI) else { + self.logMessage("Error creating URL for : \(kRedirectURI)") + return + } + + guard let appDelegate = self.appDelegate else { + self.logMessage("Error accessing AppDelegate") + return + } + + guard let presentingVC = self.presentingViewController() else { + return + } + + // builds authentication request + let request = OIDAuthorizationRequest(configuration: configuration, + clientId: clientID, + clientSecret: clientSecret, + scopes: [OIDScopeOpenID, OIDScopeProfile], + redirectURL: redirectURI, + responseType: OIDResponseTypeCode, + additionalParameters: nil) + + // performs authentication request + logMessage("Initiating authorization request with scope: \(request.scope ?? "DEFAULT_SCOPE")") + + appDelegate.currentAuthorizationFlow = OIDAuthState.authState(byPresenting: request, presenting: presentingVC) { authState, error in + if let authState = authState { + self.setAuthState(authState) + self.logMessage("Got authorization tokens. Access token: \(authState.lastTokenResponse?.accessToken ?? "DEFAULT_TOKEN")") + } else { + self.logMessage("Authorization error: \(error?.localizedDescription ?? "DEFAULT_ERROR")") + self.setAuthState(nil) + } + } + } + + private func doAuthWithoutCodeExchange(configuration: OIDServiceConfiguration, clientID: String, clientSecret: String?) { + guard let redirectURI = URL(string: kRedirectURI) else { + self.logMessage("Error creating URL for : \(kRedirectURI)") + return + } + + guard let appDelegate = self.appDelegate else { + self.logMessage("Error accessing AppDelegate") + return + } + + guard let presentingVC = self.presentingViewController() else { + return + } + + // builds authentication request + let request = OIDAuthorizationRequest(configuration: configuration, + clientId: clientID, + clientSecret: clientSecret, + scopes: [OIDScopeOpenID, OIDScopeProfile], + redirectURL: redirectURI, + responseType: OIDResponseTypeCode, + additionalParameters: nil) + + // performs authentication request + logMessage("Initiating authorization request with scope: \(request.scope ?? "DEFAULT_SCOPE")") + + appDelegate.currentAuthorizationFlow = OIDAuthorizationService.present(request, presenting: presentingVC) { (response, error) in + if let response = response { + let authState = OIDAuthState(authorizationResponse: response) + self.setAuthState(authState) + self.logMessage("Authorization response with code: \(response.authorizationCode ?? "DEFAULT_CODE")") + // could just call [self tokenExchange:nil] directly, but will let the user initiate it. + } else { + self.logMessage("Authorization error: \(error?.localizedDescription ?? "DEFAULT_ERROR")") + } + } + } + + private func setAuthState(_ authState: OIDAuthState?) { + if (self.authState == authState) { + return + } + self.authState = authState + self.authState?.stateChangeDelegate = self + self.authState?.errorDelegate = self + self.stateChanged() + } + + private func stateChanged() { + self.saveState() + } + + private func saveState() { + var data: Data? = nil + + if let authState = self.authState { + do { + data = try NSKeyedArchiver.archivedData(withRootObject: authState, requiringSecureCoding: true) + } catch { + logMessage("Error archiving authState: \(error.localizedDescription)") + return + } + } + + UserDefaults.standard.set(data, forKey: kAppAuthExampleAuthStateKey) + } + + private func loadState() { + guard let data = UserDefaults.standard.data(forKey: kAppAuthExampleAuthStateKey) else { + return + } + + do { + if let authState = try NSKeyedUnarchiver.unarchivedObject(ofClass: OIDAuthState.self, from: data) { + self.setAuthState(authState) + } + } catch { + logMessage("Error unarchiving authState: \(error.localizedDescription)") + } + } + + private func logMessage(_ message: String?) { + guard let message = message else { + return + } + + print(message) + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "hh:mm:ss" + let dateString = dateFormatter.string(from: Date()) + + // appends to output log + DispatchQueue.main.async { + let logText = "\(self.logText)\n\(dateString): \(message)" + self.logText = logText + } + } + + private func presentingViewController() -> UIViewController? { + let viewController = UIApplication.shared.connectedScenes + .compactMap { $0 as? UIWindowScene } + .first?.windows + .first(where: { $0.isKeyWindow })? + .rootViewController + if viewController == nil { + logMessage("Error: no presenting view controller available") + } + return viewController + } +} + +// MARK: OIDAuthState Delegate + +extension AuthManager: OIDAuthStateChangeDelegate, OIDAuthStateErrorDelegate { + func didChange(_ state: OIDAuthState) { + self.stateChanged() + } + + func authState(_ state: OIDAuthState, didEncounterAuthorizationError error: Error) { + self.logMessage("Received authorization error: \(error)") + } +} From e9543efe83ee130f80a04b5041ae3b4d9e6719f2 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:02:56 -0700 Subject: [PATCH 05/14] g-orchestrated: add Info.plist registering com.example.app redirect URL scheme --- Examples/Example_Swift-SPM/Example/Info.plist | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 Examples/Example_Swift-SPM/Example/Info.plist diff --git a/Examples/Example_Swift-SPM/Example/Info.plist b/Examples/Example_Swift-SPM/Example/Info.plist new file mode 100644 index 000000000..39f750a49 --- /dev/null +++ b/Examples/Example_Swift-SPM/Example/Info.plist @@ -0,0 +1,57 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + $(PRODUCT_BUNDLE_PACKAGE_TYPE) + CFBundleShortVersionString + $(MARKETING_VERSION) + CFBundleURLTypes + + + CFBundleTypeRole + Editor + CFBundleURLSchemes + + com.example.app + + + + CFBundleVersion + $(CURRENT_PROJECT_VERSION) + LSRequiresIPhoneOS + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + From 7e63cf7a20052856e3f8cbe94a4407b42608e794 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:04:21 -0700 Subject: [PATCH 06/14] g-orchestrated: wire local AppAuth SPM dependency and Info.plist into xcodeproj --- .../Example.xcodeproj/project.pbxproj | 52 ++++++++++++++----- 1 file changed, 40 insertions(+), 12 deletions(-) diff --git a/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj b/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj index 84a0c1702..c6ee70e72 100644 --- a/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj +++ b/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj @@ -6,6 +6,10 @@ objectVersion = 77; objects = { +/* Begin PBXBuildFile section */ + 09BB0F002F91B157004C0D4B /* AppAuth in Frameworks */ = {isa = PBXBuildFile; productRef = 09BB0E002F91B157004C0D4B /* AppAuth */; }; +/* End PBXBuildFile section */ + /* Begin PBXFileReference section */ 09BB0B542F91B157004C0D4B /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -13,16 +17,30 @@ /* Begin PBXFileSystemSynchronizedRootGroup section */ 09BB0B562F91B157004C0D4B /* Example */ = { isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + 09BB0C002F91B157004C0D4B /* Example */, + ); path = Example; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 09BB0C002F91B157004C0D4B /* Example */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 09BB0B532F91B157004C0D4B /* Example */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFrameworksBuildPhase section */ 09BB0B512F91B157004C0D4B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 09BB0F002F91B157004C0D4B /* AppAuth in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -65,6 +83,7 @@ ); name = Example; packageProductDependencies = ( + 09BB0E002F91B157004C0D4B /* AppAuth */, ); productName = Example; productReference = 09BB0B542F91B157004C0D4B /* Example.app */; @@ -94,6 +113,9 @@ ); mainGroup = 09BB0B4B2F91B157004C0D4B; minimizedProjectReferenceProxies = 1; + packageReferences = ( + 09BB0D002F91B157004C0D4B /* AppAuth */, + ); preferredProjectObjectVersion = 77; productRefGroup = 09BB0B552F91B157004C0D4B /* Products */; projectDirPath = ""; @@ -252,12 +274,8 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Example/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -283,12 +301,8 @@ CODE_SIGN_STYLE = Automatic; CURRENT_PROJECT_VERSION = 1; ENABLE_PREVIEWS = YES; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; - INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; - INFOPLIST_KEY_UILaunchScreen_Generation = YES; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; - INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Example/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -328,6 +342,20 @@ defaultConfigurationName = Release; }; /* End XCConfigurationList section */ + +/* Begin XCLocalSwiftPackageReference section */ + 09BB0D002F91B157004C0D4B /* AppAuth */ = { + isa = XCLocalSwiftPackageReference; + relativePath = "../.."; + }; +/* End XCLocalSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + 09BB0E002F91B157004C0D4B /* AppAuth */ = { + isa = XCSwiftPackageProductDependency; + productName = AppAuth; + }; +/* End XCSwiftPackageProductDependency section */ }; rootObject = 09BB0B4C2F91B157004C0D4B /* Project object */; } From c7c7188f91e15b76b21c4be1954693223bb5eba7 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:04:46 -0700 Subject: [PATCH 07/14] g-orchestrated: changelog entry for SwiftUI SPM example --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d86a0f705..d2a92ce23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# Unreleased +- Add SwiftUI + Swift Package Manager sample app under `Examples/Example_Swift-SPM`. + # 2.0.0 - Raise minimum supported iOS version to iOS 12. ([#918](https://github.com/openid/AppAuth-iOS/pull/918)) - Remove deprecated `[UIApplication openURL:]` method to compile with Xcode 16. ([#911](https://github.com/openid/AppAuth-iOS/pull/911)) From c211ab0a0d56b37b5a711af00abed879c33ff2eb Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:04:52 -0700 Subject: [PATCH 08/14] g-orchestrated: list Swift-Carthage and Swift-SPM examples in Examples/README --- Examples/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/README.md b/Examples/README.md index 820c0d5bb..87f79c43d 100644 --- a/Examples/README.md +++ b/Examples/README.md @@ -10,6 +10,8 @@ Each example has docs on how to configure: * [Example for iOS (Objective-C)](Example-iOS_ObjC/README.md) * [Example for iOS w/ Carthage (Objective-C)](Example-iOS_ObjC-Carthage/README.md) +* [Example for iOS w/ Carthage (Swift)](Example-iOS_Swift-Carthage/README.md) +* [Example for iOS w/ SPM (SwiftUI)](Example_Swift-SPM/README.md) * [Example for macOS](Example-macOS/README.md) * [Example for tvOS](Example-tvOS/README.md) From 924a7153807a7089688b7aa4ff8fec84bc0ed6a5 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:05:31 -0700 Subject: [PATCH 09/14] g-orchestrated: add README for SwiftUI + SPM sample --- Examples/Example_Swift-SPM/README.md | 45 ++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 Examples/Example_Swift-SPM/README.md diff --git a/Examples/Example_Swift-SPM/README.md b/Examples/Example_Swift-SPM/README.md new file mode 100644 index 000000000..7542642dd --- /dev/null +++ b/Examples/Example_Swift-SPM/README.md @@ -0,0 +1,45 @@ +# Example Project (SwiftUI + Swift Package Manager) + +## Setup & Open the Project + +This example uses the local AppAuth Swift Package, which is referenced via a relative path (`../..`). Xcode resolves this automatically and no `carthage bootstrap` is needed. + +To get started, just open the `Example.xcodeproj` file. + +## Configuration + +The example doesn't work out of the box, you need to configure it with your own +client ID. + +### Information You'll Need + +* Issuer +* Client ID +* Redirect URI + +How to get this information varies by IdP, but we have +[instructions](../README.md#openid-certified-providers) for some OpenID +Certified providers. + +### Configure the Example + +#### In the file `AuthManager.swift` + +1. Update `kIssuer` with the IdP's issuer. +2. Update `kClientID` with your new client id. +3. Update `kRedirectURI` redirect URI + +#### In the file `Info.plist` + +Fully expand "URL types" (a.k.a. `CFBundleURLTypes`) and replace +`com.example.app` with the *scheme* of your redirect URI. +The scheme is everything before the colon (`:`). For example, if the redirect +URI is `com.example.app:/oauth2redirect/example-provider`, then the scheme +would be `com.example.app`. + +### Running the Example + +Now your example should be ready to run. + +To get the Issuer, Client ID, and Redirect URI for your particular IdP, you +may view the IdP-specific information in the [main README](../README.md#openid-certified-providers). From 37c2637824f9c8503453c786877d114742d2897f Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:13:25 -0700 Subject: [PATCH 10/14] fix(example): import Combine for @Published and ObservableObject --- Examples/Example_Swift-SPM/Example/AuthManager.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Examples/Example_Swift-SPM/Example/AuthManager.swift b/Examples/Example_Swift-SPM/Example/AuthManager.swift index 8cf5cbc88..7a6b8bbd4 100644 --- a/Examples/Example_Swift-SPM/Example/AuthManager.swift +++ b/Examples/Example_Swift-SPM/Example/AuthManager.swift @@ -17,6 +17,7 @@ // import AppAuth +import Combine import SwiftUI import UIKit From ca7ca4f125fc535b5e66aa0f2a184922bb9922dc Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:14:37 -0700 Subject: [PATCH 11/14] fix(example): remove bogus additionalHeaders arg from OIDRegistrationRequest init --- Examples/Example_Swift-SPM/Example/AuthManager.swift | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Examples/Example_Swift-SPM/Example/AuthManager.swift b/Examples/Example_Swift-SPM/Example/AuthManager.swift index 7a6b8bbd4..111707c22 100644 --- a/Examples/Example_Swift-SPM/Example/AuthManager.swift +++ b/Examples/Example_Swift-SPM/Example/AuthManager.swift @@ -243,8 +243,7 @@ final class AuthManager: NSObject, ObservableObject { grantTypes: nil, subjectType: nil, tokenEndpointAuthMethod: "client_secret_post", - additionalParameters: nil, - additionalHeaders: nil) + additionalParameters: nil) // performs registration request self.logMessage("Initiating registration request") From 160681c46e7aaa0a73d0a0588c41722010c9119e Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:18:46 -0700 Subject: [PATCH 12/14] First commit for new Swift sample app. --- .../Example.xcodeproj/project.pbxproj | 42 +++++++++++-------- .../Example/AuthManager.swift | 6 +-- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj b/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj index c6ee70e72..d1bf05e78 100644 --- a/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj +++ b/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj @@ -14,27 +14,27 @@ 09BB0B542F91B157004C0D4B /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + 09BB0C002F91B157004C0D4B /* Exceptions for "Example" folder in "Example" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 09BB0B532F91B157004C0D4B /* Example */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + /* Begin PBXFileSystemSynchronizedRootGroup section */ 09BB0B562F91B157004C0D4B /* Example */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( - 09BB0C002F91B157004C0D4B /* Example */, + 09BB0C002F91B157004C0D4B /* Exceptions for "Example" folder in "Example" target */, ); path = Example; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ -/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ - 09BB0C002F91B157004C0D4B /* Example */ = { - isa = PBXFileSystemSynchronizedBuildFileExceptionSet; - membershipExceptions = ( - Info.plist, - ); - target = 09BB0B532F91B157004C0D4B /* Example */; - }; -/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ - /* Begin PBXFrameworksBuildPhase section */ 09BB0B512F91B157004C0D4B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; @@ -114,7 +114,7 @@ mainGroup = 09BB0B4B2F91B157004C0D4B; minimizedProjectReferenceProxies = 1; packageReferences = ( - 09BB0D002F91B157004C0D4B /* AppAuth */, + 09BB0D002F91B157004C0D4B /* XCLocalSwiftPackageReference "../.." */, ); preferredProjectObjectVersion = 77; productRefGroup = 09BB0B552F91B157004C0D4B /* Products */; @@ -271,8 +271,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Example/Info.plist; @@ -281,8 +283,9 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.openid.appauth.Example; + PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental0.dev; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; @@ -298,8 +301,10 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; - CODE_SIGN_STYLE = Automatic; + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Example/Info.plist; @@ -308,8 +313,9 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = net.openid.appauth.Example; + PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental0.dev; PRODUCT_NAME = "$(TARGET_NAME)"; + PROVISIONING_PROFILE_SPECIFIER = ""; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; @@ -344,9 +350,9 @@ /* End XCConfigurationList section */ /* Begin XCLocalSwiftPackageReference section */ - 09BB0D002F91B157004C0D4B /* AppAuth */ = { + 09BB0D002F91B157004C0D4B /* XCLocalSwiftPackageReference "../.." */ = { isa = XCLocalSwiftPackageReference; - relativePath = "../.."; + relativePath = ../..; }; /* End XCLocalSwiftPackageReference section */ diff --git a/Examples/Example_Swift-SPM/Example/AuthManager.swift b/Examples/Example_Swift-SPM/Example/AuthManager.swift index 111707c22..c8c0dd2e3 100644 --- a/Examples/Example_Swift-SPM/Example/AuthManager.swift +++ b/Examples/Example_Swift-SPM/Example/AuthManager.swift @@ -23,9 +23,9 @@ import UIKit typealias PostRegistrationCallback = (OIDServiceConfiguration?, OIDRegistrationResponse?) -> Void -let kIssuer: String = "https://issuer.example.com" -let kClientID: String? = "YOUR_CLIENT_ID" -let kRedirectURI: String = "com.example.app:/oauth2redirect/example-provider" +let kIssuer: String = "https://accounts.google.com" +let kClientID: String? = "352978860165-t1b6e11hpmom3bin0ml0kcpe8jp70qr4.apps.googleusercontent.com" +let kRedirectURI: String = "com.googleusercontent.apps.352978860165-t1b6e11hpmom3bin0ml0kcpe8jp70qr4:/oauth2redirect/google" let kAppAuthExampleAuthStateKey: String = "authState" final class AuthManager: NSObject, ObservableObject { From 3d36e1249a7cd308cf31dd19ce4e1ccb6418d447 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:13:58 -0700 Subject: [PATCH 13/14] g-orchested: Add config values for identity providers. --- Examples/Example_Swift-SPM/.gitignore | 1 + .../Example_Swift-SPM/Config/Example.xcconfig | 14 ++++++ .../Example.xcodeproj/project.pbxproj | 20 ++++++--- .../Example/AuthManager.swift | 42 ++++++++++++++++-- Examples/Example_Swift-SPM/Example/Info.plist | 8 +++- Examples/Example_Swift-SPM/README.md | 43 ++++++++++--------- 6 files changed, 98 insertions(+), 30 deletions(-) create mode 100644 Examples/Example_Swift-SPM/.gitignore create mode 100644 Examples/Example_Swift-SPM/Config/Example.xcconfig diff --git a/Examples/Example_Swift-SPM/.gitignore b/Examples/Example_Swift-SPM/.gitignore new file mode 100644 index 000000000..e3680cef6 --- /dev/null +++ b/Examples/Example_Swift-SPM/.gitignore @@ -0,0 +1 @@ +Config/Example.local.xcconfig diff --git a/Examples/Example_Swift-SPM/Config/Example.xcconfig b/Examples/Example_Swift-SPM/Config/Example.xcconfig new file mode 100644 index 000000000..0d515cdb0 --- /dev/null +++ b/Examples/Example_Swift-SPM/Config/Example.xcconfig @@ -0,0 +1,14 @@ +// This file holds public placeholder defaults for OAuth configuration. +// Real OAuth client values belong in the sibling, gitignored Config/Example.local.xcconfig, +// which overrides these defaults via the optional-include directive below. + +OIDC_ISSUER = https://issuer.example.com +OIDC_CLIENT_ID = YOUR_CLIENT_ID +OIDC_REDIRECT_URI = com.example.app:/oauth2redirect/example-provider +OIDC_REDIRECT_URI_SCHEME = com.example.app + +// Code signing. Defaults to Automatic with no team. Override in +// Example.local.xcconfig if you need Manual signing with a specific provisioning profile. +CODE_SIGN_STYLE = Automatic + +#include? "Example.local.xcconfig" diff --git a/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj b/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj index d1bf05e78..da3b94da0 100644 --- a/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj +++ b/Examples/Example_Swift-SPM/Example.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ /* Begin PBXFileReference section */ 09BB0B542F91B157004C0D4B /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 09BB0F012F91B157004C0D4B /* Config/Example.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config/Example.xcconfig; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -50,6 +51,7 @@ 09BB0B4B2F91B157004C0D4B = { isa = PBXGroup; children = ( + 09BB0F022F91B157004C0D4B /* Config */, 09BB0B562F91B157004C0D4B /* Example */, 09BB0B552F91B157004C0D4B /* Products */, ); @@ -63,6 +65,14 @@ name = Products; sourceTree = ""; }; + 09BB0F022F91B157004C0D4B /* Config */ = { + isa = PBXGroup; + children = ( + 09BB0F012F91B157004C0D4B /* Config/Example.xcconfig */, + ); + name = Config; + sourceTree = ""; + }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -268,16 +278,16 @@ }; 09BB0B602F91B158004C0D4B /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 09BB0F012F91B157004C0D4B /* Config/Example.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Example/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "AppAuth iOS Demo"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -285,7 +295,6 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental0.dev; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; @@ -298,16 +307,16 @@ }; 09BB0B612F91B158004C0D4B /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 09BB0F012F91B157004C0D4B /* Config/Example.xcconfig */; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CODE_SIGN_STYLE = Manual; CURRENT_PROJECT_VERSION = 1; - DEVELOPMENT_TEAM = ""; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = NO; INFOPLIST_FILE = Example/Info.plist; + INFOPLIST_KEY_CFBundleDisplayName = "AppAuth iOS Demo"; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -315,7 +324,6 @@ MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.google.experimental0.dev; PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; STRING_CATALOG_GENERATE_SYMBOLS = YES; SWIFT_APPROACHABLE_CONCURRENCY = YES; SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; diff --git a/Examples/Example_Swift-SPM/Example/AuthManager.swift b/Examples/Example_Swift-SPM/Example/AuthManager.swift index c8c0dd2e3..fa1b0588e 100644 --- a/Examples/Example_Swift-SPM/Example/AuthManager.swift +++ b/Examples/Example_Swift-SPM/Example/AuthManager.swift @@ -23,9 +23,29 @@ import UIKit typealias PostRegistrationCallback = (OIDServiceConfiguration?, OIDRegistrationResponse?) -> Void -let kIssuer: String = "https://accounts.google.com" -let kClientID: String? = "352978860165-t1b6e11hpmom3bin0ml0kcpe8jp70qr4.apps.googleusercontent.com" -let kRedirectURI: String = "com.googleusercontent.apps.352978860165-t1b6e11hpmom3bin0ml0kcpe8jp70qr4:/oauth2redirect/google" +let kIssuer: String = { + guard let issuer = Bundle.main.object(forInfoDictionaryKey: "OIDCIssuer") as? String, + !issuer.isEmpty, + issuer != "https://issuer.example.com" else { + preconditionFailure("Please configure OIDC_ISSUER in Example.local.xcconfig") + } + return issuer +}() +let kClientID: String? = { + let clientID = Bundle.main.object(forInfoDictionaryKey: "OIDCClientID") as? String + if clientID == "YOUR_CLIENT_ID" || clientID?.isEmpty ?? true { + return nil + } + return clientID +}() +let kRedirectURI: String = { + guard let redirectURI = Bundle.main.object(forInfoDictionaryKey: "OIDCRedirectURI") as? String, + !redirectURI.isEmpty, + redirectURI != "com.example.app:/oauth2redirect/example-provider" else { + preconditionFailure("Please configure OIDC_REDIRECT_URI in Example.local.xcconfig") + } + return redirectURI +}() let kAppAuthExampleAuthStateKey: String = "authState" final class AuthManager: NSObject, ObservableObject { @@ -40,11 +60,27 @@ final class AuthManager: NSObject, ObservableObject { override init() { super.init() + self.validateOAuthConfiguration() self.loadState() } // MARK: Public Methods + func validateOAuthConfiguration() { + assert(kClientID != nil, "Register your OIDC Client ID in Example.local.xcconfig (OIDC_CLIENT_ID).") + assert(kRedirectURI != "com.example.app:/oauth2redirect/example-provider", "Register your OIDC Redirect URI in Example.local.xcconfig (OIDC_REDIRECT_URI).") + + guard let urlTypes = Bundle.main.object(forInfoDictionaryKey: "CFBundleURLTypes") as? [[String: Any]], + let urlSchemes = urlTypes.first?["CFBundleURLSchemes"] as? [String], + let urlScheme = urlSchemes.first else { + assertionFailure("CFBundleURLSchemes not configured") + return + } + + assert(urlScheme != "com.example.app", "Register your OIDC Redirect URI scheme in Example.local.xcconfig (OIDC_REDIRECT_URI_SCHEME).") + assert(kIssuer != "https://issuer.example.com", "Register your OIDC Issuer in Example.local.xcconfig (OIDC_ISSUER).") + } + func authWithAutoCodeExchange() { guard let issuer = URL(string: kIssuer) else { self.logMessage("Error creating URL for : \(kIssuer)") diff --git a/Examples/Example_Swift-SPM/Example/Info.plist b/Examples/Example_Swift-SPM/Example/Info.plist index 39f750a49..aaf558068 100644 --- a/Examples/Example_Swift-SPM/Example/Info.plist +++ b/Examples/Example_Swift-SPM/Example/Info.plist @@ -23,7 +23,7 @@ Editor CFBundleURLSchemes - com.example.app + $(OIDC_REDIRECT_URI_SCHEME) @@ -31,6 +31,12 @@ $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS + OIDCClientID + $(OIDC_CLIENT_ID) + OIDCIssuer + $(OIDC_ISSUER) + OIDCRedirectURI + $(OIDC_REDIRECT_URI) UIApplicationSceneManifest UIApplicationSupportsMultipleScenes diff --git a/Examples/Example_Swift-SPM/README.md b/Examples/Example_Swift-SPM/README.md index 7542642dd..ac35609f4 100644 --- a/Examples/Example_Swift-SPM/README.md +++ b/Examples/Example_Swift-SPM/README.md @@ -2,14 +2,13 @@ ## Setup & Open the Project -This example uses the local AppAuth Swift Package, which is referenced via a relative path (`../..`). Xcode resolves this automatically and no `carthage bootstrap` is needed. +This sample uses the local AppAuth Swift Package via a relative path (`../..`), so Xcode resolves it automatically. -To get started, just open the `Example.xcodeproj` file. +Just open `Example.xcodeproj` to get started, and complete the configuration. ## Configuration -The example doesn't work out of the box, you need to configure it with your own -client ID. +The example doesn't work out of the box — you need to configure it with your own OpenID Connect client. ### Information You'll Need @@ -17,29 +16,33 @@ client ID. * Client ID * Redirect URI -How to get this information varies by IdP, but we have -[instructions](../README.md#openid-certified-providers) for some OpenID -Certified providers. +How to get this information varies by IdP, but we have [instructions](../README.md#openid-certified-providers) for some OpenID Certified providers. ### Configure the Example -#### In the file `AuthManager.swift` +This sample reads them from an xcconfig file. Create your local override file by copying the committed defaults: -1. Update `kIssuer` with the IdP's issuer. -2. Update `kClientID` with your new client id. -3. Update `kRedirectURI` redirect URI + cp Config/Example.xcconfig Config/Example.local.xcconfig -#### In the file `Info.plist` +Then edit `Config/Example.local.xcconfig` and set: -Fully expand "URL types" (a.k.a. `CFBundleURLTypes`) and replace -`com.example.app` with the *scheme* of your redirect URI. -The scheme is everything before the colon (`:`). For example, if the redirect -URI is `com.example.app:/oauth2redirect/example-provider`, then the scheme -would be `com.example.app`. + OIDC_ISSUER = + OIDC_CLIENT_ID = + OIDC_REDIRECT_URI = + OIDC_REDIRECT_URI_SCHEME = + +The scheme is everything before the colon (:) of your redirect URI. For example, if the redirect URI is `com.example.app:/oauth2redirect/example-provider`, the scheme is `com.example.app`. + +Note that the local version you create, `Config/Example.local.xcconfig`, is gitignored. + +The same file can also override code-signing. By default the committed xcconfig sets CODE_SIGN_STYLE = Automatic, which causes Xcode to prompt for your team on first build — the same experience as the sibling samples. To use Manual signing with a specific provisioning profile, add these lines to Config/Example.local.xcconfig: + + CODE_SIGN_STYLE = Manual + DEVELOPMENT_TEAM = + PROVISIONING_PROFILE_SPECIFIER = + +Note: Xcode may cache Info.plist substitutions — after editing the xcconfig, run **Product > Clean Build Folder**. ### Running the Example Now your example should be ready to run. - -To get the Issuer, Client ID, and Redirect URI for your particular IdP, you -may view the IdP-specific information in the [main README](../README.md#openid-certified-providers). From 688c81e0387beb094900ffa4a515cae5dbadb6b7 Mon Sep 17 00:00:00 2001 From: Worthing ~ <115107835+w-goog@users.noreply.github.com> Date: Tue, 21 Apr 2026 17:20:11 -0700 Subject: [PATCH 14/14] Add the weird little override for xcconfig's slashes --- Examples/Example_Swift-SPM/Config/Example.xcconfig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Examples/Example_Swift-SPM/Config/Example.xcconfig b/Examples/Example_Swift-SPM/Config/Example.xcconfig index 0d515cdb0..0c33ad3b0 100644 --- a/Examples/Example_Swift-SPM/Config/Example.xcconfig +++ b/Examples/Example_Swift-SPM/Config/Example.xcconfig @@ -2,7 +2,7 @@ // Real OAuth client values belong in the sibling, gitignored Config/Example.local.xcconfig, // which overrides these defaults via the optional-include directive below. -OIDC_ISSUER = https://issuer.example.com +OIDC_ISSUER = https:/$()/issuer.example.com OIDC_CLIENT_ID = YOUR_CLIENT_ID OIDC_REDIRECT_URI = com.example.app:/oauth2redirect/example-provider OIDC_REDIRECT_URI_SCHEME = com.example.app