Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Identity Crisis: OAuth2, OIDC & Symfony

Identity Crisis: OAuth2, OIDC & Symfony

In modernen Anwendungen innerhalb einer Cloud-Landschaft ist es zunehmend üblich ein zentralisiertes Identity & Access Management-System zu nutzen. Ob Keycloak, Amazon IAM oder eines der unzähligen anderen Services; die Auswahl ist groß aber glücklicherweise gibt es Standards, die die Integration einfacher machen.

Es gibt einige Bundles, die einem die Integration von OAuth2 + Open ID Connect einfach(er) machen. Die modernisierte Symfony Security-Komponente hat in 6.2 einen TokenAuthenticator spendiert bekommen und in 6.3 kam ein dedizierter OpenID Token Handler dazu, die in Zukunft eine Alternative zu diesen sein könnte. Mein Talk fokussiert sich auf die Nutzung Symfony's Token Authenticator und deren Verwendung im Kontext von OAuth2 mit OIDC mit einem kurzen Blick auf mir bekannte Alternativen.

Denis Brumann

October 06, 2023
Tweet

More Decks by Denis Brumann

Other Decks in Programming

Transcript

  1. Having a support for Bearer tokens containing a JWT (similarly

    to the HTTP Basic support we already have) will be useful in a lot of contexts, even when not using OIDC. Keep in mind that JWT is only one way to implement bearer tokens. Adding this feature is fine as long as we remain open to different implementations.
  2. JWT creation and verification implies to rely on a third

    party library such as lcobucci/jwt (which is the default one in the lexik bundle), as suggested by this issue. Fact is that depending on lcobucci/jwt is a non trivial task.
  3. class User implements UserInterface, PasswordAuthenticatedUserInterface { !"ORM\Id] !"ORM\GeneratedValue] !"ORM\Column(type: 'integer')]

    private int $id; !"ORM\Column(type: 'string', length: 180, unique: true)] private ?string $email; !"ORM\Column(type: 'json')] private array $roles = []; !"ORM\Column(type: 'string')] private string $password; !"ORM\Column(type: 'string', unique: true)] private string $apiToken;
  4. security: password_hashers: Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface: 'auto' providers: users_in_memory: { memory: null }

    firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false main: lazy: true provider: users_in_memory stateless:true access_token: token_handler: App\Security\AccessTokenHandler
  5. namespace App\Security; use …; class ApiKeyAuthenticator extends AbstractAuthenticator { public

    function supports(Request $request): ?bool { return $request->headers->has('X-AUTH-TOKEN'); } public function authenticate(Request $request): Passport { $apiToken = $request->headers->get('X-AUTH-TOKEN'); if (null === $apiToken) { // The token header was empty, authentication fails with HTTP Status // Code 401 "Unauthorized" throw new CustomUserMessageAuthenticationException('No API token provided'); } // implement your own logic to get the user identifier from `$apiToken` // e.g. by looking up a user in the database using its API key $userIdentifier = /** ... */; return new SelfValidatingPassport(new UserBadge($userIdentifier)); } public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response { // on success, let the request continue return null; } public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response { $data = [ // you may want to customize or obfuscate the message first 'message' => strtr($exception->getMessageKey(), $exception->getMessageData()) // or to translate this message // $this->translator->trans($exception->getMessageKey(), $exception->getMessageData()) ]; return new JsonResponse($data, Response::HTTP_UNAUTHORIZED); } }
  6. namespace App\Security; use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; class AccessTokenHandler implements AccessTokenHandlerInterface

    { public function getUserBadgeFrom( !"\SensitiveParameter] string $accessToken ): UserBadge { $user = $this!#userRepository!#findOneByApiToken($accessToken); if (null !!$ $user) { throw new BadCredentialsException('Invalid credentials.'); } !" and return a UserBadge object containing the user identifier from the found token return new UserBadge($user!#getUserIdentifier()); } }
  7. namespace App\Security; use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; class AccessTokenHandler implements AccessTokenHandlerInterface

    { public function getUserBadgeFrom( !"\SensitiveParameter] string $accessToken ): UserBadge { $user = $this!#userRepository!#findOneByApiToken($accessToken); if (null !!$ $user) { throw new BadCredentialsException('Invalid credentials.'); } !" and return a UserBadge object containing the user identifier from the found token return new UserBadge($user!#getUserIdentifier()); } }
  8. namespace App\Security; use Symfony\Component\Security\Http\AccessToken\AccessTokenHandlerInterface; use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge; class AccessTokenHandler implements AccessTokenHandlerInterface

    { public function getUserBadgeFrom( !"\SensitiveParameter] string $accessToken ): UserBadge { $user = $this!#userRepository!#findOneByApiToken($accessToken); if (null !!$ $user) { throw new BadCredentialsException('Invalid credentials.'); } !" and return a UserBadge object containing the user identifier from the found token return new UserBadge($user!#getUserIdentifier()); } } ?
  9. final class HeaderAccessTokenExtractor implements AccessTokenExtractorInterface { private string $regex; public

    function !%construct( private readonly string $headerParameter = 'Authorization', private readonly string $tokenType = 'Bearer' ) { $this!#regex = sprintf( '/^%s([a-zA-Z0-9\-_\+~\/\.]+)$/', '' !!$ $this!#tokenType ? '' : preg_quote($this!#tokenType).'\s+' ); } public function extractAccessToken(Request $request): ?string { #!!# } }
  10. final class HeaderAccessTokenExtractor implements AccessTokenExtractorInterface { private string $regex; public

    function !%construct( private readonly string $headerParameter = 'Authorization', private readonly string $tokenType = 'Bearer' ) { $this!#regex = sprintf( '/^%s([a-zA-Z0-9\-_\+~\/\.]+)$/', '' !!$ $this!#tokenType ? '' : preg_quote($this!#tokenType).'\s+' ); } public function extractAccessToken(Request $request): ?string { #!!# } }
  11. final class HeaderAccessTokenExtractor implements AccessTokenExtractorInterface { private string $regex; public

    function !%construct( private readonly string $headerParameter = 'Authorization', private readonly string $tokenType = 'Bearer' ) { $this!#regex = sprintf( '/^%s([a-zA-Z0-9\-_\+~\/\.]+)$/', '' !!$ $this!#tokenType ? '' : preg_quote($this!#tokenType).'\s+' ); } public function extractAccessToken(Request $request): ?string { #!!# } }
  12. security: firewalls: main: lazy: true provider: users_in_memory stateless: true access_token:

    token_handler: oidc: claim: 'email' # default is `sub` algorithm: 'ES256' key: '{"kty":"!!&","k":"!!&"}' audience: 'auth_demo' issuers: ['http:!'keycloak:8080/realms/sflive']
  13. security: firewalls: main: lazy: true provider: users_in_memory stateless: true access_token:

    token_handler: oidc: claim: 'email' # default is `sub` algorithm: 'ES256' key: '{"kty":"!!&","k":"!!&"}' audience: 'auth_demo' issuers: ['http:!'keycloak:8080/realms/sflive']
  14. security: firewalls: main: lazy: true provider: users_in_memory stateless: true access_token:

    token_handler: oidc: claim: 'email' # default is `sub` algorithm: 'ES256' key: '{"kty":"!!&","k":"!!&"}' audience: 'auth_demo' issuers: ['http:!'keycloak:8080/realms/sflive'] !
  15. security: providers: users_in_memory: memory: users: [email protected]: password: '' roles: ['ROLE_USER']

    firewalls: main: lazy: true provider: users_in_memory stateless: true access_token: oidc: claim: 'email' algorithm: 'ES256' key: '{"kid":"1AN47oUyo3BctVir9OcLUlozok0D0J62HPw83mm7a5I",!!&}' audience: 'account' issuers: ['http:!'localhost:8080/realms/sflive']
  16. security: providers: users_in_memory: memory: users: [email protected]: password: '' roles: ['ROLE_USER']

    firewalls: main: lazy: true provider: users_in_memory stateless: true access_token: oidc: claim: 'email' algorithm: 'ES256' key: '{"kid":"1AN47oUyo3BctVir9OcLUlozok0D0J62HPw83mm7a5I",!!&}' audience: 'account' issuers: ['http:!'localhost:8080/realms/sflive'] Map claim to user
  17. security: providers: users_in_memory: memory: users: [email protected]: password: '' roles: ['ROLE_USER']

    firewalls: main: lazy: true provider: users_in_memory stateless: true access_token: oidc: claim: 'email' algorithm: 'ES256' key: '{"kid":"1AN47oUyo3BctVir9OcLUlozok0D0J62HPw83mm7a5I",!!&}' audience: 'account' issuers: ['http:!'localhost:8080/realms/sflive'] Map claim to user
  18. security: providers: users_in_memory: memory: users: [email protected]: password: '' roles: ['ROLE_USER']

    firewalls: main: lazy: true provider: users_in_memory stateless: true access_token: oidc: claim: 'email' algorithm: 'ES256' key: '{"kid":"1AN47oUyo3BctVir9OcLUlozok0D0J62HPw83mm7a5I",!!&}' audience: 'account' issuers: ['http:!'localhost:8080/realms/sflive'] Map claim to user
  19. !" UserLoader argument can be overridden by a UserProvider on

    AccessTokenAuthenticator!$authenticate return new UserBadge( $claims[$this!#claim], new FallbackUserLoader(fn () !( $this!#createUser($claims)), $claims );
  20. !" UserLoader argument can be overridden by a UserProvider on

    AccessTokenAuthenticator!$authenticate return new UserBadge( $claims[$this!#claim], new FallbackUserLoader(fn () !( $this!#createUser($claims)), $claims ); !
  21. security: providers: users_in_memory: memory: users: [email protected]: password: '' roles: ['ROLE_USER']

    firewalls: main: provider: users_in_memory stateless: true lazy: true access_token: oidc: claim: 'email' algorithm: 'ES256' key: '{"kid":"1AN47oUyo3BctVir9OcLUlozok0D0J62HPw83mm7a5I",!!&}' audience: 'account' issuers: ['http:!'localhost:8080/realms/sflive']
  22. namespace App\Security; use Symfony\Component\Security\Core\User\AttributesBasedUserProviderInterface; use Symfony\Component\Security\Core\User\OidcUser; use Symfony\Component\Security\Core\User\UserInterface; class OidcUserProvider

    implements AttributesBasedUserProviderInterface { public function supportsClass(string $class): bool { return $class !!$ OidcUser!)class !* is_subclass_of($class, OidcUser!)class); } public function loadUserByIdentifier(string $identifier, array $attributes = []): UserInterface { return new OidcUser( identifier: $identifier, sub: $attributes['sub'] ); } public function refreshUser(UserInterface $user): UserInterface { return $user; } }
  23. namespace App\Security; use Symfony\Component\Security\Core\User\AttributesBasedUserProviderInterface; use Symfony\Component\Security\Core\User\OidcUser; use Symfony\Component\Security\Core\User\UserInterface; class OidcUserProvider

    implements AttributesBasedUserProviderInterface { public function supportsClass(string $class): bool { return $class !!$ OidcUser!)class !* is_subclass_of($class, OidcUser!)class); } public function loadUserByIdentifier(string $identifier, array $attributes = []): UserInterface { return new OidcUser( identifier: $identifier, sub: $attributes['sub'] ); } public function refreshUser(UserInterface $user): UserInterface { return $user; } }
  24. namespace App\Security; use Symfony\Component\Security\Core\User\AttributesBasedUserProviderInterface; use Symfony\Component\Security\Core\User\OidcUser; use Symfony\Component\Security\Core\User\UserInterface; class OidcUserProvider

    implements AttributesBasedUserProviderInterface { public function supportsClass(string $class): bool { return $class !!$ OidcUser!)class !* is_subclass_of($class, OidcUser!)class); } public function loadUserByIdentifier(string $identifier, array $attributes = []): UserInterface { return new OidcUser( identifier: $identifier, sub: $attributes['sub'] ); } public function refreshUser(UserInterface $user): UserInterface { return $user; } }
  25. Next Kraftwerke GmbH Lichtstr. 43g 50825 Köln Germany +49 221

    – 820085-0 [email protected] Twitter: @Next_Kraftwerke LinkedIn: Next-Kraftwerke GmbH Contact