CloudFormation now supports Identity Center (SSO) users

Marcin Sodkiewicz
5 min readNov 18, 2022

--

Actually it does not officially and it was click-bait… sort of, but please give me a minute to explain.

TLDR;

AWS does not support creating identity store users using CloudFormation, but I have created custom resource that can help you doing it https://github.com/SodaDev/aws-identity-center-custom-resources

Motivation

I wanted to create Identity Center (SSO) for my own dev account to get rid of using IAM Users in favour of using SSO for accessing AWS. I wanted to use temporary credentials and MFA. I have found out that there is no way to do it, as it was not supported by any SDK and CLI, so I gave up.

On September 1st 2022 it got introduced by AWS. I quickly tried to find the way to setup SSO with CloudFormation, but with no luck. It was launched only for for SDKs and CLI. It was not available back then - and it still is not. Managing resources through CLI was not good enough for me, so I decided to create my own custom resource.

Solution

Not sure if you are familiar how custom resources works exactly in AWS, but the workflow looks more or less like that:

  1. Lambda receives event from CloudFormation with resource and action details (Create/Update/Delete)
  2. Lambda is taking some actions
  3. Lambda sends confirmation to AWS S3 presigned URL that was received in the event with info about:
    - Data that can be retrieved later with !GetAtt
    - Status
    - Reason
    - Physical id

cfn-response module or cfn-response-ts module

Usually I developed custom resources using Go, but this time I decided to write that lambda in TypeScript using SAM.

There is module provided by AWS and guide, yet it’s not available from npm registry. You have to copy it over from documentaion or use one on from npm that is not the official one. It’s just module with code from AWS Docs.

There was major improvement introduced for writing custom resources in the CDK in that matter, but that’s different story.

When it comes to error handling during development of custom resource it might be tricky and your CloudFormation stack might get stuck using that basic module (in case that due to failure you haven’t sent request with action result for example). All error handling and lifecycle are left to the developer and, as I wrote above, it might be tricky in some cases.

I created module in Typescript under: https://www.npmjs.com/package/cfn-response-ts that provides:

  • simple way of creating lambda handler,
  • adds direct link to CloudWatch Logs (as it’s attached as part of the reason) to make it really easy to find errors from AWS CloudFormation view,

and I hope it won’t allow you to block your CloudFormation stacks. I hope that you will find it useful. It supports both CommonJS and ES Modules. It is my first official npm module so PRs are more than welcome if you would like to improve it.

Identity Store SDK

I have played with both CLI and SDK for AWS Identity Store and it’s great that we have both of them. Create and Delete API are simple and straight-forward. Only thing to watch out is that as it supports only single e-mail and address despite of having list in the contract for both of them.

Using Update API on the other hand is definitely not easy. It lacks documentation and using it feels a little bit like guessing. Regarding updating users I think that I don’t have to say that it’s actually the most critical part. User resource is not one that could be updated with replace strategy. User might have already confirmed their e-mail address and registered their MFA device.

What example resource looks like?

Example user resource with all fields populated might look like resource on snippet below. And it exposes identity store UserId that can be referenced through: !GetAtt UserResource.UserId

DevopsUser:
Type: Custom::IAMIdentityCenterUser
Properties:
ServiceToken:
Fn::ImportValue: SsoUserFunction::Arn
IdentityStoreId: !Ref IdentityStoreId
UserName: "devops"
DisplayName: "some devops user"
Emails:
- Value: devops@domain.com
Primary: True
Type: Official
Name:
GivenName: "Devops name"
FamilyName: "Devops surname"
MiddleName: "SomeMiddleName"
HonorificPrefix: "Xyz."
HonorificSuffix: "Q.W.E"
NickName: "devops guru"
ProfileUrl: https://some-portalcom/devops
Addresses:
- Primary: true
Type: "Workshop"
Country: "xx"
StreetAddress: "AnotherStreet 21"
Region: "AnotherRegion"
PostalCode: "00-666"
Locality: "AnotherLocality"
PhoneNumbers:
- Primary: true
Type: "Mobile"
Value: "+1 (800) 123-4567"
UserType: "manager"
Title: "Maestro"
PreferredLanguage: "en-us"
Locale: "en-en"
Timezone: "GMT+2"

How can I use it?

Deploy custom resource lambda to your account

You need to have custom resource lambda in your account to make it work. When you are creating custom resource you have to specify which lambda should handle that particular resource through ServiceToken field:

DevopsUser:
Type: Custom::IAMIdentityCenterUser
Properties:
ServiceToken:
Fn::ImportValue: SsoUserFunction::Arn

Just deploy custom resource lambda in your account according to: https://github.com/SodaDev/aws-identity-center-custom-resources#how-to-deploy-it.

Use it in you SSO stack

When lambda is already deployed in your account you can just mix it into already supported resources from AWS. For example in the stack that might look like that:

AWSTemplateFormatVersion: 2010-09-09
Description: SSO Account Setup

Parameters:
InstanceARN:
Type: String
AllowedPattern: arn:aws:sso:::instance/(sso)?ins-[a-zA-Z0-9-.]{16}
Description: 'Enter AWS SSO InstanceARN. Ex: arn:aws:sso:::instance/ssoins-xxxxxxxxxxxxxxxx'
ConstraintDescription: AWS SSO InstanceARN - gather with aws sso-admin list-instances
IdentityStoreId:
Type: String
Description: 'Enter AWS SSO IdentityStoreId. Ex: d-1234567890'

Resources:
### Permission sets ###
ReadOnlyPermissionSet:
Type: AWS::SSO::PermissionSet
Properties:
InstanceArn: !Ref InstanceARN
Name: IaaC-ReadOnlyAccess
SessionDuration: PT4H
ManagedPolicies:
- arn:aws:iam::aws:policy/ReadOnlyAccess

PowerUserPermissionSet:
Type: AWS::SSO::PermissionSet
Properties:
InstanceArn: !Ref InstanceARN
Name: IaaC-PowerUserAccess
SessionDuration: PT1H
ManagedPolicies:
- arn:aws:iam::aws:policy/PowerUserAccess

### Groups ###
DevopsGroup:
Type: AWS::IdentityStore::Group
Properties:
DisplayName: devops
IdentityStoreId: !Ref IdentityStoreId

DevopsGroupAssignment:
Type: AWS::SSO::Assignment
Properties:
InstanceArn: !Ref InstanceARN
PermissionSetArn: !GetAtt PowerUserPermissionSet.PermissionSetArn
PrincipalId: !GetAtt DevopsGroup.GroupId
PrincipalType: GROUP
TargetId: !Ref AWS::AccountId
TargetType: AWS_ACCOUNT

QaGroup:
Type: AWS::IdentityStore::Group
Properties:
DisplayName: QAs
IdentityStoreId: !Ref IdentityStoreId

QaGroupAssignment:
Type: AWS::SSO::Assignment
Properties:
InstanceArn: !Ref InstanceARN
PermissionSetArn: !GetAtt ReadOnlyPermissionSet.PermissionSetArn
PrincipalId: !GetAtt QaGroup.GroupId
PrincipalType: GROUP
TargetId: !Ref AWS::AccountId
TargetType: AWS_ACCOUNT

### Users ###
DevopsUser:
Type: Custom::IAMIdentityCenterUser
Properties:
ServiceToken:
Fn::ImportValue: SsoUserFunction::Arn
IdentityStoreId: !Ref IdentityStoreId
UserName: "devops"
DisplayName: "some devops user"
Emails:
- Value: devops@domain.com
Primary: True
Type: Official
Name:
GivenName: "Devops name"
FamilyName: "Devops surname"
MiddleName: "SomeMiddleName"
HonorificPrefix: "Xyz."
HonorificSuffix: "Q.W.E"
NickName: "devops guru"
ProfileUrl: https://some-portalcom/devops
Addresses:
- Primary: true
Type: "Workshop"
Country: "xx"
StreetAddress: "AnotherStreet 21"
Region: "AnotherRegion"
PostalCode: "00-666"
Locality: "AnotherLocality"
PhoneNumbers:
- Primary: true
Type: "Mobile"
Value: "+1 (800) 123-4567"
UserType: "manager"
Title: "Maestro"
PreferredLanguage: "en-us"
Locale: "en-en"
Timezone: "GMT+2"

DevopsUserMembership:
Type: AWS::IdentityStore::GroupMembership
Properties:
IdentityStoreId: !Ref IdentityStoreId
GroupId: !GetAtt DevopsGroup.GroupId
MemberId:
UserId: !GetAtt DevopsUser.UserId

QaUser:
Type: Custom::IAMIdentityCenterUser
Properties:
ServiceToken:
Fn::ImportValue: SsoUserFunction::Arn
IdentityStoreId: !Ref IdentityStoreId
UserName: "qa"
DisplayName: "some qa user"
Emails:
- Value: qa@domain.com
Primary: false
Type: Work
Name:
GivenName: "QA name"

QaUserMembership:
Type: AWS::IdentityStore::GroupMembership
Properties:
IdentityStoreId: !Ref IdentityStoreId
GroupId: !GetAtt QaGroup.GroupId
MemberId:
UserId: !GetAtt QaUser.UserId

and just deploy it with

INSTANCE_ARN="$(aws sso-admin list-instances --query 'Instances[0].InstanceArn' --output text)"
IDENTITY_STORE_ID="$(aws sso-admin list-instances --query 'Instances[0].IdentityStoreId' --output text)"
aws cloudformation deploy \
--template-file ./sso.yaml \
--stack-name SSO \
--parameter-overrides \
InstanceARN="${INSTANCE_ARN}" \
IdentityStoreId="${IDENTITY_STORE_ID}"

Conclusion

I’m looking forward for CloudFormation support for IdentityStore users, but since then you can create your Identity Store users by just using this custom resource: https://github.com/SodaDev/aws-identity-center-custom-resources.
I hope it will make your life easier!

--

--