TL;DR

No time? No problem... We provide expert level Laravel development and consulting. We also help customers scaling their projects. If you think we could fit together, drop us a message or give us a call. We are here to help.

Contact
Grants Pass, Oregon, U.S.A.
phone:
(+1) 541 592 9181

Laravel Authentication with AWS Cognito

Introduction

Laravel already comes with a powerful authentication feature which we all know and love right? You simply execute the following command, and have authentication working:

1
2php artisan make:auth
3

That's super easy and will not require any actual programming. For most projects, this will be enough. You can register, login, have your password reset and you have a remember me feature. Actually, that's enough for most use cases, but let's start by defining a use case where it is not enough: What if we want to give a user, registered in Project A, access to Project B or the other way round? Well, sadly Laravel doesn't come with a built-in Single Sign-On feature (check Laravel Socialite for authentication with Facebook, Twitter, LinkedIn, Google, GitHub and Bitbucket), but its actually pretty easy to implement one ourselves.

During this post, we will dive into the authentication functionality and how it works behind the scene. We will also implement a Single Sign On, which will solve our use case.

AWS Cognito

Cognito is a powerful Authentication handler provided by AWS. We will use it in the background to store all of our user credentials and identifications.

To set up a Cognito user pool, log into your management console and navigate to Cognito. Oh, great news by the way. Cognito is 100% free for up to 50.000 monthly active users.

Once you are on the Cognito page, click Create a user pool. It is actually pretty easy to set up, so we won't walk through the whole process here.

IMPORTANT: Don't forget to activate the checkbox to Enable sign-in API for server-based Authentication.
The Auth Flow is called: ADMIN_NO_SRP_AUTH

So now that we have set up a valid Cognito Pool we can go into our Laravel project.

At this point, we cannot communicate with AWS. We could implement their API by ourselves using Guzzle or Zttp as a Guzzle wrapper, but this would be too much work for our task.

Luckily AWS provides a PHP SDK which we can require using composer:

1
2composer require aws/aws-sdk-php
3

Once installed the SDK will give us a Client which we can use for the API communication: CognitoIdentityProviderClient.

By implementing the authentication functionality we will build a CognitoClient which we can then easily use in our controller.

Laravel Authentication Setup

Okay, so as already mentioned in the introduction, it is pretty easy to get authentication up and running in Laravel. Let's dive into it.

What does the make:auth command actually do? First of all, it creates the needed directories were it will store the authentication files which we can edit later.

They are located in the following directories:

  • resources/views/auth
  • resources/views/layouts

All files you get from those two directories are copied from the make:auth command.

Additionally, we get a HomeController plus some AuthControllers.

To activate the new routes, Laravel automatically appends an Auth::routes() call to your routes/web.php file. Now we got all the scaffolding done and our authentication is up and running.

With just one call from the command line, we created views, a controller, and routes. That's just awesome. Let's have a deeper look at the registration process.

Registration

As we all know, the registration process starts in the RegistrationController we have previously created. Normally, after the user enters valid credentials into the form, we will create the new User object and store it into the database. Ok, you might ask yourself, how am I able to push the user into our Cognito Pool?

The answer is easy. We need to add a little programming logic of our own.

First, we create a class called CognitoClient under app\Cognito. Once it's developed, it will look like this:

1<?php
2namespace App\Cognito;
3
4use Aws\CognitoIdentityProvider\CognitoIdentityProviderClient;
5use Aws\CognitoIdentityProvider\Exception\CognitoIdentityProviderException;
6use Illuminate\Support\Facades\Password;
7use Illuminate\Support\Str;
8
9class CognitoClient
10{
11    const NEW_PASSWORD_CHALLENGE = 'NEW_PASSWORD_REQUIRED';
12    const FORCE_PASSWORD_STATUS  = 'FORCE_CHANGE_PASSWORD';
13    const RESET_REQUIRED         = 'PasswordResetRequiredException';
14    const USER_NOT_FOUND         = 'UserNotFoundException';
15    const USERNAME_EXISTS        = 'UsernameExistsException';
16    const INVALID_PASSWORD       = 'InvalidPasswordException';
17    const CODE_MISMATCH          = 'CodeMismatchException';
18    const EXPIRED_CODE           = 'ExpiredCodeException';
19
20    /**
21     * @var CognitoIdentityProviderClient
22     */
23    protected $client;
24
25    /**
26     * @var string
27     */
28    protected $clientId;
29
30    /**
31     * @var string
32     */
33    protected $clientSecret;
34
35    /**
36     * @var string
37     */
38    protected $poolId;
39
40    /**
41     * CognitoClient constructor.
42     * @param CognitoIdentityProviderClient $client
43     * @param string $clientId
44     * @param string $clientSecret
45     * @param string $poolId
46     */
47    public function __construct(
48        CognitoIdentityProviderClient $client,
49        $clientId,
50        $clientSecret,
51        $poolId
52    ) {
53        $this->client       = $client;
54        $this->clientId     = $clientId;
55        $this->clientSecret = $clientSecret;
56        $this->poolId       = $poolId;
57    }
58
59    /**
60     * Checks if credentials of a user are valid
61     *
62     * @see http://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminInitiateAuth.html
63     * @param string $email
64     * @param string $password
65     * @return \Aws\Result|bool
66     */
67    public function authenticate($email, $password)
68    {
69        try
70        {
71            $response = $this->client->adminInitiateAuth([
72                'AuthFlow'       => 'ADMIN_NO_SRP_AUTH',
73                'AuthParameters' => [
74                    'USERNAME'     => $email,
75                    'PASSWORD'     => $password,
76                    'SECRET_HASH'  => $this->cognitoSecretHash($email)
77                ],
78                'ClientId'   => $this->clientId,
79                'UserPoolId' => $this->poolId
80            ]);
81        }
82        catch (CognitoIdentityProviderException $exception)
83        {
84            if ($exception->getAwsErrorCode() === self::RESET_REQUIRED ||
85                $exception->getAwsErrorCode() === self::USER_NOT_FOUND) {
86                return false;
87            }
88
89            throw $exception;
90        }
91
92        return $response;
93    }
94
95    /**
96     * Registers a user in the given user pool
97     *
98     * @param $email
99     * @param $password
100     * @param array $attributes
101     * @return bool
102     */
103    public function register($email, $password, array $attributes = [])
104    {
105        $attributes['email'] = $email;
106
107        try
108        {
109            $response = $this->client->signUp([
110                'ClientId' => $this->clientId,
111                'Password' => $password,
112                'SecretHash' => $this->cognitoSecretHash($email),
113                'UserAttributes' => $this->formatAttributes($attributes),
114                'Username' => $email
115            ]);
116        }
117        catch (CognitoIdentityProviderException $e) {
118            if ($e->getAwsErrorCode() === self::USERNAME_EXISTS) {
119                return false;
120            }
121
122            throw $e;
123        }
124
125        $this->setUserAttributes($email, ['email_verified' => 'true']);
126
127        return (bool) $response['UserConfirmed'];
128    }
129
130    /**
131     * Send a password reset code to a user.
132     * @see http://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_ForgotPassword.html
133     *
134     * @param  string $username
135     * @return string
136     */
137    public function sendResetLink($username)
138    {
139        try {
140            $result = $this->client->forgotPassword([
141                'ClientId' => $this->clientId,
142                'SecretHash' => $this->cognitoSecretHash($username),
143                'Username' => $username,
144            ]);
145        } catch (CognitoIdentityProviderException $e) {
146            if ($e->getAwsErrorCode() === self::USER_NOT_FOUND) {
147                return Password::INVALID_USER;
148            }
149
150            throw $e;
151        }
152
153        return Password::RESET_LINK_SENT;
154    }
155
156    # HELPER FUNCTIONS
157
158    /**
159     * Set a users attributes.
160     * http://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_AdminUpdateUserAttributes.html
161     *
162     * @param string $username
163     * @param array  $attributes
164     * @return bool
165     */
166    public function setUserAttributes($username, array $attributes)
167    {
168        $this->client->AdminUpdateUserAttributes([
169            'Username' => $username,
170            'UserPoolId' => $this->poolId,
171            'UserAttributes' => $this->formatAttributes($attributes),
172        ]);
173
174        return true;
175    }
176
177
178    /**
179     * Creates the Cognito secret hash
180     * @param string $username
181     * @return string
182     */
183    protected function cognitoSecretHash($username)
184    {
185        return $this->hash($username . $this->clientId);
186    }
187
188    /**
189     * Creates a HMAC from a string
190     *
191     * @param string $message
192     * @return string
193     */
194    protected function hash($message)
195    {
196        $hash = hash_hmac(
197            'sha256',
198            $message,
199            $this->clientSecret,
200            true
201        );
202
203        return base64_encode($hash);
204    }
205
206    /**
207     * Get user details.
208     * http://docs.aws.amazon.com/cognito-user-identity-pools/latest/APIReference/API_GetUser.html
209     *
210     * @param  string $username
211     * @return mixed
212     */
213    public function getUser($username)
214    {
215        try {
216            $user = $this->client->AdminGetUser([
217                'Username' => $username,
218                'UserPoolId' => $this->poolId,
219            ]);
220        } catch (CognitoIdentityProviderException $e) {
221            return false;
222        }
223
224        return $user;
225    }
226
227    /**
228     * Format attributes in Name/Value array
229     *
230     * @param  array $attributes
231     * @return array
232     */
233    protected function formatAttributes(array $attributes)
234    {
235        $userAttributes = [];
236
237        foreach ($attributes as $key => $value) {
238            $userAttributes[] = [
239                'Name' => $key,
240                'Value' => $value,
241            ];
242        }
243
244        return $userAttributes;
245    }
246}

Now we have a client, which can actually talk to Cognito. since we have some dependencies in this, we want to turn it into a singleton service, so what we are going to do, is create a CognitoAuthServiceProvider.

1<?php
2namespace App\Providers;
3
4use App\Auth\CognitoGuard;
5use App\Cognito\CognitoClient;
6use Illuminate\Support\ServiceProvider;
7use Illuminate\Foundation\Application;
8use Aws\CognitoIdentityProvider\CognitoIdentityProviderClient;
9
10class CognitoAuthServiceProvider extends ServiceProvider
11{
12    public function boot()
13    {
14        $this->app->singleton(CognitoClient::class, function (Application $app) {
15            $config = [
16                'credentials' => config('cognito.credentials'),
17                'region'      => config('cognito.region'),
18                'version'     => config('cognito.version')
19            ];
20
21            return new CognitoClient(
22                new CognitoIdentityProviderClient($config),
23                config('cognito.app_client_id'),
24                config('cognito.app_client_secret'),
25                config('cognito.user_pool_id')
26            );
27        });
28  
29    }
30}

And finally, add this ServiceProvider to your config\app.php file to activate it. Now that we have our Cognito Client available in the application, we want to use it in the RegisterController. So let's copy the register function from the trait, and slightly adjust it to fit our needs.

1public function register(Request $request)
2    {
3        $this->validator($request->all())->validate();
4
5        $attributes = [];
6
7        $userFields = ['name', 'email'];
8
9        foreach($userFields as $userField) {
10
11            if ($request->$userField === null) {
12                throw new \Exception("The configured user field $userField is not provided in the request.");
13            }
14
15            $attributes[$userField] = $request->$userField;
16        }
17
18        app()->make(CognitoClient::class)->register($request->email, $request->password, $attributes);
19
20        event(new Registered($user = $this->create($request->all())));
21
22        return $this->registered($request, $user)
23            ?: redirect($this->redirectPath());
24    }

We first validate the user post data, go through the user fields we want to store in Cognito, and extract them from our request. Then create a new CognitoClient Singleton and call its register with the values.

When you register now in your Laravel App you should have the first user in your Cognito pool. Congratulations!

Let's move on to the Login logic.

Login

To be able to let the user login via Cognito, we need to adjust the way Laravel authenticates the user. You will be able to adjust it in the config\auth.php file. Currently, it looks like this:

1'guards' => [
2        'web' => [
3            'driver' => 'session',
4            'provider' => 'users',
5        ],
6
7        'api' => [
8            'driver' => 'token',
9            'provider' => 'users',
10        ],
11    ],

Laravel works with so-called Guards. One guard has the responsibility to authenticate the user by its given credentials and perform the login process, so the user can access the application.

What we want to do now is create a new AuthenticationGuard, so we can use our own Cognito driver.

This can be achieved quite easily.

Let's create a folder called Auth and place it underneath the app directory and create a class called CognitoGuard. This class should extend the SessionGuard from Laravel, and implement the StatefulGuard interface.

In our case it will look like this:

1<?php
2namespace App\Auth;
3
4use App\Cognito\CognitoClient;
5use App\Exceptions\InvalidUserModelException;
6use App\Exceptions\NoLocalUserException;
7use Aws\Result;
8use Illuminate\Auth\SessionGuard;
9use Illuminate\Contracts\Auth\Authenticatable;
10use Illuminate\Contracts\Auth\StatefulGuard;
11use Illuminate\Contracts\Auth\UserProvider;
12use Illuminate\Contracts\Session\Session;
13use Illuminate\Database\Eloquent\Model;
14use Symfony\Component\HttpFoundation\Request;
15
16class CognitoGuard extends SessionGuard implements StatefulGuard
17{
18    /**
19     * @var CognitoClient
20     */
21    protected $client;
22
23    /**
24     * CognitoGuard constructor.
25     * @param string $name
26     * @param CognitoClient $client
27     * @param UserProvider $provider
28     * @param Session $session
29     * @param null|Request $request
30
31     */
32    public function __construct(
33        string $name,
34        CognitoClient $client,
35        UserProvider $provider,
36        Session $session,
37        ?Request $request = null
38    ) {
39        $this->client = $client;
40        parent::__construct($name, $provider, $session, $request);
41    }
42
43    /**
44     * @param mixed $user
45     * @param array $credentials
46     * @return bool
47     * @throws InvalidUserModelException
48     */
49    protected function hasValidCredentials($user, $credentials)
50    {
51        /** @var Result $response */
52        $result = $this->client->authenticate($credentials['email'], $credentials['password']);
53
54        if ($result && $user instanceof Authenticatable) {
55            return true;
56        }
57
58        return false;
59    }
60
61    /**
62     * Attempt to authenticate a user using the given credentials.
63     *
64     * @param  array  $credentials
65     * @param  bool   $remember
66     * @throws
67     * @return bool
68     */
69    public function attempt(array $credentials = [], $remember = false)
70    {
71        $this->fireAttemptEvent($credentials, $remember);
72
73        $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);
74
75        if ($this->hasValidCredentials($user, $credentials)) {
76            $this->login($user, $remember);
77            return true;
78        }
79
80        $this->fireFailedEvent($user, $credentials);
81
82        return false;
83    }
84}

This looks pretty straightforward and should do the job. What we are doing here is, we are taking the credentials from the login and using the provider to retrieve a user by its credentials. You now see the combination. The guard uses a provider. That's what we saw in the guard config array some lines above.

And this is what a provider definition looks like:

1'providers' => [
2   'users' => [
3       'driver' => 'eloquent',
4       'model' => App\User::class
5   ],
6],

The Provider has a driver, which is currently set to eloquent and a model definition, which tells Laravel about the model class it needs to use.

Now we need to make our guard visible to the application and as you might have guessed, we can do so by adapting our boot method from our CognitoAuthServiceProvider.

1<?php
2namespace App\Providers;
3
4use App\Auth\CognitoGuard;
5use App\Cognito\CognitoClient;
6use Illuminate\Support\ServiceProvider;
7use Illuminate\Foundation\Application;
8use Aws\CognitoIdentityProvider\CognitoIdentityProviderClient;
9
10class CognitoAuthServiceProvider extends ServiceProvider
11{
12    public function boot()
13    {
14       // ...
15        $this->app['auth']->extend('cognito', function (Application $app, $name, array $config) {
16            $guard = new CognitoGuard(
17                $name,
18                $client = $app->make(CognitoClient::class),
19                $app['auth']->createUserProvider($config['provider']),
20                $app['session.store'],
21                $app['request']
22            );
23
24            $guard->setCookieJar($this->app['cookie']);
25            $guard->setDispatcher($this->app['events']);
26            $guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
27
28            return $guard;
29        });
30    }
31}
32

Here we are going to extend the Authentication internals from Laravel and create a new driver which we can use in our application. The first parameter is the name we have to set as a driver.

Now set the new driver so your auth config looks like this:

1<?php
2
3return [
4
5    /*
6    |--------------------------------------------------------------------------
7    | Authentication Defaults
8    |--------------------------------------------------------------------------
9    |
10    | This option controls the default authentication "guard" and password
11    | reset options for your application. You may change these defaults
12    | as required, but they're a perfect start for most applications.
13    |
14    */
15
16    'defaults' => [
17        'guard' => 'web',
18        'passwords' => 'users',
19    ],
20
21    /*
22    |--------------------------------------------------------------------------
23    | Authentication Guards
24    |--------------------------------------------------------------------------
25    |
26    | Next, you may define every authentication guard for your application.
27    | Of course, a great default configuration has been defined for you
28    | here which uses session storage and the Eloquent user provider.
29    |
30    | All authentication drivers have a user provider. This defines how the
31    | users are actually retrieved out of your database or other storage
32    | mechanisms used by this application to persist your user's data.
33    |
34    | Supported: "session", "token"
35    |
36    */
37
38    'guards' => [
39        'web' => [
40            'driver' => 'cognito', <--- This is the important line
41            'provider' => 'users',
42        ],
43        'api' => [
44            'driver' => 'token',
45            'provider' => 'users',
46        ],
47    ],
48    'providers' => [
49        'users' => [
50            'driver' => 'eloquent',
51            'model' => App\User::class
52        ]
53    ],
54];

The last step is to adjust the LoginController. For our task, we need to overwrite the basic login functionality as we did in the registration process.

Go to your LoginController and add those to methods:

1public function login(Request $request)
2{
3        $this->validateLogin($request);
4
5        if ($this->hasTooManyLoginAttempts($request)) {
6            $this->fireLockoutEvent($request);
7
8            return $this->sendLockoutResponse($request);
9        }
10
11        try
12        {
13            if ($this->attemptLogin($request)) {
14                return $this->sendLoginResponse($request);
15            }
16        }
17        catch(CognitoIdentityProviderException $c) {
18            return $this->sendFailedCognitoResponse($c);
19        }
20        catch (\Exception $e) {
21            return $this->sendFailedLoginResponse($request);
22        }
23
24        return $this->sendFailedLoginResponse($request);
25    }
26
27    private function sendFailedCognitoResponse(CognitoIdentityProviderException $exception)
28    {
29        throw ValidationException::withMessages([
30            $this->username() => $exception->getAwsErrorMessage(),
31        ]);
32    }
33

Now when you use your credentials, you can log in your user with the new Cognito driver. That's cool!

You can see that it takes quite a bit of time to implement this! Remember our original use case? We wanted to create a single sign-on, so we have to repeat this at least one more time. Now, take a step back and imagine you have 5 projects you want to connect with a single sign-on. This is not maintainable!

So, we decided to create a package which solves all of this work for you, and cut it down to just 5 minutes of work to get it up and running!

Laravel Package to easily manage authentication with AWS Cognito

Let's throw all the work away we have done so far and start from scratch, and I'll show you how easy it is to implement a single sign-on.

To get started you will need to require our package using composer:

1
2composer require black-bits/laravel-cognito-auth
3

Once installed you can go on and publish the config and the view:

1
2php artisan vendor:publish --provider="BlackBits\LaravelCognitoAuth\CognitoAuthServiceProvider"
3

Now move on to your config\auth.php file and activate the new cognito driver like we did before:

1'guards' => [
2    'web' => [
3        'driver' => 'cognito', // This line is important 
4        'provider' => 'users',
5    ],
6    'api' => [
7        'driver' => 'token',
8        'provider' => 'users',
9    ],
10],

Add the following fields to your .env file:

1AWS_COGNITO_KEY=
2AWS_COGNITO_SECRET=
3AWS_COGNITO_REGION=
4AWS_COGNITO_CLIENT_ID=
5AWS_COGNITO_CLIENT_SECRET=
6AWS_COGNITO_USER_POOL_ID=

Now you walk through the AuthControllers and swap out the Laravel specific traits with our traits. Don't be afraid to do something wrong, they are just called like the ones in Laravel.

1BlackBits\LaravelCognitoAuth\Auth\AuthenticatesUsers
2BlackBits\LaravelCognitoAuth\Auth\RegistersUsers
3BlackBits\LaravelCognitoAuth\Auth\ResetsPasswords
4BlackBits\LaravelCognitoAuth\Auth\SendsPasswordResetEmails

And that's it. You have implemented our package and enabled AWS Cognito authentication. Last but not least we want to take care of the Single Sign-On.

Single Sign On

With our package and AWS Cognito, we provide you a simple way to use Single Sign On's. To enable it just go to the cognito.php file in the config directory and set USE_SSO=true. But how does it work?

When you have SSO enabled in your config, and a user tries to log into your application, the cognito client will check if he exists in your Cognito pool. If the user exists he will be created automatically in your database and is logged in simultaneously.

That's what we need the fields sso_user_model and sso_user_fields for. In sso_user_model you define the class of your user model. In most cases, this will simply be App\User.

With sso_user_fields you can define the fields which should be stored in Cognito. Pay attention here! If you define a field which you do not send with the Register Request, it will throw an InvalidUserFieldException and you won't be able to register.

So now you have registered your user with its attributes in the Cognito pool and your local database, and you want to attach a second app which uses the same pool. Well, that's actually pretty easy. You set up your project like you are used to and install our laravel-cognito-auth package. On both sites set USE_SSO=true. Also be sure you entered exactly the same pool id. Now when a user is registered in your other app but not in your second and wants to login he gets created. And that's all you need to do.

Conclusion

Today you've learned how simple it is to spread a single user database over multiple projects.

In the future, we will work on this package to make it even better and more customizable. For the moment you can only log in with an email address, but in the future, we want to make it customizable to be any field of choice.

Let us know what you think.

https://github.com/black-bits/laravel-cognito-auth

View all articles

Expert Level Laravel Web Development & Consulting Agency

We love Laravel, and so should you. Let us show you why.

Find out about Laravel

About Us

About Us

Founded in 2014, Black Bits helps customers to reach their goals and to expand their market positions. We work with a wide range of companies, from startups that have a vision and first funding to get to market faster, to big industry leaders that want to profit from modern technologies. If you want to start on your project without building an internal dev-team first, or if you need extra expertise or resources, Black Bits - the Laravel Web Development Agency is here to help.

Laravel

Laravel is one of the most popular PHP Frameworks. Perfect for Sites and API's of all sizes. Clean, fast and reliable.

Lumen

Lumen is the perfect solution for building Laravel based micro-services and blazing fast APIs.

Vue.js

Live-Updating Dashboards, Components and Reactivity. Your project needs a modern user-interface? Vue.js is the right choice.

React

Live-Updating Dashboards, Components and Reactivity. Your project needs a modern user-interface? React is the right choice.

Bootstrap

Bootstrap is the second most-starred project on GitHub and the leading framework for designing modern front-ends.

TailwindCSS

Tailwind is a utility-first CSS framework for rapidly building custom user interfaces.

Amazon Web Services

Amazon Web Services (AWS) leads the worldwide cloud computing market. With a wide range of services, AWS is a perfect option for fast development.

Digital Ocean

Leave complex pricing structures behind. Always know what you'll pay per month. Service customers around the world from eight different locations.

Contact Us

Contact Us

You have the vision, we have the development expertise. We would love to hear about your project.

Send us a message or give us a call and see how Black Bits can help you!

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
Mountain view.