feat: Add external SAML/OIDC identity provider federation with Cognito#254
Conversation
- Add optional CloudFormation parameters for SAML and OIDC federation - Support PingOne, Okta, and Microsoft Entra ID as external IdPs - Add PreTokenGeneration Lambda for automatic IdP group to Cognito group mapping - Add federated sign-in button to the web UI login page - Add auto-login option to bypass Cognito hosted UI - Add federated logout through Cognito logout endpoint - Update UserPoolClient with conditional SupportedIdentityProviders and LogoutURLs - Add custom:idp_groups attribute to UserPool schema for group claim mapping - Add comprehensive documentation in docs/external-idp.md - Update aws-exports.js to conditionally configure OAuth for federation
|
This is a very awesome PR, @rcgeorge - Thanks!! Some issues to address before merging Resolve merge conflicts Address items in Claude's review below:
Resource: !Sub "arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/*"
This is correct in logic (handles both branches: with and without email domain verification), and the nested !If is valid CloudFormation. However, this is a subtle 3-way conditional that should have a YAML comment explaining the matrix of ShouldAllowSignUpEmailDomain × ShouldMapExternalIdPGroups combinations for future maintainers.
The rest of the UI uses Cloudscape Design System components. This button should use a Cloudscape Button component for visual consistency:
This hardcodes Amplify's internal storage key prefixes, which could change in future Amplify versions. Consider using Amplify's signOut({ global: true }) with the oauth config instead, or at least add a comment noting this coupling.
This module-level mutable variable persists across route navigations within the same SPA session. If the auto-login redirect fails silently (e.g., network issue), the user gets stuck seeing the login form with no retry mechanism. Consider using sessionStorage (like the idp_signed_out flag already does) for consistency and to survive page refreshes.
This only retries once. If the PreTokenGeneration Lambda hasn't completed group assignment by the time the refresh happens, the user still gets no groups. Consider a brief delay or retry with backoff. Also, this forceRefresh fires on every render cycle where groups are empty, since useEffect re-runs — ensure that's not causing excessive token refresh calls.
In the Outputs section, these values are always emitted even when federation is not configured (unlike the CodeBuild env vars which use !If). When ExternalIdPName is empty, this emits VITE_EXTERNAL_IDP_NAME= which is fine functionally but inconsistent with the !If pattern used in the CodeBuild section. 🟢 Low / Nit The groups scope is not part of the OIDC standard and not all providers support it. This could cause auth errors with some IdPs. Consider making this configurable or documenting that it may need adjustment.
Overall this is a high-quality PR with excellent documentation and strong backward compatibility. The architecture is well-thought-out. After fixing the IAM wildcard and the medium-severity UX issues, it would be ready to merge. |
- Scope IAM policy to specific UserPool (break circular dep with standalone IAM::Policy) - Add LambdaConfig condition matrix comment - Move OIDC client secret to Secrets Manager via Custom Resource Lambda - Replace inline styles with Cloudscape Button component - Replace manual localStorage cleanup with Amplify signOut() - Switch autoLoginAttempted to sessionStorage - Add try/catch to federated token refresh - Make Outputs VITE_ vars conditional with !Join/!If - Remove non-standard groups OIDC scope - Add VITE_EXTERNAL_IDP_AUTO_LOGIN to aws-exports.js destructuring - Fix whitespace-only idp_groups edge case in group mapping Lambda - Add 26 unit tests for ExternalIdPGroupMappingFunction - Add OIDC client secret security docs
|
Thanks for the thorough review! All items have been addressed and the branch has been tested with a PingOne SAML deployment. Merge conflicts with upstream develop are clean. Here's what was done for each item:
Bonus fix: The whitespace-only edge case in the group parsing logic — Tested: Deployed with PingOne SAML federation to an existing stack in us-east-1. Federation sign-in, group mapping, sign-out, and auto-login flag all working as expected. |
|
Awesome @rcgeorge - approved and ready to merge.. |
Adds optional support for federating authentication through an external SAML or OIDC identity provider via Amazon Cognito. This enables organizations to use their existing enterprise identity providers (PingOne, Okta, Microsoft Entra ID, etc.) for single sign-on to the GenAI IDP solution.
All federation functionality is opt-in through CloudFormation parameters — leaving them empty results in zero additional resources and identical behavior to the existing Cognito-native authentication.
What's included
Infrastructure (template.yaml)
AWS::Cognito::UserPoolIdentityProviderresource supporting both SAML and OIDCPreTokenGenerationLambda trigger that maps external IdP group claims to Cognito groups (Admin, Author, Reviewer, Viewer)custom:idp_groupsattribute on the UserPool schema for receiving group claimsSupportedIdentityProviders,LogoutURLs, andCallbackURLson the UserPoolClientWaitConditionHandlepattern for conditional resource dependency orderingFrontend (src/ui)
/logoutendpointaws-exports.jsconditionally enabled when an external IdP is configuredDocumentation (docs/external-idp.md)
Testing
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.