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

Initial python3 compatible changes. #12

Open
wants to merge 5 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
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
# lp-aws-saml

This repository contains the LastPass AWS SAML login tool.
This repository contains the LastPass AWS SAML login tool. Compatible with Python3.

If you are using LastPass Enterprise SAML with AWS, then this script eases the
process of using the AWS CLI utility. It retrieves a SAML assertion from
LastPass and then converts it into credentials for use with ```aws```.
If you are using LastPass Enterprise SAML with AWS, then please raise a ticket asking LastPass to merge my changes with their master branch.

It retrieves a SAML assertion from LastPass and then converts it into credentials for use with ```aws```.

## Requirements

You will need python with the Amazon boto3 module and the AWS CLI tool.
The latter may be installed with pip:
```
# pip install boto3 awscli
# pip3 install boto3 awscli requests lastpass-python
```
On recent Mac platforms, you may need to pass --ignore-installed:

```
# pip install boto3 awscli --ignore-installed
# pip3 install boto3 awscli requests lastpass-python --ignore-installed
```

Make sure you're using the correct pip3, there can be differences between the OS installed pip and what you install with hombrew. Check with ```which``` that python3 and pip3 are in the same directory.

You will also need to have integrated AWS with LastPass SAML through the
AWS and LastPass management consoles. See the SAML setup instructions on the
LastPass AWS configuration page for more information.
Expand All @@ -41,9 +43,10 @@ You may now invoke the aws CLI tool as follows:

aws --profile [email protected] [...]

This token expires in one hour.
This token expires in 900s.
```

The duration is dependent on the configuration of the role, if you do not have permission to query the maximum session duration of the role you are using then 900s will be chosen.

Once completed, the ```aws``` tool may be used to execute commands as that
user by specifying the appropriate profile.

151 changes: 45 additions & 106 deletions lp-aws-saml.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#!/usr/bin/env python
#!/usr/bin/env python3
# -*- coding: utf8 -*-
#
# Amazon Web Services CLI - LastPass SAML integration
Expand Down Expand Up @@ -42,6 +42,8 @@
from six.moves import configparser

from getpass import getpass
from lastpass import fetcher


LASTPASS_SERVER = 'https://lastpass.com'

Expand Down Expand Up @@ -83,92 +85,6 @@ def extract_form(html):
return form


def xorbytes(a, b):
""" xor all bytes in a string """
return ''.join(map(lambda x, y: chr(ord(x) ^ ord(y)), a, b))


def prf(h, data):
""" internal hash update for pbkdf2/hmac-sha256 """
hm = h.copy()
hm.update(data)
return hm.digest()


def pbkdf2(password, salt, rounds, length):
"""
PBKDF2-SHA256 password derivation.
"""
key = ''
h = hmac.new(password, None, hashlib.sha256)
for block in range(0, (length + 31) / 32):
ib = hval = prf(h, salt + pack(">I", block + 1))

for i in range(1, rounds):
hval = prf(h, hval)
ib = xorbytes(ib, hval)

key = key + ib
return binascii.hexlify(key[0:length])


def lastpass_login_hash(username, password, iterations):
"""
Determine the number of PBKDF2 iterations needed for a user.
"""
key = binascii.unhexlify(pbkdf2(password, username, iterations, 32))
result = pbkdf2(key, password, 1, 32)
return result


def lastpass_iterations(session, username):
"""
Determine the number of PBKDF2 iterations needed for a user.
"""
iterations = 5000
lp_iterations_page = '%s/iterations.php' % LASTPASS_SERVER
params = {
'email': username
}
r = session.post(lp_iterations_page, data=params, verify=should_verify())
if r.status_code == 200:
iterations = int(r.text)

return iterations


def lastpass_login(session, username, password, otp = None):
"""
Log into LastPass with a given username and password.
"""
logger.debug("logging into lastpass as %s" % username)
iterations = lastpass_iterations(session, username)

lp_login_page = '%s/login.php' % LASTPASS_SERVER

params = {
'method': 'web',
'xml': '1',
'username': username,
'hash': lastpass_login_hash(username, password, iterations),
'iterations': iterations
}
if otp is not None:
params['otp'] = otp

r = session.post(lp_login_page, data=params, verify=should_verify())
r.raise_for_status()

doc = ET.fromstring(r.text)
error = doc.find("error")
if error is not None:
cause = error.get('cause')
if cause == 'googleauthrequired':
raise MfaRequiredException('Need MFA for this login')
else:
reason = error.get('message')
raise ValueError("Could not login to lastpass: %s" % reason)


def get_saml_token(session, username, password, saml_cfg_id):
"""
Expand All @@ -180,7 +96,7 @@ def get_saml_token(session, username, password, saml_cfg_id):
# now logged in, grab the SAML token from the IdP-initiated login
idp_login = '%s/saml/launch/cfg/%d' % (LASTPASS_SERVER, saml_cfg_id)

r = session.get(idp_login, verify=should_verify())
r = requests.get(idp_login,cookies={'PHPSESSID': session.id}, verify=should_verify())

form = extract_form(r.text)
if not form['action']:
Expand Down Expand Up @@ -233,10 +149,10 @@ def prompt_for_role(roles):
if len(roles) == 1:
return roles[0]

print 'Please select a role:'
print ('Please select a role:')
count = 1
for r in roles:
print ' %d) %s' % (count, r[0])
print (' %d) %s' % (count, r[0]))
count = count + 1

choice = 0
Expand All @@ -249,12 +165,35 @@ def prompt_for_role(roles):
return roles[choice - 1]


def aws_assume_role(session, assertion, role_arn, principal_arn):
client = boto3.client('sts')
return client.assume_role_with_saml(
def aws_assume_role(assertion, role_arn, principal_arn):
client = boto3.client('sts',
aws_access_key_id="",
aws_secret_access_key="",
aws_session_token="",
)
short_creds = client.assume_role_with_saml(
RoleArn=role_arn,
PrincipalArn=principal_arn,
SAMLAssertion=b64encode(assertion).decode("utf-8"),
DurationSeconds=900)
credentials = short_creds['Credentials']
role_name = role_arn.rsplit('/', 1)[1]
iam = boto3.resource(
'iam',
aws_access_key_id=credentials['AccessKeyId'],
aws_secret_access_key=credentials['SecretAccessKey'],
aws_session_token=credentials['SessionToken'],
)
try:
duration = iam.Role(role_name).max_session_duration
except:
return [short_creds,900]

return [client.assume_role_with_saml(
RoleArn=role_arn,
PrincipalArn=principal_arn,
SAMLAssertion=b64encode(assertion))
SAMLAssertion=b64encode(assertion).decode("utf-8"),
DurationSeconds=duration) , duration ]


def aws_set_profile(profile_name, response):
Expand Down Expand Up @@ -299,39 +238,39 @@ def main():
help='the name of AWS profile to save the data in (default username)')

args = parser.parse_args()

username = args.username
saml_cfg_id = args.saml_config_id

if args.profile_name is not None:
profile_name = args.profile_name
else:
profile_name = username

password = getpass()

session = requests.Session()

try:
lastpass_login(session, username, password)
except MfaRequiredException:
session = fetcher.login(username, password)
except:
otp = input("OTP: ")
lastpass_login(session, username, password, otp)
session = fetcher.login(username, password, otp)

assertion = get_saml_token(session, username, password, saml_cfg_id)
roles = get_saml_aws_roles(assertion)
user = get_saml_nameid(assertion)

role = prompt_for_role(roles)
response = aws_assume_role(assertion, role[0], role[1])
aws_set_profile(profile_name, response[0])

response = aws_assume_role(session, assertion, role[0], role[1])
aws_set_profile(profile_name, response)

print "A new AWS CLI profile '%s' has been added." % profile_name
print "You may now invoke the aws CLI tool as follows:"
print ("A new AWS CLI profile '%s' has been added." % profile_name)
print ("You may now invoke the aws CLI tool as follows:")
print
print " aws --profile %s [...] " % profile_name
print (" aws --profile %s [...] " % profile_name)
print
print "This token expires in one hour."
print ("This profile is valid for %ds" % response[1] )


if __name__ == "__main__":
Expand Down