On this page
Sign in mobile users with a self-hosted page
Add authentication to your mobile app using a self-hosted sign-in page with the Okta Client SDK for Swift.
Learning outcomes
- Configure your Okta org for password-based direct authentication.
- Set up an iOS project with the necessary Okta SDKs.
- Build a credential manager to handle authentication state.
- Create a SwiftUI interface for username/password sign-in flows.
- Implement token refresh and session management.
What you need
- Okta Integrator Free Plan org (opens new window)
- Familiarity with Xcode, Swift, and basic iOS development concepts
Overview
Building a streamlined authentication experience is essential for modern iOS apps. While multifactor authentication provides enhanced security, many apps start with a simpler approach, such as username and password authentication. With the Okta Client SDK, you can implement a fully native, password-based sign-in flow like direct authentication. This keeps users within your app while still using the Okta identity platform.
Understand Okta direct authentication for password authentication
Direct authentication enables your iOS app to authenticate users directly through the Okta APIs without browser-based flows. This means that your app maintains complete control over the UI and user experience while Okta handles the security and token management behind the scenes.
For password-only authentication, the flow is straightforward:
- The user enters credentials in your mobile UI.
- Your app sends credentials to Okta using direct authentication.
- Okta validates the credentials and returns OAuth 2.0 tokens.
- Your app securely stores these tokens for API access.
This approach works well for internal apps, rapid prototyping, or scenarios where you want to add MFA capabilities later without restructuring your codebase.
Security considerations
While this implementation provides a functional authentication system, keep these security points in mind:
Use only HTTPS: Ensure that all API calls use secure connections. Okta enforces this by default.
Consider adding MFA: Password-only authentication is less secure than password + MFA. Consider adding support for more authenticators as your app matures.
Handle token expiration: Always implement token refresh logic to maintain sessions without requiring the user to repeatedly sign in.
Secure storage: Never store passwords locally. The
AuthFoundationlibrary handles secure token storage in the keychain automatically.Error handling: Provide clear error messages without exposing sensitive security details.
Beyond username and password
You've built a complete, mobile authentication system for iOS using Okta direct authentication with username and password. Your app now handles credential validation, secure token storage, session management, and token refresh all without leaving your mobile Swift UI.
This foundation makes it easy to add more sophisticated authentication features later like biometric verification or passwordless, while maintaining the same clean architecture.
SwiftUI's reactive UI framework and the Okta Client SDK provide a secure and user-friendly authentication experience that can scale with your app's needs.
How the components work together
In this guide, you create the user interface using several components. The following displays the flow of data through the components:
- The user enters credentials in the
LoginViewtext fields. - The text field values are bound to the
LoginViewModelproperties. - The user taps Sign In.
LoginViewcallsviewModel.login().LoginViewModelcallsauthService.authenticate().AuthServiceupdates its state.LoginViewModelobserves the state change.LoginViewautomatically re-renders based on the new state.- When authenticated, the user can go to
ProfileVieworTokenDetailsView.
This architecture keeps concerns separated: the view handles presentation, the view model handles UI logic, and the service handles authentication business logic.
Configure your Okta org
Before diving into code, set up your Okta org to support direct authentication with password authentication.
Set up a mobile app
- Go to Applications > Applications in your Admin Console.
- Click Create App Integration and select OIDC - OpenID Connect as the sign-in method.
- Choose Native Application as the app type, and then click Next.
- Give your app a name, and then configure the other app settings:
- Grant type: Ensure that Authorization Code is selected.
- Sign-in redirect URIs:
com.okta.{yourOktaDomain}:/callback - Sign-out redirect URIs:
com.okta.{yourOktaDomain}:/
Note: Replace
{yourOktaDomain}with your actual Okta domain, such as,dev-123456.okta.com. - In the Assignments > Controlled access section, choose your preferred access control method.
- Click Save and note the client ID. You need this later.
Configure your authorization server
To enable password-based authentication, follow these steps:
- Go to Security > API.
- Select the authorization server that you want to use.
- Click the Access Policies tab.
- Create an access policy:
- Click Add Policy.
- Name the policy and give it a description.
- Assign the policy to All clients.
- Click Create Policy.
- Add a rule to your policy:
- Click Add rule.
- Name the policy rule.
- In the IF Grant type is section, click Advanced and select Resource Owner Password.
- Leave the Any user assigned the app default for AND User is.
- Leave the Any scopes default for AND Scopes requested.
- Click Create rule.
- Go to Applications > Applications and select the app that you created.
- Select the Sign On tab (or Authentication, depending on your org configuration) and scroll down to the User authentication section.
- For this example, select Password only.
Caution: You need to enable the Resource Owner Password grant type for use with the direct authentication password flows.
Set up your iOS project
Now that you've configured your Okta org, create the iOS app.
Create an Xcode project
- Open Xcode and select File > New > Project.
- Choose iOS > App, and then click Next.
- Configure your project:
- Product Name:
OktaPasswordAuth - Interface:
SwiftUI - Language: Swift
- Product Name:
- Click Next and save your project.
Add Okta SDK dependencies
Use Swift Package Manager to add the Okta Client SDK:
- In Xcode, go to File > Add Package Dependencies.
- Enter the repository URL:
https://github.com/okta/okta-mobile-swift. - Click Add Package.
- When prompted to choose products, select the following dependencies:
OktaDirectAuthAuthFoundation
- Ensure that both are added to your app target.
- Click Add Package.
Create the Okta configuration file
Rather than hardcoding configuration values, use a property list file:
- Right-click your project's root folder.
- Select New File From Template.
- Choose Property List and click Next.
- Name the property file Okta.plist.
- Click Create.
- Right-click Okta.plist, select Open As > Source Code, and paste the following xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>scopes</key>
<string>openid profile offline_access</string>
<key>redirectUri</key>
<string>com.okta.{yourOktaDomain}:/callback</string>
<key>clientId</key>
<string>{yourClientID}</string>
<key>issuer</key>
<string>https://{yourOktaDomain}/oauth2/default</string>
<key>logoutRedirectUri</key>
<string>com.okta.{yourOktaDomain}:/</string>
</dict>
</plist>
- Replace
{yourOktaDomain}and{yourClientID}with the actual values from the app that you created.
Build the authentication service
With setup complete, implement the core authentication logic and create a service layer that handles all interactions with the Okta DirectAuth API (opens new window).
Understand the AuthService architecture
The AuthService is the heart of the Okta authentication system. It serves as a centralized layer that manages the entire authentication lifecycle, from the initial sign-in flow to session maintenance. By encapsulating all authentication logic in a single service, Okta achieves several important goals:
Separation of concerns: The service isolates authentication logic from UI code, making it easier to both test and maintain. Your SwiftUI views don't need to know how direct authentication works, they simply call methods like authenticate() or logout().
State management: The service maintains the current authentication state (idle, authenticating, authenticated, or error), allowing your UI to respond appropriately. This state-driven approach makes it easy to show loading indicators, error messages, or authenticated content.
Security best practices: All token handling and storage is managed through the service, ensuring that credentials are stored securely in the iOS keychain through AuthFoundation (opens new window). Your UI never directly touches sensitive data.
Testability: By defining a protocol (AuthServicing), you can easily create mock implementations for unit testing your views without making actual network calls to Okta.
The service handles five key responsibilities:
- Authentication: Validate user credentials with Okta.
- Token storage: Persist tokens securely in the keychain.
- Session management: Track whether a user is authenticated.
- Token refresh: Obtain new access tokens without re-authentication.
- User profile retrieval: Fetch user information from Okta.
Build this step by step, starting with the protocol that defines the service contract.
Create the service protocol
Create a folder named Services in your project, then add a file called AuthService.swift:
import Foundation
import AuthFoundation
import OktaDirectAuth
import Observation
protocol AuthServicing {
var isAuthenticated: Bool { get }
var currentToken: String? { get }
var authenticationState: AuthService.AuthState { get }
func authenticate(username: String, password: String) async throws
func logout() async throws
func refreshAccessToken() async throws
func getCurrentUser() async throws -> UserInfo?
}
This protocol defines the contract for the Okta authentication service, making it easy to test and mock later.
Implement the AuthService
Below the protocol, add the implementation. Build this step by step, starting with the basic structure:
@Observable
final class AuthService: AuthServicing {
// MARK: - Authentication States
enum AuthState: Equatable {
case notAuthenticated
case authenticating
case authenticated
case error(String)
}
// MARK: - Properties
private(set) var authenticationState: AuthState = .notAuthenticated
private let directAuth: DirectAuthenticationFlow?
var isAuthenticated: Bool {
authenticationState == .authenticated
}
var currentToken: String? {
Credential.default?.token.accessToken
}
// MARK: - Initialization
init() {
// Initialize DirectAuth with configuration from Okta.plist
if let config = try? OAuth2Client.PropertyListConfiguration() {
self.directAuth = try? DirectAuthenticationFlow(client: OAuth2Client(config))
} else {
self.directAuth = try? DirectAuthenticationFlow()
}
// Check for existing credential
if Credential.default?.token != nil {
authenticationState = .authenticated
}
}
}
This sets up the basic structure of our AuthService. The following code breaks down what each part does:
Authentication states
enum AuthState: Equatable {
case notAuthenticated
case authenticating
case authenticated
case error(String)
}
The AuthState enum represents all possible states of the authentication flow. Your UI observes this state and updates accordingly:
notAuthenticated: User is signed out, show sign-in formauthenticating: Sign-in attempt is in progress, show loading indicatorauthenticated: User is signed in, show authenticated contenterror(String): Authentication failed, show error message
Properties
The authenticationState property holds the current state and is marked as private(set), meaning only the AuthService can modify it, but external code can read it.
The directAuth property holds our direct authentication flow instance that communicates with the Okta APIs.
The computed properties isAuthenticated and currentToken provide convenient access to authentication status and the current access token.
Initialization
The initializer does two important things:
- Configures direct authentication: It attempts to load your Okta configuration from
Okta.plist. If that fails, it falls back to a default configuration. - Restores existing sessions: It checks if a valid credential exists in the keychain. If it does, the user is automatically signed in without needing to re-enter their credentials.
Understand session persistence
One of the key features of this implementation is automatic session restoration. When users close and reopen the app, they remain signed in because of the following lines in AuthService:
.init() {
// ... initialization code ...
// Check for existing credential
if Credential.default?.token != nil {
authenticationState = .authenticated
}
}
The AuthFoundation library stores tokens securely in the iOS keychain, which persists across app launches. This creates a seamless experience while maintaining security.
Understand the authentication flow
The authenticate method orchestrates the entire sign-in process:
1️⃣ Update state to show authentication is in progress.
authenticationState = .authenticating
This immediately updates the UI to show a loading indicator, providing instant feedback to the user that their sign-in attempt is being processed.
2️⃣ Send credentials to Okta using direct authentication.
let response = try await directAuth.start(username, with: .password(password))
This line does the heavy lifting. It sends the username and password to the ??Okta DirectAuth API endpoint??. The await keyword means this is an asynchronous call. The app waits for the response from Okta without blocking the UI thread.
3️⃣ Process the authentication response.
Okta returns a response that can be one of several types. For password-only authentication, Okta primarily cares about the success case.
4️⃣ Store credential securely in the keychain.
case .success(let token):
let credential = try Credential.store(token)
Credential.default = credential
authenticationState = .authenticated
When authentication succeeds, Okta returns a Token object that contains the following tokens:
- Access token: Used to authorize API requests
- ID token: Contains user identity information
- Refresh token: Used to obtain new access tokens without re-authentication
The Credential.store(token) method securely persists these tokens in the iOS keychain using the AuthFoundation library. Setting Credential.default makes this the active credential for the current session.
5️⃣ Handle unexpected responses.
default:
authenticationState = .error("Authentication failed")
If you receive a response type you don't handle (like an MFA challenge when MFA isn't configured), treat it as an error.
6️⃣ Handle errors and update state.
catch {
authenticationState = .error(error.localizedDescription)
throw error
}
Any network errors, invalid credentials, or other issues are caught here. Update the state so the UI can show an error message, and re-throw the error so calling code can also handle it if needed.
Understand the log-out process
The logout method ensures a complete and secure log-out flow:
1️⃣ Revoke tokens on the Okta server
if let credential = Credential.default {
try? await credential.revoke()
}
This tells Okta to invalidate the current tokens. Even if someone somehow obtained a copy of the tokens, they can no longer be used after revocation. Use try? because you want the log-out flow to succeed locally even if the network call fails.
2️⃣ Clear local credential from the keychain.
Credential.default = nil
This removes the stored tokens from the device's keychain, ensuring that no sensitive data remains locally.
3️⃣ Reset the authentication state.
authenticationState = .notAuthenticated
This updates the UI to show the sign-in screen again.
Understand token refresh
Access tokens have a limited lifetime (typically one hour) for security. Rather than forcing users to sign in again, use the refresh token:
1️⃣ Verify that a credential exists.
guard let credential = Credential.default else {
throw NSError(...)
}
You can't refresh if there's no stored credential. This guard ensures that you have a credential before attempting refresh.
2️⃣ Exchange the refresh token for a new access token.
try await credential.refresh()
This method automatically performs the following actions:
- Sends the refresh token to Okta
- Receives a new access token (and potentially a new refresh token)
- Updates the stored credential in the keychain
All of these actions occur without requiring the user to re-enter their password.
Understand user profile retrieval
The getCurrentUser method fetches user profile information from Okta:
1️⃣ Return cached user info if available
if let userInfo = Credential.default?.userInfo {
return userInfo
}
After the first fetch, AuthFoundation caches the user info. This avoids unnecessary network calls for data that rarely changes.
2️⃣ Fetch user info from the Okta /userinfo endpoint
guard let userInfo = try await Credential.default?.userInfo() else {
return nil
}
If not cached, this method calls the Okta /userinfo endpoint using the current access token. The response includes standard OpenID Connect claims:
sub: Unique user identifiername: User's full nameemail: Email addresspreferred_username: Username or email used for sign-in
3️⃣ Return nil if fetch fails.
catch {
return nil
}
If the network request fails or the token is invalid, return nil rather than crashing. The calling code can decide how to handle the missing data.
Finally, add user profile retrieval. Add this method after refreshAccessToken:
func getCurrentUser() async throws -> UserInfo? {
// 1️⃣ Return cached user info if available
if let userInfo = Credential.default?.userInfo {
return userInfo
}
// 2️⃣ Fetch user info from the Okta /userinfo endpoint
do {
guard let userInfo = try await Credential.default?.userInfo() else {
return nil
}
return userInfo
} catch {
// 3️⃣ Return nil if fetch fails
return nil
}
}
Now add token refresh capability. Add this method after logout:
func refreshAccessToken() async throws {
// 1️⃣ Verify that a credential exists
guard let credential = Credential.default else {
throw NSError(domain: "AuthService",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "No credential available"])
}
// 2️⃣ Exchange refresh token for new access token
try await credential.refresh()
}
Next, implement the log-out functionality. Add this method after authenticate:
func logout() async throws {
// 1️⃣ Revoke tokens on the Okta server
if let credential = Credential.default {
try? await credential.revoke()
}
// 2️⃣ Clear local credential from the keychain
Credential.default = nil
// 3️⃣ Reset authentication state
authenticationState = .notAuthenticated
}
Now implement the authentication methods. Add the following code after the init() method:
// MARK: - Authentication Methods
func authenticate(username: String, password: String) async throws {
// 1️⃣ Update state to show authentication is in progress
authenticationState = .authenticating
do {
// 2️⃣ Send credentials to Okta using DirectAuth
let response = try await directAuth.start(username, with: .password(password))
// 3️⃣ Process the authentication response
switch response {
case .success(let token):
// 4️⃣ Store credential securely in keychain
let credential = try Credential.store(token)
Credential.default = credential
authenticationState = .authenticated
default:
// 5️⃣ Handle unexpected response
authenticationState = .error("Authentication failed")
}
} catch {
// 6️⃣ Handle errors and update state
authenticationState = .error(error.localizedDescription)
throw error
}
}
The full AuthService code
With the AuthService complete, you now have a robust authentication layer that handles the sign-in and sign-out flows, token management, and user profile retrieval. This service forms the foundation of your app's security, and because it's protocol-based, it's easy to test and maintain.
Here's the complete AuthService implementation with all methods together for reference:
import Foundation
import AuthFoundation
import OktaDirectAuth
import Observation
protocol AuthServicing {
var isAuthenticated: Bool { get }
var currentToken: String? { get }
var authenticationState: AuthService.AuthState { get }
func authenticate(username: String, password: String) async throws
func logout() async throws
func refreshAccessToken() async throws
func getCurrentUser() async throws -> UserInfo?
}
@Observable
final class AuthService: AuthServicing {
// MARK: - Authentication States
enum AuthState: Equatable {
case notAuthenticated
case authenticating
case authenticated
case error(String)
}
// MARK: - Properties
private(set) var authenticationState: AuthState = .notAuthenticated
private let directAuth: DirectAuthenticationFlow?
var isAuthenticated: Bool {
authenticationState == .authenticated
}
var currentToken: String? {
Credential.default?.token.accessToken
}
// MARK: - Initialization
init() {
// Initialize DirectAuth with configuration from Okta.plist
if let config = try? OAuth2Client.PropertyListConfiguration() {
self.directAuth = (try? DirectAuthenticationFlow(client: OAuth2Client(config)))
} else {
self.directAuth = try? DirectAuthenticationFlow()
}
// Check for existing credential
if Credential.default?.token != nil {
authenticationState = .authenticated
}
}
// MARK: - Authentication Methods
func authenticate(username: String, password: String) async throws {
// Update state to show authentication is in progress
authenticationState = .authenticating
do {
// Send credentials to Okta through DirectAuth
let response = try await directAuth.start(username, with: .password(password))
// Process the authentication response
switch response {
case .success(let token):
// Store credential securely in keychain
let credential = try Credential.store(token)
Credential.default = credential
authenticationState = .authenticated
default:
// Handle unexpected response
authenticationState = .error("Authentication failed")
}
} catch {
// Handle errors and update state
authenticationState = .error(error.localizedDescription)
throw error
}
}
func logout() async throws {
// Revoke tokens on Okta's server
if let credential = Credential.default {
try? await credential.revoke()
}
// Clear local credential from keychain
Credential.default = nil
// Reset authentication state
authenticationState = .notAuthenticated
}
func refreshAccessToken() async throws {
// Verify a credential exists
guard let credential = Credential.default else {
throw NSError(domain: "AuthService",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "No credential available"])
}
// Exchange refresh token for new access token
try await credential.refresh()
}
func getCurrentUser() async throws -> UserInfo? {
// Return cached user info if available
if let userInfo = Credential.default?.userInfo {
return userInfo
}
// Fetch user info from Okta's UserInfo endpoint
do {
guard let userInfo = try await Credential.default?.userInfo() else {
return nil
}
return userInfo
} catch {
// 3️⃣ Return nil if fetch fails
return nil
}
}
}
Build the SwiftUI interface
With the service layer complete, create the user interface. Use the MVVM (Model-View-ViewModel) pattern to keep your views clean and testable.
Understand the UI architecture
Before we dive into code, let's understand the components that you build and how they work together:
LoginViewModel: The bridge between UI and business logic
The LoginViewModel acts as an intermediary layer between your SwiftUI views and the AuthService. This separation provides several benefits:
- UI state management: The view model maintains the UI-specific state (like loading indicators and error messages) separately from the authentication state.
- User input handling: It holds the username and password values bound to text fields, keeping form data out of the service layer.
- Action coordination: It translates user actions (button taps) into service calls, handling any UI-specific logic before and after.
- Testability: You can test view logic independently by injecting a mock
AuthService.
Think of the view model as a translator: it speaks "SwiftUI" to your views and "business logic" to your service.
Create the view model
Create a folder named ViewModels and add LoginViewModel.swift:
import Foundation
import AuthFoundation
import Observation
@Observable
final class LoginViewModel {
// MARK: - Dependencies
private let credentialManager: CredentialManaging
// MARK: - UI State
var username: String = ""
var password: String = ""
var isLoading: Bool = false
var errorMessage: String?
var authState: CredentialManager.AuthState {
credentialManager.authenticationState
}
var canSubmit: Bool {
!username.isEmpty && !password.isEmpty && !isLoading
}
var token: String {
authService.currentToken ?? "No Token"
}
// MARK: - Initialization
init(credentialManager: CredentialManaging = CredentialManager()) {
self.credentialManager = credentialManager
}
// MARK: - Actions
@MainActor
func login() async {
errorMessage = nil
isLoading = true
defer { isLoading = false }
do {
try await credentialManager.authenticate(username: username, password: password)
// Clear password after successful login
password = ""
} catch {
errorMessage = error.localizedDescription
}
}
@MainActor
func logout() async {
isLoading = true
defer { isLoading = false }
do {
try await credentialManager.logout()
username = ""
password = ""
errorMessage = nil
} catch {
errorMessage = "Logout failed: \(error.localizedDescription)"
}
}
@MainActor
func refreshToken() async {
isLoading = true
defer { isLoading = false }
do {
try await credentialManager.refreshAccessToken()
} catch {
errorMessage = "Token refresh failed: \(error.localizedDescription)"
}
}
@MainActor
func fetchUserProfile() async -> UserInfo? {
do {
return try await credentialManager.getCurrentUser()
} catch {
errorMessage = "Failed to fetch user profile: \(error.localizedDescription)"
return nil
}
}
}
LoginView: The main authentication interface
The LoginView is your app's primary authentication screen. It dynamically displays different content based on the authentication state:
- Login form (
notAuthenticated)`: Username and password fields with a sign-in button - Loading state (
authenticating): A progress indicator while credentials are being verified - Success view (
authenticated): Token preview, action buttons, and navigation to other screens - Error display: User-friendly error messages when authentication fails
The view observes the LoginViewModel and automatically updates when the authentication state changes, providing a reactive and responsive user experience.
Create the login view
Create a Views folder, move ContentView.swift in there and rename ContentView.swift to LoginView.swift. Replace its contents with the following code:
import SwiftUI
import AuthFoundation
struct LoginView: View {
@State private var viewModel = LoginViewModel()
@State private var showingProfile = false
@State private var showingTokenInfo = false
var body: some View {
NavigationStack {
Group {
switch viewModel.authState {
case .notAuthenticated, .error:
loginFormView
case .authenticating:
loadingView
case .authenticated:
authenticatedView
}
}
.navigationTitle("Okta DirectAuth")
}
.sheet(isPresented: $showingProfile) {
ProfileView(viewModel: viewModel)
}
.sheet(isPresented: $showingTokenInfo) {
TokenDetailsView()
}
}
}
// MARK: - Login Form
private extension LoginView {
var loginFormView: some View {
VStack(spacing: 24) {
headerView
VStack(spacing: 16) {
usernameField
passwordField
}
.padding(.horizontal)
loginButton
if let error = viewModel.errorMessage {
errorView(message: error)
}
Spacer()
}
.padding()
}
var headerView: some View {
VStack(spacing: 8) {
Image(systemName: "lock.shield")
.font(.system(size: 60))
.foregroundColor(.blue)
Text("Welcome Back")
.font(.title)
.fontWeight(.bold)
Text("Sign in with your Okta credentials")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.top, 40)
}
var usernameField: some View {
TextField("Email or Username", text: $viewModel.username)
.textFieldStyle(.roundedBorder)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.emailAddress)
.textContentType(.username)
}
var passwordField: some View {
SecureField("Password", text: $viewModel.password)
.textFieldStyle(.roundedBorder)
.textContentType(.password)
.onSubmit {
if viewModel.canSubmit {
Task { await viewModel.login() }
}
}
}
var loginButton: some View {
Button(action: { Task { await viewModel.login() } }) {
Text("Sign In")
.fontWeight(.semibold)
.frame(maxWidth: .infinity)
.padding()
.background(viewModel.canSubmit ? Color.blue : Color.gray)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(!viewModel.canSubmit)
.padding(.horizontal)
}
func errorView(message: String) -> some View {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
Text(message)
.font(.footnote)
}
.foregroundColor(.red)
.padding()
.background(Color.red.opacity(0.1))
.cornerRadius(8)
.padding(.horizontal)
}
}
// MARK: - Loading View
private extension LoginView {
var loadingView: some View {
VStack(spacing: 16) {
ProgressView()
.scaleEffect(1.5)
Text("Signing in...")
.font(.headline)
}
}
}
// MARK: - Authenticated View
private extension LoginView {
var authenticatedView: some View {
VStack(spacing: 24) {
successHeader
tokenPreview
actionButtons
Spacer()
}
.padding()
}
var successHeader: some View {
VStack(spacing: 12) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 70))
.foregroundColor(.green)
Text("Successfully Authenticated")
.font(.title2)
.fontWeight(.bold)
Text("You're now signed in to your account")
.font(.subheadline)
.foregroundColor(.secondary)
}
.padding(.top, 20)
}
var tokenPreview: some View {
VStack(alignment: .leading, spacing: 8) {
Text("Access Token")
.font(.caption)
.foregroundColor(.secondary)
ScrollView {
Text(viewModel.token)
.font(.system(.caption, design: .monospaced))
.textSelection(.enabled)
.padding()
}
.frame(height: 120)
.background(Color.secondary.opacity(0.1))
.cornerRadius(8)
}
}
var actionButtons: some View {
VStack(spacing: 12) {
Button(action: { showingProfile = true }) {
Label("View Profile", systemImage: "person.circle")
.frame(maxWidth: .infinity)
.padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: { showingTokenInfo = true }) {
Label("Token Details", systemImage: "key.fill")
.frame(maxWidth: .infinity)
.padding()
.background(Color.indigo)
.foregroundColor(.white)
.cornerRadius(10)
}
Button(action: { Task { await viewModel.refreshToken() } }) {
Label("Refresh Token", systemImage: "arrow.clockwise")
.frame(maxWidth: .infinity)
.padding()
.background(Color.orange)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(viewModel.isLoading)
Button(action: { Task { await viewModel.logout() } }) {
Label("Sign Out", systemImage: "rectangle.portrait.and.arrow.right")
.frame(maxWidth: .infinity)
.padding()
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(10)
}
.disabled(viewModel.isLoading)
}
}
}
Create supporting views
ProfileView: Display user information
After the user is authenticated, they want to see their profile information. The ProfileView performs the following actions:
- Fetch and display the user's profile data from Okta (name, email, username, user ID)
- Show metadata like when the profile was last updated
- Handle loading states while fetching data
- Display a friendly error message if the profile can't be loaded
This view demonstrates how to use the authenticated access token to retrieve more user information beyond basic authentication.
Create ProfileView.swift in the Views folder:
import SwiftUI
import AuthFoundation
struct ProfileView: View {
let viewModel: LoginViewModel
@Environment(\.dismiss) var dismiss
@State private var userInfo: UserInfo?
@State private var isLoading = true
var body: some View {
NavigationStack {
Group {
if isLoading {
ProgressView()
} else if let user = userInfo {
profileContent(user: user)
} else {
errorContent
}
}
.navigationTitle("Profile")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") { dismiss() }
}
}
.task {
userInfo = await viewModel.fetchUserProfile()
isLoading = false
}
}
}
private func profileContent(user: UserInfo) -> some View {
List {
Section("User Information") {
ProfileRow(label: "Name", value: user.name ?? "Not provided")
ProfileRow(label: "Email", value: user.email ?? "Not provided")
ProfileRow(label: "Username", value: user.preferredUsername ?? "Not provided")
ProfileRow(label: "User ID", value: user.subject ?? "Unknown")
}
if let updatedAt = user.updatedAt {
Section("Metadata") {
ProfileRow(label: "Last Updated",
value: updatedAt.formatted(date: .long, time: .shortened))
}
}
}
}
private var errorContent: some View {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle")
.font(.system(size: 50))
.foregroundColor(.orange)
Text("Unable to load profile")
.font(.headline)
Text("Please try again later")
.font(.subheadline)
.foregroundColor(.secondary)
}
}
}
struct ProfileRow: View {
let label: String
let value: String
var body: some View {
VStack(alignment: .leading, spacing: 4) {
Text(label)
.font(.caption)
.foregroundColor(.secondary)
Text(value)
.font(.body)
}
}
}
TokenDetailsView: Developer-friendly token inspector
The TokenDetailsView serves as a debugging and verification tool that displays the following information:
- Token type: The OAuth token type (typically "Bearer")
- Access token: The JWT used for authorizing API requests
- ID token: The JWT that contains user identity claims
- Refresh token: The token used to obtain new access tokens
- Scopes: The permissions granted to this token
This view is useful during development to verify that tokens are issued correctly and to understand what's stored in the credentials. In production apps, you typically remove or restrict access to this view.
Create TokenDetailsView.swift:
import SwiftUI
import AuthFoundation
struct TokenDetailsView: View {
@Environment(\.dismiss) var dismiss
private var credential: Credential? {
Credential.default
}
var body: some View {
NavigationStack {
ScrollView {
VStack(alignment: .leading, spacing: 20) {
if let token = credential?.token {
tokenSection(title: "Token Type", value: token.tokenType)
tokenSection(title: "Access Token",
value: token.accessToken,
monospaced: true)
if let scopes = token.scope {
tokenSection(title: "Scopes",
value: scopes.joined(separator: ", "))
}
if let idToken = token.idToken?.rawValue {
tokenSection(title: "ID Token",
value: idToken,
monospaced: true)
}
if let refreshToken = token.refreshToken {
tokenSection(title: "Refresh Token",
value: refreshToken,
monospaced: true)
}
if let expiresIn = token.expiresIn {
tokenSection(title: "Expires In",
value: "\(expiresIn) seconds")
}
} else {
emptyState
}
}
.padding()
}
.navigationTitle("Token Details")
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Done") { dismiss() }
}
}
}
}
private func tokenSection(title: String, value: String, monospaced: Bool = false) -> some View {
VStack(alignment: .leading, spacing: 8) {
Text(title)
.font(.headline)
.foregroundColor(.primary)
Text(value)
.font(monospaced ? .system(.caption, design: .monospaced) : .caption)
.foregroundColor(.secondary)
.textSelection(.enabled)
.padding()
.frame(maxWidth: .infinity, alignment: .leading)
.background(Color.secondary.opacity(0.1))
.cornerRadius(8)
}
}
private var emptyState: some View {
VStack(spacing: 16) {
Image(systemName: "key.slash")
.font(.system(size: 50))
.foregroundColor(.gray)
Text("No Token Available")
.font(.headline)
Text("Please sign in to view token details")
.font(.subheadline)
.foregroundColor(.secondary)
.multilineTextAlignment(.center)
}
.padding()
}
}
Test your implementation
You're now ready to test the complete authentication flow:
- Build and run: Press
Cmd+Rto build and run your app in the simulator. - Enter credentials: Use valid Okta user credentials from your org.
- Sign in: Click Sign In.
- View token: After successful authentication, your access token appears.
- Explore features:
- Click View Profile to see user information.
- Click Token Details to inspect all tokens.
- Click Refresh Token to get a new access token.
- Click Sign Out to clear the session.
Handle common issues
Invalid credentials
If you see an authentication error, verify the following:
- The username and password are correct
- The user is assigned to your app in Okta
- The Resource Owner Password grant type is enabled in your authorization server
Configuration errors
If the app crashes on launch, verify the following:
- Double-check your
Okta.plistvalues. - Ensure that your client ID and issuer URL are correct.
- Verify that the redirect URIs match exactly.
Token expiration
Access tokens typically expire after one hour. Use the Refresh Token button to get a new one without reauthenticating.