def _validate_metadata_values_are_valid(claims, required_metadata): """ Validate metadata from the JWT claims that are required by the schema/runner. Ensures that the values adhere to the expected format. :param claims: :param required_metadata: """ try: for metadata_field in required_metadata: name = metadata_field['name'] claim = claims.get(name) if name not in claims: raise InvalidTokenException( 'Missing required key {} from claims'.format(name)) logger.debug('parsing metadata', key=name, value=claim) VALIDATORS[metadata_field['validator']](claim) except (RuntimeError, ValueError, TypeError) as error: logger.error('Unable to parse metadata', key=name, value=claim, exc_info=error) raise InvalidTokenException( 'incorrect data in token for {}'.format(name)) from error except KeyError as key_error: error_msg = 'Invalid validator for schema metadata - {}'.format( key_error.args[0]) logger.error(error_msg, exc_info=key_error) raise KeyError(error_msg) from key_error
def login(): """ Initial url processing - expects a token parameter and then will authenticate this token. Once authenticated it will be placed in the users session :return: a 302 redirect to the next location for the user """ # logging in again clears any session state if cookie_session: cookie_session.clear() decrypted_token = decrypt_token(request.args.get("token")) validate_jti(decrypted_token) try: runner_claims = validate_runner_claims(decrypted_token) except ValidationError as e: raise InvalidTokenException("Invalid runner claims") from e g.schema = load_schema_from_metadata(runner_claims) schema_metadata = g.schema.json["metadata"] try: questionnaire_claims = validate_questionnaire_claims( decrypted_token, schema_metadata) except ValidationError as e: raise InvalidTokenException("Invalid questionnaire claims") from e claims = {**runner_claims, **questionnaire_claims} schema_name = claims["schema_name"] tx_id = claims["tx_id"] ru_ref = claims["ru_ref"] case_id = claims["case_id"] logger.bind( schema_name=schema_name, tx_id=tx_id, ru_ref=ru_ref, case_id=case_id, ) logger.info("decrypted token and parsed metadata") store_session(claims) cookie_session["theme"] = g.schema.json["theme"] cookie_session["survey_title"] = g.schema.json["title"] cookie_session["expires_in"] = get_session_timeout_in_seconds(g.schema) if claims.get("account_service_url"): cookie_session["account_service_url"] = claims.get( "account_service_url") if claims.get("account_service_log_out_url"): cookie_session["account_service_log_out_url"] = claims.get( "account_service_log_out_url") return redirect(url_for("questionnaire.get_questionnaire"))
def extract_kid_from_header(token): header = token.split('.')[:1][0] if not header: raise InvalidTokenException("Missing Headers") try: protected_header = base64url_decode(header).decode() protected_header_data = json.loads(protected_header) if 'kid' in protected_header: return protected_header_data['kid'] except ValueError: raise InvalidTokenException("Invalid Header") raise InvalidTokenException("Missing kid")
def get_key_by_kid(self, purpose, kid, key_type): try: key = self.keys[kid] if key.purpose != purpose or key.key_type != key_type: raise InvalidTokenException except(KeyError, InvalidTokenException): raise InvalidTokenException("Invalid {} Key Identifier [{}] for Purpose [{}]".format(key_type, kid, purpose)) else: return key
def _get_token_data(metadata): ru_ref = metadata.get('ru_ref') collection_exercise_sid = metadata.get('collection_exercise_sid') eq_id = metadata.get('eq_id') form_type = metadata.get('form_type') if ru_ref and collection_exercise_sid and eq_id and form_type: return collection_exercise_sid, eq_id, form_type, ru_ref logger.error( 'missing values for ru_ref, collection_exercise_sid, form_type or eq_id', metadata=metadata) raise InvalidTokenException('Missing values in JWT token')
def decrypt_with_key(encrypted_token, key): """ Decrypts JWE token with supplied key :param encrypted_token: :param key: A (:class:`jwcrypto.jwk.JWK`) decryption key or a password :returns: The payload of the decrypted token """ try: jwe_token = jwe.JWE(algs=['RSA-OAEP', 'A256GCM']) jwe_token.deserialize(encrypted_token) jwe_token.decrypt(key) return jwe_token.payload.decode() except (ValueError, InvalidJWEData) as e: raise InvalidTokenException(str(e)) from e
def decrypt(encrypted_token, key_store=None, purpose=None): try: jwe_token = jwe.JWE(algs=['RSA-OAEP', 'A256GCM']) jwe_token.deserialize(encrypted_token) jwe_kid = extract_kid_from_header(encrypted_token) logger.info("Decrypting JWE kid is {}".format(jwe_kid)) private_jwk = key_store.get_private_key_by_kid(purpose, jwe_kid).as_jwk() jwe_token.decrypt(private_jwk) return jwe_token.payload.decode() except (ValueError, InvalidJWEData) as e: raise InvalidTokenException(str(e)) from e
def encrypt_with_key(payload, kid, key): try: logger.info( "Encrypting JWE with provided key and kid {}".format(kid)) protected_header = { "alg": "RSA-OAEP", "enc": "A256GCM", "kid": kid, } token = jwe.JWE(plaintext=payload, protected=protected_header) token.add_recipient(key) except (ValueError, InvalidJWEData) as e: raise InvalidTokenException(str(e)) from e return token.serialize(compact=True)
def encrypt(payload, kid, key_store=None, purpose=None): try: logger.info("Encrypting JWE kid is {}".format(kid)) public_jwk = key_store.get_public_key_by_kid(purpose, kid).as_jwk() protected_header = { "alg": "RSA-OAEP", "enc": "A256GCM", "kid": kid, } token = jwe.JWE(plaintext=payload, protected=protected_header) token.add_recipient(public_jwk) except (ValueError, InvalidJWEData) as e: raise InvalidTokenException(str(e)) from e return token.serialize(compact=True)
def decrypt(token, key_store, key_purpose, leeway=120): """This decrypts the provided jwe token, then decodes resulting jwt token and returns the payload. :param str token: The jwe token. :param key_store: The key store. :param str key_purpose: Context for the key. :param int leeway: Extra allowed time in seconds after expiration to account for clock skew. :return: The decrypted payload. """ tokens = token.split('.') if len(tokens) != 5: raise InvalidTokenException("Incorrect number of tokens") decrypted_token = JWEHelper.decrypt(token, key_store, key_purpose) payload = JWTHelper.decode(decrypted_token, key_store, key_purpose, leeway) return payload
def decode(jwt_token, key_store, purpose, leeway=None, check_claims={}): try: jwt_kid = extract_kid_from_header(jwt_token) logger.info("Decoding JWT kid is {}".format(jwt_kid)) public_jwk = key_store.get_public_key_by_kid(purpose, jwt_kid).as_jwk() signed_token = jwt.JWT(algs=['RS256'], check_claims=check_claims) if leeway: signed_token.leeway = leeway signed_token.deserialize(jwt_token, key=public_jwk) return json.loads(signed_token.claims) except (InvalidJWSObject, InvalidJWSSignature, JWTInvalidClaimFormat, JWTMissingClaim, JWTExpired, ValueError) as e: raise InvalidTokenException(str(e)) from e
def _validate_metadata_is_present(metadata, required_metadata): """ Validate that JWT claims contain the required metadata (both eq-runner and survey specific required metadata). :param metadata: :param required_metadata: """ for metadata_field in required_metadata: name = metadata_field['name'] if name == 'trad_as_or_ru_name': # either of 'trad_as' or 'ru_name' is required valid = bool(metadata.get('trad_as') or metadata.get('ru_name')) else: # Validate that the value is one of: # a) A boolean value # b) A non empty value value = metadata.get(name) valid = isinstance(value, bool) or bool(value) if not valid: raise InvalidTokenException( 'Missing key/value for {}'.format(name))