mirror of
https://github.com/home-assistant/iOS.git
synced 2026-05-04 05:51:13 -05:00
Improve handling around refresh token errors (#1661)
Resolves #1542. ## Summary Logs the error code we get when refreshing a token and log out if we encounter a 403 during token refresh. ## Any other notes - Fixes an auth handling issue where the WebSocket connection doesn't connect initially. - Logs out when we encounter a 403 during token refresh. Only a few auth providers do this; specifically, the `trusted_networks` one will reject token refreshes for auth tokens it vends when not on the trusted network. Otherwise, we'll keep retrying and end up getting banned (if enabled). - Includes the error that we get into the event log so we can trace back the failure easily.
This commit is contained in:
@@ -165,6 +165,8 @@ class ConnectInstanceViewController: UIViewController {
|
||||
)
|
||||
|
||||
self.setAnimationStatus(self.sensorsConfigured, state: .success)
|
||||
}.get { _ in
|
||||
Current.apiConnection.connect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,11 +6,17 @@ import PromiseKit
|
||||
typealias URLRequestConvertible = Alamofire.URLRequestConvertible
|
||||
|
||||
public class AuthenticationAPI {
|
||||
public enum AuthenticationError: Error {
|
||||
case unexepectedType
|
||||
case unexpectedResponse
|
||||
case invalidCode
|
||||
public enum AuthenticationError: LocalizedError {
|
||||
case noConnectionInfo
|
||||
case serverError(statusCode: Int, errorCode: String?, error: String?)
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .noConnectionInfo: return L10n.HaApi.ApiError.notConfigured
|
||||
case let .serverError(statusCode: statusCode, errorCode: errorCode, error: error):
|
||||
return [String(describing: statusCode), errorCode, error].compactMap { $0 }.joined(separator: ", ")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let forcedConnectionInfo: ConnectionInfo?
|
||||
@@ -39,8 +45,8 @@ public class AuthenticationAPI {
|
||||
let request = Session.default.request(routeInfo)
|
||||
|
||||
let context = TokenInfo.TokenInfoContext(oldTokenInfo: tokenInfo)
|
||||
request.validate().responseObject(context: context) { (dataresponse: DataResponse<TokenInfo, AFError>) in
|
||||
switch dataresponse.result {
|
||||
request.validateAuth().responseObject(context: context) { (response: DataResponse<TokenInfo, AFError>) in
|
||||
switch response.result {
|
||||
case let .failure(error):
|
||||
seal.reject(error)
|
||||
case let .success(value):
|
||||
@@ -59,7 +65,7 @@ public class AuthenticationAPI {
|
||||
)
|
||||
let request = Session.default.request(routeInfo)
|
||||
|
||||
request.validate().response { _ in
|
||||
request.validateAuth().response { _ in
|
||||
// https://developers.home-assistant.io/docs/en/auth_api.html#revoking-a-refresh-token says:
|
||||
//
|
||||
// The request will always respond with an empty body and HTTP status 200,
|
||||
@@ -77,30 +83,10 @@ public class AuthenticationAPI {
|
||||
)
|
||||
let request = Session.default.request(routeInfo)
|
||||
|
||||
request.validate().responseObject { (dataresponse: DataResponse<TokenInfo, AFError>) in
|
||||
request.validateAuth().responseObject { (dataresponse: DataResponse<TokenInfo, AFError>) in
|
||||
switch dataresponse.result {
|
||||
case let .failure(networkError):
|
||||
|
||||
guard case let AFError.responseValidationFailed(reason: reason) = networkError,
|
||||
case let AFError.ResponseValidationFailureReason.unacceptableStatusCode(code: code)
|
||||
= reason, code == 400, let errorData = dataresponse.data else {
|
||||
seal.reject(networkError)
|
||||
return
|
||||
}
|
||||
do {
|
||||
let jsonObject = try JSONSerialization.jsonObject(
|
||||
with: errorData,
|
||||
options: .allowFragments
|
||||
)
|
||||
if let errorDictionary = jsonObject as? [String: AnyObject],
|
||||
let errorString = errorDictionary["error_description"] as? String,
|
||||
errorString == "Invalid code" {
|
||||
seal.reject(AuthenticationError.invalidCode)
|
||||
return
|
||||
}
|
||||
} catch {
|
||||
Current.Log.error("Error deserializing failure json response: \(error)")
|
||||
}
|
||||
case let .failure(error):
|
||||
seal.reject(error)
|
||||
case let .success(value):
|
||||
seal.fulfill(value)
|
||||
}
|
||||
@@ -108,3 +94,35 @@ public class AuthenticationAPI {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension DataRequest {
|
||||
@discardableResult
|
||||
func validateAuth() -> Self {
|
||||
validate { _, response, data in
|
||||
if case 200 ..< 300 = response.statusCode {
|
||||
return .success(())
|
||||
} else if let data = data {
|
||||
let errorCode: String?
|
||||
let error: String?
|
||||
|
||||
if let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
|
||||
errorCode = json["error"] as? String
|
||||
error = json["error_description"] as? String
|
||||
} else {
|
||||
errorCode = nil
|
||||
error = String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
return .failure(AuthenticationAPI.AuthenticationError.serverError(
|
||||
statusCode: response.statusCode,
|
||||
errorCode: errorCode,
|
||||
error: error
|
||||
))
|
||||
} else {
|
||||
return .failure(AFError.responseValidationFailed(
|
||||
reason: .unacceptableStatusCode(code: response.statusCode)
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,12 +168,15 @@ public class TokenManager {
|
||||
case let .rejected(error):
|
||||
Current.Log.error("refresh token got error: \(error)")
|
||||
|
||||
if let networkError = error as? AFError, let statusCode = networkError.responseCode,
|
||||
statusCode == 400 {
|
||||
if let underlying = (error as? AFError)?.underlyingError as? AuthenticationAPI.AuthenticationError,
|
||||
case .serverError(400 ... 403, _, _) = underlying {
|
||||
/// Server rejected the refresh token. All is lost.
|
||||
let event = ClientEvent(
|
||||
text: "Refresh token is invalid, showing onboarding",
|
||||
type: .networkRequest
|
||||
type: .networkRequest,
|
||||
payload: [
|
||||
"error": String(describing: underlying),
|
||||
]
|
||||
)
|
||||
Current.clientEventStore.addEvent(event)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user