How to use Access Token Authentication
Access tokens or API tokens are commonly used as authentication mechanism in API contexts. The access token is a string, obtained during authentication (using the application or an authorization server). The access token's role is to verify the user identity and receive consent before the token is issued.
Access tokens can be of any kind, for instance opaque strings, JSON Web Tokens (JWT) or SAML2 (XML structures). Please refer to the RFC6750: The OAuth 2.0 Authorization Framework: Bearer Token Usage for a detailed specification.
Using the Access Token Authenticator
This guide assumes you have set up security and have created a user object in your application. Follow the main security guide if this is not yet the case.
1) Configure the Access Token Authenticator
To use the access token authenticator, you must configure a token_handler.
The token handler receives the token from the request and returns the
correct user identifier. To get the user identifier, implementations may
need to load and validate the token (e.g. revocation, expiration time,
digital signature, etc.).
1 2 3 4 5 6
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler: App\Security\AccessTokenHandler
This handler must implement AccessTokenHandlerInterface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
// src/Security/AccessTokenHandler.php
namespace App\Security;
use App\Repository\AccessTokenRepository;
use Symfony\Component\Security\Core\Exception\BadCredentialsException;
use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
class AccessTokenHandler implements AccessTokenHandlerInterface
{
public function __construct(
private AccessTokenRepository $repository
) {
}
public function getUserBadgeFrom(string $accessToken): UserBadge
{
// e.g. query the "access token" database to search for this token
$accessToken = $this->repository->findOneByValue($accessToken);
if (null === $accessToken || !$accessToken->isValid()) {
throw new BadCredentialsException('Invalid credentials.');
}
// and return a UserBadge object containing the user identifier from the found token
// (this is the same identifier used in Security configuration; it can be an email,
// a UUID, a username, a database ID, etc.)
return new UserBadge($accessToken->getUserId());
}
}
The access token authenticator will use the returned user identifier to load the user using the user provider.
Warning
It is important to check whether the token is valid. For instance, the
example above verifies whether the token has not expired. With
self-contained access tokens such as JWT, the handler is required to
verify the digital signature and understand all claims, especially
sub, iat, nbf and exp.
2) Configure the Token Extractor (Optional)
The application is now ready to handle incoming tokens. A token extractor retrieves the token from the request (e.g. a header or request body).
By default, the access token is read from the request header parameter
Authorization with the scheme Bearer (e.g. Authorization: Bearer the-token-value).
Symfony provides other extractors as per the RFC6750:
header(default)-
The token is sent through the request header. Usually
Authorizationwith theBearerscheme. query_string-
The token is part of the request query string. Usually
access_token. request_body-
The token is part of the request body during a POST request. Usually
access_token.
Warning
Because of the security weaknesses associated with the URI method,
including the high likelihood that the URL or the request body
containing the access token will be logged, methods query_string
and request_body SHOULD NOT be used unless it is impossible to
transport the access token in the request header field.
You can also create a custom extractor. The class must implement AccessTokenExtractorInterface.
1 2 3 4 5 6 7 8 9 10 11 12
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler: App\Security\AccessTokenHandler
# use a different built-in extractor
token_extractors: request_body
# or provide the service ID of a custom extractor
token_extractors: 'App\Security\CustomTokenExtractor'
It is possible to set multiple extractors. In this case, the order is important: the first in the list is called first.
1 2 3 4 5 6 7 8 9
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler: App\Security\AccessTokenHandler
token_extractors:
- 'header'
- 'App\Security\CustomTokenExtractor'
3) Submit a Request
That's it! Your application can now authenticate incoming requests using an API token.
Using the default header extractor, you can test the feature by submitting a request like this:
1 2
$ curl -H 'Authorization: Bearer an-accepted-token-value' \
https://localhost:8000/api/some-route
Customizing the Success Handler
By default, the request continues (e.g. the controller for the route is
run). If you want to customize success handling, create your own success
handler by creating a class that implements
AuthenticationSuccessHandlerInterface
and configure the service ID as the success_handler:
1 2 3 4 5 6 7
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler: App\Security\AccessTokenHandler
success_handler: App\Security\Authentication\AuthenticationSuccessHandler
Tip
If you want to customize the default failure handling, use the
failure_handler option and create a class that implements
AuthenticationFailureHandlerInterface.
Publishing the Protected Resource Metadata
8.2
The resource_metadata option was introduced in Symfony 8.2.
Clients that don't have an access token yet (e.g. MCP clients) must find out
which authorization servers issue the tokens accepted by your API. RFC 9728
solves this with a JSON document that the API publishes at
/.well-known/oauth-protected-resource and with a resource_metadata
parameter in the WWW-Authenticate header of 401 responses, which points
to that document.
Add the resource_metadata option to the access_token authenticator to
publish this document and to add its URL to the 401 responses:
1 2 3 4 5 6 7 8 9 10 11 12 13
# config/packages/security.yaml
security:
firewalls:
api:
access_token:
realm: 'My API'
token_extractors: ['header', 'query_string']
token_handler: App\Security\AccessTokenHandler
resource_metadata:
authorization_servers: ['https://accounts.example.com']
scopes_supported: ['profile', 'email']
resource_name: 'My API'
resource_documentation: 'https://api.example.com/docs'
Then, import the route loader that defines the route of this document:
1 2 3 4
# config/routes/security.yaml
_oauth_protected_resource_metadata:
resource: security.authenticator.access_token.route_loader
type: service
Clients must be able to get this document without a token, so don't add any access control rule that requires authentication for its path.
If your API runs at https://api.example.com, a
GET /.well-known/oauth-protected-resource request now returns:
1 2 3 4 5 6 7 8
{
"resource": "https://api.example.com",
"authorization_servers": ["https://accounts.example.com"],
"scopes_supported": ["profile", "email"],
"bearer_methods_supported": ["header", "query"],
"resource_name": "My API",
"resource_documentation": "https://api.example.com/docs"
}
Requests without a token get a 401 response with the following header,
unless another authenticator of the firewall (e.g. form_login) is its
entry point:
1
WWW-Authenticate: Bearer realm="My API",resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"
Requests with a token rejected by the firewall get the same URL next to the error details:
1
WWW-Authenticate: Bearer realm="My API",error="invalid_token",error_description="Invalid credentials.",resource_metadata="https://api.example.com/.well-known/oauth-protected-resource"
These are the options of resource_metadata. Each of them is a metadata
parameter defined by RFC 9728 and it's left out of the document when it
has no value:
resource-
The identifier of the protected resource. It must be an HTTPS URL without a fragment, but HTTP is allowed for loopback hosts (
localhost,127.0.0.1,::1) and hostnames reserved for testing (*.localhost,*.test). By default, it's the origin (scheme, host and port) of the request, which is correct when the firewall protects the whole application.If the URL includes a path, that path is added after the well-known path, as defined in Section 3.1 of the RFC. For example, the metadata of
https://example.com/apiis served at/.well-known/oauth-protected-resource/api. This allows several firewalls of the same host to publish their own metadata, as long as each of them uses a different path.Symfony reads this path when compiling the container, so you can't use an environment variable as the whole value. Use it inside the URL instead (e.g.
https://%env(API_HOST)%/v1). authorization_servers-
The issuer identifiers of the authorization servers that issue the access
tokens accepted by this firewall (e.g.
https://accounts.example.com). Clients use them to know where to get a token. jwks_uri- The URL of the JWK Set with the keys that your API uses to sign its own responses. These are not the keys used to verify the access tokens, which belong to the authorization server.
scopes_supported- The scope values used by your API.
bearer_methods_supported-
The ways clients can send the token to your API (
header,bodyorquery). If you don't set this option, Symfony computes it from thetoken_extractorsoption (headerbecomesheader,request_bodybecomesbodyandquery_stringbecomesquery). Custom extractors are ignored, so set this option explicitly when using them. resource_name- The human-readable name of your API, which clients can display to end users.
resource_documentation- The URL of the developer documentation of your API.
resource_policy_uri- The URL of the policy that explains how clients can use the data returned by your API.
resource_tos_uri- The URL of the terms of service of your API.
Using OpenID Connect (OIDC)
OpenID Connect (OIDC) is the third generation of OpenID technology and it's a RESTful HTTP API that uses JSON as its data format. OpenID Connect is an authentication layer on top of the OAuth 2.0 authorization framework. It allows you to verify the identity of an end user based on the authentication performed by an authorization server.
1) Configure the OidcUserInfoTokenHandler
The OidcUserInfoTokenHandler requires the symfony/http-client package to
make the needed HTTP requests. If you haven't installed it yet, run this command:
1
$ composer require symfony/http-client
Symfony provides a generic OidcUserInfoTokenHandler to call your OIDC server
and retrieve the user info:
1 2 3 4 5 6 7
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler:
oidc_user_info: https://www.example.com/realms/demo/protocol/openid-connect/userinfo
To enable OpenID Connect Discovery, the OidcUserInfoTokenHandler
requires the symfony/cache package to store the OIDC configuration in
the cache. If you haven't installed it yet, run the following command:
1
$ composer require symfony/cache
Next, configure the base_uri and discovery options:
1 2 3 4 5 6 7 8 9 10 11
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler:
oidc_user_info:
base_uri: https://www.example.com/realms/demo/
discovery:
cache:
id: cache.app
Following the OpenID Connect Specification, the sub claim is used as user
identifier by default. To use another claim, specify it using the claim option:
1 2 3 4 5 6 7 8 9
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler:
oidc_user_info:
claim: email
base_uri: https://www.example.com/realms/demo/protocol/openid-connect/userinfo
The oidc_user_info token handler automatically creates an HTTP client with
the specified base_uri. If you prefer using your own client, you can
specify the service name via the client option:
1 2 3 4 5 6 7 8
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler:
oidc_user_info:
client: oidc.client
By default, the OidcUserInfoTokenHandler creates an OidcUser with the
claims. To create your own user object from the claims, you must
create your own UserProvider:
1 2 3 4 5 6 7 8 9 10
// src/Security/Core/User/OidcUserProvider.php
use Symfony\Component\Security\Core\User\AttributesBasedUserProviderInterface;
class OidcUserProvider implements AttributesBasedUserProviderInterface
{
public function loadUserByIdentifier(string $identifier, array $attributes = []): UserInterface
{
// implement your own logic to load and return the user object
}
}
2) Configure the OidcTokenHandler
The OidcTokenHandler requires the web-token/jwt-library package.
If you haven't installed it yet, run this command:
1
$ composer require web-token/jwt-library
Warning
For production use, ensure the GMP PHP extension is installed. The
web-token/jwt-library depends on brick/math, which silently falls
back to a pure PHP implementation when neither the GMP nor BCMath extensions
are available. This can make JWT verification orders of magnitude slower.
Symfony provides a generic OidcTokenHandler that decodes the token, validates
it, and retrieves the user information from it. Optionally, the token can be encrypted (JWE):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler:
oidc:
# Algorithms used to sign the JWS
algorithms: ['ES256', 'RS256']
# A JSON-encoded JWK
keyset: '{"keys":[{"kty":"...","k":"..."}]}'
# Audience (`aud` claim): required for validation purpose
audience: 'api-example'
# Issuers (`iss` claim): required for validation purpose
issuers: ['https://oidc.example.com']
# Tolerance in seconds for clock differences between the token
# issuer and this application, applied when validating the
# time-based claims (`iat`, `nbf`, `exp`)
allowed_time_drift: 5 # Default to 0 (no tolerance)
# Requires the `typ` header of the token to be
# `at+jwt` or `application/at+jwt` (RFC 9068)
enforce_at_jwt_type: true # Default to false
encryption:
enabled: true # Default to false
enforce: false # Default to false, requires an encrypted token when true
algorithms: ['ECDH-ES', 'A128GCM']
keyset: '{"keys": [...]}' # Encryption private keyset
8.2
The allowed_time_drift and enforce_at_jwt_type options were
introduced in Symfony 8.2.
RFC 9068 requires JWT access tokens to define a typ header with the
at+jwt or application/at+jwt value. When enforce_at_jwt_type is
true, the handler rejects tokens without that header or with any other
value (the comparison is case-insensitive). For encrypted tokens, the handler
checks the header of the signed token after decrypting it.
This check prevents "cross-JWT confusion" attacks. If your API and your login
application use the same client on the OpenID Connect provider, the ID tokens
are signed by an allowed issuer and include your audience in their aud
claim. Without this check, the handler accepts those ID tokens as access tokens.
The option is false by default to keep compatibility with providers that
don't follow RFC 9068 and issue tokens with a JWT type. Its default value
will change to true in the next major version, so set it explicitly. The
tokens generated from the
command line use the
at+jwt type, so they pass this check.
8.2
Not setting the enforce_at_jwt_type option was deprecated in Symfony 8.2.
To enable OpenID Connect Discovery, the OidcTokenHandler requires the
symfony/cache package to store the OIDC configuration in the cache. If you
haven't installed it yet, run the following command:
1
$ composer require symfony/cache
Then, you can remove the keyset configuration option (it will be imported
from the OpenID Connect Discovery), and configure the discovery option:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler:
oidc:
claim: email
algorithms: ['ES256', 'RS256']
audience: 'api-example'
issuers: ['https://oidc.example.com']
discovery:
base_uri: https://www.example.com/realms/demo/
cache:
id: cache.app
By default, when using OpenID Connect Discovery, only keys explicitly designated
for signature verification (i.e. keys with "use": "sig" or "key_ops"
containing "sign" or "verify" per RFC 7517) are accepted. If your
identity provider serves keys without any usage designation (no use or
key_ops field), you can disable this strict filtering by setting the
enforce_key_usage_verification option to false:
1 2 3 4 5 6 7 8 9 10 11 12
security:
firewalls:
main:
access_token:
token_handler:
oidc:
# ...
discovery:
base_uri: https://www.example.com/realms/demo/
cache:
id: cache.app
enforce_key_usage_verification: false
When disabled, keys are still filtered: those explicitly marked for encryption
only ("use": "enc" or "key_ops" containing only encryption operations)
are excluded. Keys without any usage designation are included.
8.1
The enforce_key_usage_verification option was introduced in Symfony 8.1.
Following the OpenID Connect Specification, the sub claim is used by
default as user identifier. To use another claim, specify it on the
configuration:
1 2 3 4 5 6 7 8 9 10 11 12
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler:
oidc:
claim: email
algorithms: ['ES256', 'RS256']
keyset: '{"keys":[{"kty":"...","k":"..."}]}'
audience: 'api-example'
issuers: ['https://oidc.example.com']
By default, the OidcTokenHandler creates an OidcUser with the claims. To
create your own User from the claims, you must
create your own UserProvider:
1 2 3 4 5 6 7 8 9 10
// src/Security/Core/User/OidcUserProvider.php
use Symfony\Component\Security\Core\User\AttributesBasedUserProviderInterface;
class OidcUserProvider implements AttributesBasedUserProviderInterface
{
public function loadUserByIdentifier(string $identifier, array $attributes = []): UserInterface
{
// implement your own logic to load and return the user object
}
}
Configuring Multiple OIDC Discovery Endpoints
The OidcTokenHandler supports multiple OIDC discovery endpoints, allowing it
to validate tokens from different identity providers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler:
oidc:
algorithms: ['ES256', 'RS256']
audience: 'api-example'
# each "issuer" announced by the discovery documents
issuers:
- https://idp1.example.com/realms/demo
- https://idp2.example.com/realms/demo
discovery:
base_uri:
- https://idp1.example.com/realms/demo/
- https://idp2.example.com/realms/demo/
cache:
id: cache.app
The token handler fetches the JWK set of each discovery endpoint and binds its
keys to the issuer announced by that discovery document, so each token is only
verified with the keys of its own issuer (the iss claim). That's why every
announced issuer must be listed in the issuers option exactly as announced
(including any trailing slash) and two discovery documents can't announce the
same issuer.
Creating an OIDC token from the command line
The security:oidc:generate-token command helps you generate JWTs. It's mostly
useful when developing or testing applications that use OIDC authentication:
1 2 3 4 5 6 7 8
# generate a token using the default configuration
$ php bin/console security:oidc:generate-token john.doe@example.com
# specify the firewall, algorithm, and issuer if multiple are available
$ php bin/console security:oidc:generate-token john.doe@example.com \
--firewall="api" \
--algorithm="HS256" \
--issuer="https://example.com"
Note
The JWK used for signing must have the appropriate key operation flags set.
Using CAS 2.0
Central Authentication Service (CAS) is an enterprise multilingual single sign-on solution and identity provider for the web and attempts to be a comprehensive platform for your authentication and authorization needs.
Configure the Cas2Handler
Symfony provides a generic Cas2Handler to call your CAS server. It requires
the symfony/http-client package to make the needed HTTP requests. If you
haven't installed it yet, run this command:
1
$ composer require symfony/http-client
You can configure a cas token handler as follows:
1 2 3 4 5 6 7 8
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler:
cas:
validation_url: https://www.example.com/cas/validate
The cas token handler automatically creates an HTTP client to call
the specified validation_url. If you prefer using your own client, you can
specify the service name via the http_client option:
1 2 3 4 5 6 7 8 9
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler:
cas:
validation_url: https://www.example.com/cas/validate
http_client: cas.client
By default the token handler will read the validation URL XML response with a
cas prefix but you can configure another prefix:
1 2 3 4 5 6 7 8 9
# config/packages/security.yaml
security:
firewalls:
main:
access_token:
token_handler:
cas:
validation_url: https://www.example.com/cas/validate
prefix: cas-example
Creating Users from Token
Some types of tokens (for instance OIDC) contain all information required to create a user entity (e.g. username and roles). In this case, you don't need a user provider to create a user from the database:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// src/Security/AccessTokenHandler.php
namespace App\Security;
// ...
class AccessTokenHandler implements AccessTokenHandlerInterface
{
// ...
public function getUserBadgeFrom(string $accessToken): UserBadge
{
// get the data from the token
$payload = ...;
return new UserBadge(
$payload->getUserId(),
fn (string $userIdentifier) => new User($userIdentifier, $payload->getRoles())
);
}
}
When using this strategy, you can omit the user_provider configuration
for stateless firewalls.