Skip to content

Instantly share code, notes, and snippets.

@ulcuber
Last active July 21, 2026 16:28
Show Gist options
  • Select an option

  • Save ulcuber/7b098e5e9d5e3cd106aeefbafb950974 to your computer and use it in GitHub Desktop.

Select an option

Save ulcuber/7b098e5e9d5e3cd106aeefbafb950974 to your computer and use it in GitHub Desktop.

What about having a decoupled laravel OAuth Resource Server

The discussion started long ago at 2016:

laravel/passport#24

Good description on what is The Resource Server:

https://www.oauth.com/oauth2-servers/the-resource-server/

Brief terminology explanation:

https://oauth2.thephpleague.com/terminology/

We have Self-Encoded Access Tokens in Laravel like described there:

https://www.oauth.com/oauth2-servers/access-tokens/self-encoded-access-tokens/

And we have a problem when it comes to Invalidating

League\OAuth2\Server\AuthorizationValidators\BearerTokenValidator is responsible for validating JWT tokens. It is used by League\OAuth2\Server\ResourceServer which itself has AccessTokenRepository and publicKey that it shares with the validator. And Laravel is responsible for providing such repository via the bridge — Laravel\Passport\Bridge\AccessTokenRepository

As mentioned earlier about Self-Encoded Access Tokens, they include claims as in rfc9068 and the first issue in the BearerTokenValidator starts like:

<?php
        $claims = $token->claims();

        // Check if token has been revoked
        if ($this->accessTokenRepository->isAccessTokenRevoked($claims->get('jti'))) {
            throw OAuthServerException::accessDenied('Access token has been revoked');
        }

So it gets token claims and Laravel provides info using the AccessTokenRepository like:

<?php
    public function isAccessTokenRevoked(string $tokenId): bool
    {
        return Passport::token()->newQuery()->whereKey($tokenId)->where('revoked', false)->doesntExist();
    }

That is how we check Invalidation on the Authorization Server

So we can't rely on the Self-Encoded Access Tokens alone and use Laravel Passport Models to access the database. On the Resource Server it can be achieved via PASSPORT_CONNECTION environment variable (passport.connection in config that is used by getConnectionName in the Models). And like this we can use a shared database probably in closed infrastracture because exposing databases is dangerous. And what is stored in Models we call Token Meta-Information


The next step is using Token Introspection

Also described:

https://www.oauth.com/oauth2-servers/token-introspection-endpoint/

So we can try implementing rfc7662. See the progress for the API Endpoint itself:

Pull requests above also suggest Token Revocation as described in rfc7009. It describes TokenServer class in addition to existing ResourceServer and AuthorizationServer. As per 13.x Laravel Passport only provides revokation via Models:

https://laravel.com/docs/13.x/passport#revoking-tokens

Aside that, we will also need a Token Introspection Endpoint Consumer. To achieve it we can implement in Resource Server the whole AccessTokenRepositoryInterface to use external Introspection Endpoint of the Authorization server. That way the Resource Server itself needs to authorize in the Authorization Server using something like Authorization Code Grant (need for a custom middleware for the Endpoint). And the Authorization Server will provide a proxy to Laravel\Passport\Bridge\AccessTokenRepository via Introspection Endpoint aka AccessTokenRepositoryInterface on Passport Models

And distributed infrastructure will add performance issues. As mentioned in Token Introspection brief from oauth site, that creates needs for caching. And revoking tokens might use a Message Broker to notify Consumers about invalidating cache. And the Message Broker is another tend to a shared infrastructure

And still to do a check like isAccessTokenRevoked Resource Server will need to extract claims (jti) from the token requiring publicKey (not the private). So the Resource Server probably will use Laravel\Passport\Guards\TokenGuard with League\OAuth2\Server\ResourceServer replaced with an instance with a custom injections that use Authorization Server | Introspection Endpoint so the TokenGuard will receive response even without shared publicKey there:

<?php
    protected function authenticateViaBearerToken(): ?Authenticatable
    {
        if (! $psr = $this->getPsrRequestViaBearerToken()) {
            return null;
        }

But still TokenGuard will try to find the Client after that:

<?php
        $client = $this->clients->findActive(
            $psr->getAttribute('oauth_client_id')
        );

So the Laravel\Passport\ClientRepository will need some rework and probably TokenGuard itself need to be extended to adapt Client checks and maybe use Client info in extended $psr from getPsrRequestViaBearerToken enfilled with both the claims (from the JWT token itself) and the Token Meta-Information (from the Token Introspection Endpoint)

To summarize with proposed Pull Requests:

  • Authorization Server: Token Introspection Endpoint -> TokenServer -> BearerTokenValidator@validateBearerToken -> AccessTokenRepositoryInterface -> Passport::token()
  • Resource Server: TokenGuard -> ResourceServer -> BearerTokenValidator@validateAuthorization -> AccessTokenRepositoryInterface -> Token Introspection Endpoint

So to completely remove need in the publicKey the BearerTokenValidator need to be reimplemented too to rely solely on the Introspection response as it extracts all claims and the Endpoint relies on the JWT token itself (not on the jti like AccessTokenRepository). Though rfc7662 describes that only active field is required in Introspection Response, others are optional. According to spec Endpoint MAY have own fields and we can see such claims in the pull request

And, until official implementation of the Token Introspection Endpoint, the shared database seems to be the easiest way to perform all Laravel Passport checks. Without it we could just implement simple strategy without checking revoked tokens but comparabaly the same amount of code: all League\OAuth2\Server\ResourceServer dependencies and custom Guard with or without extending Passport TokenGuard. That way it will utilize Self-Encoded Access Token without Invalidating so the claims only without the Token Meta-Information will be used


The other solution is a Monolitic approach with machine-to-machine communication via Message Broker or non-public Authorization Code Grant userless client interactions

Or a Gateway so the Resource Server can use simple X-Gateway-Secret and X-User-Id within internal infrastructure with custom middleware like Nginx auth_request or Nginx Plus fully featured OAuth, NGINX Ingress Controller auth-url, Traefik ForwardAuth Middleware. Something like oauth2-proxy can be used as middleware or front but it focused on external OAuth providers

Sequence diagrams

What happens now:

sequenceDiagram
    actor SPA as SPA Client
    box Resource Server
        participant TokenGuard as Laravel<br/>TokenGuard
        participant ResourceServer as League<br/>ResourceServer
        participant BearerTokenValidator as League<br/>BearerTokenValidator
        participant AccessTokenRepository as Laravel<br/>AccessTokenRepository
        participant ClientRepository as Laravel<br/>ClientRepository
        participant PassportUserProvider as Laravel<br/>PassportUserProvider
    end
    box Authorization Server
        participant DB as (shared)<br/>Database
    end

    SPA ->>+ TokenGuard: check
    TokenGuard ->>+ TokenGuard: user
    TokenGuard ->>+ TokenGuard: authenticateViaBearerToken
    TokenGuard ->>+ TokenGuard: getPsrRequestViaBearerToken
    TokenGuard ->>+ ResourceServer: validate<br/>Authenticated<br/>Request
    ResourceServer ->>+ BearerTokenValidator: validate<br/>Authorization
    BearerTokenValidator ->>+ AccessTokenRepository: isAccessTokenRevoked
    AccessTokenRepository ->> DB:
    DB -->> AccessTokenRepository:
    AccessTokenRepository -->>- BearerTokenValidator:
    BearerTokenValidator -->>- ResourceServer: jti<br/>aud<br/>sub<br/>scopes
    ResourceServer -->>- TokenGuard: oauth_access_token_id<br/>oauth_client_id<br/>oauth_user_id<br/>oauth_scopes
    TokenGuard -->>- TokenGuard: psr
    TokenGuard ->>+ ClientRepository: findActive(psr->oauth_client_id)
    ClientRepository ->> DB:
    DB -->> ClientRepository:
    ClientRepository -->>- TokenGuard: client
    TokenGuard ->>+ PassportUserProvider: retrieveById(psr->oauth_user_id)
    PassportUserProvider -->>- TokenGuard:
    TokenGuard -->>- TokenGuard: user (withAccessToken)
    TokenGuard -->>- TokenGuard: user
    TokenGuard -->>- SPA: !is_null user
Loading

How new Introspection would work:

sequenceDiagram
    actor ResourceServer
    box Authorization Server
        participant TokenServer as League<br/>TokenServer
        participant TokenIntrospectionHandler as League<br/>TokenIntrospectionHandler<br/>AbstractTokenHandler<br/>AbstractHandler
        participant BearerTokenValidator as League<br/>BearerTokenValidator
        participant ClientRepository as Laravel<br/>ClientRepository
        # revokation
        # participant AccessTokenRepository as Laravel<br/>AccessTokenRepository
        # participant RefreshTokenRepository as Laravel<br/>RefreshTokenRepository
    end
    participant DB as Database

    ResourceServer ->>+ TokenServer: respondToTokenIntrospectionRequest
    TokenServer ->>+ TokenIntrospectionHandler: respondToRequest
    TokenIntrospectionHandler ->>+ TokenIntrospectionHandler: validateClient
    TokenIntrospectionHandler ->>+ TokenIntrospectionHandler: getClientEntityOrFail
    TokenIntrospectionHandler ->>+ ClientRepository: getClientEntity<br/>clientId
    ClientRepository ->> DB:
    DB -->> ClientRepository:
    ClientRepository -->>- TokenIntrospectionHandler:
    TokenIntrospectionHandler -->>- TokenIntrospectionHandler:
    TokenIntrospectionHandler ->>+ ClientRepository: validateClient<br/>clientId
    ClientRepository ->> DB:
    DB -->> ClientRepository:
    ClientRepository -->>- TokenIntrospectionHandler:
    TokenIntrospectionHandler -->>- TokenIntrospectionHandler:
    TokenIntrospectionHandler ->>+ TokenIntrospectionHandler: validateToken
    TokenIntrospectionHandler ->>+ TokenIntrospectionHandler: validateAccessToken
    TokenIntrospectionHandler ->>+ BearerTokenValidator: validateBearerToken
    BearerTokenValidator -->>- TokenIntrospectionHandler: claims
    TokenIntrospectionHandler -->>- TokenIntrospectionHandler:
    TokenIntrospectionHandler -->>- TokenIntrospectionHandler: token
    TokenIntrospectionHandler -->>- TokenServer: active<br/>claims if active
    TokenServer -->>- ResourceServer: active
Loading

What expected with new Introspection:

sequenceDiagram
    actor SPA as SPA Client
    box Resource Server
        participant TokenGuard as Laravel<br/>TokenGuard<br/>(?IntrospectedTokenGuard)
        participant ResourceServer as League<br/>ResourceServer
        participant BearerTokenValidator as League<br/>BearerTokenValidator
        participant AccessTokenRepository as Laravel<br/>AccessTokenRepository
        participant ClientRepository as Laravel<br/>ClientRepository
        participant PassportUserProvider as Laravel<br/>PassportUserProvider
    end
    participant AuthorizationServer

    SPA ->>+ TokenGuard: check
    TokenGuard ->>+ TokenGuard: user
    TokenGuard ->>+ TokenGuard: authenticateViaBearerToken
    TokenGuard ->>+ TokenGuard: getPsrRequestViaBearerToken
    TokenGuard ->>+ ResourceServer: validate<br/>Authenticated<br/>Request
    ResourceServer ->>+ BearerTokenValidator: validate<br/>Authorization
    BearerTokenValidator ->>+ BearerTokenValidator: validateBearerToken
    BearerTokenValidator ->>+ AccessTokenRepository: isAccessTokenRevoked
    AccessTokenRepository ->> AuthorizationServer: /introspect
    note over AccessTokenRepository,AuthorizationServer: will return only active=false<br/>for any reason either revoked or other
    AuthorizationServer -->> AccessTokenRepository: active(+claims+meta)
    AccessTokenRepository -->>- BearerTokenValidator:
    BearerTokenValidator -->>- BearerTokenValidator: claims
    BearerTokenValidator -->>- ResourceServer: jti<br/>aud<br/>sub<br/>scopes
    ResourceServer -->>- TokenGuard: oauth_access_token_id<br/>oauth_client_id<br/>oauth_user_id<br/>oauth_scopes
    TokenGuard -->>- TokenGuard: psr
    opt either introspection meta or local client to match user provider
    TokenGuard ->>+ ClientRepository: findActive(psr->oauth_client_id)
    ClientRepository ->> AuthorizationServer: /introspect
    AuthorizationServer -->> ClientRepository: active(+claims+meta)
    ClientRepository -->>- TokenGuard: client
    end
    TokenGuard ->>+ PassportUserProvider: retrieveById(psr->oauth_user_id)
    PassportUserProvider -->>- TokenGuard:
    TokenGuard -->>- TokenGuard: user (withAccessToken)
    TokenGuard -->>- TokenGuard: user
    TokenGuard -->>- SPA: !is_null user
Loading

How we can make a workaround via GatewayAPI / Auth Proxy on the example of Nginx auth_request:

https://github.com/ulcuber/laravel-passport-introspection

And yes, this is the solution I made on Rust specifically for this use case

Whole notable chronology of RFCs:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment