While digging into a PAT auth problem I realised that AuthToken accepting PATs is basically an accident.
The handler is explicit about what it wants to allow:
// only get principal from service user assertions
principal, err := h.GetLoggedInPrincipal(ctx,
authenticate.SessionClientAssertion,
authenticate.ClientCredentialsClientAssertion,
authenticate.JWTGrantClientAssertion)
No PAT in that list. Going by this code alone, a Bearer fpt_... request should get rejected: session skips (no cookie), client credentials skips (no basic auth), and the JWT grant authenticator fails hard trying to parse an opaque PAT as a JWT.
The reason it works anyway is that the authentication interceptor runs first — AuthToken is not in authenticationSkipList — and it calls GetLoggedInPrincipal with no assertions, which falls back to APIAssertions, and that list does include PAT. The interceptor authenticates the PAT and caches the principal in the request context. By the time the handler asks again with its restricted list, GetPrincipal returns the cached principal before ever looking at the assertions:
if val, ok := GetPrincipalFromContext(ctx); ok {
return *val, nil
}
So the whitelist in the handler is effectively dead code, and PAT token exchange only works because three unrelated things happen to line up: AuthToken isn't in the interceptor's skip list, the interceptor passes no assertions, and GetPrincipal prefers whatever is already in context.
This is fragile in both directions:
- If someone ever adds AuthToken to the skip list — a pretty reasonable cleanup, given Authenticate, AuthCallback and ListAuthStrategies are already there — PAT exchange breaks everywhere, silently. Nothing fails at compile time and there's no test covering PAT → AuthToken.
- The restriction the comment asks for isn't actually enforced. For example an existing frontier access token can be presented at AuthToken to mint a fresh one, indefinitely, even though
AccessTokenClientAssertion was deliberately left out of the handler's list.
I think the fix is to make this explicit instead of accidental: add PATClientAssertion to the handler's list, decide whether handler-level assertion lists should win over the interceptor-cached principal (or put AuthToken in the skip list so the handler's list is the real gate), and pin it all with tests — Bearer fpt_... at AuthToken should return 200 with a sub_type=app/pat JWT, and the credential types we want rejected should have tests saying so.
While digging into a PAT auth problem I realised that
AuthTokenaccepting PATs is basically an accident.The handler is explicit about what it wants to allow:
No PAT in that list. Going by this code alone, a
Bearer fpt_...request should get rejected: session skips (no cookie), client credentials skips (no basic auth), and the JWT grant authenticator fails hard trying to parse an opaque PAT as a JWT.The reason it works anyway is that the authentication interceptor runs first —
AuthTokenis not inauthenticationSkipList— and it callsGetLoggedInPrincipalwith no assertions, which falls back toAPIAssertions, and that list does include PAT. The interceptor authenticates the PAT and caches the principal in the request context. By the time the handler asks again with its restricted list,GetPrincipalreturns the cached principal before ever looking at the assertions:So the whitelist in the handler is effectively dead code, and PAT token exchange only works because three unrelated things happen to line up: AuthToken isn't in the interceptor's skip list, the interceptor passes no assertions, and GetPrincipal prefers whatever is already in context.
This is fragile in both directions:
AccessTokenClientAssertionwas deliberately left out of the handler's list.I think the fix is to make this explicit instead of accidental: add
PATClientAssertionto the handler's list, decide whether handler-level assertion lists should win over the interceptor-cached principal (or put AuthToken in the skip list so the handler's list is the real gate), and pin it all with tests —Bearer fpt_...at AuthToken should return 200 with asub_type=app/patJWT, and the credential types we want rejected should have tests saying so.