Open keokilee opened 2 years ago
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,
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.
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:
tokens: Tokens
and isAuthenticated: boolean
handle
method.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;
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;
}
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.
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;
};
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.