Skip to content

Commit

Permalink
feat: send only valid JWT in Authorization header
Browse files Browse the repository at this point in the history
  • Loading branch information
grdsdev committed Oct 28, 2024
1 parent 24c6b22 commit e0989a3
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 11 deletions.
14 changes: 7 additions & 7 deletions Sources/Auth/Internal/Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,16 @@ func extractParams(from url: URL) -> [String: String] {
private func extractParams(from fragment: String) -> [URLQueryItem] {
let components =
fragment
.split(separator: "&")
.map { $0.split(separator: "=") }
.split(separator: "&")
.map { $0.split(separator: "=") }

return
components
.compactMap {
$0.count == 2
? URLQueryItem(name: String($0[0]), value: String($0[1]))
: nil
}
.compactMap {
$0.count == 2
? URLQueryItem(name: String($0[0]), value: String($0[1]))
: nil
}
}

func decode(jwt: String) throws -> [String: Any]? {
Expand Down
60 changes: 60 additions & 0 deletions Sources/Supabase/Helpers.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import Foundation
import HTTPTypes
import IssueReporting

let base64UrlRegex = try! NSRegularExpression(
pattern: "^([a-z0-9_-]{4})*($|[a-z0-9_-]{3}$|[a-z0-9_-]{2}$)", options: .caseInsensitive)

/// Checks that the value somewhat looks like a JWT, does not do any additional parsing or verification.
func isJWT(_ value: String) -> Bool {
var token = value

if token.hasPrefix("Bearer ") {
token = String(token.dropFirst("Bearer ".count))
}

token = token.trimmingCharacters(in: .whitespacesAndNewlines)

guard !token.isEmpty else {
return false
}

let parts = token.split(separator: ".")

guard parts.count == 3 else {
return false
}

for part in parts {
if part.count < 4 || !isBase64Url(String(part)) {
return false
}
}

return true
}

func isBase64Url(_ value: String) -> Bool {
let range = NSRange(location: 0, length: value.utf16.count)
return base64UrlRegex.firstMatch(in: value, options: [], range: range) != nil
}

func checkAuthorizationHeader(
_ headers: HTTPFields,
fileID: StaticString = #fileID,
filePath: StaticString = #filePath,
line: UInt = #line,
column: UInt = #column
) {
guard let authorization = headers[.authorization] else { return }

if !isJWT(authorization) {
reportIssue(
"Authorization header does not contain a JWT",
fileID: fileID,
filePath: filePath,
line: line,
column: column
)
}
}
10 changes: 8 additions & 2 deletions Sources/Supabase/SupabaseClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ public final class SupabaseClient: Sendable {
])
.merging(with: HTTPFields(options.global.headers))

checkAuthorizationHeader(_headers)

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

Expand Down Expand Up @@ -351,14 +353,18 @@ public final class SupabaseClient: Sendable {
}

private func adapt(request: URLRequest) async -> URLRequest {
let defaultAccessToken = isJWT(supabaseKey) ? supabaseKey : nil

let token: String? = if let accessToken = options.auth.accessToken {
try? await accessToken()
} else if let accessToken = try? await auth.session.accessToken {
accessToken
} else {
try? await auth.session.accessToken
defaultAccessToken
}

var request = request
if let token {
if let token, isJWT(token), request.value(forHTTPHeaderField: "Authorization") == nil {
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
}
return request
Expand Down
4 changes: 2 additions & 2 deletions Supabase.xcworkspace/xcshareddata/swiftpm/Package.resolved
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/pointfreeco/swift-concurrency-extras",
"state" : {
"revision" : "bb5059bde9022d69ac516803f4f227d8ac967f71",
"version" : "1.1.0"
"revision" : "6054df64b55186f08b6d0fd87152081b8ad8d613",
"version" : "1.2.0"
}
},
{
Expand Down
17 changes: 17 additions & 0 deletions Tests/SupabaseTests/HelpersTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
@testable import Supabase
import XCTest

final class HeleperTests: XCTestCase {
func testIsJWT() {
XCTAssertTrue(isJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"))
XCTAssertTrue(isJWT("Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"))
XCTAssertFalse(isJWT("invalid.token.format"))
XCTAssertFalse(isJWT("part1.part2.part3.part4"))
XCTAssertFalse(isJWT("part1.part2"))
XCTAssertFalse(isJWT(".."))
XCTAssertFalse(isJWT("a.a.a"))
XCTAssertFalse(isJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.*&@!.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"))
XCTAssertFalse(isJWT(""))
XCTAssertFalse(isJWT("Bearer "))
}
}

0 comments on commit e0989a3

Please sign in to comment.