Adding Auth to Our Serverless App
So far we’ve created the DynamoDB table, S3 bucket, and API parts of our serverless backend. Now let’s add auth into the mix. As we talked about in the previous chapter, we are going to use Cognito User Pool to manage user sign ups and logins. While we are going to use Cognito Identity Pool to manage which resources our users have access to.
Setting this all up can be pretty complicated in Terraform. SST has simple CognitoUserPool
and CognitoIdentityPool
components to help with this.
Create the Components
Add the following to a new file in infra/auth.ts
.
import { api } from "./api";
import { bucket } from "./storage";
const region = aws.getRegionOutput().name;
export const userPool = new sst.aws.CognitoUserPool("UserPool", {
usernames: ["email"]
});
export const userPoolClient = userPool.addClient("UserPoolClient");
export const identityPool = new sst.aws.CognitoIdentityPool("IdentityPool", {
userPools: [
{
userPool: userPool.id,
client: userPoolClient.id,
},
],
permissions: {
authenticated: [
{
actions: ["s3:*"],
resources: [
$concat(bucket.arn, "/private/${cognito-identity.amazonaws.com:sub}/*"),
],
},
{
actions: [
"execute-api:*",
],
resources: [
$concat(
"arn:aws:execute-api:",
region,
":",
aws.getCallerIdentityOutput({}).accountId,
":",
api.nodes.api.id,
"/*/*/*"
),
],
},
],
},
});
Let’s go over what we are doing here.
-
The
CognitoUserPool
component creates a Cognito User Pool for us. We are using theusernames
prop to state that we want our users to login with their email. -
We are using
addClient
to create a client for our User Pool. You create one for each “client” that’ll connect to it. Since we only have a frontend we only need one. You can later add another if you add a mobile app for example. -
The
CognitoIdentityPool
component creates an Identity Pool. TheattachPermissionsForAuthUsers
function allows us to specify the resources our authenticated users have access to. -
We want them to access our S3 bucket and API. Both of which we are importing from
api.ts
andstorage.ts
respectively. We’ll look at this in detail below.
Securing Access
We are creating an IAM policy to allow our authenticated users to access our API. You can learn more about IAM here.
{
actions: [
"execute-api:*",
],
resources: [
$concat(
"arn:aws:execute-api:",
region,
":",
aws.getCallerIdentityOutput({}).accountId,
":",
api.nodes.api.id,
"/*/*/*"
),
],
},
This looks a little complicated but Amazon API Gateway has a format it uses to define its endpoints. We are building that here.
We are also creating a specific IAM policy to secure the files our users will upload to our S3 bucket.
{
actions: ["s3:*"],
resources: [
$concat(bucket.arn, "/private/${cognito-identity.amazonaws.com:sub}/*"),
],
},
Let’s look at how this works.
In the above policy we are granting our logged in users access to the path private/${cognito-identity.amazonaws.com:sub}/
within our S3 bucket’s ARN. Where cognito-identity.amazonaws.com:sub
is the authenticated user’s federated identity id (their user id). So a user has access to only their folder within the bucket. This allows us to separate access to our user’s file uploads within the same S3 bucket.
One other thing to note is that, the federated identity id is a UUID that is assigned by our Identity Pool. This id is different from the one that a user is assigned in a User Pool. This is because you can have multiple authentication providers. The Identity Pool federates these identities and gives each user a unique id.
Add to the Config
Let’s add this to our sst.config.ts
.
Add this below the await import("./infra/api")
line in your sst.config.ts
.
const auth = await import("./infra/auth");
return {
UserPool: auth.userPool.id,
Region: aws.getRegionOutput().name,
IdentityPool: auth.identityPool.id,
UserPoolClient: auth.userPoolClient.id,
};
Here we are importing our new config and the return
allows us to print out some useful info about our new auth resources in the terminal.
Add Auth to the API
We also need to enable authentication in our API.
Add the following prop into the transform
options below the handler: {
block in infra/api.ts
.
args: {
auth: { iam: true }
},
So it should look something like this.
// Create the API
export const api = new sst.aws.ApiGatewayV2("Api", {
transform: {
route: {
handler: {
link: [table],
},
args: {
auth: { iam: true }
},
}
}
});
This tells our API that we want to use AWS_IAM
across all our routes.
Deploy Our Changes
If you switch over to your terminal, you will notice that your changes are being deployed.
You should see that the new auth resources are being deployed.
+ Complete
Api: https://5bv7x0iuga.execute-api.us-east-1.amazonaws.com
---
IdentityPool: us-east-1:9bd0357e-2ac1-418d-a609-bc5e7bc064e3
Region: us-east-1
UserPool: us-east-1_TYEz7XP7P
UserPoolClient: 3fetogamdv9aqa0393adsd7viv
Let’s create a test user so that we can test our API.
Create a Test User
We’ll use AWS CLI to sign up a user with their email and password.
In your terminal, run.
$ aws cognito-idp sign-up \
--region <COGNITO_REGION> \
--client-id <USER_POOL_CLIENT_ID> \
--username admin@example.com \
--password Passw0rd!
Make sure to replace COGNITO_REGION
and USER_POOL_CLIENT_ID
with the Region
and UserPoolClient
from above.
Now we need to verify this email. For now we’ll do this via an administrator command.
In your terminal, run.
$ aws cognito-idp admin-confirm-sign-up \
--region <COGNITO_REGION> \
--user-pool-id <USER_POOL_ID> \
--username admin@example.com
Replace the COGNITO_REGION
and USER_POOL_ID
with the Region
and UserPool
from above.
Now that the auth infrastructure and a test user has been created, let’s use them to secure our APIs and test them.
For help and discussion
Comments on this chapter