Implement "Sign in with SSO" in native iOS/Swift applications using SSOJet OIDC with AppAuth.
This expert AI assistant guide walks you through integrating "Sign in with SSO" functionality into an existing iOS application using SSOJet as an OIDC identity provider via AppAuth for iOS.
openid/AppAuth-iOS.com.example.myapp://auth/callback).Note: For native/mobile apps, use Authorization Code with PKCE (no Client Secret on the device).
Add AppAuth via Swift Package Manager:
https://github.com/openid/AppAuth-iOS.git.Or via CocoaPods:
# Podfile
pod 'AppAuth'
pod install
Add a custom URL scheme in your Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>com.example.myapp</string>
</array>
<key>CFBundleURLName</key>
<string>com.example.myapp</string>
</dict>
</array>
Create an auth configuration helper (e.g., AuthConfig.swift):
// AuthConfig.swift
import Foundation
struct AuthConfig {
static let issuerURL = URL(string: "https://auth.ssojet.com")!
static let clientID = "your_client_id"
static let redirectURI = URL(string: "com.example.myapp://auth/callback")!
static let scopes = ["openid", "profile", "email"]
}
Create a centralised auth manager (e.g., AuthManager.swift):
// AuthManager.swift
import UIKit
import AppAuth
class AuthManager: NSObject {
static let shared = AuthManager()
var currentAuthorizationFlow: OIDExternalUserAgentSession?
var authState: OIDAuthState?
func login(from viewController: UIViewController, loginHint: String?, completion: @escaping (Result<OIDAuthState, Error>) -> Void) {
// Discover OIDC configuration
OIDAuthorizationService.discoverConfiguration(forIssuer: AuthConfig.issuerURL) { config, error in
guard let config = config else {
completion(.failure(error ?? NSError(domain: "OIDC", code: -1, userInfo: [NSLocalizedDescriptionKey: "Discovery failed"])))
return
}
// Build authorization request
var additionalParams: [String: String] = [:]
if let hint = loginHint, !hint.isEmpty {
additionalParams["login_hint"] = hint
}
let request = OIDAuthorizationRequest(
configuration: config,
clientId: AuthConfig.clientID,
scopes: AuthConfig.scopes,
redirectURL: AuthConfig.redirectURI,
responseType: OIDResponseTypeCode,
additionalParameters: additionalParams
)
// Launch auth flow
self.currentAuthorizationFlow = OIDAuthState.authState(
byPresenting: request,
presenting: viewController
) { authState, error in
if let authState = authState {
self.authState = authState
print("Authenticated! Access Token: \(authState.lastTokenResponse?.accessToken ?? "nil")")
completion(.success(authState))
} else {
print("OIDC Error: \(error?.localizedDescription ?? "Unknown error")")
completion(.failure(error ?? NSError(domain: "OIDC", code: -1)))
}
}
}
}
func logout() {
authState = nil
}
}
// AppDelegate.swift (or SceneDelegate.swift)
import AppAuth
// In AppDelegate:
func application(_ app: UIApplication, open url: URL, options: [UIApplication.OpenURLOptionsKey: Any] = [:]) -> Bool {
if let flow = AuthManager.shared.currentAuthorizationFlow,
flow.resumeExternalUserAgentFlow(with: url) {
AuthManager.shared.currentAuthorizationFlow = nil
return true
}
return false
}
// If using SceneDelegate:
func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
guard let url = URLContexts.first?.url else { return }
if let flow = AuthManager.shared.currentAuthorizationFlow,
flow.resumeExternalUserAgentFlow(with: url) {
AuthManager.shared.currentAuthorizationFlow = nil
}
}
// LoginViewController.swift
import UIKit
class LoginViewController: UIViewController {
private let emailTextField = UITextField()
private let passwordTextField = UITextField()
private let signInButton = UIButton(type: .system)
private let ssoToggleButton = UIButton(type: .system)
private var isSSO = false
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
}
private func setupUI() {
view.backgroundColor = .systemBackground
title = "Sign In"
emailTextField.placeholder = "Email"
emailTextField.keyboardType = .emailAddress
emailTextField.borderStyle = .roundedRect
passwordTextField.placeholder = "Password"
passwordTextField.isSecureTextEntry = true
passwordTextField.borderStyle = .roundedRect
signInButton.setTitle("Sign In", for: .normal)
signInButton.addTarget(self, action: #selector(handleSignIn), for: .touchUpInside)
ssoToggleButton.setTitle("Sign in with SSO", for: .normal)
ssoToggleButton.addTarget(self, action: #selector(toggleSSO), for: .touchUpInside)
let stack = UIStackView(arrangedSubviews: [emailTextField, passwordTextField, signInButton, ssoToggleButton])
stack.axis = .vertical
stack.spacing = 16
stack.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(stack)
NSLayoutConstraint.activate([
stack.centerYAnchor.constraint(equalTo: view.centerYAnchor),
stack.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
stack.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
])
}
@objc private func toggleSSO() {
isSSO.toggle()
passwordTextField.isHidden = isSSO
signInButton.setTitle(isSSO ? "Continue with SSO" : "Sign In", for: .normal)
ssoToggleButton.setTitle(isSSO ? "Back to Password Login" : "Sign in with SSO", for: .normal)
}
@objc private func handleSignIn() {
let email = emailTextField.text ?? ""
if isSSO {
AuthManager.shared.login(from: self, loginHint: email) { result in
DispatchQueue.main.async {
switch result {
case .success:
let dashboard = DashboardViewController()
self.navigationController?.pushViewController(dashboard, animated: true)
case .failure(let error):
let alert = UIAlertController(title: "Error", message: error.localizedDescription, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "OK", style: .default))
self.present(alert, animated: true)
}
}
}
} else {
print("Processing traditional login...")
}
}
}
OIDAuthState.authState(byPresenting:) call in a coordinator or use ASWebAuthenticationSession directly.Search for places (restaurants, cafes, etc.) via Google Places API proxy on localhost.
Interact with GitHub using the `gh` CLI. Use `gh issue`, `gh pr`, `gh run`, and `gh api` for issues, PRs, CI runs, and advanced queries.
Create or update AgentSkills. Use when designing, structuring, or packaging skills with scripts, references, and assets.
Start voice calls via the OpenClaw voice-call plugin.
Notion API for creating and managing pages, databases, and blocks.
Gemini CLI for one-shot Q&A, summaries, and generation.
Category:developer