@@ -30,6 +30,34 @@ struct AuthClientLoggerDecorator: SupabaseLogger {
30
30
}
31
31
}
32
32
33
+ /// JWKS cache TTL (Time To Live) - 10 minutes
34
+ private let JWKS_TTL : TimeInterval = 10 * 60
35
+
36
+ /// Cached JWKS value with timestamp
37
+ private struct CachedJWKS {
38
+ let jwks : JWKS
39
+ let cachedAt : Date
40
+ }
41
+
42
+ /// Global JWKS cache shared across all clients with the same storage key.
43
+ /// This is especially useful for shared-memory execution environments such as
44
+ /// AWS Lambda or serverless functions. Regardless of how many clients are created,
45
+ /// if they share the same storage key they will use the same JWKS cache,
46
+ /// significantly speeding up getClaims() with asymmetric JWTs.
47
+ private actor GlobalJWKSCache {
48
+ private var cache : [ String : CachedJWKS ] = [ : ]
49
+
50
+ func get( for key: String ) -> CachedJWKS ? {
51
+ cache [ key]
52
+ }
53
+
54
+ func set( _ value: CachedJWKS , for key: String ) {
55
+ cache [ key] = value
56
+ }
57
+ }
58
+
59
+ private let globalJWKSCache = GlobalJWKSCache ( )
60
+
33
61
public actor AuthClient {
34
62
static var globalClientID = 0
35
63
nonisolated let clientID : AuthClientID
@@ -53,9 +81,6 @@ public actor AuthClient {
53
81
nonisolated private var sessionStorage : SessionStorage { Dependencies [ clientID] . sessionStorage }
54
82
nonisolated private var pkce : PKCE { Dependencies [ clientID] . pkce }
55
83
56
- /// Cache for JWKS (JSON Web Key Set)
57
- private var jwksCache : JWKS ?
58
-
59
84
/// Returns the session, refreshing it if necessary.
60
85
///
61
86
/// If no session can be found, a ``AuthError/sessionMissing`` error is thrown.
@@ -1452,16 +1477,24 @@ public actor AuthClient {
1452
1477
return url
1453
1478
}
1454
1479
1455
- /// Fetches a JWK from the JWKS endpoint
1480
+ /// Fetches a JWK from the JWKS endpoint with caching
1456
1481
private func fetchJWK( kid: String , jwks: JWKS ? = nil ) async throws -> JWK {
1457
1482
// Try fetching from the supplied jwks
1458
1483
if let jwk = jwks? . keys. first ( where: { $0. kid == kid } ) {
1459
1484
return jwk
1460
1485
}
1461
1486
1462
- // Try fetching from cache
1463
- if let jwk = jwksCache? . keys. first ( where: { $0. kid == kid } ) {
1464
- return jwk
1487
+ let now = date ( )
1488
+ let storageKey = configuration. storageKey ?? defaultStorageKey
1489
+
1490
+ // Try fetching from global cache
1491
+ if let cached = await globalJWKSCache. get ( for: storageKey) ,
1492
+ let jwk = cached. jwks. keys. first ( where: { $0. kid == kid } )
1493
+ {
1494
+ // Check if cache is still valid (not stale)
1495
+ if cached. cachedAt. addingTimeInterval ( JWKS_TTL) > now {
1496
+ return jwk
1497
+ }
1465
1498
}
1466
1499
1467
1500
// Fetch from well-known endpoint
@@ -1478,8 +1511,11 @@ public actor AuthClient {
1478
1511
throw AuthError . jwtVerificationFailed ( message: " JWKS is empty " )
1479
1512
}
1480
1513
1481
- // Cache the JWKS
1482
- jwksCache = fetchedJWKS
1514
+ // Cache the JWKS globally
1515
+ await globalJWKSCache. set (
1516
+ CachedJWKS ( jwks: fetchedJWKS, cachedAt: now) ,
1517
+ for: storageKey
1518
+ )
1483
1519
1484
1520
// Find the signing key
1485
1521
guard let jwk = fetchedJWKS. keys. first ( where: { $0. kid == kid } ) else {
@@ -1489,22 +1525,27 @@ public actor AuthClient {
1489
1525
return jwk
1490
1526
}
1491
1527
1492
- /// Verifies and extracts claims from a JWT.
1528
+ /// Extracts the JWT claims present in the access token by first verifying the
1529
+ /// JWT against the server's JSON Web Key Set endpoint `/.well-known/jwks.json`
1530
+ /// which is often cached, resulting in significantly faster responses. Prefer
1531
+ /// this method over ``user(jwt:)`` which always sends a request to the Auth
1532
+ /// server for each JWT.
1493
1533
///
1494
- /// This method verifies the JWT signature and returns the claims if valid. For symmetric JWTs (HS256),
1495
- /// it validates against the server using the `getUser` method. For asymmetric JWTs (RS256, ES256),
1496
- /// it verifies the signature using the JWKS (JSON Web Key Set) from the well-known endpoint .
1534
+ /// If the project is not using an asymmetric JWT signing key (like ECC or RSA)
1535
+ /// it always sends a request to the Auth server (similar to ``user(jwt:)``) to
1536
+ /// verify the JWT .
1497
1537
///
1498
1538
/// - Parameters:
1499
- /// - jwt: The JWT to verify. If nil, uses the access token from the current session.
1500
- /// - jwks: Optional JWKS to use for verification. If nil, fetches from the server .
1539
+ /// - jwt: An optional specific JWT you wish to verify, not the one you can obtain from `` session`` .
1540
+ /// - options: Various additional options that allow you to customize the behavior of this method .
1501
1541
///
1502
1542
/// - Returns: A `JWTClaimsResponse` containing the verified claims, header, and signature.
1503
1543
///
1504
1544
/// - Throws: `AuthError.jwtVerificationFailed` if verification fails, or `AuthError.sessionMissing` if no session exists.
1505
- ///
1506
- /// - Note: This is an experimental method and may change in future versions.
1507
- public func getClaims( jwt: String ? = nil , jwks: JWKS ? = nil ) async throws -> JWTClaimsResponse {
1545
+ public func getClaims(
1546
+ jwt: String ? = nil ,
1547
+ options: GetClaimsOptions = GetClaimsOptions ( )
1548
+ ) async throws -> JWTClaimsResponse {
1508
1549
let token : String
1509
1550
if let jwt {
1510
1551
token = jwt
@@ -1519,11 +1560,13 @@ public actor AuthClient {
1519
1560
throw AuthError . jwtVerificationFailed ( message: " Invalid JWT structure " )
1520
1561
}
1521
1562
1522
- // Validate expiration
1523
- if let exp = decodedJWT. payload [ " exp " ] as? TimeInterval {
1524
- let now = Date ( ) . timeIntervalSince1970
1525
- if exp <= now {
1526
- throw AuthError . jwtVerificationFailed ( message: " JWT has expired " )
1563
+ // Validate expiration unless allowExpired is true
1564
+ if !options. allowExpired {
1565
+ if let exp = decodedJWT. payload [ " exp " ] as? TimeInterval {
1566
+ let now = date ( ) . timeIntervalSince1970
1567
+ if exp <= now {
1568
+ throw AuthError . jwtVerificationFailed ( message: " JWT has expired " )
1569
+ }
1527
1570
}
1528
1571
}
1529
1572
@@ -1535,8 +1578,14 @@ public actor AuthClient {
1535
1578
if alg == " HS256 " || alg == " RS256 " || kid == nil {
1536
1579
_ = try await user ( jwt: token)
1537
1580
// getUser succeeds, so claims can be trusted
1538
- let claims = try configuration. decoder. decode ( JWTClaims . self, from: JSONSerialization . data ( withJSONObject: decodedJWT. payload) )
1539
- let header = try configuration. decoder. decode ( JWTHeader . self, from: JSONSerialization . data ( withJSONObject: decodedJWT. header) )
1581
+ let claims = try configuration. decoder. decode (
1582
+ JWTClaims . self,
1583
+ from: JSONSerialization . data ( withJSONObject: decodedJWT. payload)
1584
+ )
1585
+ let header = try configuration. decoder. decode (
1586
+ JWTHeader . self,
1587
+ from: JSONSerialization . data ( withJSONObject: decodedJWT. header)
1588
+ )
1540
1589
return JWTClaimsResponse ( claims: claims, header: header, signature: decodedJWT. signature)
1541
1590
}
1542
1591
@@ -1545,7 +1594,7 @@ public actor AuthClient {
1545
1594
throw AuthError . jwtVerificationFailed ( message: " Missing kid in JWT header " )
1546
1595
}
1547
1596
1548
- let signingKey = try await fetchJWK ( kid: kid, jwks: jwks)
1597
+ let signingKey = try await fetchJWK ( kid: kid, jwks: options . jwks)
1549
1598
1550
1599
let isValid = try JWTVerifier . verify ( jwt: decodedJWT, jwk: signingKey)
1551
1600
@@ -1554,8 +1603,14 @@ public actor AuthClient {
1554
1603
}
1555
1604
1556
1605
// Decode claims and header
1557
- let claims = try configuration. decoder. decode ( JWTClaims . self, from: JSONSerialization . data ( withJSONObject: decodedJWT. payload) )
1558
- let header = try configuration. decoder. decode ( JWTHeader . self, from: JSONSerialization . data ( withJSONObject: decodedJWT. header) )
1606
+ let claims = try configuration. decoder. decode (
1607
+ JWTClaims . self,
1608
+ from: JSONSerialization . data ( withJSONObject: decodedJWT. payload)
1609
+ )
1610
+ let header = try configuration. decoder. decode (
1611
+ JWTHeader . self,
1612
+ from: JSONSerialization . data ( withJSONObject: decodedJWT. header)
1613
+ )
1559
1614
1560
1615
return JWTClaimsResponse ( claims: claims, header: header, signature: decodedJWT. signature)
1561
1616
}
0 commit comments