SecureTokenService.swift 9.0 KB

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