AuthenticationInterceptor.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. //
  2. // AuthenticationInterceptor.swift
  3. //
  4. // Copyright (c) 2020 Alamofire Software Foundation (http://alamofire.org/)
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. // THE SOFTWARE.
  23. //
  24. import Foundation
  25. /// Types adopting the `AuthenticationCredential` protocol can be used to authenticate `URLRequest`s.
  26. ///
  27. /// One common example of an `AuthenticationCredential` is an OAuth2 credential containing an access token used to
  28. /// authenticate all requests on behalf of a user. The access token generally has an expiration window of 60 minutes
  29. /// which will then require a refresh of the credential using the refresh token to generate a new access token.
  30. public protocol AuthenticationCredential {
  31. /// Whether the credential requires a refresh. This property should always return `true` when the credential is
  32. /// expired. It is also wise to consider returning `true` when the credential will expire in several seconds or
  33. /// minutes depending on the expiration window of the credential.
  34. ///
  35. /// For example, if the credential is valid for 60 minutes, then it would be wise to return `true` when the
  36. /// credential is only valid for 5 minutes or less. That ensures the credential will not expire as it is passed
  37. /// around backend services.
  38. var requiresRefresh: Bool { get }
  39. }
  40. // MARK: -
  41. /// Types adopting the `Authenticator` protocol can be used to authenticate `URLRequest`s with an
  42. /// `AuthenticationCredential` as well as refresh the `AuthenticationCredential` when required.
  43. public protocol Authenticator: AnyObject {
  44. /// The type of credential associated with the `Authenticator` instance.
  45. associatedtype Credential: AuthenticationCredential
  46. /// Applies the `Credential` to the `URLRequest`.
  47. ///
  48. /// In the case of OAuth2, the access token of the `Credential` would be added to the `URLRequest` as a Bearer
  49. /// token to the `Authorization` header.
  50. ///
  51. /// - Parameters:
  52. /// - credential: The `Credential`.
  53. /// - urlRequest: The `URLRequest`.
  54. func apply(_ credential: Credential, to urlRequest: inout URLRequest)
  55. /// Refreshes the `Credential` and executes the `completion` closure with the `Result` once complete.
  56. ///
  57. /// Refresh can be called in one of two ways. It can be called before the `Request` is actually executed due to
  58. /// a `requiresRefresh` returning `true` during the adapt portion of the `Request` creation process. It can also
  59. /// be triggered by a failed `Request` where the authentication server denied access due to an expired or
  60. /// invalidated access token.
  61. ///
  62. /// In the case of OAuth2, this method would use the refresh token of the `Credential` to generate a new
  63. /// `Credential` using the authentication service. Once complete, the `completion` closure should be called with
  64. /// the new `Credential`, or the error that occurred.
  65. ///
  66. /// In general, if the refresh call fails with certain status codes from the authentication server (commonly a 401),
  67. /// the refresh token in the `Credential` can no longer be used to generate a valid `Credential`. In these cases,
  68. /// you will need to reauthenticate the user with their username / password.
  69. ///
  70. /// Please note, these are just general examples of common use cases. They are not meant to solve your specific
  71. /// authentication server challenges. Please work with your authentication server team to ensure your
  72. /// `Authenticator` logic matches their expectations.
  73. ///
  74. /// - Parameters:
  75. /// - credential: The `Credential` to refresh.
  76. /// - session: The `Session` requiring the refresh.
  77. /// - completion: The closure to be executed once the refresh is complete.
  78. func refresh(_ credential: Credential, for session: Session, completion: @escaping (Result<Credential, Error>) -> Void)
  79. /// Determines whether the `URLRequest` failed due to an authentication error based on the `HTTPURLResponse`.
  80. ///
  81. /// If the authentication server **CANNOT** invalidate credentials after they are issued, then simply return `false`
  82. /// for this method. If the authentication server **CAN** invalidate credentials due to security breaches, then you
  83. /// will need to work with your authentication server team to understand how to identify when this occurs.
  84. ///
  85. /// In the case of OAuth2, where an authentication server can invalidate credentials, you will need to inspect the
  86. /// `HTTPURLResponse` or possibly the `Error` for when this occurs. This is commonly handled by the authentication
  87. /// server returning a 401 status code and some additional header to indicate an OAuth2 failure occurred.
  88. ///
  89. /// It is very important to understand how your authentication server works to be able to implement this correctly.
  90. /// For example, if your authentication server returns a 401 when an OAuth2 error occurs, and your downstream
  91. /// service also returns a 401 when you are not authorized to perform that operation, how do you know which layer
  92. /// of the backend returned you a 401? You do not want to trigger a refresh unless you know your authentication
  93. /// server is actually the layer rejecting the request. Again, work with your authentication server team to understand
  94. /// how to identify an OAuth2 401 error vs. a downstream 401 error to avoid endless refresh loops.
  95. ///
  96. /// - Parameters:
  97. /// - urlRequest: The `URLRequest`.
  98. /// - response: The `HTTPURLResponse`.
  99. /// - error: The `Error`.
  100. ///
  101. /// - Returns: `true` if the `URLRequest` failed due to an authentication error, `false` otherwise.
  102. func didRequest(_ urlRequest: URLRequest, with response: HTTPURLResponse, failDueToAuthenticationError error: Error) -> Bool
  103. /// Determines whether the `URLRequest` is authenticated with the `Credential`.
  104. ///
  105. /// If the authentication server **CANNOT** invalidate credentials after they are issued, then simply return `true`
  106. /// for this method. If the authentication server **CAN** invalidate credentials due to security breaches, then
  107. /// read on.
  108. ///
  109. /// When an authentication server can invalidate credentials, it means that you may have a non-expired credential
  110. /// that appears to be valid, but will be rejected by the authentication server when used. Generally when this
  111. /// happens, a number of requests are all sent when the application is foregrounded, and all of them will be
  112. /// rejected by the authentication server in the order they are received. The first failed request will trigger a
  113. /// refresh internally, which will update the credential, and then retry all the queued requests with the new
  114. /// credential. However, it is possible that some of the original requests will not return from the authentication
  115. /// server until the refresh has completed. This is where this method comes in.
  116. ///
  117. /// When the authentication server rejects a credential, we need to check to make sure we haven't refreshed the
  118. /// credential while the request was in flight. If it has already refreshed, then we don't need to trigger an
  119. /// additional refresh. If it hasn't refreshed, then we need to refresh.
  120. ///
  121. /// Now that it is understood how the result of this method is used in the refresh lifecyle, let's walk through how
  122. /// to implement it. You should return `true` in this method if the `URLRequest` is authenticated in a way that
  123. /// matches the values in the `Credential`. In the case of OAuth2, this would mean that the Bearer token in the
  124. /// `Authorization` header of the `URLRequest` matches the access token in the `Credential`. If it matches, then we
  125. /// know the `Credential` was used to authenticate the `URLRequest` and should return `true`. If the Bearer token
  126. /// did not match the access token, then you should return `false`.
  127. ///
  128. /// - Parameters:
  129. /// - urlRequest: The `URLRequest`.
  130. /// - credential: The `Credential`.
  131. ///
  132. /// - Returns: `true` if the `URLRequest` is authenticated with the `Credential`, `false` otherwise.
  133. func isRequest(_ urlRequest: URLRequest, authenticatedWith credential: Credential) -> Bool
  134. }
  135. // MARK: -
  136. /// Represents various authentication failures that occur when using the `AuthenticationInterceptor`. All errors are
  137. /// still vended from Alamofire as `AFError` types. The `AuthenticationError` instances will be embedded within
  138. /// `AFError` `.requestAdaptationFailed` or `.requestRetryFailed` cases.
  139. public enum AuthenticationError: Error {
  140. /// The credential was missing so the request could not be authenticated.
  141. case missingCredential
  142. /// The credential was refreshed too many times within the `RefreshWindow`.
  143. case excessiveRefresh
  144. }
  145. // MARK: -
  146. /// The `AuthenticationInterceptor` class manages the queuing and threading complexity of authenticating requests.
  147. /// It relies on an `Authenticator` type to handle the actual `URLRequest` authentication and `Credential` refresh.
  148. public class AuthenticationInterceptor<AuthenticatorType>: RequestInterceptor where AuthenticatorType: Authenticator {
  149. // MARK: Typealiases
  150. /// Type of credential used to authenticate requests.
  151. public typealias Credential = AuthenticatorType.Credential
  152. // MARK: Helper Types
  153. /// Type that defines a time window used to identify excessive refresh calls. When enabled, prior to executing a
  154. /// refresh, the `AuthenticationInterceptor` compares the timestamp history of previous refresh calls against the
  155. /// `RefreshWindow`. If more refreshes have occurred within the refresh window than allowed, the refresh is
  156. /// cancelled and an `AuthorizationError.excessiveRefresh` error is thrown.
  157. public struct RefreshWindow {
  158. /// `TimeInterval` defining the duration of the time window before the current time in which the number of
  159. /// refresh attempts is compared against `maximumAttempts`. For example, if `interval` is 30 seconds, then the
  160. /// `RefreshWindow` represents the past 30 seconds. If more attempts occurred in the past 30 seconds than
  161. /// `maximumAttempts`, an `.excessiveRefresh` error will be thrown.
  162. public let interval: TimeInterval
  163. /// Total refresh attempts allowed within `interval` before throwing an `.excessiveRefresh` error.
  164. public let maximumAttempts: Int
  165. /// Creates a `RefreshWindow` instance from the specified `interval` and `maximumAttempts`.
  166. ///
  167. /// - Parameters:
  168. /// - interval: `TimeInterval` defining the duration of the time window before the current time.
  169. /// - maximumAttempts: The maximum attempts allowed within the `TimeInterval`.
  170. public init(interval: TimeInterval = 30.0, maximumAttempts: Int = 5) {
  171. self.interval = interval
  172. self.maximumAttempts = maximumAttempts
  173. }
  174. }
  175. private struct AdaptOperation {
  176. let urlRequest: URLRequest
  177. let session: Session
  178. let completion: (Result<URLRequest, Error>) -> Void
  179. }
  180. private enum AdaptResult {
  181. case adapt(Credential)
  182. case doNotAdapt(AuthenticationError)
  183. case adaptDeferred
  184. }
  185. private struct MutableState {
  186. var credential: Credential?
  187. var isRefreshing = false
  188. var refreshTimestamps: [TimeInterval] = []
  189. var refreshWindow: RefreshWindow?
  190. var adaptOperations: [AdaptOperation] = []
  191. var requestsToRetry: [(RetryResult) -> Void] = []
  192. }
  193. // MARK: Properties
  194. /// The `Credential` used to authenticate requests.
  195. public var credential: Credential? {
  196. get { $mutableState.credential }
  197. set { $mutableState.credential = newValue }
  198. }
  199. let authenticator: AuthenticatorType
  200. let queue = DispatchQueue(label: "org.alamofire.authentication.inspector")
  201. @Protected
  202. private var mutableState: MutableState
  203. // MARK: Initialization
  204. /// Creates an `AuthenticationInterceptor` instance from the specified parameters.
  205. ///
  206. /// A `nil` `RefreshWindow` will result in the `AuthenticationInterceptor` not checking for excessive refresh calls.
  207. /// It is recommended to always use a `RefreshWindow` to avoid endless refresh cycles.
  208. ///
  209. /// - Parameters:
  210. /// - authenticator: The `Authenticator` type.
  211. /// - credential: The `Credential` if it exists. `nil` by default.
  212. /// - refreshWindow: The `RefreshWindow` used to identify excessive refresh calls. `RefreshWindow()` by default.
  213. public init(authenticator: AuthenticatorType,
  214. credential: Credential? = nil,
  215. refreshWindow: RefreshWindow? = RefreshWindow()) {
  216. self.authenticator = authenticator
  217. mutableState = MutableState(credential: credential, refreshWindow: refreshWindow)
  218. }
  219. // MARK: Adapt
  220. public func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) {
  221. let adaptResult: AdaptResult = $mutableState.write { mutableState in
  222. // Queue the adapt operation if a refresh is already in place.
  223. guard !mutableState.isRefreshing else {
  224. let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
  225. mutableState.adaptOperations.append(operation)
  226. return .adaptDeferred
  227. }
  228. // Throw missing credential error is the credential is missing.
  229. guard let credential = mutableState.credential else {
  230. let error = AuthenticationError.missingCredential
  231. return .doNotAdapt(error)
  232. }
  233. // Queue the adapt operation and trigger refresh operation if credential requires refresh.
  234. guard !credential.requiresRefresh else {
  235. let operation = AdaptOperation(urlRequest: urlRequest, session: session, completion: completion)
  236. mutableState.adaptOperations.append(operation)
  237. refresh(credential, for: session, insideLock: &mutableState)
  238. return .adaptDeferred
  239. }
  240. return .adapt(credential)
  241. }
  242. switch adaptResult {
  243. case let .adapt(credential):
  244. var authenticatedRequest = urlRequest
  245. authenticator.apply(credential, to: &authenticatedRequest)
  246. completion(.success(authenticatedRequest))
  247. case let .doNotAdapt(adaptError):
  248. completion(.failure(adaptError))
  249. case .adaptDeferred:
  250. // No-op: adapt operation captured during refresh.
  251. break
  252. }
  253. }
  254. // MARK: Retry
  255. public func retry(_ request: Request, for session: Session, dueTo error: Error, completion: @escaping (RetryResult) -> Void) {
  256. // Do not attempt retry if there was not an original request and response from the server.
  257. guard let urlRequest = request.request, let response = request.response else {
  258. completion(.doNotRetry)
  259. return
  260. }
  261. // Do not attempt retry unless the `Authenticator` verifies failure was due to authentication error (i.e. 401 status code).
  262. guard authenticator.didRequest(urlRequest, with: response, failDueToAuthenticationError: error) else {
  263. completion(.doNotRetry)
  264. return
  265. }
  266. // Do not attempt retry if there is no credential.
  267. guard let credential = credential else {
  268. let error = AuthenticationError.missingCredential
  269. completion(.doNotRetryWithError(error))
  270. return
  271. }
  272. // Retry the request if the `Authenticator` verifies it was authenticated with a previous credential.
  273. guard authenticator.isRequest(urlRequest, authenticatedWith: credential) else {
  274. completion(.retry)
  275. return
  276. }
  277. $mutableState.write { mutableState in
  278. mutableState.requestsToRetry.append(completion)
  279. guard !mutableState.isRefreshing else { return }
  280. refresh(credential, for: session, insideLock: &mutableState)
  281. }
  282. }
  283. // MARK: Refresh
  284. private func refresh(_ credential: Credential, for session: Session, insideLock mutableState: inout MutableState) {
  285. guard !isRefreshExcessive(insideLock: &mutableState) else {
  286. let error = AuthenticationError.excessiveRefresh
  287. handleRefreshFailure(error, insideLock: &mutableState)
  288. return
  289. }
  290. mutableState.refreshTimestamps.append(ProcessInfo.processInfo.systemUptime)
  291. mutableState.isRefreshing = true
  292. // Dispatch to queue to hop out of the lock in case authenticator.refresh is implemented synchronously.
  293. queue.async {
  294. self.authenticator.refresh(credential, for: session) { result in
  295. self.$mutableState.write { mutableState in
  296. switch result {
  297. case let .success(credential):
  298. self.handleRefreshSuccess(credential, insideLock: &mutableState)
  299. case let .failure(error):
  300. self.handleRefreshFailure(error, insideLock: &mutableState)
  301. }
  302. }
  303. }
  304. }
  305. }
  306. private func isRefreshExcessive(insideLock mutableState: inout MutableState) -> Bool {
  307. guard let refreshWindow = mutableState.refreshWindow else { return false }
  308. let refreshWindowMin = ProcessInfo.processInfo.systemUptime - refreshWindow.interval
  309. let refreshAttemptsWithinWindow = mutableState.refreshTimestamps.reduce(into: 0) { attempts, refreshTimestamp in
  310. guard refreshWindowMin <= refreshTimestamp else { return }
  311. attempts += 1
  312. }
  313. let isRefreshExcessive = refreshAttemptsWithinWindow >= refreshWindow.maximumAttempts
  314. return isRefreshExcessive
  315. }
  316. private func handleRefreshSuccess(_ credential: Credential, insideLock mutableState: inout MutableState) {
  317. mutableState.credential = credential
  318. let adaptOperations = mutableState.adaptOperations
  319. let requestsToRetry = mutableState.requestsToRetry
  320. mutableState.adaptOperations.removeAll()
  321. mutableState.requestsToRetry.removeAll()
  322. mutableState.isRefreshing = false
  323. // Dispatch to queue to hop out of the mutable state lock
  324. queue.async {
  325. adaptOperations.forEach { self.adapt($0.urlRequest, for: $0.session, completion: $0.completion) }
  326. requestsToRetry.forEach { $0(.retry) }
  327. }
  328. }
  329. private func handleRefreshFailure(_ error: Error, insideLock mutableState: inout MutableState) {
  330. let adaptOperations = mutableState.adaptOperations
  331. let requestsToRetry = mutableState.requestsToRetry
  332. mutableState.adaptOperations.removeAll()
  333. mutableState.requestsToRetry.removeAll()
  334. mutableState.isRefreshing = false
  335. // Dispatch to queue to hop out of the mutable state lock
  336. queue.async {
  337. adaptOperations.forEach { $0.completion(.failure(error)) }
  338. requestsToRetry.forEach { $0(.doNotRetryWithError(error)) }
  339. }
  340. }
  341. }