SecureTokenService.swift 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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 FirebaseCoreInternal
  15. private let kFiveMinutes = 5 * 60.0
  16. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  17. actor SecureTokenServiceInternal {
  18. /// Fetch a fresh ephemeral access token for the ID associated with this instance. The token
  19. /// received in the callback should be considered short lived and not cached.
  20. ///
  21. /// Invoked asynchronously on the auth global work queue in the future.
  22. /// - Parameter forceRefresh: Forces the token to be refreshed.
  23. /// - Returns : A tuple with the token and flag of whether it was updated.
  24. func fetchAccessToken(forcingRefresh forceRefresh: Bool,
  25. service: SecureTokenService,
  26. backend: AuthBackend) async throws -> (String?, Bool) {
  27. if !forceRefresh, hasValidAccessToken(service: service) {
  28. return (service.accessToken, false)
  29. } else {
  30. AuthLog.logDebug(code: "I-AUT000017", message: "Fetching new token from backend.")
  31. return try await requestAccessToken(retryIfExpired: true, service: service, backend: backend)
  32. }
  33. }
  34. /// Makes a request to STS for an access token.
  35. ///
  36. /// This handles both the case that the token has not been granted yet and that it just needs
  37. /// needs to be refreshed.
  38. ///
  39. /// - Returns: Token and Bool indicating if update occurred.
  40. private func requestAccessToken(retryIfExpired: Bool,
  41. service: SecureTokenService,
  42. backend: AuthBackend) async throws -> (String?, Bool) {
  43. // TODO: This was a crash in ObjC SDK, should it callback with an error?
  44. guard let refreshToken = service.refreshToken,
  45. let requestConfiguration = service.requestConfiguration else {
  46. fatalError("refreshToken and requestConfiguration should not be nil")
  47. }
  48. let request = SecureTokenRequest.refreshRequest(refreshToken: refreshToken,
  49. requestConfiguration: requestConfiguration)
  50. let response = try await backend.call(with: request)
  51. var tokenUpdated = false
  52. if let newAccessToken = response.accessToken,
  53. newAccessToken.count > 0,
  54. newAccessToken != service.accessToken {
  55. if let tokenResult = try? AuthTokenResult.tokenResult(token: newAccessToken) {
  56. // There is an edge case where the request for a new access token may be made right
  57. // before the app goes inactive, resulting in the callback being invoked much later
  58. // with an expired access token. This does not fully solve the issue, as if the
  59. // callback is invoked less than an hour after the request is made, a token is not
  60. // re-requested here but the approximateExpirationDate will still be off since that
  61. // is computed at the time the token is received.
  62. if retryIfExpired {
  63. let expirationDate = tokenResult.expirationDate
  64. if expirationDate.timeIntervalSinceNow <= kFiveMinutes {
  65. // We only retry once, to avoid an infinite loop in the case that an end-user has
  66. // their local time skewed by over an hour.
  67. return try await requestAccessToken(
  68. retryIfExpired: false,
  69. service: service,
  70. backend: backend
  71. )
  72. }
  73. }
  74. }
  75. service.accessToken = newAccessToken
  76. service.accessTokenExpirationDate = response.approximateExpirationDate
  77. tokenUpdated = true
  78. AuthLog.logDebug(
  79. code: "I-AUT000017",
  80. message: "Updated access token. Estimated expiration date: " +
  81. "\(String(describing: service.accessTokenExpirationDate)), current date: \(Date())"
  82. )
  83. }
  84. if let newRefreshToken = response.refreshToken,
  85. newRefreshToken != service.refreshToken {
  86. service.refreshToken = newRefreshToken
  87. tokenUpdated = true
  88. }
  89. return (response.accessToken, tokenUpdated)
  90. }
  91. private func hasValidAccessToken(service: SecureTokenService) -> Bool {
  92. if let accessTokenExpirationDate = service.accessTokenExpirationDate,
  93. accessTokenExpirationDate.timeIntervalSinceNow > kFiveMinutes {
  94. AuthLog.logDebug(code: "I-AUT000017",
  95. message: "Has valid access token. Estimated expiration date:" +
  96. "\(accessTokenExpirationDate), current date: \(Date())")
  97. return true
  98. }
  99. AuthLog.logDebug(
  100. code: "I-AUT000017",
  101. message: "Does not have valid access token. Estimated expiration date:" +
  102. "\(String(describing: service.accessTokenExpirationDate)), current date: \(Date())"
  103. )
  104. return false
  105. }
  106. }
  107. /// A class represents a credential that proves the identity of the app.
  108. @available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
  109. @objc(FIRSecureTokenService) // objc Needed for decoding old versions
  110. final class SecureTokenService: NSObject, NSSecureCoding, Sendable {
  111. /// Internal actor to enforce serialization
  112. private let internalService: SecureTokenServiceInternal
  113. /// The configuration for making requests to server.
  114. var requestConfiguration: AuthRequestConfiguration? {
  115. get { _requestConfiguration.withLock { $0 } }
  116. set { _requestConfiguration.withLock { $0 = newValue } }
  117. }
  118. let _requestConfiguration: FIRAllocatedUnfairLock<AuthRequestConfiguration?>
  119. /// The cached access token.
  120. ///
  121. /// This method is specifically for providing the access token to internal clients during
  122. /// deserialization and sign-in events, and should not be used to retrieve the access token by
  123. /// anyone else.
  124. ///
  125. /// - Note: The atomic wrapper can be removed when the SDK is fully
  126. /// synchronized with structured concurrency.
  127. var accessToken: String {
  128. get { _accessToken.withLock { $0 } }
  129. set { _accessToken.withLock { $0 = newValue } }
  130. }
  131. private let _accessToken: FIRAllocatedUnfairLock<String>
  132. /// The refresh token for the user, or `nil` if the user has yet completed sign-in flow.
  133. ///
  134. /// This property needs to be set manually after the instance is decoded from archive.
  135. var refreshToken: String? {
  136. get { _refreshToken.withLock { $0 } }
  137. set { _refreshToken.withLock { $0 = newValue } }
  138. }
  139. private let _refreshToken: FIRAllocatedUnfairLock<String?>
  140. /// The expiration date of the cached access token.
  141. var accessTokenExpirationDate: Date? {
  142. get { _accessTokenExpirationDate.withLock { $0 } }
  143. set { _accessTokenExpirationDate.withLock { $0 = newValue } }
  144. }
  145. private let _accessTokenExpirationDate: FIRAllocatedUnfairLock<Date?>
  146. /// Creates a `SecureTokenService` with access and refresh tokens.
  147. /// - Parameter requestConfiguration: The configuration for making requests to server.
  148. /// - Parameter accessToken: The STS access token.
  149. /// - Parameter accessTokenExpirationDate: The approximate expiration date of the access token.
  150. /// - Parameter refreshToken: The STS refresh token.
  151. init(withRequestConfiguration requestConfiguration: AuthRequestConfiguration?,
  152. accessToken: String,
  153. accessTokenExpirationDate: Date?,
  154. refreshToken: String) {
  155. internalService = SecureTokenServiceInternal()
  156. _requestConfiguration = FIRAllocatedUnfairLock(initialState: requestConfiguration)
  157. _accessToken = FIRAllocatedUnfairLock(initialState: accessToken)
  158. _accessTokenExpirationDate = FIRAllocatedUnfairLock(initialState: accessTokenExpirationDate)
  159. _refreshToken = FIRAllocatedUnfairLock(initialState: refreshToken)
  160. }
  161. /// Fetch a fresh ephemeral access token for the ID associated with this instance. The token
  162. /// received in the callback should be considered short lived and not cached.
  163. ///
  164. /// Invoked asynchronously on the auth global work queue in the future.
  165. /// - Parameter forceRefresh: Forces the token to be refreshed.
  166. /// - Returns : A tuple with the token and flag of whether it was updated.
  167. func fetchAccessToken(forcingRefresh forceRefresh: Bool,
  168. backend: AuthBackend) async throws -> (String?, Bool) {
  169. return try await internalService
  170. .fetchAccessToken(forcingRefresh: forceRefresh, service: self, backend: backend)
  171. }
  172. // MARK: NSSecureCoding
  173. // Secure coding keys
  174. private let kAPIKeyCodingKey = "APIKey"
  175. private static let kRefreshTokenKey = "refreshToken"
  176. private static let kAccessTokenKey = "accessToken"
  177. private static let kAccessTokenExpirationDateKey = "accessTokenExpirationDate"
  178. static let supportsSecureCoding = true
  179. required convenience init?(coder: NSCoder) {
  180. guard let refreshToken = coder.decodeObject(of: [NSString.self],
  181. forKey: Self.kRefreshTokenKey) as? String,
  182. let accessToken = coder.decodeObject(of: [NSString.self],
  183. forKey: Self.kAccessTokenKey) as? String else {
  184. return nil
  185. }
  186. let accessTokenExpirationDate = coder.decodeObject(
  187. of: [NSDate.self], forKey: Self.kAccessTokenExpirationDateKey
  188. ) as? Date
  189. // requestConfiguration is filled in after User is set by Auth.protectedDataInitialization.
  190. self.init(withRequestConfiguration: nil,
  191. accessToken: accessToken,
  192. accessTokenExpirationDate: accessTokenExpirationDate,
  193. refreshToken: refreshToken)
  194. }
  195. func encode(with coder: NSCoder) {
  196. // The API key is encoded even it is not used in decoding to be compatible with previous
  197. // versions of the library.
  198. coder.encode(requestConfiguration?.apiKey, forKey: kAPIKeyCodingKey)
  199. // Authorization code is not encoded because it is not long-lived.
  200. coder.encode(refreshToken, forKey: SecureTokenService.kRefreshTokenKey)
  201. coder.encode(accessToken, forKey: SecureTokenService.kAccessTokenKey)
  202. coder.encode(
  203. accessTokenExpirationDate,
  204. forKey: SecureTokenService.kAccessTokenExpirationDateKey
  205. )
  206. }
  207. }