Symfony on a lambda: sessions

hyoa

Matsounga Jules

Posted on January 20, 2020

Symfony on a lambda: sessions

Symfony on a lambda: sessions

If you are lost, you can find the project on Github: link
Each branch will match a chapter.

Sessions handling

Handling session is quite important when dealing with a website. It can be used to store many data that we will be able to use easily everywhere on the site.
Sessions are generated by the server, send to the client through a cookie that the client will reuse to authenticate on the application.

By default, Symfony save the session in memory on the server. But lambda don't last forever, so sessions stored on the lambda could disappear at any moment.

Fortunately, we can store sessions on database. In our case, DynamoDB. DynamoDB is a key-value and document database provided by AWS.

Creation of the session table

We will need new resource on our cloudformation, a table to store our sessions.
Let's edit our serverless.yml file to add this new resource.

//serverless.yml
service: [name of your project]

provider:
    // ...
custom:
    // ...
plugins:
    // ...

functions:
    // ...

resources:
    Resources:
        // ...
    SessionsTable:
      Type: AWS::DynamoDB::Table
      Properties:
        TableName: ${self:custom.prefix}-sessionsTable
            AttributeDefinitions:
            - AttributeName: id
                AttributeType: S
            KeySchema:
            - AttributeName: id
                KeyType: HASH
        ProvisionedThroughput:
            ReadCapacityUnits: 1
            WriteCapacityUnits: 1

As I said earlier, DynamoDB is a key-value document database. That's mean we will have to define the key that we will use to retrieve our document. Unlike SQL database, we don't have to define a entire structure as we will store document. See more information here.

We create a new resource of type: AWS::DynamoDB::Table. This table will have the name sessionsTable prefixed by the custom prefix we created on the previous chapter.
Then we define our HASH key that we will use to get our document: id and say it is of type string S.
We also define the ProvisionedThroughput, that define the number of read by second that we can achieve. As it's only a exercise, 1 is enough. (More information here: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadWriteCapacityMode.html)

We can now create our new table on AWS using serverless deploycommand.

Storing session in the table

Now that our table is created, let's change the way Symfony store our session.
At first, we need to install the SDK provided by AWS to interact with the table. Type composer require aws/aws-sdk-php ^3.112 to install it.

We will then create a service that will handle our sessions.
In src/Services create a file DynamoDbSessionHandlerFactory.php that will contain the following class:

<?php

namespace App\Services;

use Aws\DynamoDb\DynamoDbClient;
use Aws\DynamoDb\SessionHandler;

class DynamoDbSessionHandlerFactory
{
    public static function build(DynamoDbClient $dbClient): SessionHandler
    {
        return SessionHandler::fromClient($dbClient, ['table_name' => 'sessionsTable']);
    }
}

We will use this factory to create our service.

In src/Services create a file AwsClient.php that will contain the following class:

<?php

namespace App\Services;

use Aws\DynamoDb\DynamoDbClient;
use Aws\S3\S3Client;
use Aws\Sdk;

class AwsClient
{
    /** @var Sdk */
    protected $sdk;

    public function __construct(
        string $region,
        string $version,
        string $dynamoDbCredentialKey,
        string $dynamoDbCredentialSecretKey
    ) {
        $this->sdk = new Sdk([
            'region'   => $region,
            'version'  => $version,
            'Dynamodb' => [
                'credentials' => [
                    'key' => $dynamoDbCredentialKey,
                    'secret' => $dynamoDbCredentialSecretKey
                ]
            ]
        ]);
    }

    public function dynamoDbClient(): DynamoDbClient
    {
        return $this->sdk->createDynamoDb();
    }
}

This service handle the connection to AWS and create the required client, here the client for DynamoDB.

We can now define both of this services so Symfony can use them correctly. Edit config/services.yaml:

// ...

services:
    // ...
  App\Services\AwsClient:
    arguments:
        $region: '%env(resolve:APP_AWS_REGION)%'
        $version: '%env(resolve:APP_AWS_VERSION)%'
        $dynamoDbCredentialKey: '%env(resolve:APP_AWS_DYNAMODB_CREDENTIAL_KEY)%'
        $dynamoDbCredentialSecretKey: '%env(resolve:APP_AWS_DYNAMODB_CREDENTIAL_SECRET)%'   

    App\Services\SessionHandler:
        factory: ['App\Services\DynamoDbSessionHandlerFactory', 'build']
    arguments:
        - "@=service('App\\\\Services\\\\AwsClient').dynamoDbClient()"
        - '%env(resolve:APP_AWS_RESOURCE_PREFIX)%'
    calls:
        - method: register

Our AwsClient require some environment variables. Let's define them.
Edit .env at the root of your folder and add the following lines:

APP_AWS_REGION=''
APP_AWS_VERSION=''
APP_AWS_DYNAMODB_CREDENTIAL_KEY=''
APP_AWS_DYNAMODB_CREDENTIAL_SECRET=''
APP_AWS_RESOURCE_PREFIX=''

Do not put your credentials in this file ! As it will be committed and push to your repository. You don't want to have your secrets expose to internet.

Now create .env.local at the root of your projects and add the previous lines. This files is ignored thanks to .gitignore, so you can put your information here.
APP_AWS_RESOURCE_PREFIX is the custom prefix define in serverless.yml: [service-name]-[region]-[stage]

We can now edit the handler of session of Symphony. Edit config/packages/framework.yaml

framework:
    secret: '%env(APP_SECRET)%'
    session:
        handler_id: App\Services\SessionHandler
        cookie_secure: auto
        cookie_samesite: lax
    php_errors:
        log: true

Our handler_id refer to our service created with the factory.

As a quick test to see if it's working. Edit src/Controller/HomeController.php

// ...    

public function homeAction(SessionInterface $session): Response
{
    $session->set('foo', 'bar');
    return $this->render('home/index.html.twig');
}

Now, go in the AWS Console, search for DynamoDB, click on the link. At the left, select Tables then your session table. In elements, you should see the data stored.

Making it work on the lambda

Almost everything is already set to have it working on a lambda, except that we don't have any of the environments variables that we just added.

We will have to add them in our serverless.yml

service: // ...

provider:
    // ...
    environment:
            // ...
        APP_AWS_REGION: 'eu-west-2'
        APP_AWS_VERSION: 'latest'
        APP_AWS_DYNAMODB_CREDENTIAL_KEY: ''
        APP_AWS_DYNAMODB_CREDENTIAL_SECRET: ''
        APP_AWS_RESOURCE_PREFIX: ${self:custom.prefix}

As said earlier, we cannot store our keys on files that are committed to our repository. This one is, so we can't put our key here neither. But we still need to have this information to be passed to serverless and we can't make a .local this time.

We will use AWS Systems Manager to secure our keys. Visit the AWS Console, search for Systems Manager and select Parameter Store in the left panel.
We can now create our 2 keys, click on create parameter:

  • Name: [service]/[stage]/key
  • Description: Add what you want
  • Tier: Standard
  • Type: String
  • Value: Put your key here

Then click on Create parameter
Repeat the operation for the secret (name: [service]/[stage]/secret)

We can now reference both of this parameter in serverless.yml

service: // ...

provider:
    // ...
    environment:
        APP_ENV: prod
        APP_AWS_REGION: 'eu-west-2'
        APP_AWS_VERSION: 'latest'
        APP_AWS_DYNAMODB_CREDENTIAL_KEY: ${ssm:/${self:service}/${self:provider.stage}/key}
        APP_AWS_DYNAMODB_CREDENTIAL_SECRET: ${ssm:/${self:service}/${self:provider.stage}/secret}
        APP_AWS_RESOURCE_PREFIX: ${self:custom.prefix}

ssm will interact with the Systems Manager to get the data at the path specified.

By default, a lambda cannot interact with other resource for security purpose. So will have to give it the right to interact with DynamoDB, otherwise we will get a fancy error about permission.
Still in serverless.yml, add the following lines:

service: // ...

provider:
        // ...
    iamRoleStatements:
        - Effect: Allow
          Action:
              - dynamodb:Query
              - dynamodb:Scan
              - dynamodb:GetItem
              - dynamodb:PutItem
              - dynamodb:UpdateItem
              - dynamodb:DeleteItem
          Resource: "arn:aws:dynamodb:[your region]:*:table/*"

This will give the lambda the ability to do the action defined in all the table. Of course, we can be more precise on which table we want to give permission and what permission we want but I prefer to keep it simple here.

With all of this, we can deploy the solution again with serverless deploy.
If you go back in the DynamoDB console, and then you browse the application through the link in the output at the end of the deploy command, you should see a new entry in the table. Well done !

Cleanup

First, let's remove the $session->set from HomeController.php, we don't need that anymore.
In a second time, we now have sessions working on lambda, but for local development we want to use the default storage method. We use our computer so we don't need to store our sessions in DynamoDB. To do so, we will have to override the session handling when we are in dev.

Create a file framework.yaml in config/packages/dev

framework:
    session:
        handler_id: null

This will prevent Symfony to use our new service and use the default method.


Now that our session are working, it's possible to deal with authentication ! See you in the next chapters

💖 💪 🙅 🚩
hyoa
Matsounga Jules

Posted on January 20, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related