def test_fetching_oidc_keys(self): """Test to fetch OpenID Connect formatted keys.""" test_jwt = create_mock_jwt( MOCK_RSA_PRIVATE_KEY_PEM, algorithm=OIDC_JWT_ALGORITHM, key_id='oidc_1234', audience=OIDC_VALID_AUDIENCE, issuer=OIDC_VALID_ISSUER) public_key = get_public_key_for_jwt(test_jwt, OIDC_PUBLIC_KEY_URL) self.assertIsInstance(public_key, _RSAPublicKey)
def test_fetching_oidc_keys(self): """Test to fetch OpenID Connect formatted keys.""" test_jwt = create_mock_jwt(MOCK_RSA_PRIVATE_KEY_PEM, algorithm=OIDC_JWT_ALGORITHM, key_id='oidc_1234', audience=OIDC_VALID_AUDIENCE, issuer=OIDC_VALID_ISSUER) public_key = get_public_key_for_jwt(test_jwt, OIDC_PUBLIC_KEY_URL) self.assertIsInstance(public_key, _RSAPublicKey)
def test_invalid_audience_raises_jwt_validation_error(self): """Test to validate a JWT with wrong audience.""" test_jwt = create_mock_jwt( MOCK_EC_PRIVATE_KEY, algorithm=IAP_JWT_ALGORITHM, key_id='iap_1234', audience=IAP_VALID_AUDIENCE, issuer=IAP_VALID_ISSUER) public_key = get_public_key_for_jwt(test_jwt, IAP_PUBLIC_KEY_URL) self.assertRaises( JwtValidationError, validate_jwt, test_jwt, public_key, IAP_JWT_ALGORITHM, IAP_INVALID_AUDIENCE, IAP_VALID_ISSUER)
def _test_header_raises_jwt_validation_error(self, header): """Test JWT with supplied header.""" test_jwt = create_mock_jwt( MOCK_EC_PRIVATE_KEY, algorithm=IAP_JWT_ALGORITHM, key_id='iap_1234', audience=IAP_VALID_AUDIENCE, issuer=IAP_VALID_ISSUER, header=header) public_key = get_public_key_for_jwt(test_jwt, IAP_PUBLIC_KEY_URL) self.assertRaises( JwtValidationError, validate_jwt, test_jwt, public_key, IAP_JWT_ALGORITHM, IAP_VALID_AUDIENCE, IAP_VALID_ISSUER)
def test_invalid_audience_raises_jwt_validation_error(self): """Test to validate a JWT with wrong audience.""" test_jwt = create_mock_jwt(MOCK_EC_PRIVATE_KEY, algorithm=IAP_JWT_ALGORITHM, key_id='iap_1234', audience=IAP_VALID_AUDIENCE, issuer=IAP_VALID_ISSUER) public_key = get_public_key_for_jwt(test_jwt, IAP_PUBLIC_KEY_URL) self.assertRaises(JwtValidationError, decode_jwt, test_jwt, public_key, IAP_JWT_ALGORITHM, IAP_INVALID_AUDIENCE)
def test_valid_jwt(self): """Test to validate a valid JWT.""" test_jwt = create_mock_jwt( MOCK_EC_PRIVATE_KEY, algorithm=IAP_JWT_ALGORITHM, key_id='iap_1234', audience=IAP_VALID_AUDIENCE, issuer=IAP_VALID_ISSUER) public_key = get_public_key_for_jwt(test_jwt, IAP_PUBLIC_KEY_URL) valid_jwt = validate_jwt( test_jwt, public_key, IAP_JWT_ALGORITHM, IAP_VALID_AUDIENCE, IAP_VALID_ISSUER) self.assertIsInstance(valid_jwt, dict) self.assertEqual(valid_jwt.get('email'), '*****@*****.**')
def test_valid_oidc_jwt(self): """Test to validate a valid OpenID Connect JWT.""" test_jwt = create_mock_jwt( MOCK_RSA_PRIVATE_KEY_PEM, algorithm=OIDC_JWT_ALGORITHM, key_id='oidc_1234', audience=OIDC_VALID_AUDIENCE, issuer=OIDC_VALID_ISSUER) public_key = get_public_key_for_jwt(test_jwt, OIDC_PUBLIC_KEY_URL) valid_jwt = validate_jwt( test_jwt, public_key, OIDC_JWT_ALGORITHM, OIDC_VALID_AUDIENCE, OIDC_VALID_ISSUER) self.assertIsInstance(valid_jwt, dict) self.assertEqual(valid_jwt.get('email'), '*****@*****.**')
def _test_header_raises_jwt_validation_error(self, header): """Test JWT with supplied header.""" test_jwt = create_mock_jwt(MOCK_EC_PRIVATE_KEY, algorithm=IAP_JWT_ALGORITHM, key_id='iap_1234', audience=IAP_VALID_AUDIENCE, issuer=IAP_VALID_ISSUER, header=header) public_key = get_public_key_for_jwt(test_jwt, IAP_PUBLIC_KEY_URL) self.assertRaises(JwtValidationError, validate_jwt, test_jwt, public_key, IAP_JWT_ALGORITHM, IAP_VALID_AUDIENCE, IAP_VALID_ISSUER)
def test_valid_oidc_jwt(self): """Test to validate a valid OpenID Connect JWT.""" test_jwt = create_mock_jwt(MOCK_RSA_PRIVATE_KEY_PEM, algorithm=OIDC_JWT_ALGORITHM, key_id='oidc_1234', audience=OIDC_VALID_AUDIENCE, issuer=OIDC_VALID_ISSUER) public_key = get_public_key_for_jwt(test_jwt, OIDC_PUBLIC_KEY_URL) valid_jwt = validate_jwt(test_jwt, public_key, OIDC_JWT_ALGORITHM, OIDC_VALID_AUDIENCE, OIDC_VALID_ISSUER) self.assertIsInstance(valid_jwt, dict) self.assertEqual(valid_jwt.get('email'), '*****@*****.**')
def test_valid_jwt(self): """Test to validate a valid JWT.""" test_jwt = create_mock_jwt(MOCK_EC_PRIVATE_KEY, algorithm=IAP_JWT_ALGORITHM, key_id='iap_1234', audience=IAP_VALID_AUDIENCE, issuer=IAP_VALID_ISSUER) public_key = get_public_key_for_jwt(test_jwt, IAP_PUBLIC_KEY_URL) valid_jwt = validate_jwt(test_jwt, public_key, IAP_JWT_ALGORITHM, IAP_VALID_AUDIENCE, IAP_VALID_ISSUER) self.assertIsInstance(valid_jwt, dict) self.assertEqual(valid_jwt.get('email'), '*****@*****.**')
def _test_payload_raises_jwt_validation_error(self, payload, domain=None): """Test JWT with supplied payload.""" test_jwt = create_mock_jwt(MOCK_EC_PRIVATE_KEY, algorithm=IAP_JWT_ALGORITHM, key_id='iap_1234', audience=IAP_VALID_AUDIENCE, issuer=IAP_VALID_ISSUER, payload=payload) public_key = get_public_key_for_jwt(test_jwt, IAP_PUBLIC_KEY_URL) with self.assertRaises(JwtValidationError): test_decoded_jwt = decode_jwt(test_jwt, public_key, IAP_JWT_ALGORITHM, IAP_VALID_AUDIENCE) validate_jwt(test_decoded_jwt, IAP_VALID_ISSUER, domain)
def test_valid_domain(self): """Test to validate a JWT with domain.""" valid_domain = 'example.com' test_jwt = create_mock_jwt(MOCK_EC_PRIVATE_KEY, algorithm=IAP_JWT_ALGORITHM, key_id='iap_1234', audience=IAP_VALID_AUDIENCE, issuer=IAP_VALID_ISSUER) public_key = get_public_key_for_jwt(test_jwt, IAP_PUBLIC_KEY_URL) test_decoded_jwt = decode_jwt(test_jwt, public_key, IAP_JWT_ALGORITHM, IAP_VALID_AUDIENCE) validate_jwt(test_decoded_jwt, IAP_VALID_ISSUER, valid_domain) self.assertIsInstance(test_decoded_jwt, dict) self.assertEqual(test_decoded_jwt.get('hd'), 'example.com')
def test_invalid_algorithm_raises_jwt_validation_error(self): """Test to validate a JWT with invalid algorithm.""" # Hard coding a JWT with MOCK_EC_PRIVATE_KEY as key and "HS256" as alg # in the header. Newer versions of PyJWT won't encode JWTs with this # configuration. test_jwt = ( b'eyJ0eXAiOiJKV1QiLCJhbGciOiJFUzI1NiIsImtpZCI6ImlhcF8xMjM0In0.eyJzd' b'WIiOiIxMjM0NTY3ODkwIiwiZW1haWwiOiJ0ZXN0QGV4YW1wbGUuY29tIiwiaGQiOi' b'JleGFtcGxlLmNvbSIsImlhdCI6MTY1NzU5NDE4NSwiZXhwIjoxNjU3NTk0Nzg1LCJ' b'hdWQiOiIvcHJvamVjdHMvMTIzNC9nbG9iYWwvYmFja2VuZFNlcnZpY2VzLzEyMzQi' b'LCJpc3MiOiJodHRwczovL2Nsb3VkLmdvb2dsZS5jb20vaWFwIn0.s49RJ_Fhoaqpo' b'GHfXTjEi5Ma373Zr69BU8rG3ZObNq0EJJXGgBq4E48LwaD_WMR4z3dMxv-UkcShmU' b'3p6qnv7w') public_key = get_public_key_for_jwt(test_jwt, IAP_PUBLIC_KEY_URL) with self.assertRaises(JwtValidationError): test_decoded_jwt = decode_jwt(test_jwt, public_key, IAP_JWT_ALGORITHM, IAP_VALID_AUDIENCE) validate_jwt(test_decoded_jwt, IAP_VALID_ISSUER)
def login(): """Handler for the login page view. There are three ways of authentication. 1) Google Cloud Identity-Aware Proxy. 2) If Single Sign On (SSO) is enabled in the configuration and the environment variable is present, e.g. REMOTE_USER then the system will get or create the user object and setup a session for the user. 3) Local authentication is used if SSO login is not enabled. This will authenticate the user against the local user database. Returns: Redirect if authentication is successful or template with context otherwise. """ # Google OpenID Connect authentication. if current_app.config.get('GOOGLE_OIDC_ENABLED', False): hosted_domain = current_app.config.get('GOOGLE_OIDC_HOSTED_DOMAIN') return redirect(get_oauth2_authorize_url(hosted_domain)) # Google Identity-Aware Proxy authentication (using JSON Web Tokens) if current_app.config.get('GOOGLE_IAP_ENABLED', False): encoded_jwt = request.environ.get('HTTP_X_GOOG_IAP_JWT_ASSERTION', None) if encoded_jwt: expected_audience = current_app.config.get('GOOGLE_IAP_AUDIENCE') expected_issuer = current_app.config.get('GOOGLE_IAP_ISSUER') algorithm = current_app.config.get('GOOGLE_IAP_ALGORITHM') url = current_app.config.get('GOOGLE_IAP_PUBLIC_KEY_URL') try: public_key = get_public_key_for_jwt(encoded_jwt, url) decoded_jwt = decode_jwt(encoded_jwt, public_key, algorithm, expected_audience) validate_jwt(decoded_jwt, expected_issuer) email = decoded_jwt.get('email') if email: user = User.get_or_create(username=email, name=email) login_user(user) except (ImportError, NameError, UnboundLocalError): raise except (JwtValidationError, JwtKeyError, Exception) as e: # pylint: disable=broad-except current_app.logger.error('{}'.format(e)) # SSO login based on environment variable, e.g. REMOTE_USER. if current_app.config.get('SSO_ENABLED', False): remote_user_env = current_app.config.get('SSO_USER_ENV_VARIABLE', 'REMOTE_USER') sso_group_env = current_app.config.get('SSO_GROUP_ENV_VARIABLE', None) remote_user = request.environ.get(remote_user_env, None) if remote_user: user = User.get_or_create(username=remote_user, name=remote_user) login_user(user) # If we get groups from the SSO system create the group(s) in # Timesketch and add/remove the user from it. if sso_group_env: groups_string = request.environ.get(sso_group_env, '') separator = current_app.config.get('SSO_GROUP_SEPARATOR', ';') not_member_sign = current_app.config.get( 'SSO_GROUP_NOT_MEMBER_SIGN', None) for group_name in groups_string.split(separator): remove_group = False if not_member_sign: remove_group = group_name.startswith(not_member_sign) group_name = group_name.lstrip(not_member_sign) # Get or create the group in the Timesketch database. group = Group.get_or_create(name=group_name) if remove_group: if group in user.groups: user.groups.remove(group) else: if group not in user.groups: user.groups.append(group) # Commit the changes to the database. db_session.commit() # Login form POST form = UsernamePasswordForm() if form.validate_on_submit: user = User.query.filter_by(username=form.username.data).first() if user: if user.check_password(plaintext=form.password.data): login_user(user) # Log the user in and setup the session. if current_user.is_authenticated: return redirect(request.args.get('next') or '/') return render_template('login.html', form=form)
def google_openid_connect(): """Handler for the Google OpenID Connect callback. Reference: https://developers.google.com/identity/protocols/OpenIDConnect Returns: Redirect response. """ error = request.args.get('error', None) if error: current_app.logger.error('OAuth2 flow error: {}'.format(error)) return abort(HTTP_STATUS_CODE_BAD_REQUEST, 'OAuth2 flow error: {0!s}'.format(error)) try: code = request.args['code'] client_csrf_token = request.args.get('state') server_csrf_token = session[CSRF_KEY] except KeyError as e: return abort(HTTP_STATUS_CODE_BAD_REQUEST, 'Client CSRF error, no CSRF key stored') if client_csrf_token != server_csrf_token: return abort(HTTP_STATUS_CODE_BAD_REQUEST, 'Invalid CSRF token') try: encoded_jwt = get_encoded_jwt_over_https(code) except JwtFetchError as e: return abort(HTTP_STATUS_CODE_BAD_REQUEST, 'Jwt Fetch error, {0!s}'.format(e)) try: discovery_document = get_oauth2_discovery_document() except DiscoveryDocumentError as e: return abort( HTTP_STATUS_CODE_BAD_REQUEST, 'Unable to discover document, with error: {0!s}'.format(e)) algorithm = discovery_document['id_token_signing_alg_values_supported'][0] expected_audience = current_app.config.get('GOOGLE_OIDC_CLIENT_ID') expected_domain = current_app.config.get('GOOGLE_OIDC_HOSTED_DOMAIN') expected_issuer = discovery_document['issuer'] # Fetch the public key and try to validate the JWT. try: public_key = get_public_key_for_jwt(encoded_jwt, discovery_document['jwks_uri']) decoded_jwt = decode_jwt(encoded_jwt, public_key, algorithm, expected_audience) validate_jwt(decoded_jwt, expected_issuer, expected_domain) except (JwtValidationError, JwtKeyError) as e: current_app.logger.error('{}'.format(e)) return abort(HTTP_STATUS_CODE_UNAUTHORIZED, 'Unable to validate request, with error: {0!s}'.format(e)) validated_email = decoded_jwt.get('email') user_whitelist = current_app.config.get('GOOGLE_OIDC_USER_WHITELIST') # Check if the authenticating user is on the whitelist. if user_whitelist: if validated_email not in user_whitelist: return abort(HTTP_STATUS_CODE_UNAUTHORIZED, 'Unauthorized request, user not in whitelist') user = User.get_or_create(username=validated_email, name=validated_email) login_user(user) # Log the user in and setup the session. if current_user.is_authenticated: return redirect(request.args.get('next') or '/') return abort(HTTP_STATUS_CODE_BAD_REQUEST, 'User is not authenticated.')
def login(): """Handler for the login page view. There are three ways of authentication. 1) Google Cloud Identity-Aware Proxy. 2) If Single Sign On (SSO) is enabled in the configuration and the environment variable is present, e.g. REMOTE_USER then the system will get or create the user object and setup a session for the user. 3) Local authentication is used if SSO login is not enabled. This will authenticate the user against the local user database. Returns: Redirect if authentication is successful or template with context otherwise. """ # Google OpenID Connect authentication. if current_app.config.get('GOOGLE_OIDC_ENABLED', False): hosted_domain = current_app.config.get('GOOGLE_OIDC_HOSTED_DOMAIN') return redirect(get_oauth2_authorize_url(hosted_domain)) # Google Identity-Aware Proxy authentication (using JSON Web Tokens) if current_app.config.get('GOOGLE_IAP_ENABLED', False): encoded_jwt = request.environ.get( 'HTTP_X_GOOG_IAP_JWT_ASSERTION', None) if encoded_jwt: expected_audience = current_app.config.get('GOOGLE_IAP_AUDIENCE') expected_issuer = current_app.config.get('GOOGLE_IAP_ISSUER') algorithm = current_app.config.get('GOOGLE_IAP_ALGORITHM') url = current_app.config.get('GOOGLE_IAP_PUBLIC_KEY_URL') try: public_key = get_public_key_for_jwt(encoded_jwt, url) validated_jwt = validate_jwt( encoded_jwt, public_key, algorithm, expected_audience, expected_issuer) email = validated_jwt.get('email') if email: user = User.get_or_create(username=email, name=email) login_user(user) except (ImportError, NameError, UnboundLocalError): # pylint: disable=try-except-raise raise except (JwtValidationError, JwtKeyError, Exception) as e: # pylint: disable=broad-except current_app.logger.error('{}'.format(e)) # SSO login based on environment variable, e.g. REMOTE_USER. if current_app.config.get('SSO_ENABLED', False): remote_user_env = current_app.config.get('SSO_USER_ENV_VARIABLE', 'REMOTE_USER') sso_group_env = current_app.config.get('SSO_GROUP_ENV_VARIABLE', None) remote_user = request.environ.get(remote_user_env, None) if remote_user: user = User.get_or_create(username=remote_user, name=remote_user) login_user(user) # If we get groups from the SSO system create the group(s) in # Timesketch and add/remove the user from it. if sso_group_env: groups_string = request.environ.get(sso_group_env, '') separator = current_app.config.get('SSO_GROUP_SEPARATOR', ';') not_member_sign = current_app.config.get( 'SSO_GROUP_NOT_MEMBER_SIGN', None) for group_name in groups_string.split(separator): remove_group = False if not_member_sign: remove_group = group_name.startswith(not_member_sign) group_name = group_name.lstrip(not_member_sign) # Get or create the group in the Timesketch database. group = Group.get_or_create(name=group_name) if remove_group: if group in user.groups: user.groups.remove(group) else: if group not in user.groups: user.groups.append(group) # Commit the changes to the database. db_session.commit() # Login form POST form = UsernamePasswordForm() if form.validate_on_submit: user = User.query.filter_by(username=form.username.data).first() if user: if user.check_password(plaintext=form.password.data): login_user(user) # Log the user in and setup the session. if current_user.is_authenticated: return redirect(request.args.get('next') or '/') return render_template('user/login.html', form=form)
def google_openid_connect(): """Handler for the Google OpenID Connect callback. Reference: https://developers.google.com/identity/protocols/OpenIDConnect Returns: Redirect response. """ error = request.args.get('error', None) if error: current_app.logger.error('OAuth2 flow error: {}'.format(error)) return abort(HTTP_STATUS_CODE_BAD_REQUEST) try: code = request.args['code'] client_csrf_token = request.args.get('state') server_csrf_token = session[CSRF_KEY] except KeyError: return abort(HTTP_STATUS_CODE_BAD_REQUEST) if client_csrf_token != server_csrf_token: return abort(HTTP_STATUS_CODE_BAD_REQUEST, 'Invalid CSRF token') try: encoded_jwt = get_encoded_jwt_over_https(code) except JwtFetchError: return abort(HTTP_STATUS_CODE_BAD_REQUEST) try: discovery_document = get_oauth2_discovery_document() except DiscoveryDocumentError: return abort(HTTP_STATUS_CODE_BAD_REQUEST) algorithm = discovery_document['id_token_signing_alg_values_supported'][0] expected_audience = current_app.config.get('GOOGLE_OIDC_CLIENT_ID') expected_domain = current_app.config.get('GOOGLE_OIDC_HOSTED_DOMAIN') expected_issuer = discovery_document['issuer'] # Fetch the public key and try to validate the JWT. try: public_key = get_public_key_for_jwt( encoded_jwt, discovery_document['jwks_uri']) validated_jwt = validate_jwt( encoded_jwt, public_key, algorithm, expected_audience, expected_issuer, expected_domain) except (JwtValidationError, JwtKeyError) as e: current_app.logger.error('{}'.format(e)) return abort(HTTP_STATUS_CODE_UNAUTHORIZED) validated_email = validated_jwt.get('email') user_whitelist = current_app.config.get('GOOGLE_OIDC_USER_WHITELIST') # Check if the authenticating user is on the whitelist. if user_whitelist: if validated_email not in user_whitelist: return abort(HTTP_STATUS_CODE_UNAUTHORIZED) user = User.get_or_create(username=validated_email, name=validated_email) login_user(user) # Log the user in and setup the session. if current_user.is_authenticated: return redirect(request.args.get('next') or '/') return abort(HTTP_STATUS_CODE_BAD_REQUEST)
def google_openid_connect(): """Handler for the Google OpenID Connect callback. Reference: https://developers.google.com/identity/protocols/OpenIDConnect Returns: Redirect response. """ error = request.args.get("error", None) if error: current_app.logger.error("OAuth2 flow error: {}".format(error)) return abort(HTTP_STATUS_CODE_BAD_REQUEST, "OAuth2 flow error: {0!s}".format(error)) try: code = request.args["code"] client_csrf_token = request.args.get("state") server_csrf_token = session[CSRF_KEY] except KeyError as e: return abort(HTTP_STATUS_CODE_BAD_REQUEST, "Client CSRF error, no CSRF key stored") if client_csrf_token != server_csrf_token: return abort(HTTP_STATUS_CODE_BAD_REQUEST, "Invalid CSRF token") try: encoded_jwt = get_encoded_jwt_over_https(code) except JwtFetchError as e: return abort(HTTP_STATUS_CODE_BAD_REQUEST, "Jwt Fetch error, {0!s}".format(e)) try: discovery_document = get_oauth2_discovery_document() except DiscoveryDocumentError as e: return abort( HTTP_STATUS_CODE_BAD_REQUEST, "Unable to discover document, with error: {0!s}".format(e), ) algorithm = discovery_document["id_token_signing_alg_values_supported"][0] expected_audience = current_app.config.get("GOOGLE_OIDC_CLIENT_ID") expected_domain = current_app.config.get("GOOGLE_OIDC_HOSTED_DOMAIN") expected_issuer = discovery_document["issuer"] # Fetch the public key and try to validate the JWT. try: public_key = get_public_key_for_jwt(encoded_jwt, discovery_document["jwks_uri"]) decoded_jwt = decode_jwt(encoded_jwt, public_key, algorithm, expected_audience) validate_jwt(decoded_jwt, expected_issuer, expected_domain) except (JwtValidationError, JwtKeyError) as e: current_app.logger.error("{}".format(e)) return abort( HTTP_STATUS_CODE_UNAUTHORIZED, "Unable to validate request, with error: {0!s}".format(e), ) validated_email = decoded_jwt.get("email") allowed_users = current_app.config.get("GOOGLE_OIDC_ALLOWED_USERS") # Check if the authenticating user is allowed. if allowed_users: if validated_email not in allowed_users: return abort(HTTP_STATUS_CODE_UNAUTHORIZED, "Unauthorized request, user not allowed") user = User.get_or_create(username=validated_email, name=validated_email) login_user(user) # Log the user in and setup the session. if current_user.is_authenticated: return redirect(request.args.get("next") or "/") return abort(HTTP_STATUS_CODE_BAD_REQUEST, "User is not authenticated.")