Skip to content

Commit 198704a

Browse files
committed
feat: send only valid JWT in Authorization header
1 parent 4fd8252 commit 198704a

File tree

4 files changed

+87
-8
lines changed

4 files changed

+87
-8
lines changed

Sources/Auth/Internal/Helpers.swift

+7-7
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,14 @@ func extractParams(from url: URL) -> [String: String] {
2828
private func extractParams(from fragment: String) -> [URLQueryItem] {
2929
let components =
3030
fragment
31-
.split(separator: "&")
32-
.map { $0.split(separator: "=") }
31+
.split(separator: "&")
32+
.map { $0.split(separator: "=") }
3333

3434
return
3535
components
36-
.compactMap {
37-
$0.count == 2
38-
? URLQueryItem(name: String($0[0]), value: String($0[1]))
39-
: nil
40-
}
36+
.compactMap {
37+
$0.count == 2
38+
? URLQueryItem(name: String($0[0]), value: String($0[1]))
39+
: nil
40+
}
4141
}

Sources/Supabase/Helpers.swift

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Foundation
2+
import HTTPTypes
3+
import IssueReporting
4+
5+
let base64UrlRegex = try! NSRegularExpression(
6+
pattern: "^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)", options: .caseInsensitive)
7+
8+
/// Checks that the value somewhat looks like a JWT, does not do any additional parsing or verification.
9+
func isJWT(_ value: String) -> Bool {
10+
var token = value
11+
12+
if token.hasPrefix("Bearer ") {
13+
token = String(token.dropFirst("Bearer ".count))
14+
}
15+
16+
token = token.trimmingCharacters(in: .whitespacesAndNewlines)
17+
18+
guard !token.isEmpty else {
19+
return false
20+
}
21+
22+
let parts = token.split(separator: ".")
23+
24+
guard parts.count == 3 else {
25+
return false
26+
}
27+
28+
for part in parts {
29+
if part.count < 4 || !isBase64Url(String(part)) {
30+
return false
31+
}
32+
}
33+
34+
return true
35+
}
36+
37+
func isBase64Url(_ value: String) -> Bool {
38+
let range = NSRange(location: 0, length: value.utf16.count)
39+
return base64UrlRegex.firstMatch(in: value, options: [], range: range) != nil
40+
}
41+
42+
func checkAuthorizationHeader(
43+
_ headers: HTTPFields,
44+
fileID: StaticString = #fileID,
45+
filePath: StaticString = #filePath,
46+
line: UInt = #line,
47+
column: UInt = #column
48+
) {
49+
guard let authorization = headers[.authorization] else { return }
50+
51+
if !isJWT(authorization) {
52+
reportIssue(
53+
"Authorization header does not contain a JWT",
54+
fileID: fileID,
55+
filePath: filePath,
56+
line: line,
57+
column: column
58+
)
59+
}
60+
}

Sources/Supabase/SupabaseClient.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ public final class SupabaseClient: Sendable {
170170
])
171171
.merging(with: HTTPFields(options.global.headers))
172172

173+
checkAuthorizationHeader(_headers)
174+
173175
// default storage key uses the supabase project ref as a namespace
174176
let defaultStorageKey = "sb-\(supabaseURL.host!.split(separator: ".")[0])-auth-token"
175177

@@ -351,7 +353,7 @@ public final class SupabaseClient: Sendable {
351353
let token = try? await _getAccessToken()
352354

353355
var request = request
354-
if let token {
356+
if let token, isJWT(token), request.value(forHTTPHeaderField: "Authorization") == nil {
355357
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
356358
}
357359
return request
+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
@testable import Supabase
2+
import XCTest
3+
4+
final class HeleperTests: XCTestCase {
5+
func testIsJWT() {
6+
XCTAssertTrue(isJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"))
7+
XCTAssertTrue(isJWT("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"))
8+
XCTAssertFalse(isJWT("invalid.token.format"))
9+
XCTAssertFalse(isJWT("part1.part2.part3.part4"))
10+
XCTAssertFalse(isJWT("part1.part2"))
11+
XCTAssertFalse(isJWT(".."))
12+
XCTAssertFalse(isJWT("a.a.a"))
13+
XCTAssertFalse(isJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.*&@!.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"))
14+
XCTAssertFalse(isJWT(""))
15+
XCTAssertFalse(isJWT("Bearer "))
16+
}
17+
}

0 commit comments

Comments
 (0)