Skip to content

Commit

Permalink
fix: Emit a Sentry event on Token auth error (#1460)
Browse files Browse the repository at this point in the history
  • Loading branch information
PhilippeWeidmann committed Jun 19, 2024
2 parents 2d307f6 + 5029173 commit 598b7bc
Show file tree
Hide file tree
Showing 3 changed files with 179 additions and 40 deletions.
104 changes: 77 additions & 27 deletions MailCore/API/SyncedAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,34 +23,63 @@ import InfomaniakDI
import InfomaniakLogin
import Sentry

public extension ApiToken {
var isInfinite: Bool {
expirationDate == nil
}

var metadata: [String: Any] {
[
"User id": userId,
"Expiration date": expirationDate?.timeIntervalSince1970 ?? "infinite",
"Access Token": truncatedAccessToken,
"Refresh Token": truncatedRefreshToken
]
}
}

final class SyncedAuthenticator: OAuthAuthenticator {
@LazyInjectService var keychainHelper: KeychainHelper
@LazyInjectService var tokenStore: TokenStore
@LazyInjectService var networkLoginService: InfomaniakNetworkLoginable

func handleFailedRefreshingToken(oldToken: ApiToken, error: Error?) -> Result<OAuthAuthenticator.Credential, Error> {
func handleFailedRefreshingToken(oldToken: ApiToken,
newToken: ApiToken?,
error: Error?) -> Result<OAuthAuthenticator.Credential, Error> {
guard let error else {
// Couldn't refresh the token, keep the old token and fetch it later. Maybe because of bad network ?
SentrySDK
.addBreadcrumb(oldToken.generateBreadcrumb(level: .error,
message: "Refreshing token failed - Other \(error.debugDescription)"))
Log.tokenAuthentication(
"Refreshing token failed - Other \(error.debugDescription)",
oldToken: oldToken,
newToken: newToken,
level: .error
)

return .failure(MailError.unknownError)
}

if case .noRefreshToken = (error as? InfomaniakLoginError) {
// Couldn't refresh the token because we don't have a refresh token
SentrySDK
.addBreadcrumb(oldToken.generateBreadcrumb(level: .error,
message: "Refreshing token failed - Cannot refresh infinite token"))
Log.tokenAuthentication(
"Refreshing token failed - Cannot refresh infinite token",
oldToken: oldToken,
newToken: newToken,
level: .error
)

refreshTokenDelegate?.didFailRefreshToken(oldToken)
return .failure(error)
}

if (error as NSError).domain == "invalid_grant" {
// Couldn't refresh the token, API says it's invalid
SentrySDK
.addBreadcrumb(oldToken.generateBreadcrumb(level: .error,
message: "Refreshing token failed - Invalid grant"))
Log.tokenAuthentication(
"Refreshing token failed - Invalid grant",
oldToken: oldToken,
newToken: newToken,
level: .error
)

refreshTokenDelegate?.didFailRefreshToken(oldToken)
return .failure(error)
}
Expand All @@ -64,34 +93,51 @@ final class SyncedAuthenticator: OAuthAuthenticator {
for session: Session,
completion: @escaping (Result<OAuthAuthenticator.Credential, Error>) -> Void
) {
SentrySDK.addBreadcrumb((credential as ApiToken).generateBreadcrumb(level: .info, message: "Refreshing token - Starting"))
let storedToken = tokenStore.tokenFor(userId: credential.userId, fetchLocation: .keychain)

Log.tokenAuthentication(
"Refreshing token - Starting",
oldToken: storedToken,
newToken: credential,
level: .info
)

guard keychainHelper.isKeychainAccessible else {
SentrySDK
.addBreadcrumb((credential as ApiToken)
.generateBreadcrumb(level: .error, message: "Refreshing token failed - Keychain unaccessible"))
Log.tokenAuthentication(
"Refreshing token failed - Keychain unaccessible",
oldToken: storedToken,
newToken: credential,
level: .error
)

completion(.failure(MailError.keychainUnavailable))
return
}

if let storedToken = tokenStore.tokenFor(userId: credential.userId, fetchLocation: .keychain) {
if let storedToken {
// Someone else refreshed our token and we already have an infinite token
if storedToken.expirationDate == nil && credential.expirationDate != nil {
SentrySDK.addBreadcrumb(storedToken.generateBreadcrumb(
level: .info,
message: "Refreshing token - Success with local (infinite)"
))
Log.tokenAuthentication(
"Refreshing token - Success with local (infinite)",
oldToken: storedToken,
newToken: credential,
level: .info
)

completion(.success(storedToken))
return
}
// Someone else refreshed our token and we don't have an infinite token
if let storedTokenExpirationDate = storedToken.expirationDate,
let tokenExpirationDate = credential.expirationDate,
tokenExpirationDate > storedTokenExpirationDate {
SentrySDK.addBreadcrumb(storedToken.generateBreadcrumb(
level: .info,
message: "Refreshing token - Success with local"
))
Log.tokenAuthentication(
"Refreshing token - Success with local",
oldToken: storedToken,
newToken: credential,
level: .info
)

completion(.success(storedToken))
return
}
Expand All @@ -103,13 +149,17 @@ final class SyncedAuthenticator: OAuthAuthenticator {
networkLoginService.refreshToken(token: credential) { token, error in
// New token has been fetched correctly
if let token {
SentrySDK
.addBreadcrumb(token
.generateBreadcrumb(level: .info, message: "Refreshing token - Success with remote"))
Log.tokenAuthentication(
"Refreshing token - Success with remote",
oldToken: credential,
newToken: token,
level: .info
)

self.refreshTokenDelegate?.didUpdateToken(newToken: token, oldToken: credential)
completion(.success(token))
} else {
completion(self.handleFailedRefreshingToken(oldToken: credential, error: error))
completion(self.handleFailedRefreshingToken(oldToken: credential, newToken: token, error: error))
}
expiringActivity.endAll()
}
Expand Down
39 changes: 39 additions & 0 deletions MailCore/Utils/Logging.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import CocoaLumberjackSwift
import Foundation
import InfomaniakCore
import InfomaniakDI
import InfomaniakLogin
import RealmSwift
import Sentry

Expand Down Expand Up @@ -125,3 +126,41 @@ public enum Logging {
#endif
}
}

/// Something to centralize log methods per feature
public enum Log {
public static func tokenAuthentication(_ message: @autoclosure () -> String,
oldToken: ApiToken?,
newToken: ApiToken?,
level: SentryLevel = .debug,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line) {
let message = message()
let oldTokenMetadata: Any = oldToken?.metadata ?? "NULL"
let newTokenMetadata: Any = newToken?.metadata ?? "NULL"
var metadata = [String: Any]()
metadata["oldToken"] = oldTokenMetadata
metadata["newToken"] = newTokenMetadata

SentryDebug.asyncCapture(
message: message,
context: metadata,
level: level,
extras: ["file": "\(file)", "function": "\(function)", "line": "\(line)"]
)

SentryDebug.addAsyncBreadcrumb(level: level,
category: SentryDebug.Category.threadAlgorithm.rawValue,
message: message,
data: metadata)

if level == .error {
DDLogError(message)
DDLogError("old token: \(oldTokenMetadata)")
DDLogError("new token: \(newTokenMetadata)")
} else {
DDLogInfo(message)
}
}
}
76 changes: 63 additions & 13 deletions MailCore/Utils/SentryDebug.swift
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,9 @@ public enum SentryDebug {

// MARK: - Breadcrumb

enum Category {
static let ThreadAlgorithm = "Thread algo"
public enum Category: String {
case threadAlgorithm = "Thread algo"
case syncedAuthenticator = "SyncedAuthenticator"
}

private static func createBreadcrumb(level: SentryLevel,
Expand All @@ -189,19 +190,9 @@ public enum SentryDebug {
return crumb
}

private static func addAsyncBreadcrumb(level: SentryLevel,
category: String,
message: String,
data: [String: Any]? = nil) {
Task {
let breadcrumb = createBreadcrumb(level: level, category: category, message: message, data: data)
SentrySDK.addBreadcrumb(breadcrumb)
}
}

static func nilDateParsingBreadcrumb(uid: String) {
let breadcrumb = createBreadcrumb(level: .warning,
category: Category.ThreadAlgorithm,
category: Category.threadAlgorithm.rawValue,
message: "Nil message date decoded",
data: ["uid": uid])
SentrySDK.addBreadcrumb(breadcrumb)
Expand Down Expand Up @@ -343,3 +334,62 @@ public enum SentryDebug {
return true
}
}

// MARK: - SHARED -

public extension SentryDebug {
static func addAsyncBreadcrumb(level: SentryLevel,
category: String,
message: String,
data: [String: Any]? = nil) {
Task {
let breadcrumb = Breadcrumb(level: level, category: category)
breadcrumb.message = message
breadcrumb.data = data
SentrySDK.addBreadcrumb(breadcrumb)
}
}

static func asyncCapture(
error: Error,
context: [String: Any]? = nil,
contextKey: String? = nil,
extras: [String: Any]? = nil
) {
Task {
SentrySDK.capture(error: error) { scope in
if let context, let contextKey {
scope.setContext(value: context, key: contextKey)
}

if let extras {
scope.setExtras(extras)
}
}
}
}

static func asyncCapture(
message: String,
context: [String: Any]? = nil,
contextKey: String? = nil,
level: SentryLevel? = nil,
extras: [String: Any]? = nil
) {
Task {
SentrySDK.capture(message: message) { scope in
if let context, let contextKey {
scope.setContext(value: context, key: contextKey)
}

if let level {
scope.setLevel(level)
}

if let extras {
scope.setExtras(extras)
}
}
}
}
}

0 comments on commit 598b7bc

Please sign in to comment.