def handler(event, _context): """Handle the signout event.""" request = event['Records'][0]['cf']['request'] domain_name = request['headers']['host'][0]['value'] extracted = extract_and_parse_cookies( request['headers'], CONFIG.get('client_id') ) if not extracted.get('idToken'): return { 'body': "Bad Request", 'status': "400", 'statusDescription': "Bad Request", 'headers': CONFIG.get('cloud_front_headers') } tokens = { 'id_token': extracted.get('idToken'), 'access_token': extracted.get('accessToken'), 'refresh_token': extracted.get('refreshToken'), } query_string = { 'logout_uri': 'https://%s%s' % (domain_name, CONFIG.get('redirect_path_sign_out')), 'client_id': CONFIG.get('client_id') } headers = { # Redirect the user to logout 'location': [ { 'key': 'location', 'value': 'https://%s/logout?%s' % ( CONFIG.get('cognito_auth_domain'), urlencode(query_string) ) } ], 'set-cookie': get_cookie_headers( CONFIG.get("client_id"), CONFIG.get("oauth_scopes"), tokens, domain_name, CONFIG.get('cookie_settings'), # Make sure we expire all the tokens during retrieval expire_all_tokens=True ) } headers.update(CONFIG.get('cloud_front_headers')) return { 'status': '307', 'statusDescription': 'Temporary Redirect', 'headers': headers }
def handler(event, _context): """Handle the signout event.""" request = event["Records"][0]["cf"]["request"] domain_name = request["headers"]["host"][0]["value"] extracted = extract_and_parse_cookies(request["headers"], CONFIG.get("client_id")) if not extracted.get("idToken"): return { "body": "Bad Request", "status": "400", "statusDescription": "Bad Request", "headers": CONFIG.get("cloud_front_headers"), } tokens = { "id_token": extracted.get("idToken"), "access_token": extracted.get("accessToken"), "refresh_token": extracted.get("refreshToken"), } query_string = { "logout_uri": "https://%s%s" % (domain_name, CONFIG.get("redirect_path_sign_out")), "client_id": CONFIG.get("client_id"), } headers = { # Redirect the user to logout "location": [{ "key": "location", "value": "https://%s/logout?%s" % (CONFIG.get("cognito_auth_domain"), urlencode(query_string)), }], "set-cookie": get_cookie_headers( CONFIG.get("client_id"), CONFIG.get("oauth_scopes"), tokens, domain_name, CONFIG.get("cookie_settings"), # Make sure we expire all the tokens during retrieval expire_all_tokens=True, ), } headers.update(CONFIG.get("cloud_front_headers")) return { "status": "307", "statusDescription": "Temporary Redirect", "headers": headers, }
def handler(event, _context): """Handle the authorization refresh. Keyword Args: event: The Lambda Event """ request = event['Records'][0]['cf']['request'] domain_name = request['headers']['host'][0]['value'] redirected_from_uri = 'https://%s' % domain_name try: parsed_qs = parse_qs(request.get('querystring')) requested_uri = parsed_qs.get('requestedUri')[0] current_nonce = parsed_qs.get('nonce')[0] # Add the requested uri path to the main redirected_from_uri += requested_uri or "" cookies = extract_and_parse_cookies(request.get('headers'), CONFIG.get('client_id')) tokens = { 'id_token': cookies.get('idToken'), 'access_token': cookies.get('accessToken'), 'refresh_token': cookies.get('refreshToken') } validate_refresh_request(current_nonce, cookies.get('nonce'), tokens) try: # Request new tokens based on the refresh_token body = { 'grant_type': 'refresh_token', 'client_id': CONFIG.get('client_id'), 'refresh_token': tokens.get('refresh_token') } res = http_post_with_retry( ('https://%s/oauth2/token' % CONFIG.get('cognito_auth_domain')), body, {'Content-Type': 'application/x-www-form-urlencoded'}) tokens['id_token'] = res.get('id_token') tokens['access_token'] = res.get('access_token') except Exception as err: # pylint: disable=broad-except LOGGER.error(err) # Otherwise clear the refresh token tokens['refresh_token'] = "" headers = { 'location': [{ 'key': 'location', 'value': redirected_from_uri }], 'set-cookie': get_cookie_headers(CONFIG.get('client_id'), CONFIG.get('oauth_scopes'), tokens, domain_name, CONFIG.get('cookie_settings')) } headers.update(CONFIG.get('cloud_front_headers')) # Redirect the user back to their requested uri # with new tokens at hand return { 'status': '307', 'statusDescription': 'Temporary Redirect', 'headers': headers } # Send a basic html error response and inform the user # why refresh was unsuccessful except Exception as err: # pylint: disable=broad-except LOGGER.info(err) LOGGER.info(traceback.print_exc()) headers = { "content-type": [{ "key": "Content-Type", "value": "text/html; charset=UTF-8" }] } headers.update(CONFIG.get('cloud_front_headers')) return { 'body': create_error_html("Bad Request", err, redirected_from_uri), 'status': '400', 'headers': headers }
def handler(event, _context): """Handle the request passed in. Keyword Args: event (Dict[str, Any]): The Lambda Event """ request = event['Records'][0]['cf']['request'] domain_name = request['headers']['host'][0]['value'] querystring = request.get('querystring') request_query_string = ("?%s" % querystring) if querystring else "" requested_uri = '%s%s' % (request['uri'], request_query_string) nonce = generate_nonce() try: # Extract the cookies received from Cognito extracted = extract_and_parse_cookies(request['headers'], CONFIG['client_id']) token_user_name = extracted.get('tokenUserName') id_token = extracted.get('idToken') refresh_token = extracted.get('refreshToken') # If the token user name or id token are missing then we need # new credentials if not token_user_name or not id_token: msg = "No valid credentials present in cookies" LOGGER.error(msg) raise Exception(msg) # Get the expiration date from the id token. decoded_token = decode_token(id_token) expiration = decoded_token.get('exp') exp_date = datetime.datetime.fromtimestamp(expiration) now = datetime.datetime.now() # If we have a refresh token and the expiration date has passed # then forward the user to the refresh agent if now > exp_date and refresh_token: headers = { # Redirect the user to the refresh agent 'location': [{ 'key': 'location', 'value': 'https://%s%s?%s' % (domain_name, CONFIG.get('redirect_path_auth_refresh'), urlencode({ 'requestedUri': requested_uri, 'nonce': nonce })) }], 'set-cookie': [ # Set the Nonce Cookie { 'key': 'set-cookie', 'value': 'spa-auth-edge-nonce=%s; %s' % (quote_plus(nonce), CONFIG.get('cookie_settings').get('nonce')) } ] } # Add all CloudFrontHeaders headers.update(CONFIG.get('cloud_front_headers')) return { 'status': '307', 'statusDescription': 'Temporary Redirect', 'headers': headers } # Validate the token information against the Cognito JWKS validate_jwt(id_token, CONFIG.get('token_jwks_uri'), CONFIG.get('token_issuer'), CONFIG.get('client_id')) return request # We need new authorization. Get the user over to Cognito except Exception: # noqa pylint: disable=broad-except pkce, pkce_hash = generate_pkce_verifier() payload = { 'redirect_uri': 'https://%s%s' % (domain_name, CONFIG['redirect_path_sign_in']), 'response_type': 'code', 'client_id': CONFIG["client_id"], 'state': base64.urlsafe_b64encode( bytes( json.dumps({ "nonce": nonce, "requestedUri": requested_uri }).encode())), 'scope': " ".join(CONFIG['oauth_scopes']), 'code_challenge_method': "S256", 'code_challenge': pkce_hash } login_query_string = urlencode(payload, quote_via=quote_plus) headers = CONFIG.get('cloud_front_headers') headers['location'] = [{ 'key': 'location', 'value': 'https://%s/oauth2/authorize?%s' % (CONFIG['cognito_auth_domain'], login_query_string) }] headers['set-cookie'] = [ # Set Nonce Cookie { 'key': 'set-cookie', 'value': 'spa-auth-edge-nonce=%s; %s' % (quote_plus(nonce), CONFIG.get('cookie_settings', {}).get('nonce')) }, # Set PKCE Cookie { 'key': 'set-cookie', 'value': 'spa-auth-edge-pkce=%s; %s' % (quote_plus(pkce), CONFIG.get('cookie_settings', {}).get('nonce')) }, ] # Redirect user to the Cognito Login return { 'status': '307', 'statusDescription': 'Temporary Redirect', 'headers': headers }
def handler(event, _context): """Handle the authorization parsing. Keyword Args: event (Any): The Lambda Event """ request = event['Records'][0]['cf']['request'] domain_name = request['headers']['host'][0]['value'] redirected_from_uri = 'https://%s' % domain_name # Attempt to parse the request and retrieve authorization # tokens to integrate with our header cookies try: qsp = parse_qs(request.get('querystring')) # Authorization code given by Cognito code = qsp.get('code') # The requested URI and current nonce information state = json.loads( base64.urlsafe_b64decode(qsp.get('state')[0]).decode()) # Missing required components if not code or not state: msg = 'Invalid query string. ' + \ 'Your query string should include parameters "state" and "code"' LOGGER.info(msg) raise Exception(msg) current_nonce = state.get('nonce') requested_uri = state.get('requestedUri') redirected_from_uri = requested_uri or "/" # Get all the cookies from the headers cookies = extract_and_parse_cookies(request.get('headers'), CONFIG.get('client_id')) # Retrieve the original nonce value as well as our PKCE code original_nonce = cookies.get('nonce') pkce = cookies.get('pkce') # If we're missing one of the nonces, or they don't match, cause an error if not current_nonce or not original_nonce or current_nonce != original_nonce: # No original nonce? CSRF violation if not original_nonce: msg = "Your browser didn't send the nonce cookie along, " + \ "but it is required for security (prevent CSRF)" LOGGER.error(msg) raise Exception(msg) # Nonce's don't match msg = "Nonce Mismatch" LOGGER.error(msg) raise Exception(msg) payload = { 'grant_type': 'authorization_code', 'client_id': CONFIG.get('client_id'), 'redirect_uri': 'https://%s%s' % (domain_name, CONFIG.get('redirect_path_sign_in')), 'code': code[0], 'code_verifier': pkce } # Request tokens from our Cognito Authorization Domain tokens = http_post_with_retry( ('https://%s/oauth2/token' % CONFIG.get('cognito_auth_domain')), payload, {"Content-Type": "application/x-www-form-urlencoded"}) if not tokens: raise Exception('Was not able to obtain tokens from Cognito') headers = { 'location': [{ 'key': 'location', 'value': redirected_from_uri }], 'set-cookie': get_cookie_headers(CONFIG.get("client_id"), CONFIG.get("oauth_scopes"), tokens, domain_name, CONFIG.get('cookie_settings')) } headers.update(CONFIG.get('cloud_front_headers')) # Redirect user to the originally requested uri with the # token header cookies response = { 'status': '307', 'statusDescription': 'Temporary Redirect', 'headers': headers } return response except Exception as err: # pylint: disable=broad-except LOGGER.error(err) LOGGER.error(traceback.print_exc()) headers = CONFIG.get('cloud_front_headers') headers['content-type'] = [{ 'key': 'Content-Type', 'value': 'text/html; charset=UTF-8' }] # Inform user of bad request and reason return { 'body': create_error_html("Bad Request", err, redirected_from_uri), 'status': '400', 'headers': headers }
def handler(event, _context): """Handle the authorization parsing. Args: event (Any): The Lambda Event. _context (Any): Lambda context object. """ request = event["Records"][0]["cf"]["request"] domain_name = request["headers"]["host"][0]["value"] redirected_from_uri = "https://%s" % domain_name id_token = None # Attempt to parse the request and retrieve authorization # tokens to integrate with our header cookies try: # Get all the cookies from the headers cookies = extract_and_parse_cookies(request.get("headers"), CONFIG["client_id"]) id_token = cookies["id_token"] code, pkce, requested_uri = validate_querystring_and_cookies( request, cookies) redirected_from_uri += requested_uri # Request tokens from our Cognito Authorization Domain body = { "grant_type": "authorization_code", "client_id": CONFIG["client_id"], "redirect_uri": "https://%s%s" % (domain_name, CONFIG.get("redirect_path_sign_in")), "code": code[0], "code_verifier": pkce, } tokens = http_post_with_retry( COGNITO_TOKEN_ENDPOINT, body, {"Content-Type": "application/x-www-form-urlencoded"}, ) if not tokens: raise Exception("Was not able to obtain tokens from Cognito") # Validate the token information against the Cognito JWKS # (and ensure group membership, if applicable) validate_and_check_id_token( tokens["id_token"], CONFIG["token_jwks_uri"], CONFIG["token_issuer"], CONFIG["client_id"], CONFIG.get("required_group"), ) # Redirect user to the originally requested uri with the # token header cookies response = { "status": "307", "statusDescription": "Temporary Redirect", "headers": { "location": [{ "key": "location", "value": redirected_from_uri }], "set-cookie": generate_cookie_headers( "new_tokens", CONFIG.get("client_id"), CONFIG.get("oauth_scopes"), tokens, domain_name, CONFIG.get("cookie_settings"), ), **CONFIG.get("cloud_front_headers", {}), }, } return response except Exception as err: # pylint: disable=broad-except if id_token: # ID token found; checking if it is valid try: validate_and_check_id_token( id_token, CONFIG["token_jwks_uri"], CONFIG["token_issuer"], CONFIG["client_id"], CONFIG.get("required_group"), ) # Token is valid; return user to where they came from return { "status": "307", "statusDescription": "Temporary Redirect", "headers": { "location": [{ "key": "location", "value": redirected_from_uri }], **CONFIG.get("cloud_front_headers", {}), }, } except Exception as err: # pylint: disable=broad-except LOGGER.debug("Id token not valid") LOGGER.debug(err) if isinstance(err, RequiresConfirmationError): html_params = [ "Confirm sign-in", "We need your confirmation to sign you (%s)" % str(err), redirected_from_uri, "Confirm", ] elif isinstance(err, MissingRequiredGroupError): html_params = [ "Not Authorized", "Your user is not authorized for this site. Please contact the admin.", redirected_from_uri, "Try Again", ] else: html_params = [ "Sign-in issue", "Sign-in unsuccessful because of a technical problem: %s" % str(err), redirected_from_uri, "Try Again", ] # Inform user of bad request and reason return { "body": create_error_html(*html_params), "status": "200", "headers": { **CONFIG.get("cloud_front_headers", {}), "content-type": [{ "key": "Content-Type", "value": "text/html; charset=UTF-8" }], }, }
def handler(event, _context): """Handle the request passed in. Args: event (Dict[str, Any]): The Lambda Event. _context (Any): Lambda context object. """ request = event["Records"][0]["cf"]["request"] domain_name = request["headers"]["host"][0]["value"] querystring = request.get("querystring") request_query_string = ("?%s" % querystring) if querystring else "" requested_uri = "%s%s" % (request["uri"], request_query_string) try: # Extract the cookies received from Cognito extracted = extract_and_parse_cookies(request["headers"], CONFIG["client_id"]) token_user_name = extracted["token_user_name"] id_token = extracted["id_token"] refresh_token = extracted["refresh_token"] # If the token user name or id token are missing then we need # new credentials if not token_user_name or not id_token: msg = "No valid credentials present in cookies" LOGGER.error(msg) raise Exception(msg) # Get the expiration date from the id token. decoded_token = decode_token(id_token) expiration = decoded_token.get("exp") exp_date = datetime.datetime.fromtimestamp(expiration) now = datetime.datetime.now() # If we have a refresh token and the expiration date has passed # then forward the user to the refresh agent if now > (exp_date - datetime.timedelta(minutes=10)) and refresh_token: nonce = generate_nonce() response = { "status": "307", "statusDescription": "Temporary Redirect", "headers": { # Redirect the user to the refresh agent "location": [{ "key": "location", "value": "https://%s%s?%s" % ( domain_name, CONFIG.get("redirect_path_auth_refresh"), urlencode({ "requestedUri": requested_uri, "nonce": nonce }), ), }], "set-cookie": [ # Set the Nonce Cookie { "key": "set-cookie", "value": "spa-auth-edge-nonce=%s; %s" % ( quote_plus(nonce), CONFIG.get("cookie_settings").get("nonce"), ), }, { "key": "set-cookie", "value": "spa-auth-edge-nonce-hmac=%s; %s" % ( quote_plus( sign(nonce, CONFIG["nonce_signing_secret"])), CONFIG.get("cookie_settings").get("nonce"), ), }, ], **CONFIG.get("cloud_front_headers", {}), }, } return response # Validate the token information against the Cognito JWKS validate_jwt( id_token, CONFIG["token_jwks_uri"], CONFIG["token_issuer"], CONFIG["client_id"], ) return request except Exception: # noqa pylint: disable=broad-except # We need new authorization. Get the user over to Cognito nonce = generate_nonce() state = { "nonce": nonce, "nonceHmac": sign(nonce, CONFIG["nonce_signing_secret"]), **generate_pkce_verifier(), } login_query_string = urlencode( { "redirect_uri": "https://%s%s" % (domain_name, CONFIG["redirect_path_sign_in"]), "response_type": "code", "client_id": CONFIG["client_id"], "state": base64.urlsafe_b64encode( bytes( json.dumps({ "nonce": state["nonce"], "requestedUri": requested_uri }).encode())), "scope": " ".join(CONFIG["oauth_scopes"]), "code_challenge_method": "S256", "code_challenge": state["pkceHash"], }, quote_via=quote_plus, ) # Redirect user to the Cognito Login response = { "status": "307", "statusDescription": "Temporary Redirect", "headers": { "location": [{ "key": "location", "value": "https://%s/oauth2/authorize?%s" % (CONFIG["cognito_auth_domain"], login_query_string), }], "set-cookie": [ { "key": "set-cookie", "value": "spa-auth-edge-nonce=%s; %s" % ( quote_plus(state["nonce"]), CONFIG.get("cookie_settings", {}).get("nonce"), ), }, { "key": "set-cookie", "value": "spa-auth-edge-nonce-hmac=%s; %s" % ( quote_plus(state["nonceHmac"]), CONFIG.get("cookie_settings", {}).get("nonce"), ), }, { "key": "set-cookie", "value": "spa-auth-edge-pkce=%s; %s" % ( quote_plus(state["pkce"]), CONFIG.get("cookie_settings", {}).get("nonce"), ), }, ], **CONFIG.get("cloud_front_headers", {}), }, } return response
def handler(event, _context): """Handle the authorization parsing. Args: event (Any): The Lambda Event. _context (Any): Lambda context object. """ request = event["Records"][0]["cf"]["request"] domain_name = request["headers"]["host"][0]["value"] redirected_from_uri = "https://%s" % domain_name # Attempt to parse the request and retrieve authorization # tokens to integrate with our header cookies try: qsp = parse_qs(request.get("querystring")) # Authorization code given by Cognito code = qsp.get("code") # The requested URI and current nonce information state = json.loads(base64.urlsafe_b64decode(qsp.get("state")[0]).decode()) # Missing required components if not code or not state: msg = ( "Invalid query string. " 'Your query string should include parameters "state" and "code"' ) LOGGER.info(msg) raise Exception(msg) current_nonce = state.get("nonce") requested_uri = state.get("requestedUri") redirected_from_uri = requested_uri or "/" # Get all the cookies from the headers cookies = extract_and_parse_cookies( request.get("headers"), CONFIG.get("client_id") ) # Retrieve the original nonce value as well as our PKCE code original_nonce = cookies.get("nonce") pkce = cookies.get("pkce") # If we're missing one of the nonces, or they don't match, cause an error if not current_nonce or not original_nonce or current_nonce != original_nonce: # No original nonce? CSRF violation if not original_nonce: msg = ( "Your browser didn't send the nonce cookie along, " "but it is required for security (prevent CSRF)" ) LOGGER.error(msg) raise Exception(msg) # Nonce's don't match msg = "Nonce Mismatch" LOGGER.error(msg) raise Exception(msg) payload = { "grant_type": "authorization_code", "client_id": CONFIG.get("client_id"), "redirect_uri": "https://%s%s" % (domain_name, CONFIG.get("redirect_path_sign_in")), "code": code[0], "code_verifier": pkce, } # Request tokens from our Cognito Authorization Domain tokens = http_post_with_retry( ("https://%s/oauth2/token" % CONFIG.get("cognito_auth_domain")), payload, {"Content-Type": "application/x-www-form-urlencoded"}, ) if not tokens: raise Exception("Was not able to obtain tokens from Cognito") headers = { "location": [{"key": "location", "value": redirected_from_uri}], "set-cookie": get_cookie_headers( CONFIG.get("client_id"), CONFIG.get("oauth_scopes"), tokens, domain_name, CONFIG.get("cookie_settings"), ), } headers.update(CONFIG.get("cloud_front_headers")) # Redirect user to the originally requested uri with the # token header cookies response = { "status": "307", "statusDescription": "Temporary Redirect", "headers": headers, } return response except Exception as err: # pylint: disable=broad-except LOGGER.error(err) LOGGER.error(traceback.print_exc()) headers = CONFIG.get("cloud_front_headers") headers["content-type"] = [ {"key": "Content-Type", "value": "text/html; charset=UTF-8"} ] # Inform user of bad request and reason return { "body": create_error_html("Bad Request", err, redirected_from_uri), "status": "400", "headers": headers, }
def handler(event, _context): """Handle the signout event.""" request = event["Records"][0]["cf"]["request"] domain_name = request["headers"]["host"][0]["value"] extracted = extract_and_parse_cookies(request["headers"], CONFIG["client_id"]) if not extracted["id_token"]: response = { "body": create_error_html( "Signed out", "You are already signed out", "https://%s%s" % (domain_name, CONFIG["redirect_path_sign_out"]), "Proceed", ), "status": "200", "headers": { "content-type": [{ "key": "Content-Type", "value": "text/html; charset=UTF-8" }], **CONFIG.get("cloud_front_headers", {}), }, } return response tokens = { "id_token": extracted["id_token"], "access_token": extracted["access_token"], "refresh_token": extracted["refresh_token"], } query_string = { "logout_uri": "https://%s%s" % (domain_name, CONFIG["redirect_path_sign_out"]), "client_id": CONFIG["client_id"], } response = { "status": "307", "statusDescription": "Temporary Redirect", "headers": { # Redirect the user to logout "location": [{ "key": "location", "value": "https://%s/logout?%s" % (CONFIG.get("cognito_auth_domain"), urlencode(query_string)), }], "set-cookie": generate_cookie_headers( "sign_out", CONFIG.get("client_id"), CONFIG.get("oauth_scopes"), tokens, domain_name, CONFIG.get("cookie_settings"), ), **CONFIG.get("cloud_front_headers", {}), }, } return response
def handler(event, _context): """Handle the authorization refresh. Args: event: The Lambda Event. _context (Any): Lambda context object. """ request = event["Records"][0]["cf"]["request"] domain_name = request["headers"]["host"][0]["value"] redirected_from_uri = "https://%s" % domain_name try: parsed_qs = parse_qs(request.get("querystring")) requested_uri = parsed_qs.get("requestedUri")[0] current_nonce = parsed_qs.get("nonce")[0] # Add the requested uri path to the main redirected_from_uri += requested_uri or "" cookies = extract_and_parse_cookies( request.get("headers"), CONFIG.get("client_id") ) tokens = { "id_token": cookies.get("idToken"), "access_token": cookies.get("accessToken"), "refresh_token": cookies.get("refreshToken"), } validate_refresh_request(current_nonce, cookies.get("nonce"), tokens) try: # Request new tokens based on the refresh_token body = { "grant_type": "refresh_token", "client_id": CONFIG.get("client_id"), "refresh_token": tokens.get("refresh_token"), } res = http_post_with_retry( ("https://%s/oauth2/token" % CONFIG.get("cognito_auth_domain")), body, {"Content-Type": "application/x-www-form-urlencoded"}, ) tokens["id_token"] = res.get("id_token") tokens["access_token"] = res.get("access_token") except Exception as err: # pylint: disable=broad-except LOGGER.error(err) # Otherwise clear the refresh token tokens["refresh_token"] = "" headers = { "location": [{"key": "location", "value": redirected_from_uri}], "set-cookie": get_cookie_headers( CONFIG.get("client_id"), CONFIG.get("oauth_scopes"), tokens, domain_name, CONFIG.get("cookie_settings"), ), } headers.update(CONFIG.get("cloud_front_headers")) # Redirect the user back to their requested uri # with new tokens at hand return { "status": "307", "statusDescription": "Temporary Redirect", "headers": headers, } # Send a basic html error response and inform the user # why refresh was unsuccessful except Exception as err: # pylint: disable=broad-except LOGGER.info(err) LOGGER.info(traceback.print_exc()) headers = { "content-type": [ {"key": "Content-Type", "value": "text/html; charset=UTF-8"} ] } headers.update(CONFIG.get("cloud_front_headers")) return { "body": create_error_html("Bad Request", err, redirected_from_uri), "status": "400", "headers": headers, }