<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Alen Pokos</title>
    <description>The latest articles on DEV Community by Alen Pokos (@alenpokos).</description>
    <link>https://dev.to/alenpokos</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F599012%2Fdd263b6b-9e0f-4985-a939-f09b6a14f5f2.jpg</url>
      <title>DEV Community: Alen Pokos</title>
      <link>https://dev.to/alenpokos</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/alenpokos"/>
    <language>en</language>
    <item>
      <title>Fastest Symfony authentication - AWS Cognito integration</title>
      <dc:creator>Alen Pokos</dc:creator>
      <pubDate>Mon, 07 Feb 2022 09:35:26 +0000</pubDate>
      <link>https://dev.to/bornfightcompany/fastest-symfony-authentication-aws-cognito-integration-583i</link>
      <guid>https://dev.to/bornfightcompany/fastest-symfony-authentication-aws-cognito-integration-583i</guid>
      <description>&lt;p&gt;If you either love AWS services already, or are looking for a good option to use with your multiplatform products, AWS Cognito seems to be a good candidate to adopt into your technical stack.&lt;/p&gt;

&lt;p&gt;For me it was unknown, but once I started digging into it, I find it to solve some problems I was bored with solving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup Cognito on AWS
&lt;/h2&gt;

&lt;p&gt;For starters, we should prepare our Cognito user pool.&lt;br&gt;
We can do this via AWS UI. On the Cognito page we select "Create new user pool".&lt;br&gt;
There are no really special settings you need to configure upon creation,&lt;br&gt;
so choose either default settings or settings that fit your needs.&lt;/p&gt;

&lt;p&gt;Only thing we need from user pool is to setup APP client.&lt;br&gt;
Configure the APP client as you need.&lt;br&gt;
For the callback URL we will target symfony route &lt;code&gt;/security/cognito/check&lt;/code&gt; ie: &lt;code&gt;http://localhost:8000/security/cognito/check&lt;/code&gt;.&lt;br&gt;
If using local Symfony, be careful about using http or https domains. Spent some time to figure that one out because I was reckless :D.&lt;/p&gt;
&lt;h2&gt;
  
  
  Symfony
&lt;/h2&gt;

&lt;p&gt;At the time of writing this article, Symfony 6 was the newest version and used to test this code.&lt;/p&gt;
&lt;h2&gt;
  
  
  Symfony installation
&lt;/h2&gt;

&lt;p&gt;To be able to follow this post, you should either have an existing Symfony project or create a new one.&lt;br&gt;
For ease of example, I will provide quick intro how to set up a new clean Symfony project.&lt;/p&gt;

&lt;p&gt;We start with the new symfony project &lt;code&gt;symfony new --webapp .&lt;/code&gt;&lt;br&gt;
Please, see &lt;a href="https://symfony.com/doc/current/setup.html"&gt;Documentation&lt;/a&gt; on installation and setup of Symfony project.&lt;/p&gt;

&lt;p&gt;To verify we installed successfully, we can run server using &lt;code&gt;symfony server:start&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  Install and configure packages for Cognito integration
&lt;/h2&gt;

&lt;p&gt;Now that we have Symfony and Cognito ready, we can begin integration into our Symfony application.&lt;br&gt;
We will use &lt;code&gt;knpuniversity&lt;/code&gt; bundle that provides variety of built-in connectors, but it is missing Cognito one.&lt;br&gt;
For that, we will install Cognito agent provided by another package.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;composer require knpuniversity/oauth2-client-bundle cakedc/oauth2-cognito&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Once installation is complete we begin to configure the bundle.&lt;br&gt;
Update &lt;code&gt;config/packages/knpu_oauth2_client.yaml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;knpu_oauth2_client:
    clients:
        # configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration
        cognito: # name of our client
            type: 'generic' # type 
            provider_class: '\CakeDC\OAuth2\Client\Provider\Cognito' # class provided by agent package
            client_id: '&amp;lt;AWS_CLIENT_ID&amp;gt;' # Cognito app id
            client_secret: '&amp;lt;AWS_CLIENT_SECRET&amp;gt;' # Cognito app secret
            redirect_route: connect_cognito_check # name of the route where we wanna redirect callback, it mush be same as configued in the Cognito app
            provider_options:
                region: &amp;lt;AWS_REGION&amp;gt;
                cognitoDomain: &amp;lt;AWS_COGNITO_DOMAIN&amp;gt; # Cognito domain, just the domain, without region and aws suffix
                scope: 'email' #scopes configured in cognito
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We also need some Symfony User. If you do not already have this, we can create it now.&lt;br&gt;
If you already have user and user providers, you can skip this part and go to creation of connection controller.&lt;br&gt;
Create user using maker bundle, &lt;code&gt;bin/console make:user&lt;/code&gt; with settings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;store user in the database: NO # this is for demo purpose. In the real world you would probably wanna store it into a database&lt;/li&gt;
&lt;li&gt;unique property: Email # this can be whatever you need&lt;/li&gt;
&lt;li&gt;Hash/check password: NO # again, if you need it otherwise you can use it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This will generate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;src/Security/User.php&lt;/code&gt; Our User class&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;src/Security/UserProvider.php&lt;/code&gt; User provider class used by Symfony security.
It will also update the security configuration &lt;code&gt;config/packages/security.yaml&lt;/code&gt; with information about our new user provider.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you are not familiar with these terms, I would urge you to read basics of Symfony security in &lt;a href="https://symfony.com/doc/current/security.html"&gt;Symfony Security documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Next we need to create connection controller, that will provide routes and calls to oauth client bundle, ie:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

namespace App\Controller;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;

class SecurityCognitoController extends AbstractController
{
    /**
     * Link to this controller to start the "connect" process
     */
    #[Route("/login", name:"connect_cognito_start")]
    public function connectAction(ClientRegistry $clientRegistry)
    {
        // will redirect to AWS Cognito!
        return $clientRegistry
            -&amp;gt;getClient('cognito') // key used in config/packages/knpu_oauth2_client.yaml
            -&amp;gt;redirect();
    }

    /**
     * After going to Cognito, you're redirected back here
     * because this is the "callback URL" you configured
     * in AWS Cognito APP settings
     */
    #[Route("/security/cognito/check", name:"connect_cognito_check")]
    public function connectCheckAction(Request $request, ClientRegistry $clientRegistry)
    {
        // ** if you want to *authenticate* the user, then
        // leave this method blank and create a Guard authenticator
    }

    #[Route("/logout", name:"security_logout")]
    public function logout(){}
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Logout is currently not configured, and you can customize this to your needs later.&lt;/p&gt;

&lt;p&gt;Last code class we need to create is custom Authenticator, by following KNP docs and guidelines:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

namespace App\Security;

use CakeDC\OAuth2\Client\Provider\CognitoUser;
use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Security\Authenticator\OAuth2Authenticator;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Exception\AuthenticationException;
use Symfony\Component\Security\Http\Authenticator\Passport\Badge\UserBadge;
use Symfony\Component\Security\Http\Authenticator\Passport\Passport;
use Symfony\Component\Security\Http\Authenticator\Passport\SelfValidatingPassport;

class CognitoAuthenticator extends OAuth2Authenticator
{
    private $clientRegistry;
    private $router;

    public function __construct(ClientRegistry $clientRegistry, RouterInterface $router)
    {
        $this-&amp;gt;clientRegistry = $clientRegistry;
        $this-&amp;gt;router = $router;
    }

    public function supports(Request $request): ?bool
    {
        // continue ONLY if the current ROUTE matches the check ROUTE
        return $request-&amp;gt;attributes-&amp;gt;get('_route') === 'connect_cognito_check';
    }

    public function authenticate(Request $request): Passport
    {
        $client = $this-&amp;gt;clientRegistry-&amp;gt;getClient('cognito');
        $accessToken = $this-&amp;gt;fetchAccessToken($client);

        // NOTE: Here you can store token into session if you are using stateful authentication.

        return new SelfValidatingPassport(
            new UserBadge($accessToken-&amp;gt;getToken(), function() use ($accessToken, $client) {
                /** @var CognitoUser $user */
                $cognitoUser = $client-&amp;gt;fetchUserFromToken($accessToken);

                // NOTE: here you can load/save user from storage such as database

                $user = new User();
                $user-&amp;gt;setEmail($cognitoUser-&amp;gt;getEmail());
                return $user;
            })
        );
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
    {
        // change "app_homepage" to some route in your app
        $targetUrl = $this-&amp;gt;router-&amp;gt;generate('app_homepage');

        return new RedirectResponse($targetUrl);

        // or, on success, let the request continue to be handled by the controller
        //return null;
    }

    public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
    {
        $message = strtr($exception-&amp;gt;getMessageKey(), $exception-&amp;gt;getMessageData());

        return new Response($message, Response::HTTP_FORBIDDEN);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Please note the &lt;code&gt;targetUrl&lt;/code&gt; in the &lt;code&gt;onAuthenticationSuccess&lt;/code&gt; method. Customize this to your needs.&lt;br&gt;
I will also provide simple controller at the end of the article for convenience of testing.&lt;/p&gt;

&lt;p&gt;Last thing is to update our &lt;code&gt;security.yaml&lt;/code&gt; configuration to use our custom provider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;...
firewalls:
    ...
    main:
        ...
        custom_authenticator: App\Security\CognitoAuthenticator
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the main subsection, not the entire contents of the file. For our configuration, we only needed to add &lt;code&gt;custom_authenticator&lt;/code&gt; setting that is our authenticator class.&lt;/p&gt;

&lt;p&gt;Once done and you try it, by opening &lt;code&gt;http://localhost:8000/login&lt;/code&gt; you should be redirected to AWS hosted login page.&lt;br&gt;
After you login you will be redirected back to your symfony site with error exception from &lt;code&gt;UserProvider::refreshUser&lt;/code&gt;.&lt;br&gt;
If you just wish to try it out, you can &lt;code&gt;return $user&lt;/code&gt; in this method.&lt;br&gt;
Important!! This is for testing only, and it is not good practice. You should fit this to your implementation.&lt;/p&gt;

&lt;p&gt;If you wish to see try it out, here is a simple test controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;?php

namespace App\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Security\Core\Security;

final class DefaultController
{
    #[Route("/", name:"app_homepage")]
    #[IsGranted("ROLE_USER")]
    public function __invoke(Security $security): Response
    {
        // we return with html head and body tags as this is needed by Symfony profiler to attach to the page 
        return new Response(sprintf("&amp;lt;html&amp;gt;&amp;lt;head&amp;gt;&amp;lt;/head&amp;gt;&amp;lt;body&amp;gt;Welcome %s&amp;lt;/body&amp;gt;&amp;lt;/html&amp;gt;", $security-&amp;gt;getUser()-&amp;gt;getUserIdentifier()));
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and you also need to update &lt;code&gt;services.yml&lt;/code&gt; and add section load new controller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    ...

    # controllers are imported separately to make sure services can be injected
    # as action arguments even if you don't extend any base controller class
    App\Controller\:
        resource: '../src/Controller/'
        tags: [ 'controller.service_arguments' ]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And there you have it. You can login into your Symfony application using AWS Cognito.&lt;/p&gt;

&lt;p&gt;Feel free to clean up the code, improve the security of it and make it ready for the real world.&lt;/p&gt;

&lt;p&gt;Security is important and sometimes hard topic. I would urge you to read &lt;a href="https://symfony.com/doc/current/security.html"&gt;Symfony Security documentation&lt;/a&gt; to better understand how it works, what are good practices and how to avoid pitfalls.&lt;/p&gt;

&lt;p&gt;Have fun coding, creating and learning.&lt;/p&gt;

</description>
      <category>engineeringmonday</category>
      <category>aws</category>
      <category>authentication</category>
      <category>php</category>
    </item>
  </channel>
</rss>
