class OneloginAWS(object): """ Handles the authentication between OneLogin SAML Assertion and the AWS identity federation """ def __init__(self, config: Section): self.sts_client = boto3.client("sts") self.config = config self.saml = None self.all_roles = None self.role_arn = None self.credentials = None self.duration_seconds = int(config['duration_seconds']) self.user_credentials = UserCredentials(config) self.mfa = MFACredentials(config) base_uri_parts = self.config['base_uri'].split('.') self.ol_client = OneLoginClient( self.config['client_id'], self.config['client_secret'], base_uri_parts[1], ) def get_saml_assertion(self): """ Retrieve users credentials and get the SAML assertion from Onelogin, based on the users choice of AWS account to log into """ self.user_credentials.load_credentials() saml_resp = self.ol_client.get_saml_assertion( username_or_email=self.user_credentials.username, password=self.user_credentials.password, app_id=self.config['aws_app_id'], subdomain=self.config['subdomain'], ip_address=self.get_ip_address(), ) if saml_resp is None: raise Exception("Onelogin Error: '{error}' '{desc}'".format( error=self.ol_client.error, desc=self.ol_client.error_description )) if saml_resp.mfa: if not self.mfa.ready(): self.mfa.select_device(saml_resp.mfa.devices) if not self.mfa.has_otp: self.mfa.prompt_token() saml_resp = self.ol_client.get_saml_assertion_verifying( self.config['aws_app_id'], self.mfa.device.id, saml_resp.mfa.state_token, self.mfa.otp ) self.saml = saml_resp def get_ip_address(self) -> Optional[str]: """ Get the client IP address. Uses either the `ip_address` in config, or if `auto_determine_ip_address` is specified in config, the ipify service is used to dynamically lookup the IP address. """ # if ip address has been hard coded in config file, use that ip_address = self.config.get('ip_address') if ip_address is not None: return ip_address # if auto determine is enabled, use ipify to lookup the ip if self.config.auto_determine_ip_address: ip_address = ipify.get_ip() return ip_address def get_arns(self): """Extract the IAM Role ARNs from the SAML Assertion""" if not self.saml: self.get_saml_assertion() # Parse the returned assertion and extract the authorized roles aws_roles = [] root = ElementTree.fromstring( base64.b64decode(self.saml.saml_response)) namespace = "{urn:oasis:names:tc:SAML:2.0:assertion}" role_name = "https://aws.amazon.com/SAML/Attributes/Role" for attr in root.iter(namespace + "Attribute"): if attr.get("Name") == role_name: for val in attr.iter(namespace + "AttributeValue"): aws_roles.append(val.text) # Note the format of the attribute value should be role_arn, # principal_arn but lots of blogs list it as principal_arn,role_arn so # let's reverse them if needed aws_roles = [role.split(",") for role in aws_roles] aws_roles = [(role, principal) for role, principal in aws_roles] self.all_roles = aws_roles def get_role(self): """ Prompt the user to choose a Role ARN if more than one is available """ if not self.all_roles: self.get_arns() if not self.all_roles: raise Exception("No roles found") # If I have more than one role, ask the user which one they want, # otherwise just proceed self.role_arn, self.principal_arn = user_role_prompt( self.all_roles, saved_choice=self.config.get("role_arn"), ) def assume_role(self): """Perform an AWS SAML role assumption""" if not self.role_arn: self.get_role() if self.config['region']: self.sts_client = boto3.client("sts", region_name=self.config["region"]) res = self.sts_client.assume_role_with_saml( RoleArn=self.role_arn, PrincipalArn=self.principal_arn, SAMLAssertion=self.saml.saml_response, DurationSeconds=self.duration_seconds ) self.credentials = res def save_credentials(self): """Save the AWS Federation credentials to disk""" if not self.credentials: self.assume_role() creds = self.credentials["Credentials"] cred_file = self._initialize_credentials() cred_config = configparser.ConfigParser() cred_config.read(cred_file) # Update with new credentials name = self.credentials["AssumedRoleUser"]["Arn"] m = re.search('(arn\:aws([\w-]*)\:sts\:\:)(.*)', name) if m is not None: name = m.group(3) name = name.replace(":assumed-role", "") if "profile" in self.config: name = self.config["profile"] # Initialize the profile block if it is undefined if name not in cred_config: cred_config[name] = {} # Set each value specifically instead of overwriting the entire # profile block in case they have other parameters defined cred_config[name]['aws_access_key_id'] = creds["AccessKeyId"] cred_config[name]['aws_secret_access_key'] = creds["SecretAccessKey"] cred_config[name]['aws_session_token'] = creds["SessionToken"] # Set region for this profile if passed in via configuration if self.config['region']: cred_config[name]['region'] = self.config['region'] with open(cred_file, "w") as cred_config_file: cred_config.write(cred_config_file) print("Credentials cached in '{}'".format(cred_file)) print("Expires at {}".format(creds["Expiration"])) print("Use aws cli with --profile " + name) # Reset state in the case of another transaction self.credentials = None def _initialize_credentials(self): cred_file = os.environ.get('AWS_SHARED_CREDENTIALS_FILE', None) if cred_file is None: cred_file = os.path.expanduser("~/.aws/credentials") cred_dir = os.path.expanduser("~/.aws/") if not os.path.exists(cred_dir): os.makedirs(cred_dir) return cred_file
# Get the credentials from the user if not email: print "Email: ", email = raw_input() else: print "Using: %s" % email password = getpass.getpass() print "OTP Code (MFA): ", otp_code = raw_input() print '' client = OneLoginClient(onelogin_client_id, onelogin_client_secret, onelogin_region) onelogin_response = client.get_saml_assertion(email, password, app_id, onelogin_subdomain) saml = None if onelogin_response is None: print('Failed logging in (password was incorrect)') exit(1) elif onelogin_response and onelogin_response.type == "success": state_token = onelogin_response.mfa.state_token device_id = onelogin_response.mfa.devices[0].id mfa_response = client.get_saml_assertion_verifying(app_id, device_id, state_token, otp_code, do_not_notify=True)
class OneloginAWS(object): def __init__(self, config, args): self.sts_client = boto3.client("sts") self.config = config self.args = args self.token = None self.account_id = None self.saml = None self.all_roles = None self.role_arn = None self.principal_arn = None self.credentials = None self.username = self.args.username self.password = None base_uri_parts = self.config['base_uri'].split('.') self.ol_client = OneLoginClient( self.config['client_id'], self.config['client_secret'], base_uri_parts[1], ) def get_saml_assertion(self): if not self.username: self.username = input("Onelogin Username: "******"Onelogin Password: "******"{}. {}".format(i + 1, device.type)) device_num = input("Which OTP Device? ") device = devices[int(device_num) - 1] else: device = devices[0] otp_token = input("OTP Token: ") saml_resp = self.ol_client.get_saml_assertion_verifying( self.config['aws_app_id'], device.id, saml_resp.mfa.state_token, otp_token) self.saml = saml_resp def get_arns(self): if not self.saml: self.get_saml_assertion() # Parse the returned assertion and extract the authorized roles aws_roles = [] root = ET.fromstring(base64.b64decode(self.saml.saml_response)) namespace = "{urn:oasis:names:tc:SAML:2.0:assertion}" role_name = "https://aws.amazon.com/SAML/Attributes/Role" for attr in root.iter(namespace + "Attribute"): if attr.get("Name") == role_name: for val in attr.iter(namespace + "AttributeValue"): aws_roles.append(val.text) # Note the format of the attribute value should be role_arn, # principal_arn but lots of blogs list it as principal_arn,role_arn so # let's reverse them if needed aws_roles = [role.split(",") for role in aws_roles] aws_roles = [(role, principal) for role, principal in aws_roles] self.all_roles = aws_roles def get_role(self): if not self.all_roles: self.get_arns() if not self.all_roles: raise Exception("No roles found") selected_role = None # If I have more than one role, ask the user which one they want, # otherwise just proceed if len(self.all_roles) > 1: ind = 0 for role, principal in self.all_roles: print("[{}] {}".format(ind, role)) ind += 1 while selected_role is None: choice = int(input("Role Number: ")) if choice in range(len(self.all_roles)): selected_role = choice else: print("Invalid role index, please try again") else: selected_role = 0 self.role_arn, self.principal_arn = self.all_roles[selected_role] def assume_role(self): if not self.role_arn: self.get_role() res = self.sts_client.assume_role_with_saml( RoleArn=self.role_arn, PrincipalArn=self.principal_arn, SAMLAssertion=self.saml.saml_response) self.credentials = res def save_credentials(self): if not self.credentials: self.assume_role() creds = self.credentials["Credentials"] cred_file = os.path.expanduser("~/.aws/credentials") cred_dir = os.path.expanduser("~/.aws/") if not os.path.exists(cred_dir): os.makedirs(cred_dir) cred_config = configparser.ConfigParser() cred_config.read(cred_file) # Update with new credentials name = self.credentials["AssumedRoleUser"]["Arn"] if name.startswith("arn:aws:sts::"): name = name[13:] name = name.replace(":assumed-role", "") if self.config.get("profile"): name = self.config["profile"] elif self.args.profile != "": name = self.args.profile cred_config[name] = { "aws_access_key_id": creds["AccessKeyId"], "aws_secret_access_key": creds["SecretAccessKey"], "aws_session_token": creds["SessionToken"] } with open(cred_file, "w") as cred_config_file: cred_config.write(cred_config_file) print("Credentials cached in '{}'".format(cred_file)) print("Expires at {}".format(creds["Expiration"])) print("Use aws cli with --profile " + name) # Reset state in the case of another transaction self.token = None self.credentials = None @staticmethod def generate_config(): print("Configure Onelogin and AWS\n\n") config = configparser.ConfigParser() config.add_section("default") default = config["default"] default["base_uri"] = user_choice( "Pick a Onelogin API server:", ["https://api.us.onelogin.com/", "https://api.eu.onelogin.com/"]) print("\nOnelogin API credentials. These can be found at:\n" "https://admin.us.onelogin.com/api_credentials") default["client_id"] = input("Onelogin API Client ID: ") default["client_secret"] = input("Onelogin API Client Secret: ") print("\nOnelogin AWS App ID. This can be found at:\n" "https://admin.us.onelogin.com/apps") default["aws_app_id"] = input("Onelogin App ID for AWS: ") print("\nOnelogin subdomain is 'company' for login domain of " "'comany.onelogin.com'") default["subdomain"] = input("Onelogin subdomain: ") config_fn = os.path.expanduser("~/{}".format(CONFIG_FILENAME)) with open(config_fn, "w") as config_file: config.write(config_file) print("Configuration written to '{}'".format(config_fn)) @staticmethod def load_config(): try: config_fn = os.path.expanduser("~/{}".format(CONFIG_FILENAME)) config = configparser.ConfigParser() config.read_file(open(config_fn)) return config except FileNotFoundError: return None
class OneloginAWS(object): """ Handles the authentication between OneLogin SAML Assertion and the AWS identity federation """ def __init__(self, config: Section): self.sts_client = boto3.client("sts") self.config = config self.saml = None self.all_roles = None self.role_arn = None self.credentials = None self.duration_seconds = int(config['duration_seconds']) self.user_credentials = UserCredentials(config) self.mfa = MFACredentials(config) base_uri_parts = self.config['base_uri'].split('.') self.ol_client = OneLoginClient( self.config['client_id'], self.config['client_secret'], base_uri_parts[1], ) def get_saml_assertion(self): """ Retrieve users credentials and get the SAML assertion from Onelogin, based on the users choice of AWS account to log into """ self.user_credentials.load_credentials() saml_resp = self.ol_client.get_saml_assertion( self.user_credentials.username, self.user_credentials.password, self.config['aws_app_id'], self.config['subdomain']) if saml_resp is None: raise Exception("Onelogin Error: '{error}' '{desc}'".format( error=self.ol_client.error, desc=self.ol_client.error_description)) if saml_resp.mfa: if not self.mfa.ready(): self.mfa.select_device(saml_resp.mfa.devices) if not self.mfa.has_otp: self.mfa.prompt_token() saml_resp = self.ol_client.get_saml_assertion_verifying( self.config['aws_app_id'], self.mfa.device.id, saml_resp.mfa.state_token, self.mfa.otp) self.saml = saml_resp def get_arns(self): """Extract the IAM Role ARNs from the SAML Assertion""" if not self.saml: self.get_saml_assertion() # Parse the returned assertion and extract the authorized roles aws_roles = [] root = ElementTree.fromstring(base64.b64decode( self.saml.saml_response)) namespace = "{urn:oasis:names:tc:SAML:2.0:assertion}" role_name = "https://aws.amazon.com/SAML/Attributes/Role" for attr in root.iter(namespace + "Attribute"): if attr.get("Name") == role_name: for val in attr.iter(namespace + "AttributeValue"): aws_roles.append(val.text) # Note the format of the attribute value should be role_arn, # principal_arn but lots of blogs list it as principal_arn,role_arn so # let's reverse them if needed aws_roles = [role.split(",") for role in aws_roles] aws_roles = [(role, principal) for role, principal in aws_roles] self.all_roles = aws_roles def get_role(self): """ Prompt the user to choose a Role ARN if more than one is available """ if not self.all_roles: self.get_arns() if not self.all_roles: raise Exception("No roles found") # If I have more than one role, ask the user which one they want, # otherwise just proceed self.role_arn, self.principal_arn = user_role_prompt( self.all_roles, saved_choice=self.config.get("role_arn"), ) def assume_role(self): """Perform an AWS SAML role assumption""" if not self.role_arn: self.get_role() res = self.sts_client.assume_role_with_saml( RoleArn=self.role_arn, PrincipalArn=self.principal_arn, SAMLAssertion=self.saml.saml_response, DurationSeconds=self.duration_seconds) self.credentials = res def save_credentials(self): """Save the AWS Federation credentials to disk""" if not self.credentials: self.assume_role() creds = self.credentials["Credentials"] cred_file = self._initialize_credentials() cred_config = configparser.ConfigParser() cred_config.read(cred_file) # Update with new credentials name = self.credentials["AssumedRoleUser"]["Arn"] if name.startswith("arn:aws:sts::"): name = name[13:] name = name.replace(":assumed-role", "") if "profile" in self.config: name = self.config["profile"] cred_config[name] = { "aws_access_key_id": creds["AccessKeyId"], "aws_secret_access_key": creds["SecretAccessKey"], "aws_session_token": creds["SessionToken"] } with open(cred_file, "w") as cred_config_file: cred_config.write(cred_config_file) print("Credentials cached in '{}'".format(cred_file)) print("Expires at {}".format(creds["Expiration"])) print("Use aws cli with --profile " + name) # Reset state in the case of another transaction self.credentials = None def _initialize_credentials(self): cred_file = os.environ.get('AWS_SHARED_CREDENTIALS_FILE', None) if cred_file is None: cred_file = os.path.expanduser("~/.aws/credentials") cred_dir = os.path.expanduser("~/.aws/") if not os.path.exists(cred_dir): os.makedirs(cred_dir) return cred_file