Skip to content

Commit b22d7c4

Browse files
committed
Initial commit
0 parents  commit b22d7c4

File tree

4 files changed

+311
-0
lines changed

4 files changed

+311
-0
lines changed

LICENSE

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2017 Synaptic
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# saml_aws_jumcloud_api
2+
Script for generate credentian from aws-cli with saml used jumpcloud
3+
4+
## Install aws-cli
5+
6+
```
7+
# macOS
8+
brew install awscli
9+
```
10+
11+
### Config
12+
13+
Add in your file ~/.aws/credentials the next configs:
14+
15+
```
16+
[default]
17+
output =
18+
region =
19+
aws_access_key_id =
20+
aws_secret_access_key =
21+
aws_session_token =
22+
```
23+
24+
25+
26+
# python
27+
28+
```
29+
pip install requirements.txt
30+
python saml_jumpcloud_api.py
31+
# Then enter your email, password and aws region
32+
```

requirements.txt

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
appdirs==1.4.2
2+
beautifulsoup4==4.5.3
3+
boto==2.46.1
4+
bs4==0.0.1
5+
get==0.0.0
6+
packaging==16.8
7+
post==0.0.0
8+
public==0.0.0
9+
pyparsing==2.1.10
10+
query-string==0.0.0
11+
request==0.0.0
12+
requests==2.13.0
13+
setupfiles==0.0.0
14+
six==1.10.0

saml_jumpcloud_api.py

+244
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# -*- coding: utf-8 -*-
2+
# !/usr/bin/python
3+
4+
import sys
5+
import boto.sts
6+
import boto.s3
7+
import requests
8+
import getpass
9+
import ConfigParser
10+
import base64
11+
import logging
12+
import xml.etree.ElementTree as ET
13+
import re
14+
from bs4 import BeautifulSoup
15+
from os.path import expanduser
16+
from urlparse import urlparse
17+
18+
##########################################################################
19+
# Variables
20+
21+
# region: The default AWS region that this script will connect
22+
# to for all API calls
23+
region = 'us-west-2'
24+
25+
# output format: The AWS CLI output format that will be configured in the
26+
# saml profile (affects subsequent CLI calls)
27+
outputformat = 'json'
28+
29+
# awsconfigfile: The file where this script will store the temp
30+
# credentials under the saml profile
31+
awsconfigfile = '/.aws/credentials'
32+
33+
# SSL certificate verification: Whether or not strict certificate
34+
# verification is done, False should only be used for dev/test
35+
sslverification = True
36+
37+
# idpentryurl: The initial url that starts the authentication process.
38+
idpentryurl = 'https://sso.jumpcloud.com/saml2/aws'
39+
40+
# Uncomment to enable low level debugging
41+
logging.basicConfig(level=logging.INFO)
42+
43+
##########################################################################
44+
45+
# Get the federated credentials from the user
46+
print "Email:",
47+
username = raw_input()
48+
password = getpass.getpass()
49+
50+
print("1) us-east-1 / US East (N. Virginia)")
51+
print("2) us-east-2 / US East (Ohio)")
52+
print("3) us-west-1 / US West (N. California)")
53+
print("4) us-west-2 / US West (Oregon)")
54+
print("5) ca-central-1 / Canada (Central)")
55+
print("6) eu-west-1 / EU (Ireland)")
56+
print("7) eu-central-1 / EU (Frankfurt)")
57+
print("8) eu-west-2 / EU (London)")
58+
print("9) ap-northeast-1 / Asia Pacific (Tokyo)")
59+
print("10) ap-northeast-2 / Asia Pacific (Seoul)")
60+
print("11) ap-southeast-1 / Asia Pacific (Singapore)")
61+
print("12) ap-southeast-2 / Asia Pacific (Sydney)")
62+
print("13) ap-south-1 / Asia Pacific (Mumbai)")
63+
print("14) sa-east-1 / South America (São Paulo)")
64+
print("Select Region:"),
65+
num_region = raw_input()
66+
if not num_region or not num_region.isdigit():
67+
num_region = 1
68+
print("Region default: us-east-1 / US East (N. Virginia)")
69+
else:
70+
num_region = int(num_region)
71+
72+
# Initiate session handler
73+
session = requests.Session()
74+
75+
# Programmatically get the SAML assertion
76+
# Opens the initial IdP url and follows all of the HTTP302 redirects, and
77+
# gets the resulting login page
78+
formresponse = session.get(idpentryurl, verify=sslverification)
79+
# Capture the idpauthformsubmiturl, which is the final url after all the 302s
80+
idpauthformsubmiturl = formresponse.url
81+
82+
# Parse the response and extract all the necessary values
83+
# in order to build a dictionary of all of the form values the IdP expects
84+
formsoup = BeautifulSoup(formresponse.text.decode('utf8'), "html.parser")
85+
payload = {}
86+
87+
for inputtag in formsoup.find_all(re.compile('(INPUT|input)')):
88+
name = inputtag.get('name', '')
89+
value = inputtag.get('value', '')
90+
if "user" in name.lower():
91+
# Make an educated guess that this is the right field for the username
92+
payload[name] = username
93+
elif "email" in name.lower():
94+
# Some IdPs also label the username field as 'email'
95+
payload[name] = username
96+
elif "pass" in name.lower():
97+
# Make an educated guess that this is the right field for the password
98+
payload[name] = password
99+
else:
100+
# Simply populate the parameter with the existing value (picks up hidden fields
101+
# in the login form)
102+
payload[name] = value
103+
104+
# Debug the parameter payload if needed
105+
# Use with caution since this will print sensitive output to the screen
106+
107+
# Some IdPs don't explicitly set a form action, but if one is set we should
108+
# build the idpauthformsubmiturl by combining the scheme and hostname
109+
# from the entry url with the form action target
110+
# If the action tag doesn't exist, we just stick with the
111+
# idpauthformsubmiturl above
112+
for inputtag in formsoup.find_all(re.compile('(FORM|form)')):
113+
action = inputtag.get('action')
114+
if action:
115+
parsedurl = urlparse(idpentryurl)
116+
idpauthformsubmiturl = '%s://%s/%s' % (parsedurl.scheme, parsedurl.netloc, action)
117+
118+
# Performs the submission of the IdP login form with the above post data
119+
response = session.post(
120+
idpauthformsubmiturl, data=payload, verify=sslverification)
121+
122+
# Debug the response if needed
123+
124+
# Overwrite and delete the credential variables, just for safety
125+
username = '##############################################'
126+
password = '##############################################'
127+
del username
128+
del password
129+
130+
# Decode the response and extract the SAML assertion
131+
soup = BeautifulSoup(response.text.decode('utf8'), "html.parser")
132+
assertion = ''
133+
134+
# Look for the SAMLResponse attribute of the input tag (determined by
135+
# analyzing the debug print lines above)
136+
for inputtag in soup.find_all('input'):
137+
if(inputtag.get('name') == 'SAMLResponse'):
138+
assertion = inputtag.get('value')
139+
140+
# Better error handling is required for production use.
141+
if (assertion == ''):
142+
# TODO: Insert valid error checking/handling
143+
print 'Response did not contain a valid SAML assertion'
144+
sys.exit(0)
145+
146+
# Debug only
147+
# print(base64.b64decode(assertion))
148+
149+
# Parse the returned assertion and extract the authorized roles
150+
awsroles = []
151+
root = ET.fromstring(base64.b64decode(assertion))
152+
for saml2attribute in root.iter('{urn:oasis:names:tc:SAML:2.0:assertion}Attribute'):
153+
if (saml2attribute.get('Name') == 'https://aws.amazon.com/SAML/Attributes/Role'):
154+
saml2attribute = saml2attribute.iter(
155+
'{urn:oasis:names:tc:SAML:2.0:assertion}AttributeValue'
156+
)
157+
for saml2attributevalue in saml2attribute:
158+
awsroles.append(saml2attributevalue.text)
159+
160+
# Note the format of the attribute value should be role_arn,principal_arn
161+
# but lots of blogs list it as principal_arn,role_arn so let's reverse
162+
# them if needed
163+
for awsrole in awsroles:
164+
chunks = awsrole.split(',')
165+
if'saml-provider' in chunks[0]:
166+
newawsrole = chunks[1] + ',' + chunks[0]
167+
index = awsroles.index(awsrole)
168+
awsroles.insert(index, newawsrole)
169+
awsroles.remove(awsrole)
170+
171+
# If I have more than one role, ask the user which one they want,
172+
# otherwise just proceed
173+
print ""
174+
if len(awsroles) > 1:
175+
i = 0
176+
print "Please choose the role you would like to assume:"
177+
for awsrole in awsroles:
178+
print '[', i, ']: ', awsrole.split(',')[0]
179+
i += 1
180+
print "Selection: ",
181+
selectedroleindex = raw_input()
182+
183+
# Basic sanity check of input
184+
if int(selectedroleindex) > (len(awsroles) - 1):
185+
print 'You selected an invalid role index, please try again'
186+
sys.exit(0)
187+
188+
role_arn = awsroles[int(selectedroleindex)].split(',')[0]
189+
principal_arn = awsroles[int(selectedroleindex)].split(',')[1]
190+
else:
191+
role_arn = awsroles[0].split(',')[0]
192+
principal_arn = awsroles[0].split(',')[1]
193+
194+
# Use the assertion to get an AWS STS token using Assume Role with SAML
195+
regions = [
196+
"us-east-1",
197+
"us-east-2",
198+
"us-west-1",
199+
"us-west-2",
200+
"ca-central-1",
201+
"eu-west-1",
202+
"eu-central-1",
203+
"eu-west-2",
204+
"ap-northeast-1",
205+
"ap-northeast-2",
206+
"ap-southeast-1",
207+
"ap-southeast-2",
208+
"ap-south-1",
209+
"sa-east-1"
210+
]
211+
region_selected = regions[num_region - 1]
212+
conn = boto.sts.connect_to_region(region_selected)
213+
token = conn.assume_role_with_saml(role_arn, principal_arn, assertion)
214+
215+
# Write the AWS STS token into the AWS credential file
216+
home = expanduser("~")
217+
filename = home + awsconfigfile
218+
219+
# Read in the existing config file
220+
config = ConfigParser.RawConfigParser()
221+
config.read(filename)
222+
223+
# Put the credentials into a saml specific section instead of clobbering
224+
# the default credentials
225+
if not config.has_section('default'):
226+
config.add_section('default')
227+
228+
config.set('default', 'output', outputformat)
229+
config.set('default', 'region', region_selected)
230+
config.set('default', 'aws_access_key_id', token.credentials.access_key)
231+
config.set('default', 'aws_secret_access_key', token.credentials.secret_key)
232+
config.set('default', 'aws_session_token', token.credentials.session_token)
233+
234+
# Write the updated config file
235+
with open(filename, 'w+') as configfile:
236+
config.write(configfile)
237+
238+
# Give the user some basic info as to what has just happened
239+
print '\n\n----------------------------------------------------------------'
240+
print """Your new access key pair has been stored in the AWS configuration file {0} under the saml profile.""".format(filename)
241+
print 'Note that it will expire at {0}.'.format(token.credentials.expiration)
242+
print 'After this time, you may safely rerun this script to refresh your access key pair.'
243+
print 'To use this credential, call the AWS CLI with the --profile option (e.g. aws --profile saml ec2 describe-instances).'
244+
print '----------------------------------------------------------------\n\n'

0 commit comments

Comments
 (0)