Make API auth contexts safe to copy across threads

This commit is contained in:
jdb committed 2026-09-29 08:51:20 -05:00
1 parent 5e05be6d0e
commit e86100176c
5 files changed
+163 -13

No files matched your search

+10 -10
View File
@@ -13,7 +13,10 @@ type
AuthError* = object of CatchableError
additionalInfo*: Option[TableRef[string, JsonNode]]
ApiAuthContext* = ref object
ApiAuthContext* = object
## An owned authentication configuration and signing-key cache. Copy before
## handing to a worker; each worker's cache updates stay local to its copy.
## Concurrent access to the same mutable context still needs synchronization.
appDomain*: string ## Application domain for session cookies
cookiePrefix*: string ## Prefix for the user and session cookies
validAudiences*: seq[string] ## Expected audience values for for `aud` JWT check
@@ -26,7 +29,7 @@ type
## must be provided either when the ApiAuthContext is initialized (see
## `initApiAuthContext` or via `addSigningKeys`
issuerKeys: TableRef[string, JwkSet]
issuerKeys: Table[string, JwkSet]
proc failAuth*[T](
@@ -71,7 +74,7 @@ proc initApiAuthContext*(
issuer: issuer,
trustedIssuers: trustedIssuers,
signingKid: signingKid,
issuerKeys: newTable[string, JwkSet]([(issuer, signingKeys)]))
issuerKeys: toTable([(issuer, signingKeys)]))
proc fetchJWKs(openIdConfigUrl: string): JwkSet {.gcsafe.} =
@@ -101,17 +104,16 @@ proc fetchJWKs(openIdConfigUrl: string): JwkSet {.gcsafe.} =
parentException = getCurrentException())
proc addSigningKeys*(ctx: ApiAuthContext, issuer: string, keySet: JwkSet): void =
proc addSigningKeys*(ctx: var ApiAuthContext, issuer: string, keySet: JwkSet): void =
## Manually add a set of signing keys associated with a given issuer.
try:
for k in keySet: validateSigningKey(k)
if ctx.issuerKeys.isNil: ctx.issuerKeys = newTable[string, JwkSet]()
ctx.issuerKeys[issuer] = keySet
except:
raise getCurrentException()
proc findSigningKey*(ctx: ApiAuthContext, jwt: JWT, allowFetch = true): JWK {.gcsafe.} =
proc findSigningKey*(ctx: var ApiAuthContext, jwt: JWT, allowFetch = true): JWK {.gcsafe.} =
## Lookup the signing key for a given JWT. This method assumes that you trust
## the issuer named in the JWT.
##
@@ -124,8 +126,6 @@ proc findSigningKey*(ctx: ApiAuthContext, jwt: JWT, allowFetch = true): JWK {.gc
if jwt.claims.iss.isNone: failAuth "JWT is missing 'iss' claim."
if jwt.header.kid.isNone: failAuth "JWT is missing 'kid' header."
if ctx.issuerKeys.isNil: ctx.issuerKeys = newTable[string, JwkSet]()
let jwtIssuer = jwt.claims.iss.get
if ctx.issuerKeys.hasKey(jwtIssuer):
@@ -151,7 +151,7 @@ proc findSigningKey*(ctx: ApiAuthContext, jwt: JWT, allowFetch = true): JWK {.gc
failAuth("unable to find JWT signing key", getCurrentException())
proc validateJWT*(ctx: ApiAuthContext, jwt: JWT) =
proc validateJWT*(ctx: var ApiAuthContext, jwt: JWT) =
## Given a JWT, validate that it is a well-formed JWT, validate the issuer's
## signature on the token, and validate all the claims that it preesnts.
try:
@@ -195,7 +195,7 @@ proc validateJWT*(ctx: ApiAuthContext, jwt: JWT) =
failAuth(getCurrentExceptionMsg(), getCurrentException())
proc extractValidJwt*(ctx: ApiAuthContext, req: Request, validateCsrf = true): JWT =
proc extractValidJwt*(ctx: var ApiAuthContext, req: Request, validateCsrf = true): JWT =
## Extracts a valid JWT representing the user's authentication and
## authorization details, if present. If there are no valid credentials an
## exception is raised.