AWS Cognito: client and server authentication
I have struggled for quite sometimes on setting up Cognito and how to put it in your web application as an authentication service. As the documentation from AWS is not quite clear on setting up, how different pieces fit together, this post is how I done it.
The setup
Client side
1. Create user pool
User pool is like your user’s table. It will contain user’s information such as email, username, password, etc. So the first step with Cognito is to create a user pool.
AWS has a step-by-step guide on setting up a user pool at Tutorial: create user pool
2. Create an app client
An app client will specify how your app will authenticate with your user’s pool. We can choose a client → cogito authentication flow, client → server → cognito flow, or even a custom flow.
For more detail, refer to this guide: Configuring a user pool app client
In this post, I’ve choosen client → cognito flow(no need to implement a server).
3. Create hosted UI
Hosted UI is AWS’s provided login page, if we don’t want to create a login page, we can use hosted UI. We can make some simple modification to the look of hosted UI by provide our own css file. Here is what we can custom the hosted UI: Customizing the built-in sign-in and sign-up webpages
4. Client’s code
4.1. Redirect to hosted UI
After creating a hosted UI, Cognito will provide an endpoint for us to redirect users. They will go to this endpoint, login(or register) and after a success login will be redirected back to our application. Our app will receive a code
parameter that we’ll use to exchange for access token.
4.2. Callback endpoint
After user has successfully login, they will be redirected back to callback endpoint with code
parameter.
We will use this code to exchange for access_token.
var myHeaders = new Headers();
myHeaders.append("Content-Type", "application/x-www-form-urlencoded");
var urlencoded = new URLSearchParams();
urlencoded.append("grant_type", "authorization_code");
urlencoded.append("client_id", process.env.AWS_COGNITO_APP_CLIENT_ID);
urlencoded.append("code", code);
urlencoded.append("redirect_uri", "https://localhost:1234/callback");
var requestOptions = { method: 'POST', headers: myHeaders, body: urlencoded, redirect: 'follow' };
const response = await fetch("https://sakura-vinh.auth.ap-northeast-1.amazoncognito.com/oauth2/token", requestOptions)
const body = await response.json();
5. Server side
Now let’s say we have an API server and we need to protect it using Cogito user’s pool. I’ll use express and cognito-express package.
const cognitoExpress = new CognitoExpress({
region: "ap-northeast-1",
cognitoUserPoolId: process.env.AWS_COGNITO_USERPOOL_ID,
tokenUse: "access",
tokenExpiration: 3_600_000
});
API with authentication
We’ll add a middleware to extract access_token from Authorization header and check it against our user’s pool on Cognito
authenticatedRoute = express.Router();
app.use("/api", authenticatedRoute);
authenticatedRoute.use(function(req, res, next) {
let accessTokenFromClient = extractToken(req.headers.authorization);
if (!accessTokenFromClient) return res.status(401).send("Access Token missing from header");
cognitoExpress.validate(accessTokenFromClient, function(err, response) {
if (err) return res.status(401).send(err);
res.locals.user = response;
next();
});
});
const extractToken = (authHeader) => {
if (authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7, authHeader.length);
return token;
} else {
throw Error("invalid header");
}
}
after that we can put an API endpoint behind the middleware
authenticatedRoute.get("/hello", function(_req, res, _next) {
res.send(`Hi ${res.locals.user.username}, your API call is authenticated!`);
});
6. Cognito’s identity pool
One interesting thing to do with Cognito is we can let our user have a temporary access to AWS so that for example an authenticated user can call an Lambda on front-end.
To do this, we need to create an identity pool(also known as Federated Identities). With identity pool, we can use Cognito’s user pool as a identity source or we can use other OAuth services such as Facebook, SAML, etc.
I’ll just use my user pool as a identity source.
- Create an identity pool
- Create a role with approriate permission
- Client code
From 4.2 above we can get access_token, id_token and refresh_token from Cognito. We will use this id_token to exchange for Identity ID using GetID command
import { CognitoIdentityClient, GetIdCommand, GetCredentialsForIdentityCommand } from "@aws-sdk/client-cognito-identity";
const client = new CognitoIdentityClient({ region: process.env.AWS_DEFAULT_REGION });
const logins = {
[`cognito-idp.ap-northeast-1.amazonaws.com/${process.env.AWS_COGNITO_USERPOOL_ID}`]: info.id_token
}
const getIdParams = {
IdentityPoolId: process.env.AWS_COGNITO_IDENTITY_POOL_ID,
Logins: logins
}
const command = new GetIdCommand(getIdParams);
const getIdResponse = await client.send(command);
Then using Identity ID to exchange for AWS’s access_token by calling GetCredentialsForIdentity
const getCredsParams = {
IdentityId: getIdResponse.IdentityId,
Logins: logins
}
const credCommand = new GetCredentialsForIdentityCommand(getCredsParams);
const credsResp = await client.send(credCommand);
return credsResp.Credentials;
We can use this Credentials to call other AWS service, such as
const getSTSInfo = async (credential) => {
const { AccessKeyId, Expiration, SecretKey, SessionToken } = credential;
const credentials = {
accessKeyId: AccessKeyId,
expiration: Expiration,
secretAccessKey: SecretKey,
sessionToken: SessionToken
}
const client = new STSClient({ credentials, region: process.env.AWS_DEFAULT_REGION });
const command = new GetCallerIdentityCommand({});
try {
const response = await client.send(command);
console.log(response);
} catch (error) {
console.log(error);
}
}