├── .gitignore ├── params.json ├── LICENSE.txt ├── README.md └── provider.yml /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /params.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ParameterKey": "MetadataDocument", 4 | "ParameterValue": "" 5 | } 6 | ] 7 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Colin Panisset 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. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # A simple SAML Identity Provider (IdP) provisioner 2 | 3 | This CloudFormation template creates a SAML identity provider in Amazon Web Services' (AWS) Identity and Access 4 | Management (IAM) configuration. 5 | 6 | In order to use it, you'll need: 7 | 8 | * an AWS account 9 | * rights within that AWS account to create, update, and delete: 10 | * CloudFormation stacks 11 | * IAM Roles and Policies 12 | * Lambda functions 13 | * Identity Providers 14 | * a SAML Identity Provider (IdP) 15 | * the Federation metadata (an XML document) from the Identity Provider (how to get this differs for every IdP) 16 | 17 | ## Preparing metadata 18 | 19 | 1. Download metadata from your IdP 20 | 1. Make it all be on one line and escape the double-quote (`"`) character: 21 | `tr -d '\n' metadata.xml | sed -e 's/"/\"/g' > out.xml` 22 | 1. Copy the `out.xml` into the `ParameterValue` field of the `params.json` 23 | 24 | ## Configuration 25 | 26 | You can set the name of your identity provider via the `SamlProviderName` parameter to the stack; this can be 27 | configured in the `params.json`. It defaults to `MyProvider` 28 | 29 | ## Returned Values from the Custom Resource 30 | 31 | The `ProviderCreator` custom resource returns the ARN of the SAML provider as its physical resource ID. 32 | 33 | You can simply `Ref` the custom resource to use the SAML provider ARN. 34 | 35 | For example: 36 | 37 | ```yaml 38 | ... 39 | 40 | TrustingIdp: 41 | Type: AWS::IAM::Role 42 | Properties: 43 | AssumeRolePolicyDocument: 44 | Version: '2012-10-17' 45 | Statement: 46 | - Effect: Allow 47 | Principal: 48 | Federated: !Ref IdentityProvider 49 | Action: sts:AssumeRoleWithSAML 50 | Condition: 51 | StringEquals: 52 | "SAML:aud": "https://signin.aws.amazon.com/saml" 53 | ... 54 | ``` 55 | 56 | ## Updating the stack 57 | 58 | The stack can be updated, though the only changes you can make to the Identity Provider is to change the SAML 59 | Metadata document, in case you need to update the trust relationship. 60 | -------------------------------------------------------------------------------- /provider.yml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: 2010-09-09 2 | Description: Create a SAML identity provider 3 | 4 | Parameters: 5 | MetadataDocument: 6 | Type: String 7 | Description: The XML metadata document to use when trusting the Identity Provider 8 | SamlProviderName: 9 | Type: String 10 | Description: The name for your SAML provider in IAM 11 | Default: MyProvider 12 | 13 | Resources: 14 | IdentityProvider: 15 | Type: Custom::IdentityProvider 16 | Properties: 17 | ServiceToken: !GetAtt ProviderCreator.Arn 18 | Region: !Ref "AWS::Region" 19 | Metadata: !Ref MetadataDocument 20 | Name: !Ref SamlProviderName 21 | 22 | ProviderCreator: 23 | Type: AWS::Lambda::Function 24 | Properties: 25 | Runtime: python2.7 26 | Handler: index.lambda_handler 27 | MemorySize: 128 28 | Role: !GetAtt LambdaExecutionRole.Arn 29 | Timeout: 30 30 | Code: 31 | ZipFile: !Sub | 32 | import boto3 33 | from botocore.exceptions import ClientError 34 | import json 35 | import cfnresponse 36 | 37 | iam = boto3.client("iam") 38 | 39 | def create_provider(name, doc): 40 | try: 41 | resp = iam.create_saml_provider(SAMLMetadataDocument=doc,Name=name) 42 | return(True, resp['SAMLProviderArn']) 43 | except Exception as e: 44 | return (False, "Cannot create SAML provider: " + str(e)) 45 | 46 | def delete_provider(arn): 47 | try: 48 | resp = iam.delete_saml_provider(SAMLProviderArn=arn) 49 | return (True, "SAML provider with ARN " + arn + " deleted") 50 | except ClientError as e: 51 | if e.response['Error']['Code'] == "NoSuchEntity": 52 | # no need to delete a thing that doesn't exist 53 | return (True, "SAML provider with ARN " + arn + " does not exist, deletion succeeded") 54 | else: 55 | return (False, "Cannot delete SAML provider with ARN " + arn + ": " + str(e)) 56 | except Exception as e: 57 | return (False, "Cannot delete SAML provider with ARN " + arn + ": " + str(e)) 58 | 59 | def update_provider(arn, doc): 60 | # Need to create the ARN from the name 61 | arn = "arn:aws:iam::${AWS::AccountId}:saml-provider/" + name 62 | try: 63 | resp = iam.update_saml_provider(SAMLMetadataDocument=doc, SAMLProviderArn=arn) 64 | return (True, "SAML provider " + arn + " updated") 65 | except Exception as e: 66 | return (False, "Cannot update SAML provider " + arn + ": " + str(e)) 67 | 68 | def lambda_handler(event, context): 69 | provider_xml = event['ResourceProperties']['Metadata'] 70 | provider_name = event['ResourceProperties']['Name'] 71 | # create a default ARN from the name; will be overwritten if we are creating 72 | provider_arn = "arn:aws:iam::${AWS::AccountId}:saml-provider/" + provider_name 73 | 74 | if event['RequestType'] == 'Create': 75 | res, provider_arn = create_provider(provider_name, provider_xml) 76 | reason = "Creation succeeded" 77 | elif event['RequestType'] == 'Update': 78 | res, reason = update_provider(provider_arn, provider_xml) 79 | elif event['RequestType'] == 'Delete': 80 | res, reason = delete_provider(provider_arn) 81 | else: 82 | res = False 83 | resp = "Unknown operation: " + event['RequestType'] 84 | 85 | responseData = {} 86 | responseData['Reason'] = reason 87 | if res: 88 | cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, provider_arn) 89 | else: 90 | cfnresponse.send(event, context, cfnresponse.FAILED, responseData, provider_arn) 91 | 92 | LambdaExecutionRole: 93 | Type: AWS::IAM::Role 94 | Properties: 95 | Path: / 96 | AssumeRolePolicyDocument: 97 | Version: 2012-10-17 98 | Statement: 99 | - Effect: Allow 100 | Principal: 101 | Service: 102 | - lambda.amazonaws.com 103 | Action: 104 | - sts:AssumeRole 105 | Policies: 106 | - PolicyName: root 107 | PolicyDocument: 108 | Version: 2012-10-17 109 | Statement: 110 | - Effect: Allow 111 | Action: 112 | - iam:*SamlProvider 113 | Resource: "*" 114 | - Effect: Allow 115 | Action: 116 | - logs:CreateLogGroup 117 | - logs:CreateLogStream 118 | - logs:PutLogEvents 119 | Resource: "*" 120 | --------------------------------------------------------------------------------