Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support authentication using a JSON web token (JWT) #93 #100

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 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,34 @@ 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),
options={'verify_at_hash': False}
)
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
5 changes: 3 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@
'boto3',
'cryptography',
'ipaddress',
'marshmallow',
'kmsauth'
'marshmallow<3',
'kmsauth',
'python-jose[cryptography]'
],
extras_require={
'tests': [
Expand Down
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