Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Helper function to simply check if the user is logged in #21

Open
keokilee opened this issue Dec 18, 2021 · 4 comments
Open

Helper function to simply check if the user is logged in #21

keokilee opened this issue Dec 18, 2021 · 4 comments
Labels
feature-request New feature or request

Comments

@keokilee
Copy link

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.

@jeandek
Copy link
Contributor

jeandek commented Jan 3, 2022

Hi @keokilee,

Thanks for raising this issue and submitting a PR. I understand that you want to apply some custom logic to the request after authentication with Cognito is taken care of.

In #20, the issue creator says:

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.

I'm wondering if this approach would work for your use case?

Regards,

@jeandek jeandek added the feature-request New feature or request label Jan 3, 2022
@keokilee
Copy link
Author

keokilee commented Jan 3, 2022

Yeah, I think that'd work. Just needed something simple to check if the user is authenticated, but being able to hook into the authentication response and add custom handling would be great as well.

@67726e
Copy link

67726e commented Jun 12, 2023

Not sure if this would make sense for a pull-request as it would constitute as breaking change to the API for anyone already doing enrichment of the authenticator.handle return.

I've internally forked the code at work because we have a handful of changes and can't wait around for the Pull Request / Release cycle on the library. What I've done is the following:

  1. Add Enriched Return Type(s) with tokens: Tokens and isAuthenticated: boolean
  2. Add Access Token to the "Get Tokens from Cookies" Routine
  3. Add Access Token Verification, Enriched Return Types in the handle method.

Add Type(s) for Authenticator.handle

export type AuthenticatedResponse = { result: CloudFrontRequest, tokens: Tokens, isAuthenticated: true, };
export type CloudFrontRedirectionResponse = { result: CloudFrontRequestResult, isAuthenticated: false, };
export type CognitoRedirectionResponse = { result: CloudFrontRequestResult, isAuthenticated: false, };
export type HandleResponse =
  AuthenticatedResponse | CloudFrontRedirectionResponse | CognitoRedirectionResponse;

Update the Authenticator._getTokensFromCookie, Add Access Token

_getTokensFromCookie(cookieHeaders: Array<{ key?: string | undefined, value: string }> | undefined): Tokens {
  if (!cookieHeaders) {
    this._logger.debug("Cookies weren't present in the request");
    throw new Error("Cookies weren't present in the request");
  }
  
  this._logger.debug({ msg: 'Extracting authentication token from request cookie', cookieHeaders });
  
  const cookies = cookieHeaders.flatMap(h => Cookies.parse(h.value));
  
  const tokenCookieNamePrefix = `${this._cookieBase}.`;
  const accessTokenCookieNamePostfix = '.accessToken';
  const idTokenCookieNamePostfix = '.idToken';
  const refreshTokenCookieNamePostfix = '.refreshToken';
  
  const tokens: Tokens = {};
  for (const {name, value} of cookies){
    if (name.startsWith(tokenCookieNamePrefix) && name.endsWith(accessTokenCookieNamePostfix)) {
      tokens.accessToken = value;
    }
    if (name.startsWith(tokenCookieNamePrefix) && name.endsWith(idTokenCookieNamePostfix)) {
      tokens.idToken = value;
    }
    if (name.startsWith(tokenCookieNamePrefix) && name.endsWith(refreshTokenCookieNamePostfix)) {
      tokens.refreshToken = value;
    }
  }
  
  if (!tokens.accessToken && !tokens.idToken && !tokens.refreshToken) {
    this._logger.debug('Neither accessToken, nor idToken, nor refreshToken was present in request cookies');
  
    throw new Error('Neither accessToken, nor idToken, nor refreshToken was present in request cookies');
  }
  
  this._logger.debug({ msg: 'Found tokens in cookie', tokens });
  return tokens;
  }

Update the Authenticator.handle, Add Access Token Verification, Update Return Type(s)

async handle(event: CloudFrontRequestEvent): Promise<HandleResponse> {
  this._logger.debug({ msg: 'Handling Lambda@Edge event', event });

  const { request } = event.Records[0].cf;
  const requestParams = parse(request.querystring);
  const cfDomain = request.headers.host[0].value;
  const redirectURI = `https://${cfDomain}`;

  // 1. Fetch Token(s) from Coookie(s)
  // 2. Verify Access Token, ID Token from Cookie(s)
  // 3. Optionally, On Failure of (2), Attempt Token Refresh
  // 4. Optionally, On Failure of (3), Check for `?code=STRING` on Request Parameters
  //  - Handle as Cognito Hosted UI Authentication Redirect Response
  //  - Fetch Token(s) from Cognito Hosted UI Authentication Redirect Response
  //  - Redirect to CloudFront (Self) w/ Token(s) in Cookie Header(s), Triggering Step #1, #2
  // 5. Finally, On Failure of (4), Redirect to Cognito Hosted UI for Authentication

  try {
    // 1. Find Token(s) from Coookie(s)
    const headerTokens = this._getTokensFromCookie(request.headers.cookie);

    this._logger.debug({ msg: 'Verifying token...', tokens: headerTokens });

    try {
      // 2. Verify Access Token, ID Token from Cookie(s)
      const accessToken = await this._accessTokenVerifier.verify(headerTokens.accessToken);
      const idToken = await this._idTokenVerifier.verify(headerTokens.idToken);

      this._logger.info({ msg: 'Forwarding request', path: request.uri, accessToken, idToken, });

      return { result: request, tokens: headerTokens, isAuthenticated: true, };
    } catch (err) {
      // 3. Optionally, On Failure of (2), Attempt Token Refresh
      if (headerTokens.refreshToken) {
        this._logger.debug({ msg: 'Verifying accessToken and idToken failed, verifying refresh token instead...', tokens: headerTokens, err });

        const refreshedTokens = await this._fetchTokensFromRefreshToken(redirectURI, headerTokens.refreshToken);
        const response = await this._getRedirectToCloudFrontResponse(refreshedTokens, cfDomain, request.uri);

        return { result: response, isAuthenticated: false, };
      } else {
        throw err;
      }
    }
  } catch (err) {
    this._logger.debug("User isn't authenticated: %s", err);

    // TODO: Add `path` Configuration, i.e. `/idpresponse` a la AWS ALB?
    if (requestParams.code) {
      // 4. Optionally, On Failure of (3), Check for `?code=STRING` on Request Parameters
      const codeTokens = await this._fetchTokensFromCode(redirectURI, requestParams.code);
      const response = await this._getRedirectToCloudFrontResponse(codeTokens, cfDomain, requestParams.state as string);

      return { result: response, isAuthenticated: false, };
    } else {
      // 5. Finally, On Failure of (4), Redirect to Cognito Hosted UI for Authentication
      const response = this._getRedirectToCognitoUserPoolResponse(request, redirectURI);

      return { result: response, isAuthenticated: false, };
    }
  }
}

@jeandek would it make sense to add these changes into a Pull Request, or would this breaking change be rejected? I can happily put in an PR for you to make this all possible, update the README, etc.

@67726e
Copy link

67726e commented Jun 12, 2023

The resulting changes to my root Lambda function look like the following:

// cognito-at-edge
function authenticationHandler({ event }): Promise<HandleResponse> {
    return authenticator.handle(event);
}

// Lambda@Edge Handler
export const main = async (event, context, callback) => {
    const response = await authenticationHandler({ event });

    if (response.isAuthenticated) {
        // Add Apache-style `DocumentRoot` Functionality
        documentRootHandler({ request: response.result });

        // Add Access Token to Header, i.e. `X-Cf-Accesstoken` a la AWS ALB `X-Amzn-Accesstoken`
        authenticationHeaderHandler({ request: response.result, tokens: response.tokens });
    }

    return response.result;
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature-request New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants