Skip to content

Commit c6aa8ef

Browse files
authored
fix(auth): remove session when it has been revoked (#802)
* fix(auth): remove session when it has been revoked * test: add test for the session revoke * fix: also check for refresh_token_not_found * test: proper test token refresh logic * fix: add more error codes to the condition
1 parent 5ce77a5 commit c6aa8ef

File tree

2 files changed

+120
-6
lines changed

2 files changed

+120
-6
lines changed

Sources/Auth/Internal/APIClient.swift

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,26 @@ struct APIClient: Sendable {
2727
Dependencies[clientID].configuration
2828
}
2929

30+
var sessionManager: SessionManager {
31+
Dependencies[clientID].sessionManager
32+
}
33+
34+
var eventEmitter: AuthStateChangeEventEmitter {
35+
Dependencies[clientID].eventEmitter
36+
}
37+
3038
var http: any HTTPClientType {
3139
Dependencies[clientID].http
3240
}
3341

42+
/// Error codes that should clean up local session.
43+
private let sessionCleanupErrorCodes: [ErrorCode] = [
44+
.sessionNotFound,
45+
.sessionExpired,
46+
.refreshTokenNotFound,
47+
.refreshTokenAlreadyUsed,
48+
]
49+
3450
func execute(_ request: Helpers.HTTPRequest) async throws -> Helpers.HTTPResponse {
3551
var request = request
3652
request.headers = HTTPFields(configuration.headers).merging(with: request.headers)
@@ -42,7 +58,7 @@ struct APIClient: Sendable {
4258
let response = try await http.send(request)
4359

4460
guard 200..<300 ~= response.statusCode else {
45-
throw handleError(response: response)
61+
throw await handleError(response: response)
4662
}
4763

4864
return response
@@ -62,7 +78,7 @@ struct APIClient: Sendable {
6278
return try await execute(request)
6379
}
6480

65-
func handleError(response: Helpers.HTTPResponse) -> AuthError {
81+
func handleError(response: Helpers.HTTPResponse) async -> AuthError {
6682
guard
6783
let error = try? response.decoded(
6884
as: _RawAPIErrorResponse.self,
@@ -98,7 +114,12 @@ struct APIClient: Sendable {
98114
message: error._getErrorMessage(),
99115
reasons: error.weakPassword?.reasons ?? []
100116
)
101-
} else if errorCode == .sessionNotFound {
117+
} else if let errorCode, sessionCleanupErrorCodes.contains(errorCode) {
118+
// The `session_id` inside the JWT does not correspond to a row in the
119+
// `sessions` table. This usually means the user has signed out, has been
120+
// deleted, or their session has somehow been terminated.
121+
await sessionManager.remove()
122+
eventEmitter.emit(.signedOut, session: nil)
102123
return .sessionMissing
103124
} else {
104125
return .api(

Tests/AuthTests/AuthClientTests.swift

Lines changed: 96 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2151,6 +2151,82 @@ final class AuthClientTests: XCTestCase {
21512151
)
21522152
}
21532153

2154+
func testRemoveSessionAndSignoutIfSessionNotFoundErrorReturned() async throws {
2155+
let sut = makeSUT()
2156+
2157+
Mock(
2158+
url: clientURL.appendingPathComponent("user"),
2159+
statusCode: 403,
2160+
data: [
2161+
.get: Data(
2162+
"""
2163+
{
2164+
"error_code": "session_not_found",
2165+
"message": "Session not found"
2166+
}
2167+
""".utf8
2168+
)
2169+
]
2170+
)
2171+
.register()
2172+
2173+
Dependencies[sut.clientID].sessionStorage.store(.validSession)
2174+
2175+
try await assertAuthStateChanges(
2176+
sut: sut,
2177+
action: {
2178+
do {
2179+
_ = try await sut.user()
2180+
XCTFail("Expected failure")
2181+
} catch {
2182+
XCTAssertEqual(error as? AuthError, .sessionMissing)
2183+
}
2184+
},
2185+
expectedEvents: [.initialSession, .signedOut]
2186+
)
2187+
2188+
XCTAssertNil(Dependencies[sut.clientID].sessionStorage.get())
2189+
}
2190+
2191+
func testRemoveSessionAndSignoutIfRefreshTokenNotFoundErrorReturned() async throws {
2192+
let sut = makeSUT()
2193+
2194+
Mock(
2195+
url: clientURL.appendingPathComponent("token").appendingQueryItems([
2196+
URLQueryItem(name: "grant_type", value: "refresh_token")
2197+
]),
2198+
statusCode: 403,
2199+
data: [
2200+
.post: Data(
2201+
"""
2202+
{
2203+
"error_code": "refresh_token_not_found",
2204+
"message": "Invalid Refresh Token: Refresh Token Not Found"
2205+
}
2206+
""".utf8
2207+
)
2208+
]
2209+
)
2210+
.register()
2211+
2212+
Dependencies[sut.clientID].sessionStorage.store(.expiredSession)
2213+
2214+
try await assertAuthStateChanges(
2215+
sut: sut,
2216+
action: {
2217+
do {
2218+
_ = try await sut.session
2219+
XCTFail("Expected failure")
2220+
} catch {
2221+
XCTAssertEqual(error as? AuthError, .sessionMissing)
2222+
}
2223+
},
2224+
expectedEvents: [.signedOut]
2225+
)
2226+
2227+
XCTAssertNil(Dependencies[sut.clientID].sessionStorage.get())
2228+
}
2229+
21542230
private func makeSUT(flowType: AuthFlowType = .pkce) -> AuthClient {
21552231
let sessionConfiguration = URLSessionConfiguration.default
21562232
sessionConfiguration.protocolClasses = [MockingURLProtocol.self]
@@ -2198,6 +2274,7 @@ final class AuthClientTests: XCTestCase {
21982274
action: () async throws -> T,
21992275
expectedEvents: [AuthChangeEvent],
22002276
expectedSessions: [Session?]? = nil,
2277+
timeout: TimeInterval = 2,
22012278
fileID: StaticString = #fileID,
22022279
filePath: StaticString = #filePath,
22032280
line: UInt = #line,
@@ -2211,14 +2288,30 @@ final class AuthClientTests: XCTestCase {
22112288

22122289
let result = try await action()
22132290

2214-
let authStateChanges = await eventsTask.value
2291+
let authStateChanges = try await withTimeout(interval: timeout) {
2292+
await eventsTask.value
2293+
}
22152294
let events = authStateChanges.map(\.event)
22162295
let sessions = authStateChanges.map(\.session)
22172296

2218-
expectNoDifference(events, expectedEvents, fileID: fileID, filePath: filePath, line: line, column: column)
2297+
expectNoDifference(
2298+
events,
2299+
expectedEvents,
2300+
fileID: fileID,
2301+
filePath: filePath,
2302+
line: line,
2303+
column: column
2304+
)
22192305

22202306
if let expectedSessions = expectedSessions {
2221-
expectNoDifference(sessions, expectedSessions, fileID: fileID, filePath: filePath, line: line, column: column)
2307+
expectNoDifference(
2308+
sessions,
2309+
expectedSessions,
2310+
fileID: fileID,
2311+
filePath: filePath,
2312+
line: line,
2313+
column: column
2314+
)
22222315
}
22232316

22242317
return result

0 commit comments

Comments
 (0)