Authentication Services框架详细解析 (七) —— 使用ASWebAuthenticationSession实现OAuth(一)

版本记录

版本号 时间
V1.0 2021.04.02 星期五

前言

Authentication Services框架为用户提供了授权身份认证Authentication服务,使用户更容易登录App和服务。下面我们就一起来看一下这个框架。感兴趣的看下面几篇文章。
1. Authentication Services框架详细解析 (一) —— 基本概览(一)
2. Authentication Services框架详细解析 (二) —— 使用Sign in with Apple实现用户身份验证(一)
3. Authentication Services框架详细解析 (三) —— 密码的自动填充(一)
4. Authentication Services框架详细解析 (四) —— 使用Account Authentication Modification Extension提升账号安全(一)
5. Authentication Services框架详细解析 (五) —— 使用web authentication session对App中的用户进行身份验证(一)
6. Authentication Services框架详细解析 (六) —— Web Browser App中支持Single Sign-On(一)

开始

首先看下主要内容:

了解什么是OAuth以及如何使用ASWebAuthenticationSession实施它。内容来自翻译。

接着看下写作环境:

Swift 5, iOS 14, Xcode 12

接着,下面就是正文了。

您是否需要针对第三方应用程序对用户进行身份验证?也许您的客户要求您使用OAuth标准实施这种机制?

如果您在企业环境中工作,并且客户端使用Active Directory来管理用户,并使用OktaPing Federate来控制第三方应用程序与受保护资源的交互方式,该怎么办?

在本教程中,您将创建一个第三方GitHub应用,您可以使用ASWebAuthenticationSession通过OAuth标准对第三方GitHub应用进行身份验证,并显示用户拥有的存储库列表。在此过程中,您将了解:

  • OAuth
  • Session tokens
  • Ephemeral sessions

在针对服务器进行身份验证的传统客户端应用程序中,服务器存储用户名和密码以对用户进行身份验证并允许用户访问。当没有第三方参与时,这很好。

但是,当GitHub之类的应用程序想要添加对第三方应用程序的支持时,会发生什么?

那是事情变得棘手的地方,并且存在一些缺点。没有OAuth,可能会出现以下几个问题:

  • 为了避免不断要求用户提供凭据,第三方应用必须在某个地方存储和管理它们。
  • 密码身份验证方法更容易受到攻击。
  • GitHub无法限制,撤销或限制对第三方应用程序的访问,因为它们具有用户的完整凭据。
  • 如果第三方应用程序被盗用,那么用户的GitHub帐户也会被盗用。

GitHub(服务器应用程序)如何为您(第三方应用程序)提供对其受保护资源的访问权,而又不赋予其完全,不受限制的访问权?这就是OAuth的用武之地。


Understanding OAuth

Internet Engineering Task Force’s (IETF) 网站上,这是OAuth 2.0标准的定义:

OAuth 2.0授权框架使第三方应用能够通过协调资源所有者和HTTP服务之间的批准交互,或者通过允许第三方应用来代表资源所有者获得对HTTP服务的有限访问权限,或者代表资源所有者代表自己获得访问权限。”

注意:OAuth 1.0标准已过时,已由本教程介绍的OAuth 2.0取代。

您的第三方应用程序不会存储任何用户凭据,也不会直接处理对用户进行身份验证的操作。您希望拥有强大的基础架构和安全性的GitHub为您服务。

您希望第三方应用程序执行的操作是允许用户使用自己的GitHub凭据登录,以访问GitHub中的特定资源。

另一种情况是使用GitHub进行用户身份验证 —— 不必花费太多时间在实现和安全细节上 —— 但在验证用户身份之外不访问任何GitHub资源。

这就是为什么应用程序和网站经常使用带有按钮的登录功能来获取第三方服务(如FacebookAppleGoogle)的原因。他们将这些事务留给那些各方来处理服务器安全性和登录基础结构。

1. Authorization Versus Authentication

OAuth通过将身份验证过程(authentication process)与授权过程(authorization process)分开来提供帮助。

但是到底有什么区别呢?他们不一样吗?在多年交替使用这两个术语后,您可能会感到惊讶,但事实并非如此。

  • Authentication - 身份验证:您是谁。
  • Authorization - 授权:服务已授予您哪些权限。

在本教程中,GitHub将为您的应用程序提供某些访问和许可:authorization。但是,由用户来验证自己并验证他们是谁:authentication

2. OAuth Roles

到目前为止,您已经了解了什么是OAuth以及身份验证和授权之间的区别。在幕后,OAuth还有其他一些重要的概念需要学习。首先是角色roles

OAuth定义了四个角色:

  • Resource Owner - 资源所有者:可以授予受保护资源访问权限的任何人。
  • Resource Server - 资源服务器:包含受保护资源的服务器。
  • Client - 客户端:尝试以授权方式并代表resource owner访问受保护资源的应用程序。
  • Authorization Server - 授权服务器:成功进行身份验证和授权后,将access token提供给客户端。

3. The OAuth Flow

OAuth标准将以下内容定义为第三方应用程序的典型流程。这是本教程中要实现的:

  • 1) 您的应用程序要求access token,这是在针对resource owner的请求中使用的短暂token。为此,它必须向授权服务器(authorization server)提供一个客户端ID,并允许用户使用其凭据进行身份验证。
  • 2) 授权服务器对客户端进行身份验证,然后返回该用户的access tokenrefresh token,仅在您的应用中使用。这是假设一切正常且一切顺利。
  • 3) 您的应用程序(客户端)向resource owner发出对受保护资源的请求。然后,客户端必须出示用户的access token,否则请求将失败。
  • 4) 资源所有者会验证您用户的access token。如果有效,则返回请求的资源。
  • 5) 您的应用将继续代表用户发出请求,直到access token过期。发生这种情况时,您对resource owner的下一个请求将导致无效的token错误。
  • 6) 您的应用程序可以使用refresh token(通常是寿命较长的token)来向授权服务器(authorization server)请求新的access token。适用范围和限制与以前相同。
  • 7) 如果您的refresh token仍然有效,则authorization server将为您的应用发布新的access token。如果refresh token已过期,则用户必须再次使用其凭据进行身份验证。

注意:还有其他验证方式,包括两个客户端之间的无浏览选项,或者您无权手动输入时。这些不在本教程的讨论范围之内。

OAuth很大程度上是基于Web的,这意味着大多数实现都向用户显示某种Web视图,以使他们输入其凭据。在幕后,发生了一些重定向和回调,使所有工作正常进行。不用担心,本教程将帮助您处理所有这些。

到目前为止,已有很多理论,但是现在您对OAuth及其工作原理有了更好的了解。


Creating a GitHub App

您希望您的应用程序与GitHub对话。 GitHub需要知道用户尝试从何处(您的应用程序)访问其资源,以及应该授予访问那哪些资源。

现在是时候通过您的帐户创建GitHub应用了。 这是上手的方法!

在您的Web浏览器上,打开并登录GitHub。 在右上角,单击您的profile image。 然后,单击Settings

点击Click Developer settings

点击GitHub Apps,点击New GitHub App按钮:

创建新应用时,有很多选项和设置。 您现在将熟悉它们。

将您的应用命名为AuthHub-。 为您的应用提供您选择的description

添加您应用的homepage URL首页。 本示例使用https://www.raywenderlich.com/。 如果您要创建自己的应用程序,则该应用程序应链接到您的应用程序主页,用户可以在其中获取更多信息。

1. User Authorization Settings

下一组设置围绕用户授权。 第一个选项是授权回调URL。 这是authentication provider将在用户成功进行身份验证时将其重定向到的页面。 您在此处指定的格式是您的应用程序将收听的格式,以在用户进行身份验证后收回控制权。

将以下内容用作应用程序的回调URL:

authhub://oauth-callback

这只是您定义的回调。 您可以根据需要使用其他值,但是请注意,使用ASWebAuthenticationSession时,它将更改设置过程。 目前,最简单的方法是使用上面的值,因此您可以继续进行下去。

下一个选项是一个复选框,询问是否让用户的授权tokens过期。 启用此复选框是因为您想要获取refresh token并选择使access token过期以建立更好的安全模型。

最后,有一个选项可以在安装过程中请求用户授权,该选项可以保持未选中状态,因为您的应用程序不需要它:

2. Post Installation Options

向下移动页面,有post installation options and web hooks,您可以将它们留为空白:

确保未选中Webhook下的Active复选框!

3. Permissions Settings

下一部分与权限有关。 您可以在此处指定您的应用应能够访问的信息。

Contents设置中,将访问选项更改为Read-only

最后,有一个用于安装应用程序的设置。 对于此示例,选择Only on this account

恭喜,您已配置了您的应用。 现在,点击Create GitHub App,您将看到您的应用的详细信息页面。

4. Viewing Your Results

您已经在GitHub中创建了一个具有自己的app IDclient ID的应用。 现在,您可以设置一个客户端(在本例中为AuthHub iOS应用)来连接和使用您的GitHub应用。 如果您有三个不同的应用程序(可能是iOS,AndroidWeb)连接到GitHub应用程序,则需要生成三个不同的client secret

如果您在页面顶部看到一个黄色的横幅,告诉您必须生成一个private key,请单击链接以执行此操作。

接下来,单击Generate a new client secret按钮。 复制显示的字母数字值并将其粘贴到永久位置,因为GitHub永远不会让您再次查看此信息。

请特别注意如何存储client secret,并确保不共享它。

一切都在GitHub端完成。 现在该写一些Swift代码了!


Connecting GitHub App with ASWebAuthenticationSession

打开项目材料。 在Xcode中打开启动项目。 构建并运行。

目前,轻按Sign In不会执行任何操作。您需要在项目中实现登录功能。在此之前,您将探索入门项目。

您有一个带有两个屏幕的基本项目:一个用于登录,一个用于查看存储库。此外,您还具有用于UserRepositoryNetworkRequest的模型。

这里最有趣的模型是NetworkRequest。它充当URLSession的包装器。

打开NetworkRequest.swift。您会发现一些枚举,这些枚举封装了网络层支持的HTTP方法和错误。这里更有趣的枚举enumRequestType,它列出了应用程序可以向GitHub发出的受支持请求的案例。在枚举中,您还可以使用帮助程序方法为特定的请求类型构造NetworkRequest。很方便!

有关更多信息,请查阅GitHub’s documentation on its REST API。

1. Updating the Project with GitHub App Values

为了让GitHub知道您的应用程序,您需要将GitHub应用程序的信息添加到项目中。

打开NetworkRequest.swift。在// MARK: Private Constants下,您会找到三个静态常量:

static let callbackURLScheme = "YOUR_CALLBACK_SCHEME_HERE"
static let clientID = "YOUR_CLIENT_ID_HERE"
static let clientSecret = "YOUR_CLIENT_SECRET_HERE"

将这些values替换为新创建的GitHub应用中的值。

回调URL scheme不必是您在创建GitHub应用程序时输入的完整URL,而只需是该scheme。 对于此示例,将authhub用作callbackURLScheme的字符串。

继续,NetworkRequest中的start(responseType:completionHandler :)是实际的网络请求发出的地方。 如果您的应用程序具有access token,则可以在此处为URL请求定义一些参数以及授权HTTP header

GitHub API希望您通过Authorization HTTP header发送需要授权的请求的access token。 此header的值将采用以下格式:

Bearer YOUR_TOKEN_HERE

另外,该方法使用completion handler处理所有error,并将JSON数据解析为参数中指定的原生Swift类型。

到目前为止,一切都很好!

2. Views Overview

接下来,您将拥有视图。 这是一个相当简单的应用程序,因此仅需要两个视图:SignInView.swiftRepositoriesView.swift。 不必担心,有趣的事情发生在视图模型(view models)中。

最后,您有了视图模型。

3. View Models Overview

打开RepositoriesViewModel.swift。 您可以在此处找到代码,这些代码从GitHub请求一个已登录用户的存储库列表,并将其提供给视图以显示在列表中。

应用程序中的另一个视图模型位于SignInViewModel.swift中。 这是您要添加ASWebAuthenticationSession的地方。 现在,您将进行处理。

4. Understanding ASWebAuthenticationSession

ASWebAuthenticationSession是一种API,它是Apple Authentication Services框架的一部分,可用于通过web service对用户进行身份验证。

您可以创建此类的实例,以获取开箱即用的解决方案,在该解决方案中,您可以将应用程序指向身份验证页面,允许用户进行身份验证,然后在应用程序中使用用户的authentication token接收回调 。

这个API的优点是它将适应其运行的原生平台。 对于iOS,这意味着嵌入式安全浏览器,在macOS上,您的默认浏览器(如果支持web authentication sessions)或Safari

仅需几个参数,您就可以使用自己的(或第三方)身份验证服务启动并运行,而不必使用web view从头开始实现所有功能。

5. Adding ASWebAuthenticationSession

ASWebAuthenticationSession的工作方式是,它需要authentication URL(带有来自authentication provider的所有必需参数),应用程序的callback URL scheme(以便在成功登录后返回到您的应用程序)和completion handler,以供您执行以下操作: 处理和管理authentication token

打开SignInViewModel.swift。 将以下代码添加到signInTapped()

guard let signInURL =
  NetworkRequest.RequestType.signIn.networkRequest()?.url 
else {
  print("Could not create the sign in URL .")
  return
}

您需要用于登录的URL,因此您可以使用RequestType来获取它。 如果由于某种原因该过程失败,则将error打印到控制台,并且该方法将不执行任何操作而返回。

接下来,以相同的方法添加以下内容:

let callbackURLScheme = NetworkRequest.callbackURLScheme
let authenticationSession = ASWebAuthenticationSession(
  url: signInURL,
  callbackURLScheme: callbackURLScheme) { [weak self] callbackURL, error in
  // Code will be added here next! :)
}

在这里,您首先创建一个常量来存储您的回调URL scheme,然后继续创建一个新的ASWebAuthenticationSession。 会话初始化程序期望sign-in URL and callback scheme以及completion handler作为参数。

回调URL scheme是您刚刚在NetworkRequest中替换的scheme,但是sign-in URL呢?

打开NetworkRequest.swift。 查看url()中的.signIn情况。 在这里,您可以看到成功进行登录请求所需的hostpathparameters。 值得注意的是client_id,您在不久前将其添加到此文件中。

6. Checking for Errors

打开SignInViewModel.swift,替换// Code will be added here next! :)

// 1
guard 
  error == nil,
  let callbackURL = callbackURL,
  // 2
  let queryItems = URLComponents(string: callbackURL.absoluteString)?
    .queryItems,
  // 3
  let code = queryItems.first(where: { $0.name == "code" })?.value,
  // 4
  let networkRequest =
    NetworkRequest.RequestType.codeExchange(code: code).networkRequest() 
else {
  // 5
  print("An error occurred when attempting to sign in.")
  return
}

这是一个很大的guard statement,但仍然是必要的。下面详细说明:

  • 1) 您检查是否有错误,并确认有一个有效的回调URL
  • 2) 在此,您可以通过提取回调URL的components来获取URL的queryquery items将帮助您检查此响应是否具有您需要交换tokens的授权码。
  • 3) 加载回调URL时,它包括授权代码作为query parameter
  • 4) 接下来,获取用于代码交换的NetworkRequest
  • 5) 如果这些检查中的任何一项失败,则将打印错误并从此方法返回。

构建并运行。

点击Sign In。并检查结果……什么都没有?!

7. Setting the Presentation Context Provider

authentication session正常工作之前,您还需要做两件事。第一个是设置presentation context provider

为此,您首先需要实施必要的协议。现在,通过在SignInViewModel.swift的末尾添加以下扩展来执行此操作:

extension SignInViewModel: ASWebAuthenticationPresentationContextProviding {
  func presentationAnchor(for session: ASWebAuthenticationSession)
  -> ASPresentationAnchor {
    let window = UIApplication.shared.windows.first { $0.isKeyWindow }
    return window ?? ASPresentationAnchor()
  }
}

在这里,您实现ASWebAuthenticationPresentationContextProviding来告诉您的authentication session如何呈现自己。 在幕后,ASWebAuthenticationSession与浏览器,Cookie,会话等配合使用,向用户显示登录屏幕,然后将其重定向到您的应用程序。

现在,将以下内容添加到signInTapped()的末尾:

authenticationSession.presentationContextProvider = self

您设置授权视图的位置。 这考虑了presentation portion

8. Starting the Authentication Session

在此之前,您需要做的第二件事是启动authentication session

SignInViewModel中,将以下代码添加到signInTapped()中:

if !authenticationSession.start() {
  print("Failed to start ASWebAuthenticationSession")
}

这将验证会话是否能够启动。如果不是,则另一个错误将打印到控制台。

构建并运行。

点击Sign In按钮。如果您使用的是iOS 12.4之前的iOS版本(这是authentication session API的一部分,表明您的应用要使用GitHub登录),则可能会看到alert

点击Continue

您会在GitHub登录页面上看到一个modal controller。成功登录后,模态将关闭,再次没有任何反应。

但这很好!不,是这样。

如果输入无效的凭据,则会在GitHub页面本身中看到身份验证错误,但是如果模式已关闭且控制台中未显示任何错误,则会显示该错误。那是因为GitHub用授权码回应了您的应用。

9. Handling the Authorization Code

此时,您需要交换authorization coderefresh tokens

打开SignInViewModel.swift。在signInTapped()内部,在authenticationSessioncompletion handler的末尾添加以下代码:

self?.isLoading = true
networkRequest.start(responseType: String.self) { result in
  switch result {
  case .success:
    self?.getUser()
  case .failure(let error):
    print("Failed to exchange access code for tokens: \(error)")
  }
}

发生这种情况时,您可以告诉视图正在加载某些内容,它将Sign In按钮替换为活动视图。 这样可以防止您的用户在进行现有会话的过程中再次经历此流程。 您可以使用之前获取的网络请求作为guard statement的一部分来执行token交换。

NetworkRequeststart(responseType:completionHandler :)也有一个completion handler。 在其中,您检查请求结果是否成功。 如果成功,则继续调用getUser()。 如果失败,则将错误输出到控制台。

10. Displaying the Results

在再次运行该应用程序之前,将以下代码添加到getUser()

isLoading = true
NetworkRequest
  .RequestType
  .getUser
  .networkRequest()?
  .start(responseType: User.self) { [weak self] result in
    switch result {
    case .success:
      self?.isShowingRepositoriesView = true
    case .failure(let error):
      print("Failed to get user, or there is no valid/active session: \(error)")
    }
    self?.isLoading = false
  }

此方法与您对NetworkRequest进行的操作类似,不同之处在于它可以获取已登录用户的信息。 如果请求成功,则将布尔值设置为true,告诉视图显示存储库。 否则,您将错误打印到控制台。

不管结果如何,您都可以告诉视图加载已完成。

构建并运行。 然后,像以前一样登录。

发生两个意外事件:

  • 1) 模态只是显示和关闭而没有给您输入GitHub credentials的机会
  • 2) Xcode控制台中将显示一条错误消息。

由于ASWebAuthenticationSession在后台处理web viewsCookieWeb会话,因此它已缓存了一个状态,该状态导致自动获取授权码。请注意,这发生的时间不确定,不是永远的,但仍然不是您想要的。

您稍后将在Creating Ephemeral Sessions部分中进行处理。现在,将重点放在有关token交换期间失败的错误上。

在解决它之前,您需要对token本身有一些理论上的了解。


Understanding Tokens

登录后,用户现在可以获取authorization code。但是,这不是最终的停止点。此access code的持续时间很短,通常只能使用一次。其目的是让您将其交换为access tokenrefresh token

如前所述,access token是随每个请求一起发送以授权您的请求的tokenrefresh token通常寿命更长。您可以使用它在过期时获取新的access token


Handling Tokens

有了authorization code,就可以将其换成tokens。 您还将在您的应用中添加逻辑以正确处理它们。

打开NetworkRequest.swift

start(responseType:completionHandler :)内部,并在以下代码块的正下方:

guard 
  error == nil,
  let data = data
else {
  DispatchQueue.main.async {
    let error = error ?? NetworkRequest.RequestError.otherError
    completionHandler(.failure(error))
  }
  return
}

找到这一行:

if let object = try? JSONDecoder().decode(responseType, from: data) {

将其替换为下面这些:

// 1
if T.self == String.self,
  let responseString = String(data: data, encoding: .utf8) {
  // 2
  let components = responseString.components(separatedBy: "&")
  var dictionary: [String: String] = [:]
  // 3
  for component in components {
    let itemComponents = component.components(separatedBy: "=")
    if let key = itemComponents.first,
       let value = itemComponents.last {
      dictionary[key] = value
    }
  }
  // 4
  DispatchQueue.main.async {
    // 5
    NetworkRequest.accessToken = dictionary["access_token"]
    NetworkRequest.refreshToken = dictionary["refresh_token"]
    completionHandler(.success((response, "Success" as! T)))
  }
  return
} else if let object = try? JSONDecoder().decode(T.self, from: data) {

其他GitHub API响应是JSON格式的,而token交换响应则以字符串形式返回。处理这种情况的方法如下:

  • 1) 通过添加此语句,您首先可以查看是否有字符串响应作为其内容。
  • 2) 这个带有您的tokens的响应字符串是由与符号分隔的键值对组成的,这就是为什么您将该字符串分成键值对数组的原因。
  • 3) 您遍历每个components以获取响应的Swift字典。
  • 4) 目前,由于URLSession的默认线程模型,一切都在后台线程中运行。因此,您调用DispatchQueue来调用主线程上的下一个代码。您需要这样做,因为completion handler将更新UI,这只能在主线程上完成。
  • 5) 块中的代码在NetworkRequest的两个帮助器属性中存储access and refresh token。然后,它继续调用completion handler,指示该过程成功。

1. How NetworkRequest Handles the Tokens

要查看NetworkRequest使用tokens的功能,请切换到NetworkRequest + User.swift

这些是String类型的属性,但是在后台,它们读取token并将token写入UserDefaults,因此它们可以在应用程序启动期间持续存在。

这些token是敏感数据,因此在您的应用中,您需要确保将它们安全地存储在钥匙串中。这是一个有趣的主题,您可以在 How To Secure iOS User Data: Keychain Services and Biometrics with SwiftUI中了解更多信息。

构建并运行。登录。虽然该模式可能会再次在屏幕上闪烁,但现在您将在此后看到存储库列表:

非常好!


Creating Ephemeral Sessions

最后一个要讨论的主题是临时会话(ephemeral sessions),这是私有身份验证会话。 临时会话不会将与会话相关的数据缓存到磁盘,而是缓存到RAM。 会话无效后,临时会话的数据将清除会话数据。 这样可以方便地为用户提供更多的隐私和安全性。

打开。 SignInViewModel.swift。 在signInTapped及以下:

authenticationSession.presentationContextProvider = self

加下列代码:

authenticationSession.prefersEphemeralWebBrowserSession = true

这样可以缓解之前遇到的问题,即在第二次或第三次运行您的应用程序后立即尝试登录时,不会提示您输入用户凭据。 临时会话意味着ASWebAuthenticationSession将不会缓存任何内容,并且始终会在会话开始时向用户询问其凭据。

构建并运行。

由于该应用将保留access and refresh tokens,因此您仍将登录。 点击顶部的Sign Out以清除token。 现在,尝试登录后,系统会要求您输入GitHub用户名和密码。 该应用程序不会缓存您之前的登录信息,因为这是私密会话(private session)

做得好! 在本教程中,您对OAuth进行了了解,并使用ASWebAuthenticationSession创建了自己的第三方GitHub应用程序进行身份验证。 您可以尝试使用的一些其他功能通过将token放入钥匙串并添加更多GitHub API请求来增强token的存储方式。

如果您想了解有关OAuth的更多信息,请访问IETF’s page,其中包含整个标准。 有点多和密集,但很好的参考。

有关ASWebAuthenticationSession的详细信息和文档,请访问Apple Developer Documentation。

有关网络的综合视频课程,请查看 Networking with URLSession。

后记

本篇主要讲述了使用ASWebAuthenticationSession实现OAuth,感兴趣的给个赞或者关注~~~

你可能感兴趣的:(Authentication Services框架详细解析 (七) —— 使用ASWebAuthenticationSession实现OAuth(一))