SAML / Cognito / Laravel: How to use ADFS authentication through Cognito in Laravel
Tags: laravel, cognito, aws, saml
Published: 2021-04-30 13:19:56

Rather than integrate our Laravel application directly into ADFS, we elected to use an AWS Cognito user pool as a go-between. Here are the notes I put together while working on this.


## Intended flow & logic

Once the integration is finished, this is the intended authentication flow:

1. User attempts to visit a route on your Laravel app which is protected by the EnsureCognitoAuthenticates middleware.
2. Laravel's Authenticate middleware catches the unauthenticated request and directs the user to your login route.
3. Cognito's hosted UI prompts the user to log in with the SAML/ADFS login flow, and redirects to `/saml/login` with an authorization code.
4. The logic at the `/saml/login` route takes the authorization code, goes to AWS Cognito, and trades it for an access_token, which it inserts into the user's session data. It also creates the user in the Laravel database, and logs them in to Laravel. Then, they are redirected to the page they originally requested.
5. On each subsequent request for a new page, Laravel's Authenticate middleware checks the session token to ensure it is currently a valid session, and that the session belongs to an authenticated user. Our custom EnsureCognitoAuthenticates middleware checks for the access_token in the session data, and checks that with Cogito to ensure it is still valid. If it is, the user can continue. If not, we use the refresh_token from the Laravel user to get a new access_token from Cognito, or we log them out.

## Cognito

This part is relatively straight-forward, and is laid out well in [this video](https://www.youtube.com/watch?v=6-qEQHgGu2U) provided by AWS, but I'll go over it too if you'd prefer to read rather than watch - particularly as in my case we had an existing ADFS server to integrate with. 

* You'll need an AWS account here; once you're logged in, navigate to Cognito's console and create a new User Pool.
* Give it a name and create with the 'Review defaults' option, and go straight to create pool.
* Add an App client using the 'App clients' option from the Cogito menu. This will represent our Laravel application to Cogntio. Give it a name and leave the token expirations as their defaults (you can change these later if you need to). Uncheck the 'Generate client secret' box and finally click 'Create app client'. Take note of the app client ID displayed on this next screen.
* Next, go to 'Domain name' under 'App integration' and enter a globally unique domain prefix, and click 'save changes.'.
* At this point, it's time to talk to the person who manages your ADFS server. You'll need the ADFS metadata, which will be unique for the server you're trying to authenticate with. It comes from a url that ends in something like: `federationmetadata/2007-06/federationmetadata.xml`. You can download this and save it, or just keep the URL to pass to Cognito in the next step.
* Navigate to 'Identity providers' under 'Federation' in the Cognito menu. Choose 'SAML' from the choices displayed, and provide the metadata file or URL via the next menu. The 'provider name' can be whatever you like, this is for your reference.
* Go back to 'App client settings', where you can see the app client you set up earlier. With it, enable the SAML Identity Provider (IdP) you just set up by ticking its box. In 'Call back URL(s)', you want to provide whatever URL you will be using to handle SAML logins on your app. For example, this could be `https://mylaraveldashboard.com/saml/login`. Some notes: 
  * If you change the URL that handles this on your app later, you'll need to update this value. This also works as a kind of password when exchanging tokens later in the flow; Cognito makes sure the redirect URL you provide in your token requests matches this value, and will throw an error if it doesn't.
  * To test on your local machine, you can set the callback URL to `http://localhost:8000/saml/login` (thanks @guruprasad_mulay_1c560d99 for pointing that out!).
* Then, tick 'Authorization code grant' and 'Implicit grant' in the 'Allowed OAuth Flows' section. Next, check all of the Allowed OAuth Scopes (I leave off 'phone' as it isn't needed for our application, but that's up to you). Click 'Save changes'.
* Attribute mapping is next; this can be found under 'Federation' in the menu. Here, we tell Cognito what information about a user that it receives from ADFS we want exposed to our application. These are straight forward, and you should enter in the below information, depending on what attributes you need. In this example, we get email, given_name, and family_name.
![A table showing the correct configuration of attribute mapping, with a checkbox, schema url, and userpool attribute](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/r2f9efxkn8d93aqe2djl.png)
* Back to App client settings, scroll down and find the Hosted UI option. Click this link, and copy the URL that you are taken to. This is your sign in page! Don't worry, you can change the CSS through the Cognito menu (look for UI Customisation).
* Finally, your ADFS contact will need to enable your application. They'll need [this metadata file](https://signin.aws.amazon.com/static/saml-metadata.xml), available from AWS' documentation, and your Hosted UI URL, and some time. Sorry I can't offer further info here - it was out of my hands when I set our application up.

## Laravel

* Before we write code, lets get our .env file filled up with all the info we're going to need. Mine looks like this:
```
COGNITO_APP_ID=(this is the code that you noted earlier while setting up an app for your user pool)
COGNITO_APP_REGION=eu-west-2 (or whatever your region is)
COGNITO_HOSTED_UI_URL=(you visted this url earlier, it's your sign in page)
COGNITO_REDIRECT_URL=(this is the one you provided to the app client settings on cognito, and should be a link to a page in your application)
COGNITO_API_BASE_URI=(this will look something like https://[user-pool-chosen-domain-name].auth.[aws-region].amazoncognito.com)
```
* Now we'll need the AWS PHP SDK package from composer, so run `composer require aws/aws-sdk-php` in your command line.
* Let's set up the routes we'll need. Here's what the body of your `routes/web.php` file will look like:
```php
Route::middleware(['auth', 'cognito'])->group(function () {
    Route::get('/', function () {
        return view('welcome');
    })->name('welcome');

    // Any other pages you need to protect behind authentication go here
});

Route::get('/saml/login', [LoginController::class, 'samlLogin']);
```
* LoginController.php is next; this will manage the, you guessed it, login flow. Here, we'll log users into Cognito using SAML, then once that is confirmed, store their details in our database on the Laravel side and log them in there too. We'll put their Cognito access_token in their Laravel session, and we'll store their refresh_token in our database. [Here's a gist](https://gist.github.com/Taelkir/6763fe39ed3fa9ade9451ddd1ed61656) with a basic version of my LoginController.
* Next, we need to create middleware that will authenticate each request as they arrive with your application. This will ensure that if a user is deleted from your ADFS server, then they will immediately be denied further access to your application. So, let's run the command `php artisan make:middleware EnsureCognitoAuthenticates`. This will create the relevant file for us to update. You'll also need to make a change to `app/Http/Kernel.php`; down in the `routeMiddleware` array, add in `'cognito' => \App\Http\Middleware\EnsureCognitoAuthenticates::class,` after `auth`. Next is writing the middleware itself. I've done a stripped down version of my own, and [posted it as a gist here](https://gist.github.com/Taelkir/8a47793e1618330afc4ee9e2161de27f).

> If you want to know more about these AWS API endpoints we're calling, their documentation is available here: https://docs.aws.amazon.com/cognito/latest/developerguide/cognito-userpools-server-contract-reference.html

* Open up the `app/Http/Middleware/Authenticate.php` - this is the basic authentication that Laravel runs on each request. Ensure the `redirectTo()` function is redirecting to the page you want users to see when they need to log in. For me, this is the named route 'login', but you could return the URL for your hosted UI to skip a step. My Authenticate middleware looks like this:
```php
    /**
     * Get the path the user should be redirected to when they are not authenticated.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return string|null
     */
    protected function redirectTo($request)
    {

        $attemptedUrl = $request->fullUrl();

        if (!$request->expectsJson()) {
            return route('login', ["desired_url" => urlencode($attemptedUrl)]);
        }
    }

```
* The application is now ready to test!

## Conclusion

So there we have it, a functioning SAML/ADFS authentication experience with Laravel and AWS Cognito. Hopefully you found this helpful. 

I'm sure there are plenty of ways this could be improved - please let me know what you think.