Skip to content

Commit

Permalink
[auth] Fix getIDToken API and an error handling case (#13280)
Browse files Browse the repository at this point in the history
  • Loading branch information
paulb777 authored Jul 15, 2024
1 parent b6473f9 commit f1b0f20
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 39 deletions.
50 changes: 31 additions & 19 deletions FirebaseAuth/Sources/Swift/Auth/AuthTokenResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,12 @@

import Foundation

@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
extension AuthTokenResult: NSSecureCoding {}

/// A data class containing the ID token JWT string and other properties associated with the
/// token including the decoded payload claims.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
@objc(FIRAuthTokenResult) open class AuthTokenResult: NSObject {
/// Stores the JWT string of the ID token.
@objc open var token: String
Expand Down Expand Up @@ -47,12 +49,12 @@ extension AuthTokenResult: NSSecureCoding {}
/// reserved claims as well as custom claims set by the developer via the Admin SDK.
@objc open var claims: [String: Any]

private class func getTokenPayloadData(_ token: String) -> Data? {
private class func getTokenPayloadData(_ token: String) throws -> Data {
let tokenStringArray = token.components(separatedBy: ".")

// The JWT should have three parts, though we only use the second in this method.
if tokenStringArray.count != 3 {
return nil
throw AuthErrorUtils.malformedJWTError(token: token, underlyingError: nil)
}

/// The token payload is always the second index of the array.
Expand All @@ -69,37 +71,47 @@ extension AuthTokenResult: NSSecureCoding {}
let length = tokenPayload.count + (4 - tokenPayload.count % 4)
tokenPayload = tokenPayload.padding(toLength: length, withPad: "=", startingAt: 0)
}
return Data(base64Encoded: tokenPayload, options: [.ignoreUnknownCharacters])
guard let data = Data(base64Encoded: tokenPayload, options: [.ignoreUnknownCharacters]) else {
throw AuthErrorUtils.malformedJWTError(token: token, underlyingError: nil)
}
return data
}

private class func getTokenPayloadDictionary(_ payloadData: Data) -> [String: Any]? {
return try? JSONSerialization.jsonObject(
with: payloadData,
options: [.mutableContainers, .allowFragments]
) as? [String: Any]
private class func getTokenPayloadDictionary(_ token: String,
_ payloadData: Data) throws -> [String: Any] {
do {
if let dictionary = try JSONSerialization.jsonObject(
with: payloadData,
options: [.mutableContainers, .allowFragments]
) as? [String: Any] {
return dictionary
} else {
return [:]
}
} catch {
throw AuthErrorUtils.malformedJWTError(token: token, underlyingError: error)
}
}

private class func getJWT(_ payloadData: Data) -> JWT? {
private class func getJWT(_ token: String, _ payloadData: Data) throws -> JWT {
// These are dates since 00:00:00 January 1 1970, as described by the Terminology section in
// the JWT spec. https://tools.ietf.org/html/rfc7519
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .secondsSince1970
decoder.keyDecodingStrategy = .convertFromSnakeCase
guard let jwt = try? decoder.decode(JWT.self, from: payloadData) else {
return nil
throw AuthErrorUtils.malformedJWTError(token: token, underlyingError: nil)
}
return jwt
}

/// Parse a token string to a structured token.
/// - Parameter token: The token string to parse.
/// - Returns: A structured token result.
@objc open class func tokenResult(token: String) -> AuthTokenResult? {
guard let payloadData = getTokenPayloadData(token),
let claims = getTokenPayloadDictionary(payloadData),
let jwt = getJWT(payloadData) else {
return nil
}
class func tokenResult(token: String) throws -> AuthTokenResult {
let payloadData = try getTokenPayloadData(token)
let claims = try getTokenPayloadDictionary(token, payloadData)
let jwt = try getJWT(token, payloadData)
return AuthTokenResult(token: token, jwt: jwt, claims: claims)
}

Expand Down Expand Up @@ -132,9 +144,9 @@ extension AuthTokenResult: NSSecureCoding {}
) as? String else {
return nil
}
guard let payloadData = AuthTokenResult.getTokenPayloadData(token),
let claims = AuthTokenResult.getTokenPayloadDictionary(payloadData),
let jwt = AuthTokenResult.getJWT(payloadData) else {
guard let payloadData = try? AuthTokenResult.getTokenPayloadData(token),
let claims = try? AuthTokenResult.getTokenPayloadDictionary(token, payloadData),
let jwt = try? AuthTokenResult.getJWT(token, payloadData) else {
return nil
}
self.init(token: token, jwt: jwt, claims: claims)
Expand Down
28 changes: 15 additions & 13 deletions FirebaseAuth/Sources/Swift/SystemService/SecureTokenService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -153,19 +153,21 @@ class SecureTokenService: NSObject, NSSecureCoding {
if let newAccessToken = response.accessToken,
newAccessToken.count > 0,
newAccessToken != accessToken {
let tokenResult = AuthTokenResult.tokenResult(token: newAccessToken)
// There is an edge case where the request for a new access token may be made right
// before the app goes inactive, resulting in the callback being invoked much later
// with an expired access token. This does not fully solve the issue, as if the
// callback is invoked less than an hour after the request is made, a token is not
// re-requested here but the approximateExpirationDate will still be off since that
// is computed at the time the token is received.
if retryIfExpired,
let expirationDate = tokenResult?.expirationDate,
expirationDate.timeIntervalSinceNow <= kFiveMinutes {
// We only retry once, to avoid an infinite loop in the case that an end-user has
// their local time skewed by over an hour.
return try await requestAccessToken(retryIfExpired: false)
if let tokenResult = try? AuthTokenResult.tokenResult(token: newAccessToken) {
// There is an edge case where the request for a new access token may be made right
// before the app goes inactive, resulting in the callback being invoked much later
// with an expired access token. This does not fully solve the issue, as if the
// callback is invoked less than an hour after the request is made, a token is not
// re-requested here but the approximateExpirationDate will still be off since that
// is computed at the time the token is received.
if retryIfExpired {
let expirationDate = tokenResult.expirationDate
if expirationDate.timeIntervalSinceNow <= kFiveMinutes {
// We only retry once, to avoid an infinite loop in the case that an end-user has
// their local time skewed by over an hour.
return try await requestAccessToken(retryIfExpired: false)
}
}
}
accessToken = newAccessToken
accessTokenExpirationDate = response.approximateExpirationDate
Expand Down
35 changes: 28 additions & 7 deletions FirebaseAuth/Sources/Swift/User/User.swift
Original file line number Diff line number Diff line change
Expand Up @@ -532,9 +532,9 @@ extension User: NSSecureCoding {}
/// reason other than an expiration.
/// - Returns: The Firebase authentication token.
@available(iOS 13, tvOS 13, macOS 10.15, macCatalyst 13, watchOS 7, *)
open func getIDToken() async throws -> String {
open func getIDToken(forcingRefresh forceRefresh: Bool = false) async throws -> String {
return try await withCheckedThrowingContinuation { continuation in
self.getIDTokenForcingRefresh(false) { tokenResult, error in
self.getIDTokenForcingRefresh(forceRefresh) { tokenResult, error in
if let tokenResult {
continuation.resume(returning: tokenResult)
} else if let error {
Expand All @@ -544,6 +544,12 @@ extension User: NSSecureCoding {}
}
}

/// API included for compatibilty with a mis-named Firebase 10 API.
/// Use `getIDToken(forcingRefresh forceRefresh: Bool = false)` instead.
open func idTokenForcingRefresh(_ forceRefresh: Bool) async throws -> String {
return try await getIDToken(forcingRefresh: forceRefresh)
}

/// Retrieves the Firebase authentication token, possibly refreshing it if it has expired.
/// - Parameter completion: Optionally; the block invoked when the token is available. Invoked
/// asynchronously on the main thread in the future.
Expand Down Expand Up @@ -574,14 +580,29 @@ extension User: NSSecureCoding {}
self.internalGetToken(forceRefresh: forcingRefresh) { token, error in
var tokenResult: AuthTokenResult?
if let token {
tokenResult = AuthTokenResult.tokenResult(token: token)
AuthLog.logDebug(code: "I-AUT000017", message: "Actual token expiration date: " +
"\(String(describing: tokenResult?.expirationDate))," +
"current date: \(Date())")
do {
tokenResult = try AuthTokenResult.tokenResult(token: token)
AuthLog.logDebug(code: "I-AUT000017", message: "Actual token expiration date: " +
"\(String(describing: tokenResult?.expirationDate))," +
"current date: \(Date())")
if let completion {
DispatchQueue.main.async {
completion(tokenResult, error)
}
}
return
} catch {
if let completion {
DispatchQueue.main.async {
completion(tokenResult, error)
}
}
return
}
}
if let completion {
DispatchQueue.main.async {
completion(tokenResult, error)
completion(nil, error)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ enum UserAction: String {
case link = "Link/Unlink Auth Providers"
case requestVerifyEmail = "Request Verify Email"
case tokenRefresh = "Token Refresh"
case tokenRefreshAsync = "Token Refresh Async"
case delete = "Delete"
case updateEmail = "Email"
case updatePhotoURL = "Photo URL"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ extension User: DataSourceProvidable {
Item(title: UserAction.requestVerifyEmail.rawValue, textColor: .systemBlue),
Item(title: UserAction.updatePassword.rawValue, textColor: .systemBlue),
Item(title: UserAction.tokenRefresh.rawValue, textColor: .systemBlue),
Item(title: UserAction.tokenRefreshAsync.rawValue, textColor: .systemBlue),
Item(title: UserAction.delete.rawValue, textColor: .systemRed),
]
return Section(headerDescription: "Actions", items: actionsRows)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ class UserViewController: UIViewController, DataSourceProviderDelegate {
case .tokenRefresh:
refreshCurrentUserIDToken()

case .tokenRefreshAsync:
refreshCurrentUserIDTokenAsync()

case .delete:
deleteCurrentUser()

Expand Down Expand Up @@ -143,6 +146,17 @@ class UserViewController: UIViewController, DataSourceProviderDelegate {
}
}

public func refreshCurrentUserIDTokenAsync() {
Task {
do {
let token = try await user!.idTokenForcingRefresh(true)
print("New token: \(token)")
} catch {
self.displayError(error)
}
}
}

public func refreshUserInfo() {
user?.reload { error in
if let error = error {
Expand Down

0 comments on commit f1b0f20

Please sign in to comment.