- what are the initial steps we need to perform, before implementing authentication and authorization checks in our GraphQL API
- how to perform all of the initial steps
In order to follow this tutorial, you must use Webiny version 5.18.0 or greater.
The code that we cover in this section can also be found in our GitHub examples repository . Also, if you’d like to see the complete and final code of the application we’re building, check out the full-example
folder.
Overview
With the Hosted UI authentication flow successfully integrated into our React application, we’re now finally ready to start implementing authentication and authorization checks in our GraphQL API. But, before we do that, we need to perform a couple of initial setup steps.
First of all, we’ll add two new libraries that will make it a bit easier to work with the current user within our GraphQL API code (resolver functions). Once that’s in place, we will perform the following:
- perform a minor update to our cloud infrastructure code
- set up the added libraries (plugins)
- perform a minor but important update to the
Context
interface - add new
createdBy
field to our mainPin
GraphQL type
Let’s see how we can achieve all of these.
1. Adding the Needed Libraries
In order to implement the necessary authentication and authorization checks within our GraphQL API, like in the previous section, let’s again start by adding a couple of new libraries into the mix.
Placeholder
@webiny/api-authentication
A collection of plugins that will set up a thin authentication layer in our GraphQL API handler function.
@webiny/api-authentication-cognito
By itself, the above library doesn’t do much. It’s simply a framework, which is always paired with an authentication provider. In our case, we want to pair it up with the Amazon Cognito authentication, which is what this library does.
The following command will add both libraries to our React application:
yarn workspace pinterest-clone-api add @webiny/api-authentication @webiny/api-authentication-cognito
2. Updating Cloud Infrastructure Code
In order to start setting up the newly added libraries, we’ll first need to update our cloud infrastructure code.
More specifically, we’ll need to pass two new environment variables to our GraphQL API handler function: COGNITO_REGION
and COGNITO_USER_POOL_ID
.
import * as pulumi from "@pulumi/pulumi";import DynamoDB from "./dynamoDb";import Graphql from "./graphql";import ApiGateway from "./apiGateway";import Cloudfront from "./cloudfront";import Cognito from "./cognito";import S3 from "./s3";
// Among other things, this determines the amount of information we reveal on runtime errors.// https://www.webiny.com/docs/how-to-guides/environment-variables/#debug-environment-variableconst DEBUG = String(process.env.DEBUG);
// Enables logs forwarding.// https://www.webiny.com../../core-development-concepts/basics/watch-command#enabling-logs-forwardingconst WEBINY_LOGS_FORWARD_URL = String(process.env.WEBINY_LOGS_FORWARD_URL);
export default () => { const cognito = new Cognito(); const dynamoDb = new DynamoDB(); const s3 = new S3();
const api = new Graphql({ dbTable: dynamoDb.table, cognitoUserPool: cognito.userPool, s3Bucket: s3.bucket, env: { // The single DynamoDB table in which data can be stored and queried. DB_TABLE: dynamoDb.table.name, COGNITO_REGION: String(process.env.AWS_REGION), COGNITO_USER_POOL_ID: cognito.userPool.id, DEBUG, WEBINY_LOGS_FORWARD_URL } });
(...)};
As we’ll soon see, in our application code, these will be passed to our GraphQL API handler function, upon configuring the relevant security plugins.
In order for the new environment variables to be available in our GraphQL API handler function, we’ll need to redeploy it. If you had a watch session already up and running, this should’ve happened automatically. Otherwise, you’ll need to either start a new session via the webiny watch
, or simply deploy the change via the webiny deploy
command.
3. Setting Up the Libraries (Plugins)
Essentially, both @webiny/api-authentication
and @webiny/api-authentication-cognito
packages are exporting a collection of plugins, which are meant to be registered within our GraphQL API handler function:
import { createHandler } from "@webiny/handler-aws";import graphqlPlugins from "@webiny/handler-graphql";import logsPlugins from "@webiny/handler-logs";import authenticationPlugins from "@webiny/api-authentication";import { authenticateUsingHttpHeader } from "@webiny/api-authentication/authenticateUsingHttpHeader";import cognitoAuthenticationPlugins from "@webiny/api-authentication-cognito";
// Imports plugins created via scaffolding utilities.import scaffoldsPlugins from "./plugins/scaffolds";
const debug = process.env.DEBUG === "true";
export const handler = createHandler({ plugins: [ /** * Setup authentication plugins. */ authenticationPlugins(), authenticateUsingHttpHeader(),
/** * Setup Amazon Cognito authentication plugins. */ cognitoAuthenticationPlugins({ region: process.env.COGNITO_REGION, userPoolId: process.env.COGNITO_USER_POOL_ID, identityType: "user" }),
logsPlugins(), graphqlPlugins({ debug }), scaffoldsPlugins() ], http: { debug }});
As we can see, via the authenticationPlugins
and authenticateUsingHttpHeader
function calls, we’re first registering the plugins that set up the authentication layer. No additional configuration parameters need to be passed here.
Then, via the cognitoAuthenticationPlugins
function call, we’re registering another set of plugins that will essentially enable us to perform authentication against Amazon Cognito and the User Pool we’ve defined in a previous section.
Configuration Parameters
Except assigning the newly added environment variables to region
and userPoolId
properties, in the cognitoAuthenticationPlugins
function call, we’re also assigning "user"
to identityType
identityType
is a simple string that can, down the road, tell us what type of an identity we’re working with and which authentication provider managed to authenticate it. For us, this is not that important, as we only have a single type of a user and only a single @webiny/api-authentication-cognito
authentication provider. But, since the identityType
is a required property, we had to set it to something, so we’ve simply set it to "user"
.
4. Updating Context
Interface
The following step is not required, but it’s certainly recommended because we’ll benefit from improved type-safety and better developer experience.
If we were to open the pinterest-clone/api/code/graphql/src/types.ts
file, in it, we’d find the Context
interface, which we can utilize any time we’re actually interacting with the handler’s central context
object. In our case, this would be within our GraphQL resolver functions, but it could also happen in different ContextPlugin
, GraphQLSchemaPlugin
, and other plugins.
So, since the newly added libraries (sets of plugins) are actually modifying the context
object by adding new properties to it, let’s make sure those changes are reflected in the Context
interface as well.
import { HandlerContext } from "@webiny/handler/types";import { HttpContext } from "@webiny/handler-http/types";import { ArgsContext } from "@webiny/handler-args/types";import { ClientContext } from "@webiny/handler-client/types";import { AuthenticationContext } from "@webiny/api-authentication/types";
// When working with the `context` object (for example while defining a new GraphQL resolver function),// you can import this interface and assign it to it. This will give you full autocomplete functionality// and type safety. The easiest way to import it would be via the following import statement:// import { Context } from "~/types";// Feel free to extend it with additional context interfaces, if needed. Also, please do not change the// name of the interface, as existing scaffolding utilities may rely on it during the scaffolding process.export interface Context extends HandlerContext, HttpContext, ArgsContext, ClientContext, AuthenticationContext {}
As we can see, at the top of the file, we’ve simply imported the AuthenticationContext
interface, and then, below, added it to our Context
interface. Which means that, from now on, every time we interact with the context
object, we’ll be able to safely access all of the additional properties that the newly added security-related plugins defined.
5. Adding createdBy
Field
With all of the above steps correctly performed, we should be able to retrieve the signed in user and perform essential authentication and authorization checks within our GraphQL resolver functions. Which means that, upon creating a new pin (via the createPin(data: PinCreateInput!): Pin!
mutation), we’ll also be able to tie the signed in user to it.
In order to do this, we’ll need to expand the existing Pin
entity, and the PinEntity
type with the new createdBy
field, which will be responsible for storing signed in user information. As we’ll be able to see, the field will accept an object that consists of three properties: id
, displayName
, and type
.
Apart from that, we’ll also want to perform the same update on the GraphQL schema side, so that this information can be retrieved from our React application.
So, let’s start by updating the Pin
entity and PinEntity
type first:
// https://github.com/jeremydaly/dynamodb-toolboximport { Entity } from "dynamodb-toolbox";import table from "./table";import { PinEntity } from "../types";
/** * Once we have the table, we define the PinEntity entity. * If needed, additional entities can be defined using the same approach. */export default new Entity<PinEntity>({ table, name: "Pin", timestamps: false, attributes: { PK: { partitionKey: true }, SK: { sortKey: true }, id: { type: "string" }, title: { type: "string" }, description: { type: "string" }, coverImage: { type: "string" }, createdOn: { type: "string" }, savedOn: { type: "string" },
// For the `Pin` entity, we can use a simple "map" type as the attribute type. createdBy: { type: "map" },
// Will store current version of Webiny, for example "5.9.1". // Might be useful in the future or while performing upgrades. webinyVersion: { type: "string" } }});
import { Identity } from "@webiny/api-authentication/types";
export interface PinEntity { PK: string; SK: string; id: string; title: string; description?: string; coverImage?: string; createdOn: string; savedOn: string;
// For the `PinEntity` type, let's be specific and specify which fields are accepted. createdBy: Pick<Identity, "id" | "type" | "displayName">;
webinyVersion: string;}
In the GraphQL API section of this tutorial, we’ve used the Extend GraphQL API scaffold to extend our GraphQL API and introduce essential CRUD GraphQL queries and mutations.
By default, the application code relies on DynamoDB Toolbox , which is a library that makes it a bit easier to interact with the Amazon DynamoDB database. The Entity
class we’ve seen in the above code sample is part of it.
Once that’s in place, all that is left to do is update our GraphQL schema:
- define the new
PinCreatedBy
type - extend the existing
Pin
type with the the newcreatedBy: PinCreatedBy
field
export default /* GraphQL */ ` type Pin { id: ID! title: String! description: String coverImage: String createdOn: DateTime! savedOn: DateTime! createdBy: PinCreatedBy }
type PinCreatedBy { id: String type: String displayName: String }
(...)`;
Final Result
With all of the above-shown changes in place, the initial setup is complete, which means we’re finally ready to start implementing authentication and authorization checks in our GraphQL resolver function.
Note that the setup is only performed once. After that, the majority of security-related tasks will revolve around implementing proper authentication and authorization checks.