@@ -833,6 +833,240 @@ useful when developing or testing applications that use OIDC authentication:
833833
834834 The JWK used for signing must have the appropriate `key operation flags `_ set.
835835
836+ Using OAuth 2.0 Token Introspection
837+ -----------------------------------
838+
839+ `RFC 7662 `_ defines an introspection endpoint, where a resource server
840+ asks the authorization server what it knows about an access token. The
841+ ``oauth2 `` token handler posts the token to that endpoint and builds the
842+ user out of the answer, so your application never has to read the token
843+ itself.
844+
845+ This token handler requires the ``symfony/http-client `` package to make
846+ the needed HTTP requests. If you haven't installed it yet, run this
847+ command:
848+
849+ .. code-block :: terminal
850+
851+ $ composer require symfony/http-client
852+
853+ Where the authorization server lives and how your application
854+ authenticates there belongs to the HTTP client, not to the firewall.
855+ Declare a scoped client whose ``base_uri `` is the introspection endpoint
856+ and whose ``auth_basic `` holds the credentials of your resource server:
857+
858+ .. configuration-block ::
859+
860+ .. code-block :: yaml
861+
862+ # config/packages/framework.yaml
863+ framework :
864+ http_client :
865+ scoped_clients :
866+ oauth2.introspection :
867+ base_uri : ' https://auth.example.com/introspect'
868+ auth_basic : ' %env(OAUTH2_ID)%:%env(OAUTH2_SECRET)%'
869+
870+ .. code-block :: php
871+
872+ // config/packages/framework.php
873+ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
874+
875+ return App::config([
876+ 'framework' => [
877+ 'http_client' => [
878+ 'scoped_clients' => [
879+ 'oauth2.introspection' => [
880+ 'base_uri' => 'https://auth.example.com/introspect',
881+ 'auth_basic' => '%env(OAUTH2_ID)%:%env(OAUTH2_SECRET)%',
882+ ],
883+ ],
884+ ],
885+ ],
886+ ]);
887+
888+ Then give the service ID of that client to the ``http_client `` option of
889+ the token handler, together with what the response is confronted with:
890+
891+ .. configuration-block ::
892+
893+ .. code-block :: yaml
894+
895+ # config/packages/security.yaml
896+ security :
897+ firewalls :
898+ main :
899+ access_token :
900+ token_handler :
901+ oauth2 :
902+ http_client : ' oauth2.introspection'
903+ issuer : ' https://auth.example.com/'
904+ audience : ' https://api.example.com'
905+ claim : ' sub'
906+ allowed_time_drift : 5
907+
908+ .. code-block :: php
909+
910+ // config/packages/security.php
911+ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
912+
913+ return App::config([
914+ 'security' => [
915+ 'firewalls' => [
916+ 'main' => [
917+ 'access_token' => [
918+ 'token_handler' => [
919+ 'oauth2' => [
920+ 'http_client' => 'oauth2.introspection',
921+ 'issuer' => 'https://auth.example.com/',
922+ 'audience' => 'https://api.example.com',
923+ 'claim' => 'sub',
924+ 'allowed_time_drift' => 5,
925+ ],
926+ ],
927+ ],
928+ ],
929+ ],
930+ ],
931+ ]);
932+
933+ These are the available options:
934+
935+ ``http_client ``
936+ Service ID of the HTTP client the introspection endpoint is called
937+ with. It defaults to the ``http_client `` service, which carries
938+ neither that endpoint nor any credentials, so declare a scoped client
939+ as shown above.
940+
941+ ``issuer ``
942+ Identifier of the authorization server, checked against the ``iss ``
943+ member of the introspection response. It defaults to ``null ``, which
944+ skips that check.
945+
946+ ``audience ``
947+ Identifiers of your resource server, one of which the ``aud `` member
948+ of the response must name. Give a single identifier as a string and
949+ several ones as a list; one match is enough, because an access token
950+ minted for several resource servers is meant for each of them. It
951+ defaults to an empty list, which skips that check.
952+
953+ ``claim ``
954+ Claim holding the user identifier (e.g. ``sub ``, ``username ``,
955+ ``email ``). It defaults to ``null ``, which reads the ``sub `` claim and
956+ falls back to the ``username `` one.
957+
958+ ``allowed_time_drift ``
959+ Tolerance, in seconds, on the ``iat ``, ``nbf `` and ``exp `` members of
960+ the response, to account for the clocks of the two servers running
961+ slightly apart. It defaults to ``0 ``.
962+
963+ When the HTTP client is the only thing you configure, give it as a
964+ string:
965+
966+ .. configuration-block ::
967+
968+ .. code-block :: yaml
969+
970+ # config/packages/security.yaml
971+ security :
972+ firewalls :
973+ main :
974+ access_token :
975+ token_handler :
976+ oauth2 : ' oauth2.introspection'
977+
978+ .. code-block :: php
979+
980+ // config/packages/security.php
981+ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
982+
983+ return App::config([
984+ 'security' => [
985+ 'firewalls' => [
986+ 'main' => [
987+ 'access_token' => [
988+ 'token_handler' => [
989+ 'oauth2' => 'oauth2.introspection',
990+ ],
991+ ],
992+ ],
993+ ],
994+ ],
995+ ]);
996+
997+ A token the authorization server reports as inactive is refused, and so
998+ is a response whose dates place the token outside its validity window,
999+ or whose ``iss `` or ``aud `` members name an issuer or an audience you
1000+ did not declare. `RFC 7662 `_ makes all of those members optional, so the
1001+ dates are verified when the response carries them, and the issuer and
1002+ the audience when the firewall declares which ones it accepts.
1003+
1004+ .. versionadded :: 8.2
1005+
1006+ The ``http_client ``, ``issuer ``, ``audience ``, ``claim ``,
1007+ ``allowed_time_drift `` and ``cache `` options of the ``oauth2 `` token
1008+ handler were introduced in Symfony 8.2.
1009+
1010+ Caching the Introspection Responses
1011+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1012+
1013+ Introspecting the token on every request costs a round trip to the
1014+ authorization server. The ``cache `` option stores the responses of
1015+ active tokens in the pool of your choice, which requires the
1016+ ``symfony/cache `` package:
1017+
1018+ .. code-block :: terminal
1019+
1020+ $ composer require symfony/cache
1021+
1022+ .. configuration-block ::
1023+
1024+ .. code-block :: yaml
1025+
1026+ # config/packages/security.yaml
1027+ security :
1028+ firewalls :
1029+ main :
1030+ access_token :
1031+ token_handler :
1032+ oauth2 :
1033+ http_client : ' oauth2.introspection'
1034+ cache :
1035+ id : cache.app
1036+ ttl : 60
1037+
1038+ .. code-block :: php
1039+
1040+ // config/packages/security.php
1041+ namespace Symfony\Component\DependencyInjection\Loader\Configurator;
1042+
1043+ return App::config([
1044+ 'security' => [
1045+ 'firewalls' => [
1046+ 'main' => [
1047+ 'access_token' => [
1048+ 'token_handler' => [
1049+ 'oauth2' => [
1050+ 'http_client' => 'oauth2.introspection',
1051+ 'cache' => [
1052+ 'id' => 'cache.app',
1053+ 'ttl' => 60,
1054+ ],
1055+ ],
1056+ ],
1057+ ],
1058+ ],
1059+ ],
1060+ ],
1061+ ]);
1062+
1063+ The ``id `` option is required and the ``ttl `` one defaults to ``60 ``
1064+ seconds. The shorter that lifetime is, the sooner a revoked token stops
1065+ being accepted. No entry outlives the ``exp `` the authorization server
1066+ reported, and the response of an inactive token is never stored. Entries
1067+ are keyed by a digest of the token, so the pool holds no usable
1068+ credential.
1069+
8361070Using CAS 2.0
8371071-------------
8381072
@@ -1003,6 +1237,7 @@ for :ref:`stateless firewalls <reference-security-stateless>`.
10031237.. _`OpenID Connect Specification` : https://openid.net/specs/openid-connect-core-1_0.html
10041238.. _`OpenID Connect Discovery` : https://openid.net/specs/openid-connect-discovery-1_0.html
10051239.. _`RFC 7517` : https://datatracker.ietf.org/doc/html/rfc7517
1240+ .. _`RFC 7662` : https://datatracker.ietf.org/doc/html/rfc7662
10061241.. _`RFC6750` : https://datatracker.ietf.org/doc/html/rfc6750
10071242.. _`SAML2 (XML structures)` : https://docs.oasis-open.org/security/saml/Post2.0/sstc-saml-tech-overview-2.0.html
10081243.. _`key operation flags` : https://www.iana.org/assignments/jose/jose.xhtml#web-key-operations
0 commit comments