From dde24d42e8d8841296b101dc2c59aea1d37331eb Mon Sep 17 00:00:00 2001 From: ratulsarna Date: Mon, 9 Feb 2026 22:24:54 +0530 Subject: [PATCH] Amp: detect login redirects and fail fast Extracted from #324 (thanks @JosephDoUrden). --- .../Providers/Amp/AmpUsageFetcher.swift | 35 +++++++++++++++++++ .../CodexBarTests/AmpUsageFetcherTests.swift | 27 ++++++++++++++ 2 files changed, 62 insertions(+) diff --git a/Sources/CodexBarCore/Providers/Amp/AmpUsageFetcher.swift b/Sources/CodexBarCore/Providers/Amp/AmpUsageFetcher.swift index 02b52ccad..3ef31c4cb 100644 --- a/Sources/CodexBarCore/Providers/Amp/AmpUsageFetcher.swift +++ b/Sources/CodexBarCore/Providers/Amp/AmpUsageFetcher.swift @@ -261,6 +261,9 @@ public struct AmpUsageFetcher: Sendable { if httpResponse.statusCode == 401 || httpResponse.statusCode == 403 { throw AmpUsageError.invalidCredentials } + if diagnostics.detectedLoginRedirect { + throw AmpUsageError.invalidCredentials + } throw AmpUsageError.networkError("HTTP \(httpResponse.statusCode)") } @@ -277,6 +280,7 @@ public struct AmpUsageFetcher: Sendable { private let cookieHeader: String private let logger: ((String) -> Void)? var redirects: [String] = [] + private(set) var detectedLoginRedirect = false init(cookieHeader: String, logger: ((String) -> Void)?) { self.cookieHeader = cookieHeader @@ -293,6 +297,16 @@ public struct AmpUsageFetcher: Sendable { let from = response.url?.absoluteString ?? "unknown" let to = request.url?.absoluteString ?? "unknown" self.redirects.append("\(response.statusCode) \(from) -> \(to)") + + if let toURL = request.url, AmpUsageFetcher.isLoginRedirect(toURL) { + if let logger { + logger("[amp] Detected login redirect, aborting (invalid session)") + } + self.detectedLoginRedirect = true + completionHandler(nil) + return + } + var updated = request if AmpUsageFetcher.shouldAttachCookie(to: request.url), !self.cookieHeader.isEmpty { updated.setValue(self.cookieHeader, forHTTPHeaderField: "Cookie") @@ -364,4 +378,25 @@ public struct AmpUsageFetcher: Sendable { if host == "ampcode.com" || host == "www.ampcode.com" { return true } return host.hasSuffix(".ampcode.com") } + + static func isLoginRedirect(_ url: URL) -> Bool { + guard self.shouldAttachCookie(to: url) else { return false } + + let path = url.path.lowercased() + let components = path.split(separator: "/").map(String.init) + if components.contains("login") { return true } + if components.contains("signin") { return true } + if components.contains("sign-in") { return true } + + // Amp currently redirects to /auth/sign-in?returnTo=... when session is invalid. Keep this slightly broader + // than one exact path so we keep working if Amp changes auth routes. + if components.contains("auth") { + let query = url.query?.lowercased() ?? "" + if query.contains("returnto=") { return true } + if query.contains("redirect=") { return true } + if query.contains("redirectto=") { return true } + } + + return false + } } diff --git a/Tests/CodexBarTests/AmpUsageFetcherTests.swift b/Tests/CodexBarTests/AmpUsageFetcherTests.swift index afbf81c9c..8a4e8506c 100644 --- a/Tests/CodexBarTests/AmpUsageFetcherTests.swift +++ b/Tests/CodexBarTests/AmpUsageFetcherTests.swift @@ -17,4 +17,31 @@ struct AmpUsageFetcherTests { #expect(!AmpUsageFetcher.shouldAttachCookie(to: URL(string: "https://ampcode.com.evil.com"))) #expect(!AmpUsageFetcher.shouldAttachCookie(to: nil)) } + + @Test + func detectsLoginRedirects() throws { + let signIn = try #require(URL(string: "https://ampcode.com/auth/sign-in?returnTo=%2Fsettings")) + #expect(AmpUsageFetcher.isLoginRedirect(signIn)) + + let sso = try #require(URL(string: "https://ampcode.com/auth/sso?returnTo=%2Fsettings")) + #expect(AmpUsageFetcher.isLoginRedirect(sso)) + + let login = try #require(URL(string: "https://ampcode.com/login")) + #expect(AmpUsageFetcher.isLoginRedirect(login)) + + let signin = try #require(URL(string: "https://www.ampcode.com/signin")) + #expect(AmpUsageFetcher.isLoginRedirect(signin)) + } + + @Test + func ignoresNonLoginURLs() throws { + let settings = try #require(URL(string: "https://ampcode.com/settings")) + #expect(!AmpUsageFetcher.isLoginRedirect(settings)) + + let signOut = try #require(URL(string: "https://ampcode.com/auth/sign-out")) + #expect(!AmpUsageFetcher.isLoginRedirect(signOut)) + + let evil = try #require(URL(string: "https://ampcode.com.evil.com/auth/sign-in")) + #expect(!AmpUsageFetcher.isLoginRedirect(evil)) + } }