Coder Social home page Coder Social logo

cognito-at-edge's Introduction

Cognito@Edge

Cognito authentication made easy to protect your website with CloudFront and Lambda@Edge.

This Node.js package helps you verify that users making requests to a CloudFront distribution are authenticated using a Cognito user pool. It achieves that by looking at the cookies included in the request and, if the requester is not authenticated, it will redirect then to the user pool's login page.

Architecture

Alternatives

This package allows you to easily parse and verify Cognito cookies in a Lambda@Edge function. If you want full control over the configuration of AWS resources (CloudFront, Cognito, Lambda@Edge...), this is the solution for you.

If you want to try it out easily or to quickstart a new project, we recommend having a look at the cognito-at-edge-federated-ui-sample repository. It allows you to configure and deploy a sample application which uses Cognito@Edge in a few CLI commands.

If you need more configuration options (e.g. bring your own user pool or CloudFront distribution), you may want to use this Serverless Application Repository application (GitHub) which provides a complete Auth@Edge solution. It does not use Cognito@Edge, but provides similar functionality.

Getting started

How To Install

The preferred way to install the AWS cognito-at-edge for Node.js is to use the npm package manager for Node.js. Simply type the following into a terminal window:

npm install cognito-at-edge

Usage

To use the package, you must create a Lambda@Edge function and associate it with the CloudFront distribution's viewer request events.

Within your Lambda@Edge function, you can import and use the Authenticator class as shown here:

const { Authenticator } = require('cognito-at-edge');

const authenticator = new Authenticator({
  // Replace these parameter values with those of your own environment
  region: 'us-east-1', // user pool region
  userPoolId: 'us-east-1_tyo1a1FHH', // user pool ID
  userPoolAppId: '63gcbm2jmskokurt5ku9fhejc6', // user pool app client ID
  userPoolDomain: 'domain.auth.us-east-1.amazoncognito.com', // user pool domain
});

exports.handler = async (request) => authenticator.handle(request);

For an explanation of the interactions between CloudFront, Cognito and Lambda@Edge, we recommend reading this AWS blog article which describe the required architecture to authenticate requests in CloudFront with Cognito.

Reference - Authenticator Class

Authenticator(params)

  • params Object Authenticator parameters:
    • region string Cognito UserPool region (eg: us-east-1)
    • userPoolId string Cognito UserPool ID (eg: us-east-1_tyo1a1FHH)
    • userPoolAppId string Cognito UserPool Application ID (eg: 63gcbm2jmskokurt5ku9fhejc6)
    • userPoolAppSecret string (Optional) Cognito UserPool Application Secret (eg: oh470px2i0uvy4i2ha6sju0vxe4ata9ol3m63ufhs2t8yytwjn7p)
    • userPoolDomain string Cognito UserPool domain (eg: your-domain.auth.us-east-1.amazoncognito.com)
    • cookieExpirationDays number (Optional) Number of day to set cookies expiration date, default to 365 days (eg: 365). It's recommended to set this value to match refreshTokenValidity parameter of the pool client.
    • disableCookieDomain boolean (Optional) Sets domain attribute in cookies, defaults to false (eg: false)
    • httpOnly boolean (Optional) Forbids JavaScript from accessing the cookies, defaults to false (eg: false). Note, if this is set to true, the cookies will not be accessible to Amplify auth if you are using it client side.
    • sameSite Strict | Lax | None (Optional) Allows you to declare if your cookie should be restricted to a first-party or same-site context (eg: SameSite=None).
    • parseAuthPath string (Optional) URI path used as redirect target after successful Cognito authentication (eg: /oauth2/idpresponse), defaults to the web domain root. Needs to be a path that is handled by the library. When using this parameter, you should also provide a value for cookiePath to ensure your cookies are available for the right paths.
    • cookiePath string (Optional) Sets Path attribute in cookies
    • cookieDomain string (Optional) Sets the domain name used for the token cookies
    • cookieSettingsOverrides object (Optional) Cookie settings overrides for different token cookies -- idToken, accessToken and refreshToken
      • idToken CookieSettings (Optional) Setting overrides to use for idToken
        • expirationDays number (Optional) Number of day to set cookies expiration date, default to 365 days (eg: 365). It's recommended to set this value to match refreshTokenValidity parameter of the pool client.
        • path string (Optional) Sets Path attribute in cookies
        • httpOnly boolean (Optional) Forbids JavaScript from accessing the cookies, defaults to false (eg: false). Note, if this is set to true, the cookies will not be accessible to Amplify auth if you are using it client side.
        • sameSite Strict | Lax | None (Optional) Allows you to declare if your cookie should be restricted to a first-party or same-site context (eg: SameSite=None).
      • accessToken CookieSettings (Optional) Setting overrides to use for accessToken
      • refreshToken CookieSettings (Optional) Setting overrides to use for refreshToken
    • logoutConfiguration object (Optional) Enables logout functionality
      • logoutUri string URI path, which when matched with request, logs user out by revoking tokens and clearing cookies
      • logoutRedirectUri string The URI to which the user is redirected to after logging them out
    • csrfProtection object (Optional) Enables CSRF protection
      • nonceSigningSecret string Secret used for signing nonce cookies
    • logLevel string (Optional) Logging level. Default: 'silent'. One of 'fatal', 'error', 'warn', 'info', 'debug', 'trace' or 'silent'.

This is the class constructor.

handle(request)

Use it as your Lambda Handler. It will authenticate each query.

const authenticator = new Authenticator( ... );
exports.handler = async (request) => authenticator.handle(request);

Authentication Gateway Setup

This library can also be used in an authentication gateway setup. If you have a frontend client application that uses AWS Cognito for authentication, it fetches and stores authentication tokens in the browser. Depending on where the tokens are stored in the browser (localStorage, cookies, sessionStorage), they may susceptible to token theft and XSS (Cross-Site Scripting). In order to mitigate this risk, a set of Lambda@Edge handlers can be deployed on a CloudFront distribution which act as an authentication gateway intermediary between the frontend app and Cognito. These handlers will authenticate and fetch tokens on the frontend's behalf and set them as Secure; HttpOnly tokens inside the browser, thereby restricting access to other scripts in the app.

Handlers

  1. handleSignIn (Can be mapped to /signIn in Cloudfront setup): Redirect users to Cognito's authorize endpoint after replacing redirect uri with its own -- for instance, /parseAuth.
  2. handleParseAuth (Can be mapped to /parseAuth): Exchange Cognito's OAuth code for tokens. Store tokens in browser as HttpOnly cookies
  3. handleRefreshToken (Can be mapped to /refreshToken): Refresh idToken and accessToken using refreshToken
  4. handleSignOut (Can be mapped to /signOut): Revoke tokens, clear cookies and redirect user to the URL supplied
// signIn Lambda Handler
const authenticator = new Authenticator( ... );
exports.handler = async (request) => authenticator.handleSignIn(request);

// Similar setup for parseAuth, refreshToken and signOut handlers

Getting Help

The best way to interact with our team is through GitHub. You can open an issue and choose from one of our templates for bug reports, feature requests or question.

Contributing

We welcome community contributions and pull requests. See CONTRIBUTING.md for information on how to set up a development environment and submit code.

License

This project is licensed under the Apache License, Version 2.0, see LICENSE.txt and NOTICE.txt for more information.

cognito-at-edge's People

Contributors

akhudiakov97 avatar amazon-auto avatar borisfba avatar ckifer avatar dependabot[bot] avatar elliotsegler avatar fknittel avatar foxbox-doug avatar hugodby avatar ineale2 avatar jeandek avatar jenirain avatar jwwheeleriv avatar maverick089 avatar ottokruse avatar pedromgarcia avatar peternedap avatar piotrekwitkowski avatar timbakkum avatar tsop14 avatar vikas-reddy avatar yoavya avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar

cognito-at-edge's Issues

Cookie domain attribute should optionally be disabled

What would you like to be added:

Cognito-At-Edge by default sets all cookies with the domain attribute. This attribute really should be optional as it leads to issues where you may not want to share cookies across subdomains and could potentially lead to WAF restrictions if a WAF restricts the size of cookie headers.

Why is this needed:

The Domain attribute specifies which hosts are allowed to receive the cookie. If unspecified, it defaults to the same host that set the cookie, excluding subdomains. If Domain is specified, then subdomains are always included. Therefore, specifying Domain is less restrictive than omitting it. However, it can be helpful when subdomains need to share information about a user.

Consider using HTTP 307 instead of HTP 302

What happened:

Our tokens are being refreshed because of a background POST request that is then invalidly retried as a GET request by the browser because of the semantic ambiguity of 301 and 302.

For example:

  1. accessToken expires
  2. user-agent makes a background fetch request to POST /api/whatever
  3. Lambda@Edge refreshes the tokens using the refresh token
  4. Lambda@Edge responds with a 302 Location /api/whatever and Set-Cookie headers
  5. user-agent retries the background fetch request to /api/whatever as a GET request which fails

What did you expect to have happen:

  1. Should retry the request as a POST request again - a 307 status code rather than 302 would enforce this

How to reproduce this (as precisely and succinctly as possible):

This server behind cognito at edge should be able to reproduce the error (visible in the console/network panel)

const http = require('http')

const server = http.createServer((req, res) => {
  const url = new URL(req.url, 'http://localhost:8000')

  if (url.pathname === '/' && req.method === 'GET') {
    res.writeHead(200, { 'Content-type': 'text/html' })
    res.end(siteHtml)
    return
  } else if (url.pathname === '/api' && req.method === 'POST') {
    res.writeHead(200, { 'content-type': 'text/plain', 'access-control-allow-origin': '*', 'access-control-allow-credentials': '*' })
    res.end('Success')
    return
  }

  res.writeHead(404, { 'content-type': 'text/plain' })
  res.end('Not found')
})

server.listen(8000, () => {
  console.log('listening on http://localhost:8000')
})

const siteHtml = `
<html>
  <head><title>Test Cognito At Edge Background Redirect</title></head>
  <body>
    <script>
      setInterval(() => {
        fetch('/api', { method: 'POST', mode: 'cors', credentials: 'include' })
          .then(async res => { console.log(res.status, await res.text()) })
          .catch(err => { console.error(err) })
      }, 10_000)
    </script>
  </body>
</html>
`

Anything else you think we should know?

Environment:

  • version of cognito-at-edge being used: 1.5.0
  • node version of code base which uses cognito-at-edge: lambda runtime node20.x

Why is the authorization endpoint `/authorize` and not `/oauth2/authorize` ?

How can we help?

Firstly, I'd like to express my gratitude for developing such a useful project!

While perusing the source code, I noticed that this project uses the endpoint https://${this._userPoolDomain}/authorize for Cognito's authorization endpoint. Yet, the official Cognito documentation lists the authorization endpoint as /oauth2/authorize .

const userPoolUrl = `https://${this._userPoolDomain}/authorize?redirect_uri=${oauthRedirectUri}&response_type=code&client_id=${this._userPoolAppId}&state=${state}`;

https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html#get-authorize

Could you shed light on the choice to use /authorize instead of /oauth2/authorize ? While everything seems to be functioning correctly as is, aligning with the official documentation might be beneficial for clarity and potential future-proofing.

If there were specific considerations or historical reasons for this choice during the early implementation phases, I'd be keen to understand.

Should you consider making this adjustment, I'm more than willing to submit a PR and would be glad to have it reviewed at your convenience :)

Amplify can't recognize that the user already signed in (using Cognito@Edge).

Amplify.configure({
Auth: {
// REQUIRED - Amazon Cognito Region
region: 'xxx',
userPoolId: 'xxx',
userPoolWebClientId: 'xxx',
localStorage: {
domain: 'xxx.cloudfront.net',
},
oauth: {
domain: 'xxx.auth.eu-west-1.amazoncognito.com/',
scope: ['openid', 'email'],
redirectSignIn: 'https://xxx.cloudfront.net/',
redirectSignOut: 'https://xxx.cloudfront.net/',
responseType: 'code'
}
},
API: {
"Access-Control-Allow-Origin": "*",
"Content-Type": "text/html; charset=UTF-8",
"X-Content-Type-Options": "nosniff"
}
});

Using this configuration amplify can't recognize that the user already signed in (using Cognito@Edge).
Running Auth.currentAuthenticatedUser() gives user not authenticated error, however if I run this after Auth.SignIn() i get a valid response.

Client-side tokens and Error 502 clarification

I've used the cognito-at-edge package for my current project and I needed some clarity on a couple of things.

Firstly, I noticed that it was working as it should on with my browser on incognito. I'd be redirected from the Cloudfront distrbution link to the Cognito hosted UI I've set up; upon authentication I can view the S3 content via Cloudfront. When I'm not incognito however, it seems that once I authenticate and logout (using the logout endpoint), I can still access the content directly using the Cloudfront distribution link. I assumed that the logout functionality would clear the tokens, so the user would have to re-authenticate after logging out to view the content. Based on my understanding, the tokens on the browser are not getting cleared, which would explain why it's working correctly on incognito. How do I get around this?

Secondly, I started getting a 502 error upon authentication. This happened a day after I set everything up. Here's a screenshot of the error:
Screenshot 2023-11-19 at 11 26 17 AM
Since the cognito-at-edge package handles header manipulation internally, I'm not quite sure how to resolve this.

It's my first project using AWS services, there's definitely some gaps in my knowledge. I'd appreciate some insight on this!

Use as API Gateway Authorizer

Can this package also be used with an API Gateway Authorizer? How does it know to only validate the access token for API Gateway routes and not redirect? How would I configure that?

Would I just set the path for "parseAuthPath" like this in the API Gateway Lambda Authorizer?

const authenticator = new Authenticator({
// Replace these parameter values with those of your own environment
region: 'us-east-1', // user pool region
userPoolId: 'us-east-1_tyo1a1FHH', // user pool ID
userPoolAppId: '63gcbm2jmskokurt5ku9fhejc6', // user pool app client ID
userPoolDomain: 'domain.auth.us-east-1.amazoncognito.com', // user pool domain

// Just add this one?
parseAuthPath: 'api.my-domain.com'
});

FYI, I tried for many days to battle cloudfront LambdaEdge using various blogs about it, including AWS blogs and it was extremely painful. I plugged in this package and it does everything I wanted. I'm not sure why I kept getting various redirects but I did. I'll have to pull apart this code to better understand it all. Thank you so much for creating it.

I did note that the viewer request lambda has a 5 second time limit. Will this package always work within that time frame?

I ask because one of the blogs I was using noted to use the origin request lambda to set the cookie and check auth due to the longer time limits of the origin lambdas.

https://cloudonaut.io/authentication-at-the-edge-with-lambda-edge-cognito/

Constantly re-directing after successful authentication

Hello guys,

Currently i'm hitting with too many redirects issue, even after successful authentication with google.

My setup involves using Cognito, which allows to login via google.

I have configured my Cognito callBackUrls same as my site url https://xyz.test.com.

Has anyone had anything similar know why this could be?

Thank you!

Configurable cookie domains

What would you like to be added:

Add the ability to customize the domain used for token cookies.

// Configuration
const authenticator = new Authenticator({
  region: 'us-east-1', // user pool region
  userPoolId: 'us-east-1_tyo1a1FHH', // user pool ID
  userPoolAppId: '63gcbm2jmskokurt5ku9fhejc6', // user pool app client ID
  userPoolDomain: 'domain.auth.us-east-1.amazoncognito.com', // user pool domain
  // New property
  cookieDomain: 'training.aws.dev',
});

// Place in index.ts where it's going to be used
 return this._fetchTokensFromCode(redirectURI, requestParams.code)
          .then(tokens => this._getRedirectResponse(
            tokens,
            // Updated param
            this._cookieDomain || cfDomain,
            requestParams.state as string
          ));

Why is this needed:

We are planning to use this library for our new authentication gateway application. As opposed to the intended use case of this library, which is to use the handle method to gate static S3 files behind an authentication gate, we are planning to use the individual handler methods directly in our app. This auth gateway app will be a set of Lambda@Edge handlers that work as an intermediary between React frontend clients and AWS Cognito to do

  1. authentication duties,
  2. exchange code for tokens, and
  3. sending tokens as HttpOnly cookies, which clients can use to communicate with some Amazon internal API's

Handlers

  1. /signIn: Mapped to the existing method _getRedirectToCognitoUserPoolResponse
  2. /parseAuth: Mapped to existing method _fetchTokensFromCode
  3. /refreshToken: Mapped to existing method _fetchTokensFromRefreshToken

In our Cloudfront distribution setup, we'd do something like

// signIn Lambda@Edge handler
const authenticator = new Authenticator({...})
exports.handler = async (request) => authenticator._getRedirectToCognitoUserPoolResponse(request, redirectUri)

// parseAuth Lambda@Edge handler
const authenticator = new Authenticator({...})
exports.handler = async (request) => authenticator._fetchTokensFromCode(redirectUri, code)

Slack or email me on [email protected] for additional details

Type mismatch of the status code

What happened:

The type of the response returned by the Authenticator doesn't match the CloudFrontRequestHandler response type from the aws-lambda package

What did you expect to have happen:

The types match

How to reproduce this (as precisely and succinctly as possible):

This should work
import { SSMClient, GetParameterCommand } from "@aws-sdk/client-ssm";
import { CloudFrontRequestHandler } from "aws-lambda";
import { Authenticator } from "cognito-at-edge";

const ssm = new SSMClient({ region: process.env.CONFIG_PARAMETER_REGION });
const authenticatorPromise = ssm
  .send(new GetParameterCommand({ Name: process.env.CONFIG_PARAMETER_NAME }))
  .then(config => new Authenticator({ ...JSON.parse(config.Parameter!.Value!), logLevel: 'info' }));

export const handler: CloudFrontRequestHandler = async event => {
  try {
    const authenticator = await authenticatorPromise;
    return await authenticator.handle(event);
  } catch (error) {
    console.error(error);
    return { body: '401 Unauthorized', status: '401' };
  }
};

Anything else you think we should know?

PR will be provided

Environment:

  • version of cognito-at-edge being used: 1.2.1
  • node version of code base which uses cognito-at-edge: n/a

Refresh Token Support

What would you like to be added:

Unless I am missing something it looks like the refresh token is saved as a cookie but not used to refresh the access token after it expires. Using defaults, this means re-logging in once every 60 minutes.

Why is this needed:

So that authorization can survive beyond the expiration limit of the access token.

Using cognito-at-edge as a layer on a lambda@edge function

How can we help?

I plan to use cognito-at-edge as a lambda layer in a lambda@edge function but when I try to bind it to my CloudFront triggers I get the following error:

The function cannot have a layer. Function: arn:aws:lambda:us-east-1:XXXXXXXXXXX:function:lambda-basic-auth:15

What am I missing? Is this supposed to run on a lambda@edge function?

cognito-at-edge is not able to redirect to IdP

I am using federated cognito, where the user is redirected to SAML IdP. Cognito-at-edge does not expose the "scope" and "idp_identifier". this is based on the documentation from the aws site https://docs.aws.amazon.com/cognito/latest/developerguide/authorization-endpoint.html

What happened: When using this in lambda, user is not redirected to the IdP

What did you expect to have happen: User must be redirected to the IdP

How to reproduce this (as precisely and succinctly as possible): Create a new Cognito Pool and use a third party identity provider (like SAML). Set the properties from the pool

const authenticator = new Authenticator({
// Replace these parameter values with those of your own environment
region: 'us-west-2', // user pool region
userPoolId: 'ddffff', // user pool ID
userPoolAppId: 'xxxxx', // user pool app client ID
userPoolDomain: '.auth.us-west-2.amazoncognito.com', // user pool domain
});

Anything else you think we should know?

Environment:

  • version of cognito-at-edge being used: Latest version

  • node version of code base which uses cognito-at-edge:16.x

  • other:

Tokens exposed to users via cookies: security question

Hi, if I understand https://aws.amazon.com/blogs/mobile/understanding-amazon-cognito-user-pool-oauth-2-0-grants/ correctly, in the preferred (from a security perspective) "Authorization code grant" flow, the actual tokens (id, access, refresh) are never exposed to the user.

However, in _getRedirectResponse in cognito-at-edge, those tokens are set as cookies, ending up in the user's browser. Does this compromise the security of the flow (albeit slightly), i.e. does using cognito-at-edge implicitly change the "Authorization code grant" flow into something that's equivalent to the less-secure "Implicit grant" flow?

Federated cognito-at-edge

I have a lamnda@edge function that is configured to call cognito user pool with relevant parameters. Cognito is using SAML provider. When cloudfront is configured to use the lambda@edge (viewer request) the application is giving a CORS error. Any help is appreciated!

Is there a way to set identity_provider and scope with in the code?

Sample CDK and CloudFormation templates

What would you like to be added:

Sample CDK and CloudFormation templates to show how to integrate the AWS services together with the package.

Why is this needed:

The desired architecture may be intimidating to users who have not done it before.

Support logging out

What would you like to be added:

I would like the lambda to be capable of catching a predefined logout uri which then triggers a logout.

From my understanding this would require the lambda to do the following:

  • Remove all ${this._cookieBase}.* cookies.
  • Redirect to https://${this._userPoolDomain}/logout?redirect_uri=${redirectURI}&response_type=code&client_id=${this._userPoolAppId} to clear any Cognito cookies. Cognito then redirects to its own /login page.

I would propose adding an optional parameter to the Authenticator such as logoutUri. If this is set, for example to /logout and the user hits this uri, we trigger the above behaviour.
If this parameter is not set, the lambda works exactly as it does currently.

Why is this needed:

Right now this library has greatly simplified our authentication flow which is brilliant and it has been very simple to set up. We intend on using it to protect many internal apps, but logging out is not so simple as I need to implement this logic across each app my lambda protects.

I will be happy to open a PR with all of the above functionality if a contributor can confirm this sounds reasonable. If you have any comments on the proposed implementation please let me know. ๐Ÿ™‚

Cognito client Id with secret

It seems like this package doesn't support a cognito user pool client with secret.
according to cognito documentation when the client has a secret it must be passed through the authentication header

Authorization

If the client was issued a secret, the client must pass its client_id and client_secret in the authorization header through Basic HTTP authorization. The secret is Basic
Base64Encode(client_id:client_secret). 

currently I am able to use this package only with cognito user pool clients that don't have a secret id
it would be nice to be able to use both options :)

TypeError - Cannot read properties of undefined (reading 'querystring')

I'm facing this error while testing the Lambda@edge function with a CF distribution:

=====================================================================
From the Lambda logs -

Response
{
"errorType": "TypeError",
"errorMessage": "Cannot read properties of undefined (reading 'querystring')",
"trace": [
"TypeError: Cannot read properties of undefined (reading 'querystring')",
" at Authenticator.handle (/var/task/node_modules/cognito-at-edge/dist/index.js:511:64)",
" at Runtime.exports.handler (/var/task/index.js:11:52)",
" at Runtime.handleOnceNonStreaming (file:///var/runtime/index.mjs:1173:29)"
]
}

503 ERROR
The request could not be satisfied.
The Lambda function associated with the CloudFront distribution is invalid or doesn't have the required permissions. We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.

Lambda function:

const { Authenticator } = require('cognito-at-edge');

const authenticator = new Authenticator({
// Replace these parameter values with those of your own environment
region: 'us-east-1', // user pool region
userPoolId: 'xxx', // user pool ID
userPoolAppId: 'xxxx', // user pool app client ID
userPoolDomain: 'xxxx', // user pool domain
});

exports.handler = async (request) => authenticator.handle(request);
Can someone assist in addressing this please.

How to bundle 'cognito-at-edge' with Lambda@Edge function?

Hi, I am using Typescript CDK to deploy a Lambda@Edge function that uses cognito-at-edge.
When the Lambda runs, it returns a 503 error with: Error: Cannot find module 'cognito-at-edge'

I followed the instructions in the README exactly:

const { Authenticator } = require('cognito-at-edge');

const authenticator = new Authenticator({
        ... my details ...
});

exports.handler = async (request) => authenticator.handle(request);

How is this package meant to be bundled with Lambda@Edge? I can't find any instructions in this README, or elsewhere.

Incorrect Regex of idToken With Subdomains

What happened:

Upon visiting beta.myurl.com cognito-at-edge uses the cookie from myurl.com as opposed to beta.myurl.com. They are both sent, but according to this Regex it should only parse the idToken from that of the initialized "_userPoolAppId". From my tests this doesn't seem to be the case.

This causes an infinite loop and eventual 503 on Viewer Request.

What did you expect to have happen:

The Regex correctly parses cookies even if there are multiple on the same domain of the form userPoolAppId}\..+?\.idToken=(.*?)(?:;|$)

How to reproduce this (as precisely and succinctly as possible):

Deploy cognito-at-edge to a lambda@edge function with cloudfront distro served at url of the form:
beta.myurl.com
Deploy the same setup to myurl.com making sure cookies are set from both.

Anything else you think we should know?

Environment:

  • version of cognito-at-edge being used: latest
  • node version of code base which uses cognito-at-edge: 16
  • other: works on Firefox, does not work on Chrome

Here is a contrived example with the structure that my cookies get set and sent to Lambda@edge with:

Edit hungry-lalande-loeuce

As you can see the regex is incorrect. The order of the cookies shouldn't matter.

Regex test https://regex101.com/r/GdVCo6/1

Working Regex (needs tested more): https://regex101.com/r/vQSdRa/1
Edit: this working regex breaks the case of a username with a . character

Make handler methods public

What would you like to be added:

Rename handler methods to make them public. Method names starting with underscore _ are considered private by convention.

Even though these methods are accessible from outside now(Javascript language doesn't understand this _ prefix convention), we think it makes more sense for them to be explicitly public for our use case.

_fetchTokensFromRefreshToken -> fetchTokensFromRefreshToken
_fetchTokensFromCode -> fetchTokensFromCode
_getRedirectToCognitoUserPoolResponse -> getRedirectToCognitoUserPoolResponse

Why is this needed:

We are planning to use this library for our new authentication gateway application. As opposed to the intended use case of this library, which is to use the handle method to place static S3 files behind an authentication gate, we are planning to use the individual handler methods directly in our app. This auth gateway app will be a set of Lambda@Edge handlers that work as an intermediary between React frontend clients and AWS Cognito to do

  1. authentication duties,
  2. exchange code for tokens, and
  3. sending tokens as HttpOnly cookies, which clients can use to communicate with some Amazon internal API's

Handlers

  1. /signIn: Mapped to the existing method _getRedirectToCognitoUserPoolResponse
  2. /parseAuth: Mapped to existing method _fetchTokensFromCode
  3. /refreshToken: Mapped to existing method _fetchTokensFromRefreshToken

In our Cloudfront distribution setup, we'd do something like this (notice no underscore prefixes)

// signIn Lambda@Edge handler
const authenticator = new Authenticator({...})
exports.handler = async (request) => authenticator.getRedirectToCognitoUserPoolResponse(request, redirectUri)

// parseAuth Lambda@Edge handler
const authenticator = new Authenticator({...})
exports.handler = async (request) => authenticator.fetchTokensFromCode(redirectUri, code)

Slack or email me on [email protected] for additional details

Support SameSite directive

What would you like to be added:

As a user with secure websites I would like control over all common cookie directives. Recently HttpOnly was added to the library - this would be to support SameSite as well.

Why is this needed:

  • Provide flexible configuration to cognito-at-edge
  • improve security config options

Unhandled error if cookies disabled

What happened:

User attempted to log in with ad block. Error message in log is as follows:

User isn't authenticated: TypeError: Cannot read properties of undefined (reading 'flatMap')

What did you expect to have happen:

Error message in log indicating that no cookies were found rather than null reference exception.

How to reproduce this (as precisely and succinctly as possible):

Attempt to access protected content using an ad blocker that prevents cookies from being sent.

Anything else you think we should know?

N/A

Environment:

  • version of cognito-at-edge being used: 1.3.1
  • node version of code base which uses cognito-at-edge: 16.x
  • other:

Which headers do we need to pass to make this work?

How can we help?

I want to use this on a ReactNative app to protect my HLS streams, but I can't pass cookies or other headers in there.

So I would like to know exactly what headers I should include in the request to make this work?

Add `esModuleInterop = true` in tsconfig.json to enable bundle with Vite

What would you like to be added:

in tsconfig.json:

{
  "compilerOptions": {
+   "esModuleInterop": true
  },
  ...
}

Why is this needed:

The cognito-at-edge package is designed to be deployed to Lambda@Edge.
Lambda@Edge does not support Lambda layers, and the function size limit is up to 1 MB.
Consequently, some form of bundlers is required to deploy cognito-at-edge.
(Running npm i cognito-at-edge resulted in a node_modules directory size of >1MB)

Unlike the sample repo, I adopted terraform for IaC and Vite as a bundler.
The build and deploy processes worked fine, but I encountered the following runtime error:

TypeError: (0 , $.default) is not a function
    at new D (/.../dist/node_modules/cognito-at-edge/dist/index.js:1:1995)
    at t (/.../dist/index.js:1:499)

Node.js v18.18.0

By forking this repository, setting esModuleInterop = true, and running npm run build, the error was resolved.

Functional tests

What would you like to be added:

Functional tests that can be executed easily to confirm the behavior of the package is correct.

Why is this needed:

We need to ensure that new versions of the npm package work before publishing them.

CORS error while redirecting

I am getting a CORS error when redirecting from lambda to aws cognito. I have a viewer request attached to the cloudfront.

What did you expect to have happen: request to be redirected to cognito

Environment:

  • version of cognito-at-edge being used: latest version

  • node version of code base which uses cognito-at-edge: 16.x

  • other:

Redirect loop after refresh fetch caused by cookie path

What would you like to be added:

I'm writing the issue here with the hope it'll help someone else stuck in the same situation. Maybe a documentation update could help others. Or maybe a change to the cookie logic.

Why is this needed:

I'm seeing my application go into a redirect loop when the id token expires and we fetch tokens from refreshToken. If this happens in a deep link the browser stores the cookie with a path. The easy fix was to change my code to use cookiePath: '/'.

Now I have two idtoken cookies one with path=/ and another with path=/deep/link/path. Both get submitted by the browser (see https://stackoverflow.com/a/24214538/109102). This loop uses the last cookie as the idtoken:

for (const {name, value} of cookies){
if (name.startsWith(tokenCookieNamePrefix) && name.endsWith(idTokenCookieNamePostfix)) {
tokens.idToken = value;
}
if (name.startsWith(tokenCookieNamePrefix) && name.endsWith(refreshTokenCookieNamePostfix)) {
tokens.refreshToken = value;

Maybe this is worth a mention in the README. Or maybe rework the cookie logic to check every idtoken cookie until there is a success? Hmm, this does sound a bit clumsy.

login redirect with custom state does not work properly

When I access a web page fronted with congnito auth. I'm getting redirected to Cognito login page without any issues. But once I supply the username and password the redirect URL with auth code is resulting in an error (net::ERR_RESPONSE_HEADERS_MULTIPLE_LOCATION) in chrome browser.

Original request to the web page: (faked domain names)
https://example.com/movie/mac_11581288?session_token=test&state=ashhftg56HSJxcv

I get redirected to Cognito Login page with state param encoded as below
https://example.auth.ap-southeast-2.amazoncognito.com/login?redirect_uri=https://example.com&response_type=code&client_id=1oer36fu8hkpfsnlcdmc9463jg&state=/movie/mac_11581288%3Fsession_token%3Dtest%26state%3Dashhftg56HSJxcv

Once i supply the username,password and press login, the resulting redirection is marked as error by chrome (net::ERR_RESPONSE_HEADERS_MULTIPLE_LOCATION)

https://example.com/?code=07583bb7-c70f-4ae4-8bb6-4d4ba8fe1a4f&state=/movie/mac_11581288?session_token=test&state=ashhftg56HSJxcv

There are two state params which is causing the confusion and I think if this is properly URL encoded, chrome will then be able to redirect without any errors?

image

Lambda with cognito-at-edge too big for CloudFront

What happened:

Error: updating CloudFront Distribution: InvalidLambdaFunctionAssociation: The function code size is larger than the maximum allowed size for functions that are triggered by a CloudFront event: 1423536 Max allowed: 1048576

What did you expect to have happen:

No issues with cognito-at-edge version 1.5.0

How to reproduce this (as precisely and succinctly as possible):

package.json

{
  "name": "lambda",
  "dependencies": {
    "cognito-at-edge": "1.5.1"
  }
}

npm install

Then zip files and update lambda.

Environment:

cognito-at-edge version
1.5.1
node --version
v20.11.0
npm --version
10.2.4

IP allowlist functionality

What would you like to be added:

Functionality to configure an allow list to bypass Cognito authentication

Why is this needed:

It's a useful feature :)

See #16

Refresh functionality should not redirect on SPA applications

What would you like to be added:

I am writing an SPA application and it is my understanding that the refresh token functionality should not redirect and should instead return new cookies with a 200 response or an error.

Why is this needed:

SPA applications usually would like to handle refreshing tokens without redirection and, in case of failure display a message before redirecting.

Does this make sense, I am willing to contribute.

Thanks!

Add Cache-Control headers to redirect responses

What would you like to be added:

  • Set cache-control and pragma headers to the redirect responses

Why is this needed:

  • When the Authenticator redirects to Cognito or with the set-cookies header, some browsers (IE11 in specific) cache the 302 response as no cache-control headers are set.
  • When subsequent requests come (with valid cookies), IE11 will retrieve the cached 302 response and get stuck in a redirect loop

logoutUri does not log out user completely?

What happened:

I'm facing an "auth loop" while using logount endpoint with redirect to the main page. What is a proper logoutConfiguration configuration?

What did you expect to have happen:

logoutUri should log out the user completely.

How to reproduce this (as precisely and succinctly as possible):

Cognito domain: mypool.auth.us-east-1.amazoncognito.com
Protected URL: https://privatesite.com

My cognito-at-edge configuration:

const authenticator = new Authenticator({
  logLevel: 'debug',
  region: 'us-east-1', // user pool region
  userPoolId: 'us-east-1_', // user pool ID
  userPoolAppId: 'appid', // user pool app client ID
  userPoolDomain: 'mypool.auth.us-east-1.amazoncognito.com', // user pool domain
  cookiePath: '/',
  logoutConfiguration: {
    logoutUri: '/logout',
    logoutRedirectUri: 'https://privatesite.com'
  }
});

Current requests flow:

Standard login flow, everything is OK here:

  1. GET https://privatesite.com = 302 ->
  2. GET https://mypool.auth.us-east-1.amazoncognito.com/authorize?... = 302 ->
  3. POST https://mypool.auth.us-east-1.amazoncognito.com/login?... = 302 ->
  4. GET https://privatesite.com?code=... = (Set-Cookie)

Now trying to logout:

  1. GET https://privatesite.com/logout (clears the cookies with Set-Cookie empty response) = 302 ->
  2. GET https://privatesite.com = 302 ->
  3. GET https://mypool.auth.us-east-1.amazoncognito.com/authorize?... (cognito domain still remembers the user!) = 302 ->
  4. GET https://privatesite.com?code=... (authorized again)

What am I missing here? The cognito domain (mypool.auth.us-east-1.amazoncognito.com) stores it own state about user in cookies and restores the auth.

The only way I found to make it work is setting logoutRedirectUri to "https://mypool.auth.us-east-1.amazoncognito.com/logout?..." to force cognito domain logout.

Is it supposed way to do this?

Anything else you think we should know?

Environment:

  • version of cognito-at-edge being used: 1.5.0

  • node version of code base which uses cognito-at-edge:

  • other:

Too many redirects issue after successful authentication

Hello Team,

Currently i'm hitting with too many redirects issue, even after successful authentication and setting cookies.

Somehow consecutive request does not have idToken, so it is re-directing multiple times and erroring out.

I have configured my Cognito callBackUrls same as my site url https://xyz.test.com.

Any help here please.

Thanks
Karthik

Multiple OAuth scopes are not supported

What happened:

error=redirect_mismatch by hosted UI

What did you expect to have happen:

to get to the login page

How to reproduce this (as precisely and succinctly as possible):

simply add multiple scopes to the hosted ui and it won't work (in my case at least)

Environment:

  • version of cognito-at-edge being used: latest
  • node version of code base which uses cognito-at-edge: latest

Helper function to simply check if the user is logged in

What would you like to be added:

Trying to grok the code and think that it's pretty awesome that you can hand off the request to this library and it takes care of authenticating the user. However, we set up Cloudfront to be on a wildcard domain, and we have logic in our Viewer Request function to inspect the Host header and store that in another header for our Origin Request function. What I would like is a simple function to determine if the user is authenticated or not. With that, I would know when to trigger our custom logic.

Although, supporting multiple domains would be interesting. Maybe that's a separate request though.

Why is this needed:

It'll just give us some options when integrating this package with an existing viewer request function. The only other way would be to inspect the output of the handle function.

Mitigate CSRF attacks

What would you like to be added:

  1. Mitigate CSRF attacks by using the state, nonce, signed nonce and pkce cookies
  2. Encode and store redirect uri in state to be used later to send users to where they wanted to go, after oauth token exchange is done. (See next section for more details)

https://auth0.com/docs/secure/attack-protection/state-parameters

https://github.com/aws-samples/cloudfront-authorization-at-edge/blob/master/src/lambda-edge/check-auth/index.ts#L71-L83

// _getRedirectToCognitoUserPoolResponse would generate and store nonce, signed nonce, pkce and state in the browser cookie
const nonce = generateNonce();
  const state = {
    nonce,
    nonceHmac: common.sign(
      nonce,
      CONFIG.nonceSigningSecret,
      CONFIG.nonceLength
    ),
    ...generatePkceVerifier(),
  };

https://github.com/aws-samples/cloudfront-authorization-at-edge/blob/017089d8aea9239f22f4bd8bbe8a7b607eb1fe5b/src/lambda-edge/parse-auth/index.ts#L177

// _fetchTokensFromCode would verify that `state` returned by AWS Cognito is related to the one we set in the previous step
function validateQueryStringAndCookies(props: {
  querystring: string;
  cookies: ReturnType<typeof common.extractAndParseCookies>;
}) {
  // Check if Cognito threw an Error. Cognito puts the error in the query string
  const {
    code,
    state,
    error: cognitoError,
    error_description,
  } = parseQueryString(props.querystring);
  if (cognitoError) {
    throw new Error(`[Cognito] ${cognitoError}: ${error_description}`);
  }

  // The querystring needs to have an authorization code and state
  if (
    !code ||
    !state ||
    typeof code !== "string" ||
    typeof state !== "string"
  ) {
    throw new Error(
      [
        'Invalid query string. Your query string does not include parameters "state" and "code".',
        "This can happen if your authentication attempt did not originate from this site.",
      ].join(" ")
    );
  }

Why is this needed:

We are planning to use this library for our new authentication gateway application. As opposed to the intended use case of this library, which is to use the handle method to put static S3 files behind an authentication gate, we are planning to use the individual handler methods directly in our application. Our auth gateway app will be a set of Lambda@Edge handlers that work as an intermediary between React frontend clients and AWS Cognito to do

  1. authentication duties,
  2. exchange code for tokens, and
  3. sending tokens as HttpOnly cookies, which clients can use to communicate with some Amazon internal API's

Handlers

  1. /signIn: Mapped to the existing method _getRedirectToCognitoUserPoolResponse
  2. /parseAuth: Mapped to existing method _fetchTokensFromCode
  3. /refreshToken: Mapped to existing method _fetchTokensFromRefreshToken

In our Cloudfront distribution setup, we'd do something like this

// signIn Lambda@Edge handler
const authenticator = new Authenticator({...})
exports.handler = async (request) => authenticator._getRedirectToCognitoUserPoolResponse(request, redirectUri)

// parseAuth Lambda@Edge handler
const authenticator = new Authenticator({...})
exports.handler = async (request) => authenticator._fetchTokensFromCode(redirectUri, code)

Slack or email me on [email protected] for additional details

503 ERROR due lambda timeout after tokens are fetched

What happened:

My cloudfront dist redirects me to the cognito login UI and I successfully auth. After that, I get directed to the cloudfront 503 error page rather than my s3 static content:

503 ERROR
The request could not be satisfied.
The Lambda function associated with the CloudFront distribution is invalid or doesn't have the required permissions. We can't connect to the server for this app or website at this time. There might be too much traffic or a configuration error. Try again later, or contact the app or website owner.
If you provide content to customers through CloudFront, you can find steps to troubleshoot and help prevent this error by reviewing the CloudFront documentation.

Generated by cloudfront (CloudFront)
Request ID: BO_FjHzTRlnA-HWDVRWznjZbcpOBBMjEAT2mwvZUxje6BITPn2bbJg==

The logs in my lambda@edge appear to show the initial invocation successfully recirecting the user to the UI page:

logs
{
    "level": 20,
    "time": 1705676579380,
    "msg": "Handling Lambda@Edge event",
    "event": {
        "Records": [ ....big blob redacted here ]
    }
}
{
    "level": 20,
    "time": 1705676579400,
    "msg": "Cookies weren't present in the request"
}
{
    "level": 20,
    "time": 1705676579400,
    "msg": "User isn't authenticated: Error: Cookies weren't present in the request"
}
{
    "level": 20,
    "time": 1705676579400,
    "msg": "Redirecting user to Cognito User Pool URL https://***********"
}

Then after entering my login credentials, after 5s I am presented with the 503 screen in the browser. The lambda appears to time out after fetching the tokens.

logs
{
    "level": 20,
    "time": 1705676947082,
    "msg": "Handling Lambda@Edge event",
    "event": {
        "Records": [...redacted blob]
    }
}
{
    "level": 20,
    "time": 1705676947102,
    "msg": "Cookies weren't present in the request"
}
{
    "level": 20,
    "time": 1705676947102,
    "msg": "User isn't authenticated: Error: Cookies weren't present in the request"
}
{
    "level": 20,
    "time": 1705676947102,
    "msg": "Fetching tokens from grant code...",
    "request": {
        "url": "https://****.auth.****.amazoncognito.com/oauth2/token",
        "method": "POST",
        "headers": {
            "Content-Type": "application/x-www-form-urlencoded"
        },
        "data": "client_id=******&code=******&grant_type=authorization_code&redirect_uri=******.cloudfront.net"
    },
    "code": "******"
}
{
    "level": 20,
    "time": 1705676949484,
    "msg": "Fetched tokens",
    "tokens": {
        "id_token": "*****",
        "access_token": "****",
        "refresh_token": "****",
        "expires_in": 28800,
        "token_type": "Bearer"
    }
}

Double Decoding of the QueryParams

What happened:

The code double decodes the query params resulting in malformed queryParam or altered queryParam.

What did you expect to have happen:

The queryParam to be return as such it was called.

How to reproduce this (as precisely and succinctly as possible):

Simply use the library and call with encoded queryParam and see the double decoding happening.

Anything else you think we should know?

The fix is in https://github.com/awslabs/cognito-at-edge/blob/main/index.js#L180
Removing the extra decoding might solve the issue.

Environment:

  • version of cognito-at-edge being used: 1.1.0
  • node version of code base which uses cognito-at-edge: 14x

Switch to typescript

What would you like to be added:

I am filing this issue to gauge the interest of the maintainers of this project in switching to Typescript. I have found this project useful in my own work and think it would be a big QoL improvement if it were type annotated.

I also wanted to gauge how responsive the maintainers of this project would be to PRs to add functionality. For my use case I'd love to add a callback to the handle function which allows me to change the return logic on the basis of a user's decoded token. For example, to return a 403 error if a user doesn't have a Cognito group granting them access.

Why is this needed:

  • Given how long it takes to redeploy Lambda@Edge functions, catching silly errors at compile time would improve DX a lot

purpose of disableCookieDomain - as will default to origin domain?

How can we help?

185       domain: this._disableCookieDomain ? undefined : domain,

this will force the browser to set the domain to the origin domain, in effect it's a no-op?

setting the domain to dot domain, ".example.com", will include all subdomains and might be the behaviour most users expect from this setting?

I've forked to add a setting to allow all subdomains and just wondering about the context around disableCookieDomain as I think this setting can be safely removed.

Maintaining Query String

I have configured cognito-at-edge to work with my Cognito user pool and Lamba@edge authorizer. I am having trouble with a query string being lost when the refresh token is used to generate a new id/access token.

Works:

  1. Go to www.acme.com/mypage.html?name=bob
  2. Lambda@edge calls cognito-at-edge
  3. Either user has to enter Cognito credentials or the user already had a valid id/access token
  4. User is direct to www.acme.com/mypage.html?name=bob

Problem:

  1. Go to www.acme.com/mypage.html?name=bob
  2. Lambda@edge calls cognito-at-edge
  3. User has an expired ACCESS token but a valid REFRESH token
  4. Cognito-at-edge gets new access token
  5. User is directed to www.acme.com/mypage.html (the query string "?name=bob" is not present)

Wondering if the issue of the dropped query string is an issue with my code, cognito-at-edge, cognito, or otherwise.

Thank you for your assistance in advance.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.