The discussion started long ago at 2016:
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:
- Laravel Passport laravel/passport#1858
- League OAuth2 Server thephpleague/oauth2-server#1473
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
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
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
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
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
- 2010 The OAuth 1.0 Protocol https://datatracker.ietf.org/doc/html/rfc5849 obsoleted by rfc6749
- 2012 The OAuth 2.0 Authorization Framework https://datatracker.ietf.org/doc/html/rfc6749
- 2012 The OAuth 2.0 Authorization Framework: Bearer Token Usage https://datatracker.ietf.org/doc/html/rfc6750
- 2013 OAuth 2.0 Threat Model and Security Considerations https://datatracker.ietf.org/doc/html/rfc6819 updated by rfc9700
- 2013 OAuth 2.0 Token Revocation https://datatracker.ietf.org/doc/html/rfc7009
- 2015 JSON Web Encryption (JWE) https://datatracker.ietf.org/doc/html/rfc7516
- 2015 JSON Web Token (JWT) https://datatracker.ietf.org/doc/html/rfc7519
- 2015 Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants https://datatracker.ietf.org/doc/html/rfc7521
- 2015 Proof Key for Code Exchange by OAuth Public Clients https://datatracker.ietf.org/doc/html/rfc7636
- 2015 OAuth 2.0 Dynamic Client Registration Protocol https://datatracker.ietf.org/doc/html/rfc7591
- 2015 OAuth 2.0 Dynamic Client Registration Management Protocol https://datatracker.ietf.org/doc/html/rfc7592 Experimental
- 2015 OAuth 2.0 Token Introspection https://datatracker.ietf.org/doc/html/rfc7662
- 2016 JSON Web Signature (JWS) Unencoded Payload Option https://datatracker.ietf.org/doc/html/rfc7797 updates rfc7519
- 2019 OAuth 2.0 Device Authorization Grant https://datatracker.ietf.org/doc/html/rfc8628
- 2020 OAuth 2.0 Token Exchange https://datatracker.ietf.org/doc/html/rfc8693
- 2020 Resource Indicators for OAuth 2.0 https://datatracker.ietf.org/doc/html/rfc8707
- 2020 JSON Web Token Best Current Practices https://datatracker.ietf.org/doc/html/rfc8725 updates rfc7519
- 2021 Deprecating TLS 1.0 and TLS 1.1 https://datatracker.ietf.org/doc/html/rfc8996 updates rfc6750
- 2021 JSON Web Token (JWT) Profile for OAuth 2.0 Access Tokens https://datatracker.ietf.org/doc/html/rfc9068
- 2025 Best Current Practice for OAuth 2.0 Security https://datatracker.ietf.org/doc/html/rfc9700 updates rfc6749, rfc6750, rfc6819