Using AWS Cognito for authentication on your app

The aim here is to use AWS Cognito to authenticate users on your Symfony app, using oAuth2 so all the auth happens externally on AWS Cognito.

I’m not storing user data locally with this — it just makes sure that they’re valid users. Groups functionality would need to be added separately if required (it’s referenced in the MyBuilder article and has instructions on how groups can be obtained).

I’ve used the knpuniversity bundle as I tried to get HWI OAuth package to work but there wasn’t any providers already written up to support Cognito.

Composer packages used;

  • knpuniversity/oauth2-client-bundle
  • cakedc/oauth2-cognito

So – to get it going, follow the KNPUniversity instructions on their git-hub account.

From there, use the following for the Authenticator class;

<?php

namespace App\Security;

use KnpU\OAuth2ClientBundle\Client\ClientRegistry;
use KnpU\OAuth2ClientBundle\Client\OAuth2Client;
use KnpU\OAuth2ClientBundle\Security\Authenticator\SocialAuthenticator;
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\Core\User\UserProviderInterface;

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

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

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

    public function getCredentials(Request $request)
    {
        // this method is only called if supports() returns true
        return $this->fetchAccessToken($this->getClient());
    }

    /**
     * @return OAuth2Client
     */
    private function getClient()
    {
        return $this->clientRegistry
            ->getClient('cognito');
    }

    public function getUser($credentials, UserProviderInterface $userProvider)
    {
        return $userProvider->loadUserByUsername($this->getClient()->fetchUserFromToken($credentials)->getId());
    }

    public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey)
    {
        $targetUrl = $this->router->generate('app_default_index');

        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)
    {
        $message = strtr($exception->getMessageKey(), $exception->getMessageData());

        return new Response($message, Response::HTTP_FORBIDDEN);
    }

    /**
     * Called when authentication is needed, but it's not sent.
     * This redirects to the 'login'.
     *
     * @param Request $request
     * @param AuthenticationException|null $authException
     * @return RedirectResponse
     */
    public function start(Request $request, AuthenticationException $authException = null)
    {
        return new RedirectResponse(
            $this->router->generate('connect_cognito_start'),
            // might be the site, where users choose their oauth provider
            Response::HTTP_TEMPORARY_REDIRECT
        );
    }
}

My ‘SecurityCognitoController’ class looks like this;

<?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("/security/connect-cognito", name="connect_cognito_start")
     */
    public function connectAction(ClientRegistry $clientRegistry)
    {
        // will redirect to AWS Cognito!
        return $clientRegistry
            ->getClient('cognito') // key used in config/packages/knpu_oauth2_client.yaml
            ->redirect();
    }

    /**
     * After going to Facebook, you're redirected back here
     * because this is the "redirect_route" you configured
     * in config/packages/knpu_oauth2_client.yaml
     *
     * @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
        
    }
}

My security.yaml file looks something like this;

security:
    providers:
        oauth:
            id: knpu.oauth2.user_provider

    firewalls:
        main:
            anonymous: true
            guard:
                authenticators:
                    - App\Security\CognitoAuthenticator
    access_control:
        - { path: ^/security/connect-cognito, roles: IS_AUTHENTICATED_ANONYMOUSLY }

And the following config was used in the ‘pnpu_oauth2_client.yaml’ file;

knpu_oauth2_client:
    clients:
        # configure your clients as described here: https://github.com/knpuniversity/oauth2-client-bundle#configuration
        cognito:
            type:       'generic'
            provider_class:       '\CakeDC\OAuth2\Client\Provider\Cognito'
            client_id:  '<your client id here>'
            client_secret: '<your client secret here>'
            redirect_route: connect_cognito_check
            provider_options:
                region: <your region here>
                cognitoDomain: <your cognito domain here>
                scope: 'email'

A few things are above;

  • client id; This is found in the ‘App integration’ -> ‘App client settings’ page
  • client_secret: This is in the ‘General settings’ -> ‘App clients’ page, and generated when the ‘app client’ is added
  • region: the region your Cognito user-pool is in
  • scope: I’ve just used email, but if you want you can expand it to capture other information as well.
  • redirect_route: In my case I set it to ‘connect_cognito_check’ … this endpoint is used to receive the ‘OK’ from AWS Cognito that your user has been authenticated and pass back a code which internally is used to retrieve the actual account info of the person which was authenticated.

In AWS Cognito, in your ‘App client’ you’ve setup, make sure you have the following settings;

In AWS Cognito, in your ‘App client’ you’ve setup, make sure you have the following settings;

In this case, the ‘Authorization code grant’ is required as part of the oAuth process
The scopes ticked here are what you’ve specified in the config above.
The sign-in & out URLs should be roughly like the above. (obviously this is for dev … use a prod url for your prod environment.
The callback URLs should match your ‘check’ url

Thanks to the following sites which helped get this far;

https://tech.mybuilder.com/managing-authentication-in-your-symfony-project-with-aws-cognito/
https://github.com/knpuniversity/oauth2-client-bundle
https://github.com/CakeDC/oauth2-cognito

Leave a Reply