Skip to content

moxionio/okta-oidc-ios

 
 

Repository files navigation

iOS_14 ready

CI Status Carthage compatible Version License Platforms Swift

Okta Open ID Connect Library

This is a new version of this SDK, the new pod name is OktaOidc. The old OktaAuth pod is now deprecated.

This library is a swift wrapper around the AppAuth-iOS objective-c code for communicating with Okta as an OAuth 2.0 + OpenID Connect provider, and follows current best practice for native apps using Authorization Code Flow + PKCE.

You can learn more on the Okta + iOS page in our documentation. You can also download our sample application

Table of Contents

Getting Started

Installing the OktaOidc SDK into your project is simple. The easiest way to include this library into your project is through CocoaPods.

You'll also need:

  • An Okta account, called an organization (sign up for a free developer organization if you need one).
  • An Okta Application, configured as a Native App. This is done from the Okta Developer Console and you can find instructions here. When following the wizard, use the default properties. They are designed to work with our sample applications.

Supported Platforms

iOS

Okta OIDC supports iOS 11 and above.

macOS

Okta OIDC supports macOS (OS X) 10.10 and above. Library supports both custom schemes; a loopback HTTP redirects via a small embedded server.

Install

Swift Package Manager

Add the following to the dependencies attribute defined in your Package.swift file. You can select the version using the majorVersion and minor parameters. For example:

    dependencies: [
        .Package(url: "https://github.com/okta/okta-oidc-ios.git", majorVersion: <majorVersion>, minor: <minor>)
    ]

Cocoapods

Simply add the following line to your Podfile:

pod 'OktaOidc'

Then install it into your project:

pod install

Carthage

To integrate this SDK into your Xcode project using Carthage, specify it in your Cartfile:

github "okta/okta-oidc-ios"

Usage Guide

For an overview of this library's features and authentication flows, check out our developer docs.

You can also browse the full API reference documentation.

Configuration Reference

Create OIDC object

Before using this SDK you have to create a new object of OktaOidc. You can instantiate OktaOidc w/o parameters that means that SDK will use Okta.plist for configuration values. Alternatively you can create OktaOidc with custom configuration.

import OktaOidc

// Use the default Okta.plist configuration
let oktaOidc = OktaOidc()

// Use configuration from another resource
let config = OktaOidcConfig(/* plist */)
let config = OktaOidcConfig(/* dictionary */)

// Instantiate OktaOidc with custom configuration object
let oktaOidc = OktaOidc(configuration: config)

Need a refresh token? A refresh token is a special token that is used to generate additional access and ID tokens. Make sure to include the offline_access scope in your configuration to silently renew the user's session in your application!

Property list

The easiest way is to create a property list in your application's bundle. By default, this library checks for the existence of the file Okta.plist. However any property list file can be used to create configuration object. Ensure one is created with the following fields:

<?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>issuer</key>
    <string>https://{yourOktaDomain}.com/oauth2/default</string>
    <key>clientId</key>
    <string>{clientId}</string>
    <key>redirectUri</key>
    <string>{redirectUri}</string>
    <key>logoutRedirectUri</key>
    <string>{logoutRedirectUri}</string>
    <key>scopes</key>
    <string>openid profile offline_access</string>
  </dict>
</plist>

Configuration object

Alternatively, you can create a configuration object ( OktaOidcConfig) from dictionary with the required values:

let configuration = OktaOidcConfig(with: [
  "issuer": "https://{yourOktaDomain}/oauth2/default",
  "clientId": "{clientID}",
  "redirectUri": "{redirectUri}",
  "logoutRedirectUri": "{logoutRedirectUri}",
  "scopes": "openid profile offline_access",
  // Custom parameters
  "login_hint": "username@email.com"
])

Disable Single Sign-On for the authentication session

You can disable SSO capabilities by setting noSSO flag to true for OktaOidcConfig instance.

let configuration = OktaOidcConfig(with: {YourOidcConfiguration})
if #available(iOS 13.0, *) {
    configuration?.noSSO = true
}

Note Flag is available on iOS 13 and above versions

How to use in Objective-C project

To use this SDK in Objective-C project, you should do the following:

  • Add use_frameworks! in your Pod file.
  • Add project setting SWIFT_VERSION = 5.0. To do this open Build Settings in Xcode, select Edit -> Add Build setting -> Add User-Defined Setting. Specify SWIFT_VERSION and 5.0 as setting name and value correspondently.
  • Include autogenerated header OktaOidc/OktaOidc-Swift.h into your source code.

API Reference

signInWithBrowser

Start the authorization flow by simply calling signInWithBrowser. In case of successful authorization, this operation will return valid OktaOidcStateManager in its callback. Clients are responsible for further storage and maintenance of the manager.

Note: IDP can be passed by specifying an argument with the idp parameter.

iOS

oktaOidc.signInWithBrowser(from: viewController, additionalParameters: ["idp": "your_idp_here"]) { stateManager, error in
  if let error = error {
    // Error
    return
  }

  // stateManager.accessToken
  // stateManager.idToken
  // stateManager.refreshToken
}

Sample app example

macOS

// Create redirect server configuration and start local HTTP server if you don't want to use custom schemes
let serverConfig = OktaRedirectServerConfiguration.default
serverConfig.port = 63875
oktaOidc.signInWithBrowser(redirectServerConfiguration: serverConfig, additionalParameters: ["idp": "your_idp_here"]) { stateManager, error in
  if let error = error {
    // Error
    return
  }

  // stateManager.accessToken
  // stateManager.idToken
  // stateManager.refreshToken
}

signOutOfOkta

This method ends the user's Okta session in the browser. The method deletes Okta's persistent cookie and disables SSO capabilities.

Important: This method does not clear or revoke tokens minted by Okta. Use the revoke and clear methods of OktaOidcStateManager to terminate the user's local session in your application.

iOS

// Redirects to the configured 'logoutRedirectUri' specified in Okta.plist.
oktaOidc.signOutOfOkta(authStateManager, from: viewController) { error in
  if let error = error {
    // Error
    return
  }
}

Sample app example

macOS

// Create redirect server configuration and start local HTTP server if you don't want to use custom schemes
let serverConfig = OktaRedirectServerConfiguration.default
serverConfig.port = 63875
// Redirects to the configured 'logoutRedirectUri' specified in Okta.plist.
oktaOidc.signOutOfOkta(authStateManager: authStateManager, redirectServerConfiguration: serverConfig) { error in
  if let error = error {
    // Error
    return
  }
}

signOut

This method helps to perform a multi-step sign-out flow. The method provides options that you want to perform and the SDK runs the options as a batch. The available options are:

  • revokeAccessToken - SDK revokes access token
  • revokeRefreshToken - SDK revokes refresh token
  • removeTokensFromStorage - SDK removes tokens from the secure storage
  • signOutFromOkta - SDK calls signOutOfOkta
  • revokeTokensOptions - revokes access and refresh tokens
  • allOptions - revokes tokens, signs out from Okta, and removes tokens from the secure storage

The order of operations performed by the SDK:

  1. Revoke the access token, if the option is set. If this step fails step 3 will be omitted.
  2. Revoke the refresh token, if the option is set. If this step fails step 3 will be omitted.
  3. Remove tokens from the secure storage, if the option is set.
  4. Browser sign out, if the option is set.

iOS

let options: OktaSignOutOptions = .revokeTokensOptions
options.insert(.signOutFromOkta)
oktaOidc?.signOut(authStateManager: authStateManager, from: viewController, progressHandler: { currentOption in
    if currentOption.contains(.revokeAccessToken) {
        // update progress
    } else if currentOption.contains(.revokeRefreshToken) {
        // update progress
    } else if currentOption.contains(.signOutFromOkta) {
        // update progress
    }
}, completionHandler: { success, failedOptions in
    if !success {
        // handle error
    }
})

macOS

// Create redirect server configuration and start local HTTP server if you don't want to use custom schemes
let serverConfig = OktaRedirectServerConfiguration.default
serverConfig.port = 63875
let options: OktaSignOutOptions = .revokeTokensOptions
options.insert(.signOutFromOkta)
oktaOidc?.signOut(authStateManager: authStateManager,
                  redirectServerConfiguration: serverConfig,
                  progressHandler: { currentOption in
    if currentOption.contains(.revokeAccessToken) {
        // update progress
    } else if currentOption.contains(.revokeRefreshToken) {
        // update progress
    } else if currentOption.contains(.signOutFromOkta) {
        // update progress
    }
}, completionHandler: { success, failedOptions in
    if !success {
        // handle error
    }
})

authenticate

If you already signed in to Okta and have a valid session token, you can complete authorization by calling authenticate(withSessionToken:). Upon successful authorization, this operation returns a valid OktaOidcStateManager in the callback. Clients are responsible for further storage and maintenance of the manager.

oktaOidc.authenticate(withSessionToken: token) { stateManager, error in
  self.hideProgress()
  if let error = error {
    // Error
    return
  }

  // stateManager.accessToken
  // stateManager.idToken
  // stateManager.refreshToken
}

Sample app example

stateManager

Tokens are securely stored in the Keychain and can be retrieved by accessing the OktaOidcStateManager.

stateManager?.accessToken
stateManager?.idToken
stateManager?.refreshToken

User is responsible for storing OktaAuthStateManager returned by signInWithBrowser or authenticate operation. To store manager call the writeToSecureStorage method:

oktaOidc.signInWithBrowser(from: self) { stateManager, error in
  stateManager.writeToSecureStorage()
}

Sample app example

To retrieve stored manager call readFromSecureStorage(for: ) and pass here Okta configuration that corresponds to a manager you are interested in.

guard let stateManager = OktaOidcStateManager.readFromSecureStorage(for: oktaConfig) else {
    // unauthenticated
}

//authenticated 
// stateManager.accessToken
// stateManager.idToken
// stateManager.refreshToken

Sample app example

Note: In OktaOidc SDK 3.0 we added support for multiple Oauth 2.0 accounts. So developer can use Okta endpoint, social endpoint and others in one application. Therefore OktaOidcStateManager is stored in keychain using composite key constructed based on configuration. For backward compatibility there is a method readFromSecureStorage() that tries to read OktaOidcStateManager stored on a legacy way, so user could retrieve previously stored OktaOidcStateManager after switching to a newer version of SDK.

introspect

Calls the introspection endpoint to inspect the validity of the specified token.

stateManager?.introspect(token: accessToken, callback: { payload, error in
  guard let isValid = payload["active"] as? Bool else {
    // Error
    return
  }

  print("Is token valid? \(isValid)")
})

Sample app example

renew

Since access tokens are traditionally short-lived, you can renew expired tokens by exchanging a refresh token for new ones. See the configuration reference to ensure your app is configured properly for this flow.

stateManager?.renew { newAccessToken, error in
  if let error = error else {
    // Error
    return
  }

  // renewed TokenManager
}

Sample app example

revoke

Calls the revocation endpoint to revoke the specified token.

stateManager?.revoke(accessToken) { response, error in
  if let error = error else {
    // Error
    return
  }

  // Token was revoked
}

Sample app example

getUser

Calls the OpenID Connect UserInfo endpoint with the stored access token to return user claim information.

stateManager?.getUser { response, error in
  if let error = error {
    // Error
    return
  }

  // JSON response
}

Sample app example

clear

Removes the local authentication state by removing cached tokens in the keychain. Note: SDK deletes all keychain items accessible to an application.

stateManager.clear()

Sample app example

Development

Running Tests

To perform an end-to-end test, update the Okta.plist file to match your configuration as specified in the prerequisites. Next, export the following environment variables:

export USERNAME={username}
export PASSWORD={password}
export CLIENT_ID={clientId}
export ISSUER=https://{yourOktaDomain}/oauth2/default
export REDIRECT_URI={redirectUri}
export LOGOUT_REDIRECT_URI={logoutRedirectUri}

# Run E2E end Unit tests
bash ./scripts/build-and-test.sh

Note: You may need to update the emulator device to match your Xcode version

Modify network requests

You can track and modify network requests made by OktaOidc. In order to do this, create an object conforming to the OktaNetworkRequestCustomizationDelegate protocol and set it to the requestCustomizationDelegate property on an OktaOidcConfig instance.

let configuration = OktaOidcConfig(with: {YourOidcConfiguration})
configuration.requestCustomizationDelegate = {YourDelegateInstance}

For example, delegate could be implemented as follows:

extension SomeNSObject: OktaNetworkRequestCustomizationDelegate {

    func customizableURLRequest(_ request: URLRequest?) -> URLRequest? {
        guard var modifiedRequest = request else {
            return nil
        }
        modifiedRequest.setValue("Some value", forHTTPHeaderField: "custom-header-field")
        print("Okta OIDC network request: \(modifiedRequest)")
        return modifiedRequest
    }

    func didReceive(_ response: URLResponse?) {
        guard let response = response else {
            return
        }
        print("Okta OIDC network response: \(response)")
    }
}

Note: It is highly recommended to copy all of the existing parameters from the original URLRequest object to modified request without any changes. Altering of this data could lead network request to fail. If customizableURLRequest(_:) method returns nil default request will be used.

Known issues

iOS shows permission dialog({App} Wants to Use {Auth Domain} to Sign In) for Okta Sign Out flows

Known iOS issue where iOS doesn't provide any good ways to terminate active authentication session and delete SSO cookies. The only proper way for now is to use ASWebAuthenticationSession class to terminate the session. ASWebAuthenticationSession deletes all SSO cookies however shows Sign In persmissions dialog 🤯

You can also consider the following workarounds:

  • Use noSSO option in OIDC configuration object if you don't need SSO capabilites. Also note that this option works only on iOS 13+ versions
  • Fork repository and change user-agent implementation(OIDExternalUserAgentIOS.m) to use SFSafariViewController only. Some pitfalls of this approach described here

Carthage fails on Xcode 12

Carthage throws the error when you install the dependencies with the command carthage update. The issue happens only on Xcode 12 and higher versions:

Build Failed
	Task failed with exit code 1:
	/usr/bin/xcrun lipo -create /Users/user/Library/Caches/org.carthage.CarthageKit/DerivedData/12.4_12D4e/okta-oidc-ios/3.10.1/Build/Intermediates.noindex/ArchiveIntermediates/okta-oidc/IntermediateBuildFilesPath/UninstalledProducts/iphoneos/OktaOidc.framework/OktaOidc /Users/user/Library/Caches/org.carthage.CarthageKit/DerivedData/12.4_12D4e/okta-oidc-ios/3.10.1/Build/Products/Release-iphonesimulator/OktaOidc.framework/OktaOidc -output /Users/user/{ProjectName}/Carthage/Build/iOS/OktaOidc.framework/OktaOidc

This usually indicates that project itself failed to compile. Please check the xcodebuild log for more details: /var/folders/2x/q10zv0gx4112thm7dd13szmm0000gn/T/carthage-xcodebuild.YaJjLW.log

The reason is that Xcode 12 introduced support of the Apple Silicon and Xcode generates duplicated architectures in frameworks. XCFrameworks are still not supported by Carthage, therefore a workaround should be used.

Solution #1: XCFrameworks

You should update Carthage to the version 0.37.0 or higher.

  1. Run in Terminal the following command:
brew upgrade carthage
  1. Make sure the version is correct:
carthage version
  1. Navigate through Terminal to project folder and run:
carthage update --use-xcframeworks
  1. Open General settings tab in Xcode, in the Frameworks, Libraries, and Embedded Content section, drag and drop each XCFramework you want to use from the Carthage/Build folder.

If your existing project is based on discrete framework bundles and you may want to migrate to XCFrameworks, then follow Carthage migration documentation.

Solution #2: Workaround script

Launch Carthage via the script, it will remove duplicated architectures and produce correct framework bundles.

  1. Put the script somewhere to your PATH (e.g.: /usr/local/bin/carthage.sh).
  2. Make the script executable, so open your Terminal and execute:
chmod +x /{path_to_script_folder}/carthage.sh
  1. Run the script whenever you want to use Carthage:
carthage.sh update

For more information, follow official Carthage documentation.

Packages

No packages published

Languages

  • Objective-C 71.7%
  • Swift 28.1%
  • Other 0.2%