Skip to content

PAT auth at AuthToken works by accident — the handler's assertion list is dead code #1698

@AmanGIT07

Description

@AmanGIT07

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions