Okta IDX Swift SDK
❕ 使用此 SDK 需要使用 Okta Identity Engine。此功能目前为一般可用(GA),但正在逐步向客户推出。如果您想请求访问 Okta Identity Engine,请联系您的账户经理。如果您没有账户经理,请联系 [email protected] 获取更多信息。
此库当前为 GA 状态。有关更多信息,请参阅 发布状态。
此库是为用 Swift 编写的项目构建的,以通过 OAuth 2.0 + OpenID Connect 提供商与 Okta 进行通信。它与 Okta 的身份引擎一起使用以对用户进行身份验证和注册。
要查看此库在示例中的应用,请查看我们的 iOS 示例应用程序。您还可以查看我们的指南以获取逐步说明
目录
发布状态
此库使用语义版本控制并遵循 Okta 的 库版本策略。
版本 | 状态 |
---|---|
1.0.0 | |
2.0.1 | |
3.0.9 |
最新版本总是在 版本页面 上可以找到。
需要帮助?
如果您在使用SDK时遇到问题,您可以
- 查看OktaIdx API文档
- 在Okta开发者论坛上提问
- 在此GitHub上(针对代码错误)发布问题
安装
要开始使用,您需要
- 一个Okta账号,也就是一个组织(如果您需要,可以免费注册一个开发者组织)。
- 针对iOS 10及更高版的Xcode。
Swift包管理器
在您的Package.swift
文件中定义的dependencies
属性中添加以下内容。您可以使用majorVersion
和minor
参数选择版本。例如
dependencies: [
.Package(url: "https://github.com/okta/okta-idx-swift.git", majorVersion: <majorVersion>, minor: <minor>)
]
CocoaPods
只需在您的Podfile
中添加以下行
pod 'OktaIdx'
然后将其安装到项目中
pod install --repo-update
API模式
IDX SDK通过循环调用和响应模式实现动态用户身份验证。用户能够通过一系列选择来迭代地完成身份验证过程,每个步骤都会提供更多的选择,直到成功认证或接收到可操作的错误信息。
身份验证过程中的每个步骤都由一个表示所有可能选择的Response
对象表示,这些选择由Remediation
类表示。修复提供了关于其类型的元数据、描述应向用户展示的字段和值的表单对象树以及其他帮助开发者构建UI的相关数据。
当选择修复并提供相关输入时,可以在修复上调用proceed()
方法进入身份验证过程的下一阶段。这会返回另一个Response
对象,从而使过程继续。
注意:除非有其他说明,所有异步函数都接受Swift
Result
完成处理器或传统完成块,后者在参数元组中以分开的值提供响应和错误。
使用方法
以下代码片段将帮助您了解如何使用此库。
初始化InteractionCodeFlow
后,您可以调用方法对Okta IDX API发出请求。请参阅配置参考部分以获取更多信息。
创建流程
let flow = InteractionCodeFlow(
issuer: "https:///<#oktaDomain#>/oauth2/default", // e.g. https://foo.okta.com/oauth2/default, https://foo.okta.com/oauth2/ausar5vgt5TSDsfcJ0h7
clientId: "<#clientId#>",
scopes: ["openid", "email", "offline_access", "<#otherScopes#>"],
redirectUri: "<#redirectUri#>") // Must match the redirect uri in client app settings/console
或者,如果您在应用程序中定义了Okta.plist
配置文件,您可以使用默认的初始化器创建使用该配置的流程。
let flow = try InteractionCodeFlow()
注意:虽然您的发行者URL可能在高级配置中会有所不同,但对于大多数用途而言,它将是您的Okta域名,后跟
/oauth2/default
。
开始认证
let response = try await flow.start()
如果您的应用程序不使用Swift Concurrency,您可以使用完成块来代替
flow.start { result in
switch result {
case .success(let response):
// Handle the response
case .failure(let error):
// Handle the error
}
}
一旦接收到响应,您可以使用响应的数据来根据该响应展示适当用户界面。
使用用户名和密码获取新令牌
在此示例中,登录策略不需要验证器。
注意:识别用户的步骤可能会根据您的组织配置而改变。
let flow: InteractionCodeFlow
func signIn(username: String, password: String) async throws -> Token? {
// Start the IDX authentication session
var response = try await flow.start()
// Use the `identify` remediation option, and find the relevant form fields
guard let remediation = response.remediations[.identify],
let usernameField = remediation["identifier"],
let passwordField = remediation["credentials.passcode"],
else {
return nil
}
// Populate the form fields with the user's supplied values
usernameField.value = username
passwordField.value = password
// Proceed through the remediation option
response = try await remediation.proceed()
guard response.isLoginSuccessful
else {
return nil
}
// Exchange the successful response for tokens
return try await response.exchangeCode()
}
取消OIE事务并启动新的事务
注意:此示例假设这段代码是在前一个IDX API调用响应中调用的。
let response = try await response.cancel()
// Handle the newly-restarted IDX session
}
登录策略的修复/多因素认证场景
在认证或注册过程中选择认证因素
当用户被要求注册一个新的认证因素或验证已注册的因素时,响应可能包含.selectAuthenticatorEnroll
或.selectAuthenticatorAuthenticate
补救类型。两种情况的用法模式类似。
向用户显示可能的注册选项
if let remediation = response.remediations[.selectAuthenticatorEnroll],
let authenticatorOptions = remediation["authenticator"]?.options
{
for option in authenticatorOptions {
guard let authenticator = option.authenticator else { continue }
// Display a UI choice for this choice, optionally using
// the `authenticator` associated with this option to provide
// more context.
self.showChoice(label: option.label)
}
}
选择认证器
用户做出选择后,您的应用程序可以应用该选择并继续进行补救。
let selectedChoice = "Security Question"
if let remediation = response.remediations[.selectAuthenticatorEnroll],
let authenticator = remediation["authenticator"],
let option = authenticator.options?.first(where: { field -> Bool in
field.label == selectedChoice
})
{
authenticator.selectedOption = option
let newResponse = try await remediation.proceed()
// Handle the response to enroll in the authenticator
}
注册安全问题认证器
在此示例中,组织配置为将安全问题作为第二个认证因素。在回答密码挑战后,用户必须选择安全问题,选择一个问题,并输入答案以完成此过程。
注意:在此示例中,假设会话已启动,已提交用户名和密码,并已选择安全问题认证器。有关更多详细信息,请参阅上述部分。
guard let remediation = response.remediations[.enrollAuthenticator],
let credentials = remediation["credentials"],
let createQuestionOption = credentials.options?.first(where: { option in
option.label == "Create my own security question"
}),
let questionField = createQuestionOption["question"],
let answerField = createQuestionOption["answer"]
else {
// Handle error
return
}
credentials.selectedOption = createQuestionOption
questionField.value = "What is Trillian's real name?"
answerField.value = "Tricia MacMillan"
let newResponse = try await remediation.proceed()
// Handle the response
使用电子邮件认证器进行认证
在此示例中,组织已配置为需要电子邮件作为第二重认证器。在回答密码挑战之后,用户必须选择“电子邮件”并输入验证码以完成流程。
当用户选择电子邮件认证器时,一条包含验证码的消息会发送到他们的邮箱地址。用户将加载邮件,并可以选择复制粘贴验证码或手动输入。
注意:此示例假设已提交用户名和密码,并且已选择电子邮件认证器。
guard let remediation = response.remediations[.challengeAuthenticator],
let passcodeField = remediation["credentials.passcode"]
else {
// Handle error
return
}
passcodeField.value = "123456"
let newResponse = try await remediation.proceed()
// Handle the response
注册手机认证器(短信/语音)
在此示例中,组织配置了电话作为第二重认证器。在回答密码挑战之后,用户必须提供电话号码,然后输入验证码以完成流程。
选择短信或语音选项
注意:此示例假设已提交用户名和密码。
guard let remediation = response.remediations[.selectAuthenticatorEnroll],
let authenticatorField = remediation["authenticator"],
let phoneOption = authenticatorField.options?.first(where: { option in
option.label == "Phone"
}),
let phoneNumberField = phoneOption["phoneNumber"],
let methodTypeField = phoneOption["methodType"],
let smsMethod = methodTypeField.options?.first(where: { option in
option.label == "SMS"
}) else
{
// Handle error
return
}
authenticatorField.selectedOption = phoneOption
methodTypeField.selectedOption = smsMethod
phoneNumberField.value = "+15551234567"
let newResponse = try await remediation.proceed()
// Use this response to present the verification code UI to the user
使用验证码进行响应
guard let remediation = response.remediations[.challengeAuthenticator],
let passcodeField = remediation["credentials.passcode"],
else {
// Handle error
return
}
passcodeField.value = "123456"
let newResponse = try await remediation.proceed()
// Handle the response
注册/登记
当您配置并启用自助注册策略时,初始响应将包括一个.selectEnrollProfile
补救选项。通过此补救选项将继续,将允许用户提供姓名和电子邮件地址,使他们能够继续创建新用户个人资料。
guard let remediation = response.remediations[.selectEnrollProfile] else {
// Handle error
return
}
response = try await remediation.proceed()
guard let remediation = response?.remediations[.enrollProfile],
let firstNameField = remediation["userProfile.firstName"],
let lastNameField = remediation["userProfile.lastName"],
let emailField = remediation["userProfile.email"]
else {
return
}
firstNameField.value = "Mary"
lastNameField.value = "Smith"
emailField.value = "[email protected]"
let newResponse = try await remediation.proceed()
// Handle the response
}
在.enrollProfile
补救成功后,您可以按照后续补救步骤注册身份验证程序以选择密码,注册因素,并最终交换成功的响应以获取令牌。
密码恢复
密码恢复是通过使用当前身份验证器相关的能力来支持的。这可以通过响应的authenticators
集合来访问。不是所有身份验证器都具有相同的一组能力,因此这些附加功能通过相关能力公开。因此,可以支持账户恢复的身份验证器,您可以检查它是否提供这种能力。
if let recoverable = response.authenticators.current?.recoverable {
let newResponse = try await recoverable.recover()
// Handle the response
}
一旦执行了recover
操作,您收到的响应将包含一个.identifyRecovery
补救选项,您可以使用它来提供用户的标识符。
guard let response = response,
let remediation = response.remediations[.identifyRecovery],
let identifierField = remediation["identifier"]
else {
// Handle error
return
}
identifierField.value = "[email protected]"
let newResponse = try await recoverable.recover()
// Handle the response
后续的响应将提示用户响应不同的因素挑战以验证其账户,并重置密码。
电子邮件验证轮询
当使用电子邮件身份验证器时,用户将同时收到一个数字代码和一个验证其身份的链接。如果用户点击此链接,将会验证身份验证器和应用,可以立即进行到下一个补救步骤。
注意:此代码假定它正在运行在要求用户输入验证码的同一UI环境中。
guard let remediation = response.remediations[.challengeAuthenticator],
let authenticator = remediation.authenticators[.email]
else {
// Handle error
return
}
if let pollable = authenticator.pollable,
let response = try await pollable.startPolling()
{
// The poll was successful. Use the result to display
// the UI for the next step in the user's authentication.
}
// Or, call `stopPolling()` to stop polling for the magic link.
检查修复选项
响应可能包含多个修复选项。有几种方法可以识别哪些选项可用。
// Select the option by its name using subscripting.
let remediation = response.remediations["challenge-authenticator"]
// Select the option by its enum type using subscripting.
let remediation = response.remediations[.challengeAuthenticator]
// Select the option by iterating over the options
let remediation = response.remediations.first(where: { $0.name == "challenge-authenticator" })
从这一点开始,您可以访问与其关联的表单值。
处理修复选项表单
修复选项包含一个表单,该表单可能包含显示给用户或接受用户输入(提交到服务器)的字段。
remediation.form.forEach { formField in
// Do something with the form field
}
为了方便起见,支持键控下标来通过名称访问字段,使用点表示法检索嵌套字段。
let identifierField = remediation["identifier"]
let passcodeField = remediation["credentials"]?.form?["passcode"]
// Or
let passcodeField = remediation["credentials.passcode"]
向修复选项提供值
使用修复选项的目的是为了能够使用户在选择和提供用户数据作为对这些请求的响应。字段中仅包含两个可变属性,可以用于在API中提供用户数据和做出选择。
字段值
输入用户信息时,可以使用字段的 value
属性将此数据传递到API中。
let identifierField = remediation["identifier"]
identifierField.value = "[email protected]"
多选选择
当向用户展示多个选项时(例如,选择认证器,从预定义的安全问题列表中选择等),表单字段将包含一个嵌套的定义选择的options
数组字段。
要选择一个选项,只需将嵌套选项赋值给其父元素的selectedOption
属性。
guard let remediation = response.remediations[.selectAuthenticatorAuthenticate],
let authenticatorField = remediation["authenticator"],
let chosenOption = authenticatorField.options?.first(where: { option in
option.label == "Email"
})
{
// Handle error
return
}
authenticatorField.selectedOption = chosenOption
在您的应用程序中使用字段
因为字段不仅描述了您如何渲染UI,还接受用户提供的值,这使得它在使用户填充表单数据时成为一个方便的数据占位符。一旦所有选择都已完成,您可以在修复选项上调用proceed
方法以提交他们的表单数据。
import SwiftUI
struct UsernameView: View {
@State var username: String = ""
var remediation: Remediation
var body: some View {
Form {
TextField("Username", text: $username, onCommit: {
guard let field = self.remediation["identifier"] else { return }
field.value = self.username
})
Button("Continue") {
remediation.proceed()
}
}
}
}
使用成功响应获取令牌
在接收到任何响应时,都重要的是检查isLoginSuccessful
属性以确定用户是否能够完成其身份验证。在这种情况下,您可以在响应上调用exchangeCode
方法来接收一个Token
。
if response.isLoginSuccessful,
let token = try await response.exchangeCode()
{
// Use the token
}
错误处理
错误是身份验证过程中自然的一部分,特别是在用户可能误打用户名、忘记密码或提供错误的短信验证码的情况下。这类错误不会导致返回到方法返回对象的Error
对象。这些被视为成功响应,因为没有在与Okta通信、处理客户端凭据或构建URLRequest
时发生错误。返回到这些关闭的通常是网络请求处理错误,或者是修复提供的不完整表单数据。这些错误的errorDescription
应描述发生的错误。
所有其他非致命错误都通过关联到响应、修复措施或单个字段的MessageCollection
对象来报告。这些可以被认为是面向用户的错误消息,传达出了什么问题以及用户可以采取的潜在步骤继续操作。
信息收集
由于错误可能在流程和修复表单中的多个地方发生,因此这些消息的位置可能会有所不同;例如,如果在注册新用户时电子邮件地址无效,错误消息可能会绑定到表单字段本身。
为了方便起见,根级信息收集(可通过Response.messages
属性访问)将所有嵌套消息聚合到allMessages
属性中。
response.messages.allMessages.forEach { message in
// Display / process the message
}
否则,根级错误消息(例如,适用于整个身份验证会话的消息,而不是限于单个修复措施或表单字段的消息),通常可以通过消息集合访问。
response.messages.forEach { message in
// Display the root-level message
}
如果消息绑定到单个表单字段,它将存在于根级信息集合的allMessages
属性中,以及在字段的messages
属性中。
guard let identifierField = response?.remediations[.identify]?.identifier else { return }
if !identifierField.messages.isEmpty {
identifierField.messages.forEach { message in
// Display the field-level message
}
}
非恢复性错误状态
有时身份验证会话可能不再有效,错误状态无法恢复。例如,如果会话已过期。
在没有剩余的修复措施的情况下,可以确定这种情况。这意味着用户无法采取任何操作来修复其身份验证会话,应创建新的会话。
if response.remediations.isEmpty {
// Handle non-recoverable error
}
使用 InteractionCodeFlowDelegate 响应事件
InteractionCodeFlow
类支持使用代理来集中处理和响应。这允许单个代理对象拦截客户端接收到的所有响应、错误和令牌,无论初始调用发生在您的应用程序的哪个位置。
以下示例展示了如何使用代理实现简单的用户名/密码认证。
class LoginManager: InteractionCodeFlowDelegate {
private let flow = InteractionCodeFlow(issuer: issuerUrl,
clientId: clientId,
scopes: scopes,
redirectUri: redirectUrl)
let username: String
let password: String
init() {
flow.add(delegate: self)
}
func start() {
flow.start { result in
switch result {
case .failure(let error):
// Handle the error
case .success(_): break
}
}
}
func authentication<Flow>(flow: Flow, received response: Response) where Flow : InteractionCodeFlow {
// If login is successful, immediately exchange it for a token.
guard !response.isLoginSuccessful else {
response.exchangeCode()
return
}
// Identify the user
if let remediation = response.remediations[.identify] {
remediation["identifier"]?.value = username
remediation["credentials.passcode"]?.value = password
remediation.proceed()
}
// If the password is requested on a separate "page", supply it there.
else if let remediation = response.remediations[.challengeAuthenticator],
response.authenticators.current?.type == .password
{
remediation["credentials.passcode"]?.value = password
remediation.proceed()
}
// Handle other scenarios / remediation states here...
}
func authentication<Flow>(flow: Flow, received token: Token) where Flow : InteractionCodeFlow {
// Login succeeded, with the given token.
do {
try Credential.store(token)
} catch {
// Handle with errors
}
}
func authentication<Flow>(flow: Flow, received error: OAuth2Error) {
// Handle the error
}
}
开发
保护测试配置
该存储库包含 Samples/Shared
目录中的通用配置文件,该目录用于向自动化测试和示例应用程序公开测试凭据。
为防止意外修改此文件,建议在克隆此存储库后执行以下命令:
git config core.hooksPath ./.githooks
这将运行检查,在提交更改之前确保不会修改机密测试配置。
运行测试
要执行端到端测试,请编辑 Samples/Shared/TestCredentials.xcconfig
,使其与在 先决条件 中指定的配置匹配。接下来,您可以运行 okta-idx-ios
和 EmbeddedAuth
(在 Samples/EmbeddedAuthWithSDKs 目录中)的测试目标。
已知的Issue
贡献
我们很高兴接受贡献和PR!请参阅贡献指南了解如何构建贡献。