SecureTokenService.swift 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. // Copyright 2023 Google LLC
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. import Foundation
  15. private let kFiveMinutes = 5 * 60.0
  16. /** @class FIRAuthAppCredential
  17. @brief A class represents a credential that proves the identity of the app.
  18. */
  19. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  20. @objc(FIRSecureTokenService) // objc Needed for decoding old versions
  21. class SecureTokenService: NSObject, NSSecureCoding {
  22. /** @property requestConfiguration
  23. @brief The configuration for making requests to server.
  24. */
  25. var requestConfiguration: AuthRequestConfiguration?
  26. /** @property accessToken
  27. @brief The cached access token.
  28. @remarks This method is specifically for providing the access token to internal clients during
  29. deserialization and sign-in events, and should not be used to retrieve the access token by
  30. anyone else.
  31. */
  32. var accessToken: String
  33. /** @property refreshToken
  34. @brief The refresh token for the user, or @c nil if the user has yet completed sign-in flow.
  35. @remarks This property needs to be set manually after the instance is decoded from archive.
  36. */
  37. var refreshToken: String?
  38. /** @property accessTokenExpirationDate
  39. @brief The expiration date of the cached access token.
  40. */
  41. var accessTokenExpirationDate: Date?
  42. /** @fn initWithRequestConfiguration:accessToken:accessTokenExpirationDate:refreshToken
  43. @brief Creates a @c FIRSecureTokenService with access and refresh tokens.
  44. @param requestConfiguration The configuration for making requests to server.
  45. @param accessToken The STS access token.
  46. @param accessTokenExpirationDate The approximate expiration date of the access token.
  47. @param refreshToken The STS refresh token.
  48. */
  49. init(withRequestConfiguration requestConfiguration: AuthRequestConfiguration?,
  50. accessToken: String,
  51. accessTokenExpirationDate: Date?,
  52. refreshToken: String) {
  53. self.requestConfiguration = requestConfiguration
  54. self.accessToken = accessToken
  55. self.refreshToken = refreshToken
  56. self.accessTokenExpirationDate = accessTokenExpirationDate
  57. taskQueue = AuthSerialTaskQueue()
  58. }
  59. /** @fn fetchAccessTokenForcingRefresh:callback:
  60. @brief Fetch a fresh ephemeral access token for the ID associated with this instance. The token
  61. received in the callback should be considered short lived and not cached.
  62. @param forceRefresh Forces the token to be refreshed.
  63. @param callback Callback block that will be called to return either the token or an error.
  64. Invoked asyncronously on the auth global work queue in the future.
  65. */
  66. func fetchAccessToken(forcingRefresh forceRefresh: Bool,
  67. callback: @escaping (String?, Error?, Bool) -> Void) {
  68. taskQueue.enqueueTask { complete in
  69. if !forceRefresh, self.hasValidAccessToken() {
  70. complete()
  71. callback(self.accessToken, nil, false)
  72. } else {
  73. AuthLog.logDebug(code: "I-AUT000017", message: "Fetching new token from backend.")
  74. Task {
  75. do {
  76. let (token, tokenUpdated) = try await self.requestAccessToken(retryIfExpired: true)
  77. complete()
  78. callback(token, nil, tokenUpdated)
  79. } catch {
  80. complete()
  81. callback(nil, error, false)
  82. }
  83. }
  84. }
  85. }
  86. }
  87. private let taskQueue: AuthSerialTaskQueue
  88. // MARK: NSSecureCoding
  89. // Secure coding keys
  90. private let kAPIKeyCodingKey = "APIKey"
  91. private static let kRefreshTokenKey = "refreshToken"
  92. private static let kAccessTokenKey = "accessToken"
  93. private static let kAccessTokenExpirationDateKey = "accessTokenExpirationDate"
  94. static var supportsSecureCoding: Bool {
  95. true
  96. }
  97. required convenience init?(coder: NSCoder) {
  98. guard let refreshToken = coder.decodeObject(of: [NSString.self],
  99. forKey: Self.kRefreshTokenKey) as? String,
  100. let accessToken = coder.decodeObject(of: [NSString.self],
  101. forKey: Self.kAccessTokenKey) as? String else {
  102. return nil
  103. }
  104. let accessTokenExpirationDate = coder.decodeObject(
  105. of: [NSDate.self], forKey: Self.kAccessTokenExpirationDateKey
  106. ) as? Date
  107. // requestConfiguration is filled in after User is set by Auth.protectedDataInitialization.
  108. self.init(withRequestConfiguration: nil,
  109. accessToken: accessToken,
  110. accessTokenExpirationDate: accessTokenExpirationDate,
  111. refreshToken: refreshToken)
  112. }
  113. func encode(with coder: NSCoder) {
  114. // The API key is encoded even it is not used in decoding to be compatible with previous
  115. // versions of the library.
  116. coder.encode(requestConfiguration?.apiKey, forKey: kAPIKeyCodingKey)
  117. // Authorization code is not encoded because it is not long-lived.
  118. coder.encode(refreshToken, forKey: SecureTokenService.kRefreshTokenKey)
  119. coder.encode(accessToken, forKey: SecureTokenService.kAccessTokenKey)
  120. coder.encode(
  121. accessTokenExpirationDate,
  122. forKey: SecureTokenService.kAccessTokenExpirationDateKey
  123. )
  124. }
  125. // MARK: Private methods
  126. /** @fn requestAccessToken:
  127. @brief Makes a request to STS for an access token.
  128. @details This handles both the case that the token has not been granted yet and that it just
  129. needs to be refreshed. The caller is responsible for making sure that this is occurring in
  130. a @c _taskQueue task.
  131. @Returns token and Bool indicating if update occurred.
  132. @remarks Because this method is guaranteed to only be called from tasks enqueued in
  133. @c _taskQueue, we do not need any @synchronized guards around access to _accessToken/etc.
  134. since only one of those tasks is ever running at a time, and those tasks are the only
  135. access to and mutation of these instance variables.
  136. */
  137. private func requestAccessToken(retryIfExpired: Bool) async throws -> (String?, Bool) {
  138. // TODO: This was a crash in ObjC SDK, should it callback with an error?
  139. guard let refreshToken, let requestConfiguration else {
  140. fatalError("refreshToken and requestConfiguration should not be nil")
  141. }
  142. let request = SecureTokenRequest.refreshRequest(refreshToken: refreshToken,
  143. requestConfiguration: requestConfiguration)
  144. let response = try await AuthBackend.call(with: request)
  145. var tokenUpdated = false
  146. if let newAccessToken = response.accessToken,
  147. newAccessToken.count > 0,
  148. newAccessToken != accessToken {
  149. let tokenResult = AuthTokenResult.tokenResult(token: newAccessToken)
  150. // There is an edge case where the request for a new access token may be made right
  151. // before the app goes inactive, resulting in the callback being invoked much later
  152. // with an expired access token. This does not fully solve the issue, as if the
  153. // callback is invoked less than an hour after the request is made, a token is not
  154. // re-requested here but the approximateExpirationDate will still be off since that
  155. // is computed at the time the token is received.
  156. if retryIfExpired,
  157. let expirationDate = tokenResult?.expirationDate,
  158. expirationDate.timeIntervalSinceNow <= kFiveMinutes {
  159. // We only retry once, to avoid an infinite loop in the case that an end-user has
  160. // their local time skewed by over an hour.
  161. return try await requestAccessToken(retryIfExpired: false)
  162. }
  163. accessToken = newAccessToken
  164. accessTokenExpirationDate = response.approximateExpirationDate
  165. tokenUpdated = true
  166. AuthLog.logDebug(
  167. code: "I-AUT000017",
  168. message: "Updated access token. Estimated expiration date: " +
  169. "\(String(describing: accessTokenExpirationDate)), current date: \(Date())"
  170. )
  171. }
  172. if let newRefreshToken = response.refreshToken,
  173. newRefreshToken != self.refreshToken {
  174. self.refreshToken = newRefreshToken
  175. tokenUpdated = true
  176. }
  177. return (response.accessToken, tokenUpdated)
  178. }
  179. private func hasValidAccessToken() -> Bool {
  180. if let accessTokenExpirationDate,
  181. accessTokenExpirationDate.timeIntervalSinceNow > kFiveMinutes {
  182. AuthLog.logDebug(code: "I-AUT000017",
  183. message: "Has valid access token. Estimated expiration date:" +
  184. "\(accessTokenExpirationDate), current date: \(Date())")
  185. return true
  186. }
  187. AuthLog.logDebug(code: "I-AUT000017",
  188. message: "Does not have valid access token. Estimated expiration date:" +
  189. "\(String(describing: accessTokenExpirationDate)), current date: \(Date())")
  190. return false
  191. }
  192. }