Skip to content

Commit e0989a3

Browse files
committed
feat: send only valid JWT in Authorization header
1 parent 24c6b22 commit e0989a3

File tree

5 files changed

+94
-11
lines changed

5 files changed

+94
-11
lines changed

Sources/Auth/Internal/Helpers.swift

+7-7
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,16 @@ 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
}
4242

4343
func decode(jwt: String) throws -> [String: Any]? {

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

+8-2
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,8 @@ public final class SupabaseClient: Sendable {
161161
])
162162
.merging(with: HTTPFields(options.global.headers))
163163

164+
checkAuthorizationHeader(_headers)
165+
164166
// default storage key uses the supabase project ref as a namespace
165167
let defaultStorageKey = "sb-\(supabaseURL.host!.split(separator: ".")[0])-auth-token"
166168

@@ -351,14 +353,18 @@ public final class SupabaseClient: Sendable {
351353
}
352354

353355
private func adapt(request: URLRequest) async -> URLRequest {
356+
let defaultAccessToken = isJWT(supabaseKey) ? supabaseKey : nil
357+
354358
let token: String? = if let accessToken = options.auth.accessToken {
355359
try? await accessToken()
360+
} else if let accessToken = try? await auth.session.accessToken {
361+
accessToken
356362
} else {
357-
try? await auth.session.accessToken
363+
defaultAccessToken
358364
}
359365

360366
var request = request
361-
if let token {
367+
if let token, isJWT(token), request.value(forHTTPHeaderField: "Authorization") == nil {
362368
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
363369
}
364370
return request

Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved

+2-2
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,8 @@
6868
"kind" : "remoteSourceControl",
6969
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
7070
"state" : {
71-
"revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71",
72-
"version" : "1.1.0"
71+
"revision" : "6054df64b55186f08b6d0fd87152081b8ad8d613",
72+
"version" : "1.2.0"
7373
}
7474
},
7575
{
+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)