OAuth2

Build Status License

用于 macOSiOStvOS 的 OAuth2 框架,使用 Swift 5 编写。

OAuth2 需要 Xcode 12.4,构建的框架可用于 OS X 10.15iOS 12 及更高版本。欢迎提交 pull request,请参阅 CONTRIBUTING.md

Swift 版本

由于 Swift 语言不断发展,我采用了镜像 Swift 版本的版本控制方案:框架版本的前两位数字始终是库兼容的 Swift 版本,请参阅 版本发布。与全新 Swift 版本兼容的代码可在单独的功能分支上找到,分支名称会适当命名。

使用方法

要在您自己的代码中使用 OAuth2,请在您的源文件中以 import OAuth2 开头。

在 OAuth2 中,有不同类型的流程。此库支持所有这些流程,请确保您为您的用例和授权服务器使用正确的流程。典型的代码授权流程在下面用于演示目的。其他流程的步骤大致相同,只是实例化不同的子类并使用不同的客户端设置。

仍然无法工作?请参阅特定于站点的特性

1. 使用设置字典实例化 OAuth2

在此示例中,您将构建一个 GitHub 的 iOS 客户端,因此以下代码将位于您的某个视图控制器中,可能是 app delegate。

let oauth2 = OAuth2CodeGrant(settings: [
    "client_id": "my_swift_app",
    "client_secret": "C7447242",
    "authorize_uri": "https://github.com/login/oauth/authorize",
    "token_uri": "https://github.com/login/oauth/access_token",   // code grant only
    "redirect_uris": ["myapp://oauth/callback"],   // register your own "myapp" scheme in Info.plist
    "scope": "user repo:status",
    "secret_in_body": true,    // Github needs this
    "keychain": false,         // if you DON'T want keychain integration
] as OAuth2JSON)

看到那些 redirect_uris 吗?您可以使用您想要的 scheme,但是您必须 a) 在您的 Info.plist 中声明您使用的 scheme,并且 b) 在您连接的授权服务器上注册完全相同的 URI。

请注意,从 iOS 9 开始,您应该使用 通用链接 作为您的重定向 URL,而不是自定义应用 scheme。这可以防止其他人重复使用您的 URI scheme 并拦截授权流程。
如果您以 iOS 12 及更高版本为目标,您应该使用 ASWebAuthenticationSession,这使得使用您自己的本地重定向 scheme 变得安全。

想要避免切换到 Safari 并弹出 SafariViewController 或 NSPanel?设置这个

oauth2.authConfig.authorizeEmbedded = true
oauth2.authConfig.authorizeContext = <# your UIViewController / NSWindow #>

需要指定单独的刷新令牌 URI?您可以在设置字典中设置 refresh_uri。如果指定,库将使用您指定的 refresh_uri 刷新访问令牌,否则它将使用 token_uri

需要调试?使用 .debug 甚至 .trace 记录器

oauth2.logger = OAuth2DebugLogger(.trace)

更多信息请参阅下面的高级设置

2. 让数据加载器或 Alamofire 接管

从版本 3.0 开始,有一个 OAuth2DataLoader 类,您可以用来从 API 检索数据。它会在需要时自动启动授权,并确保即使您有多个调用正在进行,它也能正常工作。有关如何配置授权的详细信息,请参阅下面的步骤 4,在本例中,我们将使用“嵌入式”授权,这意味着如果用户需要登录,我们将在 iOS 上显示一个 SFSafariViewController。

此 wiki 页面包含您轻松将 OAuth2 与 Alamofire 一起使用的所有内容

let base = URL(string: "https://api.github.com")!
let url = base.appendingPathComponent("user")

var req = oauth2.request(forURL: url)
req.setValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept")

self.loader = OAuth2DataLoader(oauth2: oauth2)
loader.perform(request: req) { response in
    do {
        let dict = try response.responseJSON()
        DispatchQueue.main.async {
            // you have received `dict` JSON data!
        }
    }
    catch let error {
        DispatchQueue.main.async {
            // an error occurred
        }
    }
}

3. 确保您拦截回调

当使用 OS 浏览器或 iOS 9+ Safari 视图控制器时,您将需要在您的 app delegate 中拦截回调,并让 OAuth2 实例处理完整的 URL

func application(_ app: UIApplication,
              open url: URL,
               options: [UIApplicationOpenURLOptionsKey: Any] = [:]) -> Bool {
    // you should probably first check if this is the callback being opened
    if <# check #> {
        // if your oauth2 instance lives somewhere else, adapt accordingly
        oauth2.handleRedirectURL(url)
    }
}

对于 iOS 13,在 SceneDelegate.swift 中进行回调

func scene(_ scene: UIScene, openURLContexts URLContexts: Set<UIOpenURLContext>) {
	if let url = URLContexts.first?.url {
		AppDelegate.shared.oauth2?.handleRedirectURL(url)
	}
}

您都设置好了!


如果您想深入研究或自己进行授权,请看这里

4. 手动授权用户

默认情况下,如果不存在访问令牌或在 keychain 中,将使用 OS 浏览器进行授权。从 iOS 12 开始,在 iOS 上启用嵌入式授权时将使用 ASWebAuthenticationSession(以前,从 iOS 9 开始,改为使用 SFSafariViewController)。

要启动授权,请调用 authorize(params:callback:),或者,要使用嵌入式授权,请使用便捷方法 authorizeEmbedded(from:callback:)

登录屏幕将仅在需要时显示(有关详细信息,请参阅下面的_手动执行授权_),并在成功时自动关闭登录屏幕。有关其他选项,请参阅高级设置

oauth2.authorize() { authParameters, error in
    if let params = authParameters {
        print("Authorized! Access token is in `oauth2.accessToken`")
        print("Authorized! Additional parameters: \(params)")
    }
    else {
        print("Authorization was canceled or went wrong: \(error)")   // error will not be nil
    }
}

// for embedded authorization you can simply use:
oauth2.authorizeEmbedded(from: <# presenting view controller / window #>) { ... }

// which is equivalent to:
oauth2.authConfig.authorizeEmbedded = true
oauth2.authConfig.authorizeContext = <# presenting view controller / window #>
oauth2.authorize() { ... }

不要忘记,当使用 OS 浏览器或 iOS 9+ Safari 视图控制器时,您将需要在您的 app delegate 中拦截回调。这在上面的步骤 2 中显示。

有关如何在 Mac 上执行此操作的详细信息,请参阅下面的手动执行授权

5. 接收回调

一切完成后,将调用回调,或者 使用非 nil 的 authParameters 字典(可能为空!),或者 使用错误。访问令牌和刷新令牌及其过期日期已被提取,并作为 oauth2.accessTokenoauth2.refreshToken 参数提供。如果您希望提取其他信息,您只需要检查 authParameters 字典。

对于下面概述的高级用法,您的 OAuth2 实例上可以使用 afterAuthorizeOrFail 代码块。internalAfterAuthorizeOrFail 闭包顾名思义,是为内部目的提供的 – 它暴露出来是为了子类化和编译原因,您不应该乱用它。从 3.0.2 版本开始,您不能再使用 onAuthorizeonFailure 回调属性,它们已被完全删除。

6. 发出请求

您现在可以获得一个 OAuth2Request,这是一个已经签名的 MutableURLRequest,用于从您的服务器检索数据。此请求使用访问令牌设置 Authorization 标头,如下所示:Authorization: Bearer {您的访问令牌}

let req = oauth2.request(forURL: <# resource URL #>)
// set up your request, e.g. `req.HTTPMethod = "POST"`
let task = oauth2.session.dataTaskWithRequest(req) { data, response, error in
    if let error = error {
        // something went wrong, check the error
    }
    else {
        // check the response and the data
        // you have just received data with an OAuth2-signed request!
    }
}
task.resume()

当然,您可以将您自己的 URLSession 与这些请求一起使用,您不必使用 oauth2.session;使用 OAuth2DataLoader,如步骤 2 所示,或将其交给 Alamofire这里有您轻松将 OAuth2 与 Alamofire 一起使用的所有内容

7. 取消授权

您可以通过调用 oauth2.abortAuthorization() 随时取消正在进行的授权。这将取消正在进行的请求(如代码交换请求)或在您等待用户在网页上登录时调用回调。后者将关闭嵌入式登录屏幕或将用户重定向回应用程序。

8. 重新授权

始终在执行请求之前调用 oauth2.authorize() 是安全的。您也可以在您的应用程序再次变为活动状态后的第一个请求之前执行授权。或者,您可以始终在您的请求中拦截 401 错误,并在重新尝试请求之前再次调用授权。

9. 注销

如果您将令牌存储到 keychain 中,您可以调用 forgetTokens() 来丢弃它们。

但是您的用户可能仍然登录到网站,因此在下一次 authorize() 调用时,web 视图可能会出现并立即消失。当在 iOS 8 上使用内置 web 视图时,可以使用以下代码片段来丢弃应用程序创建的任何 cookie。使用较新的 SFSafariViewController,或在浏览器中执行的登录,最好直接打开注销页面,以便用户看到注销发生。

let storage = HTTPCookieStorage.shared
storage.cookies?.forEach() { storage.deleteCookie($0) }

手动执行授权

authorize(params:callback:) 方法将

  1. 检查是否已有一个授权调用正在运行,如果是,它将中止并返回 OAuth2Error.alreadyAuthorizing 错误
  2. 检查是否已存在尚未过期的访问令牌(或在 keychain 中),如果否
  3. 检查是否可用的刷新令牌,如果找到
  4. 尝试使用刷新令牌获取新的访问令牌,如果失败
  5. 通过使用 authConfig 设置来确定如何向用户显示授权屏幕,从而启动 OAuth2 流程

您的 oauth2 实例将使用自动创建的 URLSession,使用 ephemeralSessionConfiguration() 配置进行其请求,在 oauth2.session 上公开。您可以将 oauth2.sessionConfiguration 设置为您自己的配置,例如,如果您想更改超时值。如果您愿意,您还可以将 oauth2.sessionDelegate 设置为您自己的会话委托。

wiki 中有 authorize() 方法的完整调用图。如果您不希望这种自动化,则显示和隐藏授权屏幕的手动步骤是

嵌入式 iOS:

let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>)
oauth2.authConfig.authorizeEmbeddedAutoDismiss = false
let web = try oauth2.authorizer.authorizeSafariEmbedded(from: <# view controller #>, at: url)
oauth2.afterAuthorizeOrFail = { authParameters, error in
    // inspect error or oauth2.accessToken / authParameters or do something else
    web.dismissViewControllerAnimated(true, completion: nil)
}

macOS 上的模态表单:

let window = <# window to present from #>
let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>)
let sheet = try oauth2.authorizer.authorizeEmbedded(from: window, at: url)
oauth2.afterAuthorizeOrFail = { authParameters, error in
    // inspect error or oauth2.accessToken / authParameters or do something else
    window.endSheet(sheet)
}

macOS 上的新窗口:

let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>)
let windowController = try oauth2.authorizer.authorizeInNewWindow(at: url)
oauth2.afterAuthorizeOrFail = { authParameters, error in
    // inspect error or oauth2.accessToken / authParameters or do something else
    windowController.window?.close()
}

iOS/macOS 浏览器:

let url = try oauth2.authorizeURL(params: <# custom parameters or nil #>)
try oauth2.authorizer.openAuthorizeURLInBrowser(url)
oauth2.afterAuthorizeOrFail = { authParameters, error in
    // inspect error or oauth2.accessToken / authParameters or do something else
}

macOS

请参阅 OAuth2 示例应用程序的 AppDelegate 类,了解如何在您的 Mac 应用程序中接收回调 URL。如果授权向用户显示代码,例如使用 Google 的 urn:ietf:wg:oauth:2.0:oob 回调 URL,您可以从用户的剪贴板检索代码,并使用以下内容继续授权

let pboard = NSPasteboard.general()
if let pasted = pboard.string(forType: NSPasteboardTypeString) {
    oauth2.exchangeCodeForToken(pasted)
}

流程

根据您需要的 OAuth2 流程,您将需要使用正确的子类。有关 OAuth 基础知识的非常好的解释:OAuth 圣经

代码授权

对于完整的 OAuth 2 代码授权流程 (response_type=code),您需要使用 OAuth2CodeGrant 类。此流程通常用于可以保护其密钥的应用程序,例如服务器端应用程序,而不是分布式二进制文件。如果应用程序无法保护其密钥,例如分布式 iOS 应用程序,您将使用隐式授权,或者在某些情况下仍然使用代码授权,但省略客户端密钥。然而,从移动设备(包括客户端密钥)仍然使用代码授权已成为常见的做法。

此类完全支持这些流程,如果客户端具有非 nil 的客户端密钥,它会自动创建“Basic” Authorization 标头。这意味着您可能必须在您的设置中指定 client_secret;如果没有(例如对于 Reddit),请指定空字符串。如果站点需要在请求正文中提供客户端凭据,请将 clientConfig.secretInBody 设置为 true,如下所述。

隐式授权

隐式授权 (response_type=token) 适用于无法保护其密钥的应用程序,例如分布式二进制文件或客户端 Web 应用程序。使用 OAuth2ImplicitGrant 类接收令牌并执行请求。

在这里添加另一个代码示例会很好,但它与代码授权几乎相同。

客户端凭据

一种双腿流程,允许应用程序通过其客户端 ID 和密钥授权自身。实例化 OAuth2ClientCredentials,像往常一样在设置字典中提供 client_id,以及 client_secret – 加上您的其他配置 – 您应该一切顺利。

用户名和密码

资源所有者密码凭据授权通过 OAuth2PasswordGrant 子类支持。如上所示创建实例,设置其 usernamepassword 属性,然后调用 authorize()

设备授权

OAuth 2.0 设备授权流程在 OAuth2DeviceGrant 子类中实现。虽然此流程是为缺少浏览器以执行基于用户代理的授权或输入受限的设备设计的,但它对于不允许 启动自己的 web 服务器(环回 URL)或注册自定义 URL scheme 以完成授权代码授权流程的应用程序也非常有用。要启动设备授权流程,需要正确配置 deviceAuthorizeURL 以指向设备授权端点。通过调用 OAuth2DeviceGrant.start(useNonTextualTransmission:params:completion:) 方法,客户端获得 完成在辅助设备或系统浏览器上授权所需的所有必要详细信息

特定于站点的特性

某些站点可能不严格遵守 OAuth2 流程,从像 Facebook 那样以不同的方式返回数据,到省略强制性返回参数,例如 Instagram 等。该框架通过创建特定于站点的子类和/或配置详细信息来处理这些偏差。如果您需要传递额外的标头参数,您可以在设置字典中提供这些,如下所示

let oauth2 = OAuth2CodeGrant(settings: [
    "client_id": "...",
    ...
    "headers": ["Accept": "application/vnd.github.v3+json"],
    "parameters": ["duration": "permanent"],
] as OAuth2JSON)

高级设置

您将与 oauth2.authConfig 一起使用的主要配置是是否使用嵌入式登录

oauth2.authConfig.authorizeEmbedded = true

类似地,如果您想自己处理关闭登录屏幕(对于下面提到的较新的授权会话不可能)

oauth2.authConfig.authorizeEmbeddedAutoDismiss = false

某些站点也希望客户端 ID/密钥组合在请求正文中,而不是在 Authorization 标头中

oauth2.clientConfig.secretInBody = true
// or in your settings:
"secret_in_body": true

有时您还需要提供额外的授权参数。这可以通过 3 种方式完成

oauth2.authParameters = ["duration": "permanent"]
// or in your settings:
"parameters": ["duration": "permanent"]
// or when you authorize manually:
oauth2.authorize(params: ["duration": "permanent"]) { ... }

类似于您指定自定义 HTTP 标头的方式

oauth2.clientConfig.authHeaders = ["Accept": "application/json, text/plain"]
// or in your settings:
"headers": ["Accept": "application/json, text/plain"]

某些站点(例如 Slack)根据支持的浏览器版本验证 User-Agent 字符串,这可能与嵌入式模式下的 WebKit 默认值不匹配。嵌入式模式 User-Agent 可以通过以下方式覆盖

oauth2.customUserAgent = "Version/15.6.1 Safari"
// or in your settings:
"custom_user_agent": "Your string of choice"

从 iOS 9 上的 2.0.1 版本开始,SFSafariViewController 将用于嵌入式授权。从 4.2 版本之后开始,在 iOS 11 (SFAuthenticationSession) 和 iOS 12 (ASWebAuthenticationSession) 上,您可以选择加入这些较新的授权会话视图控制器

oauth2.authConfig.ui.useAuthenticationSession = true

要恢复到旧的自定义 OAuth2WebViewController,您不应该这样做,因为 ASWebAuthenticationSession 更安全

oauth2.authConfig.ui.useSafariView = false

要在 iOS 8 及更低版本上使用 OAuth2WebViewController 时自定义返回按钮

oauth2.authConfig.ui.backButton = <# UIBarButtonItem(...) #>

有关 keychainPKCE 的设置,请参见下文。

与 Alamofire 一起使用

当使用 Alamofire v4 或更新版本以及 OAuth2 v3 或更新版本时,您将获得最佳体验

动态客户端注册

支持 动态客户端注册。如果在设置期间设置了 registration_url 但未设置 client_id,则 authorize() 调用会在继续实际授权之前自动尝试注册客户端。从注册返回的客户端凭据将存储到 keychain 中。

OAuth2DynReg 类负责处理客户端注册。如果您需要,您可以手动使用其 register(client:callback:) 方法。注册参数取自客户端的配置。

let oauth2 = OAuth2...()
oauth2.registerClientIfNeeded() { error in
    if let error = error {
        // registration failed
    }
    else {
        // client was registered
    }
}
let oauth2 = OAuth2...()
let dynreg = OAuth2DynReg()
dynreg.register(client: oauth2) { params, error in
    if let error = error {
        // registration failed
    }
    else {
        // client was registered with `params`
    }
}

PKCE

PKCE 支持由 useProofKeyForCodeExchange 属性和设置字典中的 use_pkce 键控制。默认情况下禁用。启用后,将为每个授权请求生成一个新的代码验证器字符串。

Keychain

此框架可以透明地使用 iOS 和 macOS keychain。它由 useKeychain 属性控制,可以在初始化期间使用 keychain 设置字典键禁用。由于这是默认启用的,如果您在初始化期间将其关闭,keychain 将查询与授权 URL 相关的令牌和客户端凭据。如果您在初始化将其关闭,keychain 将查询现有令牌,但新令牌将不会写入 keychain。

如果您想从 keychain 中删除令牌,即完全注销用户,请调用 forgetTokens()。如果您已动态注册您的客户端并希望重新开始,您可以调用 forgetClient()

理想情况下,访问令牌会附带一个“expires_in”参数,告诉您令牌的有效期。如果缺少它,如果 keychain 中找到令牌,框架仍将使用这些令牌,而不会重新执行 OAuth 流程。如果访问令牌已过期,但框架仍从 keychain 中拉取了它,您将需要拦截 401 错误并重新授权。可以通过在设置中提供 token_assume_unexpired: false 或将 clientConfig.accessTokenAssumeUnexpired 设置为 false 来关闭此行为。

这些是您可以用于更多控制的设置字典键

安装

您可以使用 Swift Package ManagergitCarthage。首选方式是使用 Swift Package Manager

Swift Package Manager

在 Xcode 11 及更高版本中,从 Xcode 菜单中选择“File”,然后选择“Swift Packages”»“Add Package Dependency...”并粘贴此 repo 的 URL:https://github.com/p2/OAuth2.git。选择一个版本,Xcode 应该完成剩下的工作。

Carthage

通过 Carthage 安装非常容易

github "p2/OAuth2" ~> 4.2

git

使用 Terminal.app,克隆 OAuth2 仓库,最好克隆到您的应用程序项目的子目录中

$ cd path/to/your/app
$ git clone --recursive https://github.com/p2/OAuth2.git

如果您使用 git,您需要将其添加为子模块。克隆完成后,在 Xcode 中打开您的应用程序项目,并将 OAuth2.xcodeproj 添加到您的应用程序

Adding to Xcode

现在将框架链接到您的应用程序

Linking

需要这三个步骤来

  1. 使您的应用程序也构建框架
  2. 将框架链接到您的应用程序中
  3. 在分发时将框架嵌入到您的应用程序中

许可证

此代码根据 Apache 2.0 许可证发布,这意味着您可以在开源和闭源项目中使用它。由于没有 NOTICE 文件,因此您无需在您的产品中包含任何内容。