Trifork Identity Manager iOS

iOS-9.0

此框架专为 Trifork Identity Manager 而设计。

示例

请在此处查看我们完整实现的示例 (SwiftUI)

https://github.com/trifork/TIM-Example-iOS

设置

安装

将此仓库添加到您的 SPM 📦

https://github.com/trifork/TIM-iOS

设置配置

在使用 TIM 中的任何函数或属性之前,您必须通过调用 configure 方法来配置框架(通常您希望在应用程序启动时执行此操作)

import TIM
import AppAuth // Required for scopes

let config = TIMConfiguration(
    timBaseUrl: URL(string: "<TIM base URL>")!,
    realm: "<realm>",
    clientId: "<clientId>",
    redirectUri: URL(string:"<urlScheme>:/")!,
    scopes: [ OIDScopeOpenID, OIDScopeProfile ],
    encryptionMethod: .aesGcm
)
TIM.configure(configuration: config)

要使用依赖项的自定义实现来配置 TIM,请参阅自定义设置说明

URL scheme

设置您的 URL scheme 或通用链接以接收登录重定向:Apple 文档

根据您的生命周期处理,您应该在以下回调之一中处理 URL 请求

SceneDelegate 示例

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
    for url: URL in URLContexts.map({ $0.url }) {
        TIM.auth.handleRedirect(url: url)
    }
}

Info.plist 中的 FaceID 权限

如果您正在使用 TIM 的生物识别功能,请不要忘记在您的 Info.plist 中设置 NSFaceIDUsageDescription Apple 文档 键。

常用用例

以下示例使用 TIMCombine 接口,它返回 Future 类。 如果您正在开发部署目标低于 iOS 13 的应用程序,则存在相同的接口,但使用完成闭包(尽管这些接口已从 iOS 13 中弃用)。

1. 注册 / OIDC 登录

所有用户都必须通过 OpenID Connect 登录进行注册。 这是通过以下方式完成的

TIM.auth.performOpenIDConnectLogin(presentingViewController: topViewController)
    .sink { (completion) in
        switch completion {
        case .failure(let error):
            print("Failed to perform OpenID Connect login: \(error.localizedDescription)")
        case .finished:
            break
        }
    } receiveValue: { (accessToken) in
        print("Successfully logged in, access and refresh token is now available. \nAT:\n\(accessToken)")
    }
    .store(in: &futureStorage)

2. 设置密码

为了避免用户每次需要有效会话时都进行 OpenID Connect 登录,您可以提供密码,这将允许您保存刷新令牌的加密版本,这样用户只需提供密码即可获得有效的访问令牌。

用户必须先成功执行 OpenID Connect 登录才能设置密码,因为刷新令牌必须可用。

guard let refreshToken = TIM.auth.refreshToken else {
    return
}

// UserId can be retrieved from the refresh token: `refreshToken.userId`

TIM.storage.storeRefreshToken(refreshToken, withNewPassword: password)
    .sink { (completion) in
        switch completion {
        case .failure(let error):
            print("Failed to store refresh token: \(error.localizedDescription)")
        case .finished:
            break
        }
    } receiveValue: { (keyId) in
        // TIM has saved the keyId for the userId of the refresh token - you don't need to do anything with the keyId at this point unless you are doing something custom work with TIMEncryptedStorage.
        print("Saved refresh token for keyId: \(keyId)")
    }
    .store(in: &futureStore)

3. 启用生物识别登录

在用户创建密码后,您可以启用生物识别访问以进行登录。 您将需要用户的密码和来自刷新令牌的 userId 才能执行此操作。

userId 可以从刷新令牌中检索:TIM.auth.refreshToken?.userId

TIM.storage.enableBiometricAccessForRefreshToken(password: password, userId: userId)
    .sink(
        receiveCompletion: { (completion} in 
            switch result {
            case .finished:
                print("Successfully enabled biometric login for user.")
            case .failure(let error):
                print("Whoops, something went wrong: \(error.localizedDescription)")
            }
        },
        receiveValue: { _ in }
    )
    .store(in: &futureStore)

4. 使用密码/生物识别登录

您必须提供希望登录的用户的用户 ID(这允许多个用户在同一设备上登录)。

如果之前启用了生物识别,用户可以使用生物识别,否则您必须提供密码。 您可以设置 storeNewRefreshToken 来控制系统是否应在成功登录时更新刷新令牌。 强烈建议 存储新的刷新令牌,因为它会在用户每次登录时保持续订用户的会话。 虽然,如果您有不想更新它的情况,您可以将其设置为 false。

密码和生物识别可以使用相同的完成处理,如下例所示。

// Login with password
TIM.auth.loginWithPassword(userId: userId, password: password, storeNewRefreshToken: true)
    .sink(
        receiveCompletion: handleResultCompletion,
        receiveValue: { _ in })
    .store(in: &futureStore)
    

// Login with biometrics
TIM.auth.loginWithBiometricId(
    userId: userId, 
    storeNewRefreshToken: true, 
    willBeginNetworkRequests: {
        // BioID succeeded, show loading indicator -> TIM will initiate network requests right now. 
    })
    .sink(
        receiveCompletion: handleResultCompletion,
        receiveValue: { _ in })
    .store(in: &futureStore)

// Completion handling
func handleResultCompletion(_ completion: Subscribers.Completion<TIMError>) {
    switch completion {
    case .failure(let error):
        print("Failed to login: \(error.localizedDescription)")
        
        switch error {
        case .storage(let storageError):
            switch storageError {
            case .incompleteUserDataSet:
                // Reset user! We cannot recover from this state!
                break
            case .encryptedStorageFailed:
                // Note that this is a simplified error handling, which uses the Bool extensions to avoid huge switch statements.
                // If you want to handle errors the right way, you should look into all error cases and decide which you need specific
                // error handling for. The ones you see here are the most common ones, which are very likely to happen.
                if storageError.isWrongPassword() {
                    // Handle wrong password
                } else if storageError.isKeyLocked() {
                    // Handle key locked (three wrong password logins)
                } else if storageError.isBiometricFailedError() {
                    // Bio failed or was cancelled, do nothing.
                } else if storageError.isKeyServiceError() {
                    // Something went wrong while communicating with the key service (possible network failure)
                } else {
                    // Something failed - please try again.
                }
            }
        case .auth(let authError):
            if case TIMAuthError.refreshTokenExpired = authError {
                // Refresh Token has expired.
            }
        }
    case .finished:
        print("Successfully logged in!")
    }
}

5. 使用数据和会话

JWT 数据

令牌类型为 JWT,它只是 Stringtypealias。 该框架具有 JWT 的扩展,允许您直接从令牌获取以下数据

用户

该框架会跟踪已创建密码并存储加密刷新令牌的用户。

TIM.storage.availableUserIds 将返回可用刷新令牌(sub 字段)中的标识符列表。 与用户相关的任何其他数据以及 ID 与用户数据之间的映射都由您负责。 TIM 将仅跟踪令牌中的标识符。

刷新令牌

在大多数情况下,您不必担心您的刷新令牌,因为 TIM 方法会为您处理此问题。 如果您处于需要它的情况下,可以从 storage 访问它

storage.getStoredRefreshToken(userId: userId, password: password)
    .sink(
        receiveCompletion: { _ in },
        receiveValue: { (rt) in
          //Valid refresh token!
        })
    .store(in: &futureStorage)

访问令牌

TIM 确保您的访问令牌始终有效并自动刷新。 这也是为什么 TIM.auth.accessToken() 是一个异步函数的原因。

大多数情况下,当令牌可用时,TIM 会立即完成调用,而在需要更新令牌时会稍慢一些。

您应该避免将访问令牌的值分配给属性,而是在需要时始终使用此函数,以确保令牌有效。

TIM.auth.accessToken()
    .sink(
        receiveCompletion: { _ in },
        receiveValue: { [weak self] (at) in
          //Valid access token!
        })
    .store(in: &futureStorage)

6. 注销

您可以注销用户,这将丢弃当前的访问令牌和刷新令牌,这样您将必须通过登录再次加载它。

TIM.auth.logout()

7. 删除用户

您可以删除为用户标识符存储的所有数据,这样刷新令牌将不再可用,并且该用户将不再存在于 availableUserIds 集中。 通常,在这种情况下您也希望注销

TIM.auth.logout() // Logout of current session
TIM.storage.clear(userId: theUserId) // Delete the stored user data

8. 启用后台超时

您可以配置 TIM 以监控应用程序在后台的时间,并在超过所需持续时间时使其自动注销。

1. The user logs in (background monitor timeout is set to 5 minutes)
2. The user sends the app to the background
3. The user opens the app after 6 minutes
4. TIM automatically calls logout, which invalidates the current session and invokes the timeout callback.
TIM.auth.enableBackgroundTimeout(durationSeconds: 60 * 5) {
    // Handle the user log out event, e.g. present login screen.
}

了解错误

由于不同的依赖项,TIM 可能会抛出大量错误。 所有错误的共同点是,它们都包装在 TIMError.auth()TIMError.storage() 类型中,具体取决于抛出错误的区域。 这些错误将包含来自框架内部的其他错误,并且其中有几个级别。

大多数错误都在帮助您作为开发人员找出您可能配置错误的地方。 一旦一切都在设置中正确配置,则只有一小部分错误,这些错误需要作为特定错误处理

// Refresh token has expired
TIMError.auth(TIMAuthError.refreshTokenExpired)

// The user pressed cancel in the safari view controller during the OpenID Connect login
TIMError.auth(TIMAuthError.safariViewControllerCancelled)

TIMError.storage(
    TIMStorageError.encryptedStorageFailed(
        TIMEncryptedStorageError.keyServiceFailed(TIMKeyServiceError.badPassword)
    )
) 

TIMError.storage(
    TIMStorageError.encryptedStorageFailed(
        TIMEncryptedStorageError.keyServiceFailed(TIMKeyServiceError.keyLocked)
    )
) 

由于 TIMKeyServiceError 深入到错误结构中,因此在 TIMStorageError 类型上有一些简写

if storageError.isKeyLocked() {
    // Handle key locked (happens on wrong password three times in a row)
}
if storageError.isWrongPassword() {
    // Handle wrong password
}
if storageError.isKeyServiceError() {
    // The communication with the KeyService failed. E.g. no internet connection.
}
if storageError.isBiometricFailedError() {
    // Handle biometric failed/was cancelled scenario.
}

当然,应该仍然处理其他错误,但可以以更通用的方式处理它们,因为它们可能是由网络问题、服务器更新或其他不可预测的情况引起的。

自定义设置配置

您可以通过注入您自己定义的协议实现,使用自定义依赖项配置 TIM。 这适用于在 TIM 的定制版本上运行的特定项目。

免责声明: 适用于与 TIM 托管产品一起使用。 如果您走这条路,最好知道自己在做什么 😅

以下是如何使用默认实现对 TIM 依赖项进行自定义配置的示例。 通过实现所需的协议,您可以将默认实现替换为您自己的实现。

let keyServiceConfig = TIMKeyServiceConfiguration(
    realmBaseUrl: "<TIM base URL>/auth/realms/<realm>",
    version: .v1
)
let openIdConfiguration = TIMOpenIDConfiguration(
    issuer: URL(string: "<TIM base URL>/auth/realms/<realm>")!,
    clientId: "<clientId>",
    redirectUri: URL(string: "<urlScheme>:/")!,
    scopes: [OIDScopeOpenID, OIDScopeProfile]
)

let encryptedStorage = TIMEncryptedStorage(
    secureStorage: TIMKeychain(),
    keyService: TIMKeyService(
        configuration: keyServiceConfig),
    encryptionMethod: .aesGcm
)
let dataStorage = TIMDataStorageDefault(encryptedStorage: encryptedStorage)
let auth = TIMAuthDefault(
    dataStorage: dataStorage,
    openIdController: AppAuthController(openIdConfiguration),
    backgroundMonitor: TIMAppBackgroundMonitorDefault()
)
TIM.configure(
    dataStorage: dataStorage,
    auth: auth,
    customLogger: TIMLogger()
)

上述自定义配置等效于此默认配置

let config = TIMConfiguration(
    timBaseUrl: URL(string: "<TIM base URL>")!,
    realm: "<realm>",
    clientId: "<clientId>",
    redirectUri: URL(string: "<urlScheme>:/")!,
    scopes: [OIDScopeOpenID, OIDScopeProfile],
    encryptionMethod: .aesGcm
)
TIM.configure(configuration: config)

架构

TIM 依赖于 AppAuthTIMEncryptedStorage,并将其用法包装用于常见用例(请参阅以上部分),以便可以轻松管理注册、登录和加密存储。

存储

TIM.storage: TIMDataStorage 处理所有存储操作,包括加密和原始数据到安全存储(默认为 iOS Keychain)。

这在很大程度上取决于 TIMEncryptedStorage 包,该包与 TIM KeyService 通信,以处理基于用户选择的密码和生物识别访问(如果启用)的加密。

认证

TIM.auth: TIMAuth 通过 AppAuth 框架处理所有 OpenID Connect 操作。 其主要目的是处理访问令牌和刷新令牌以及两者的续订。 TIMAuth 依赖于 TIMDataStorage 来存储新的刷新令牌。

TIMEncryptedStorage

TIM 依赖于 TIMEncryptedStorage 进行加密数据存储以及通过 TouchID/FaceID 进行访问:https://github.com/trifork/TIMEncryptedStorage-iOS

AppAuth

TIM 依赖于 AppAuth 进行 OpenID Connect 操作:https://github.com/openid/AppAuth-iOS

测试

TIM 被设计为可测试的,这样您就可以模拟您想要模拟的框架部分。 该框架包含一个自定义的 configure 方法,允许您完全自定义框架的内部实现

TIM.configure(dataStorage: TIMDataStorage, auth: TIMAuth, customLogger: TIMLoggerProtocol?)

TIM 中的每个依赖项都构建在协议之上,这样您就可以实现自己的模拟类进行测试。

⚠️ 注意:configure 方法允许您更改 TIM 行为。 我们强烈建议您仅将上述 configure 方法用于测试!


Trifork Logo