Skip to content

Commit

Permalink
Support authentication using a JSON web token (JWT) Netflix#93
Browse files Browse the repository at this point in the history
* Created new configuration section for JWT Auth
 - Configure a JWK to verify a JWT signature
 - Configure requried signature algorithms
 - Configure required audience and issuer claims
 - Configure name of username claim
* Added code block in lambda_handler_user to validate JWT if configured
 - Require remote_usernames == bastion_user
 - Require valid JWT signature, expiry, and signature algorithm
 - Require username_claim in JWT
 - Require username_claim == bastion_user
* Added unit tests for config and JWT validation
  • Loading branch information
stoggi committed Aug 7, 2019
1 parent 80f3c1b commit cce9d5c
Show file tree
Hide file tree
Showing 9 changed files with 424 additions and 3 deletions.
36 changes: 36 additions & 0 deletions bless/aws_lambda/bless_lambda_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,13 @@
KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION, \
VALIDATE_REMOTE_USERNAMES_AGAINST_IAM_GROUPS_OPTION, \
KMSAUTH_SERVICE_ID_OPTION, \
JWTAUTH_SECTION, \
JWTAUTH_USEJWTAUTH_OPTION, \
JWTAUTH_SIGNATURE_JWK_OPTION, \
JWTAUTH_AUDIENCE_OPTION, \
JWTAUTH_ISSUER_OPTION, \
JWTAUTH_SIGNATURE_ALGORITHM_OPTION, \
JWTAUTH_USERNAME_CLAIM_OPTION, \
TEST_USER_OPTION, \
CERTIFICATE_EXTENSIONS_OPTION, \
REMOTE_USERNAMES_VALIDATION_OPTION, \
Expand All @@ -29,6 +36,8 @@
from bless.ssh.certificates.ssh_certificate_builder_factory import get_ssh_certificate_builder
from kmsauth import KMSTokenValidator, TokenValidationError
from marshmallow.exceptions import ValidationError
from jose import jwt
from jose.exceptions import JWTError


def lambda_handler_user(
Expand Down Expand Up @@ -159,6 +168,33 @@ def lambda_handler_user(
else:
return error_response('InputValidationError', 'Invalid request, missing kmsauth token')

# Authenticate the user with JWT, if key is configured
if config.getboolean(JWTAUTH_SECTION, JWTAUTH_USEJWTAUTH_OPTION):
if request.jwtauth_token:

if request.remote_usernames != request.bastion_user:
return error_response('JWTAuthValidationError',
'remote_usernames must be the same as bastion_user')
try:
claims = jwt.decode(
request.jwtauth_token,
config.get(JWTAUTH_SECTION, JWTAUTH_SIGNATURE_JWK_OPTION),
audience=config.get(JWTAUTH_SECTION, JWTAUTH_AUDIENCE_OPTION),
issuer=config.get(JWTAUTH_SECTION, JWTAUTH_ISSUER_OPTION),
algorithms=config.get(JWTAUTH_SECTION, JWTAUTH_SIGNATURE_ALGORITHM_OPTION)
)
username_claim = config.get(JWTAUTH_SECTION, JWTAUTH_USERNAME_CLAIM_OPTION)
if username_claim not in claims.keys():
return error_response('JWTAuthValidationError',
'missing {} claim in jwt'.format(username_claim))
if request.bastion_user != claims[username_claim]:
return error_response('JWTAuthValidationError',
'bastion_user must equal {} claim in jwt'.format(username_claim))
except JWTError as e:
return error_response('JWTAuthValidationError', str(e))
else:
return error_response('InputValidationError', 'Invalid request, missing jwtauth_token')

# Build the cert
ca = get_ssh_certificate_authority(ca_private_key, ca_private_key_password)
cert_builder = get_ssh_certificate_builder(ca, SSHCertificateType.USER,
Expand Down
28 changes: 28 additions & 0 deletions bless/config/bless_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,25 @@
KMSAUTH_SERVICE_ID_OPTION = 'kmsauth_serviceid'
KMSAUTH_SERVICE_ID_DEFAULT = None

JWTAUTH_SECTION = 'JWT Auth'
JWTAUTH_USEJWTAUTH_OPTION = 'use_jwtauth'
JWTAUTH_USEJWTAUTH_DEFAULT = 'False'

JWTAUTH_SIGNATURE_JWK_OPTION = 'jwtauth_signature_jwk'
JWTAUTH_SIGNATURE_JWK_DEFAULT = ''

JWTAUTH_SIGNATURE_ALGORITHM_OPTION = 'jwtauth_signature_algorithm'
JWTAUTH_SIGNATURE_ALGORITHM_DEFAULT = 'RS256'

JWTAUTH_ISSUER_OPTION = 'jwtauth_issuer'
JWTAUTH_ISSUER_DEFAULT = ''

JWTAUTH_AUDIENCE_OPTION = 'jwtauth_audience'
JWTAUTH_AUDIENCE_DEFAULT = ''

JWTAUTH_USERNAME_CLAIM_OPTION = 'jwtauth_username_claim'
JWTAUTH_USERNAME_CLAIM_DEFAULT = 'email'

USERNAME_VALIDATION_OPTION = 'username_validation'
USERNAME_VALIDATION_DEFAULT = 'useradd'

Expand Down Expand Up @@ -102,6 +121,12 @@ def __init__(self, aws_region, config_file):
KMSAUTH_KEY_ID_OPTION: KMSAUTH_KEY_ID_DEFAULT,
KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION: KMSAUTH_REMOTE_USERNAMES_ALLOWED_OPTION_DEFAULT,
KMSAUTH_USEKMSAUTH_OPTION: KMSAUTH_USEKMSAUTH_DEFAULT,
JWTAUTH_USEJWTAUTH_OPTION: JWTAUTH_USEJWTAUTH_DEFAULT,
JWTAUTH_SIGNATURE_JWK_OPTION: JWTAUTH_SIGNATURE_JWK_DEFAULT,
JWTAUTH_SIGNATURE_ALGORITHM_OPTION: JWTAUTH_SIGNATURE_ALGORITHM_DEFAULT,
JWTAUTH_ISSUER_OPTION: JWTAUTH_ISSUER_DEFAULT,
JWTAUTH_AUDIENCE_OPTION: JWTAUTH_AUDIENCE_DEFAULT,
JWTAUTH_USERNAME_CLAIM_OPTION: JWTAUTH_USERNAME_CLAIM_DEFAULT,
CERTIFICATE_EXTENSIONS_OPTION: CERTIFICATE_EXTENSIONS_DEFAULT,
USERNAME_VALIDATION_OPTION: USERNAME_VALIDATION_DEFAULT,
REMOTE_USERNAMES_VALIDATION_OPTION: REMOTE_USERNAMES_VALIDATION_DEFAULT,
Expand All @@ -125,6 +150,9 @@ def __init__(self, aws_region, config_file):
if not self.has_section(KMSAUTH_SECTION):
self.add_section(KMSAUTH_SECTION)

if not self.has_section(JWTAUTH_SECTION):
self.add_section(JWTAUTH_SECTION)

if not self.has_option(BLESS_CA_SECTION, self.aws_region + REGION_PASSWORD_OPTION_SUFFIX):
if not self.has_option(BLESS_CA_SECTION, 'default' + REGION_PASSWORD_OPTION_SUFFIX):
raise ValueError("No Region Specific And No Default Password Provided.")
Expand Down
21 changes: 21 additions & 0 deletions bless/config/bless_deploy_example.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,24 @@ ca_private_key_file = <INSERT_YOUR_ENCRYPTED_PEM_FILE_NAME>
# the group name is "ssh-{}".format(remote_username), but that can be changed here. The groups must have a
# consistent naming scheme and must all contain the remote_username once. For example, ssh-ubuntu.
# kmsauth_iam_group_name_format = ssh-{}

# This section is optional
[JWT Auth]
# Enable authentication via a JWT, to ensure a username matches a claim in a valid JWT
# use_jwtauth = True

# The JWK containing the public key used to verify the JWT signature, in JSON Web Key format https://tools.ietf.org/html/rfc7517
# jwtauth_signature_jwk = {"kty": "RSA","e": "...","use": "sig","kid": "...","alg": "RS256","n": "..."}

# The expected signature algorithm. JWTs signed with a different algorithm will be rejected.
# jwtauth_signature_algorithm = RS256

# The issuer present as the iss claim in the JWT. This must match the issuer from your identity provider.
# jwtauth_issuer = https://accounts.google.com

# The audience present as the aud claim in the JWT. This must match the audience from your identity provider.
# jwtauth_audience = 1234567890-1234567890abcd.apps.googleusercontent.com

# The claim in the JWT that contains the username. This is compared to the requested username, and will be set in the
# principals list of the SSH certificate.
# jwtauth_username_claim = email
5 changes: 4 additions & 1 deletion bless/request/bless_request_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class BlessUserSchema(Schema):
public_key_to_sign = fields.Str(validate=validate_ssh_public_key, required=True)
remote_usernames = fields.Str(required=True)
kmsauth_token = fields.Str(required=False)
jwtauth_token = fields.Str(required=False)

@validates_schema(pass_original=True)
def check_unknown_fields(self, data, original_data):
Expand Down Expand Up @@ -125,7 +126,7 @@ def validate_remote_usernames(self, remote_usernames):

class BlessUserRequest:
def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_key_to_sign,
remote_usernames, kmsauth_token=None):
remote_usernames, kmsauth_token=None, jwtauth_token=None):
"""
A BlessRequest must have the following key value pairs to be valid.
:param bastion_ips: The source IPs where the SSH connection will be initiated from. This is
Expand All @@ -138,6 +139,7 @@ def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_k
:param remote_usernames: Comma-separated list of username(s) or authorized principals on the remote
server that will be used in the SSH request. This is enforced in the issued certificate.
:param kmsauth_token: An optional kms auth token to authenticate the user.
:param jwtauth_token: An optional jwt token to authenticate the user.
"""
self.bastion_ips = bastion_ips
self.bastion_user = bastion_user
Expand All @@ -146,6 +148,7 @@ def __init__(self, bastion_ips, bastion_user, bastion_user_ip, command, public_k
self.public_key_to_sign = public_key_to_sign
self.remote_usernames = remote_usernames
self.kmsauth_token = kmsauth_token
self.jwtauth_token = jwtauth_token

def __eq__(self, other):
return self.__dict__ == other.__dict__
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ python-dateutil==2.8.0
s3transfer==0.2.0
six==1.12.0
urllib3==1.24.3
python-jose[cryptography]==3.0.1
12 changes: 12 additions & 0 deletions tests/aws_lambda/bless-test-jwtauth.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[Bless CA]
ca_private_key_file = tests/aws_lambda/only-use-for-unit-tests.pem
us-east-1_password = bogus-password-for-unit-test
us-west-2_password = bogus-password-for-unit-test

[JWT Auth]
use_jwtauth = True
jwtauth_signature_jwk = {"kty": "RSA","e": "AQAB","use": "sig","kid": "key1","alg": "RS256","n": "7V4O45XkdzKedgfbg3U1X_UeGF00wQH6APcuRX_702h-3QZI4VmAbBFgDDAJgHa1wunKPUKmwfmzFodLX6Bd2UvgHtzhHDAnrHYSOpV0jci7zxUhPN84PBbNRKNG-yAGPvNk4YbCWHywz7BKmTVnG9q4KSdWaHpyhljxedMdkt2JqdTJcwaAEfqT_0A-gcBWxyCPwRJJRLColM9g6lZU7-17Y3UNHwBFC4lahfd009CXY7WMbKIJMG0LuBjsmCE4L__IlrFlevVFyA0ShDjDh07gKD-f5WJ6WdgcZOL7X3rf-DK6MRBUW4ItIpG7DVVWN0Vj6SNQT3x1kwq55mIZTw"}
jwtauth_signature_algorithm = RS256
jwtauth_issuer = https://issuer.example.com
jwtauth_audience = 6c1d8893-9240-4f87-be95-1f21ef664ce0
jwtauth_username_claim = username
Loading

0 comments on commit cce9d5c

Please sign in to comment.