Files
Zakaria 174cb600d4 feat(cognito): show the client's identity providers on the hosted login page
The hosted login page now offers a "Continue with <provider>" button for each
of the app client's other SupportedIdentityProviders, carrying the same
authorize parameters (state, nonce, PKCE) into the federated flow, as Cognito's
managed login does. A client that lists only external providers gets the
buttons without the password form instead of an error.
2026-10-24 12:14:04 -04:00

28 KiB

Cognito

Protocol: JSON 1.1 (X-Amz-Target: AWSCognitoIdentityProviderService.*) Endpoint: POST http://localhost:4566/

Floci serves pool-specific discovery and JWKS endpoints, plus a relaxed OAuth token endpoint, so local clients can mint and validate Cognito-like access tokens against RS256 signing keys.

CreateUserPool supports overiding several values using user-pool tags only at creation time:

  • floci:override-id, to pin the resulting UserPool.Id. Because a pinned id is caller-chosen it can be reused, which AWS never does. DeleteUserPool therefore deletes everything the pool owns (users, groups, app clients, resource servers, revoked token records and outstanding verification codes) so a pool recreated on the same id starts empty rather than inheriting the deleted pool's password hashes and client secrets.
  • floci:override-cognito-client-id
    • set to use-name to use the client name as client ID.
    • set to append-to-name:-somestring to append a string to the client name to be used as client ID.
    • set to prepend-to-name:somestring- to prepend a string to the client name to be used as client ID.
  • floci:override-cognito-client-secret, to set the secret for all clients created in this userpool.

Floci strips reserved floci:* tags from stored and returned UserPoolTags on both create and update paths, so the tag namespace acts as an input-only control channel and is never persisted as user-visible metadata.

Standalone TagResource rejects reserved floci:* keys. ListTagsForResource and UntagResource operate on the persisted user-pool tag map.

An action given a user pool ID that does not resolve returns ResourceNotFoundException with the live service's wording, User pool <poolId> does not exist., so tooling that matches Cognito error text behaves the same way locally.

Supported Actions

User Pools

Action Description
CreateUserPool Creates a local user pool, applying supported floci:* creation-time overrides from tags.
DescribeUserPool Returns the stored user pool configuration.
ListUserPools Lists local user pools visible in the request region.
UpdateUserPool Updates mutable user pool settings and persisted user-pool tags.
DeleteUserPool Deletes a local user pool and everything it owns: users, groups, app clients, resource servers, identity providers, revoked tokens and verification codes. Refused with InvalidParameterException while DeletionProtection is ACTIVE (switch it to INACTIVE with UpdateUserPool first, as on AWS) or while a domain is still configured.
GetUserPoolMfaConfig Returns the pool's MFA mode and, once configured, its software-token setting.
SetUserPoolMfaConfig Sets MfaConfiguration (OFF/ON/OPTIONAL) and SoftwareTokenMfaConfiguration. An absent MfaConfiguration means OFF, and turning MFA off drops the factor configuration with it. Validation follows the live service: OFF alongside a software-token, email or SMS factor is rejected, and ON/OPTIONAL with none of those three is rejected, in both cases on the member being present, not on its Enabled value. WebAuthnConfiguration sits outside both rules, as it does in AWS. SMS, email and WebAuthn configurations are validated and not stored: Floci cannot deliver those factors, so keeping the config would imply a capability it does not have.

User Pool Tags

Action Description
TagResource Adds user-visible tags to a user pool and rejects reserved floci:* tag keys.
UntagResource Removes tags from a user pool's persisted tag map.
ListTagsForResource Returns the persisted user-pool tags.

User Pool Clients

Action Description
CreateUserPoolClient Creates an app client for a user pool, including optional generated secret handling.
DescribeUserPoolClient Returns the stored app client configuration.
ListUserPoolClients Lists app clients for a user pool.
DeleteUserPoolClient Deletes an app client from a user pool.

Resource Servers

Action Description
CreateResourceServer Registers a resource server and scopes for a user pool.
DescribeResourceServer Returns a registered resource server.
ListResourceServers Lists resource servers for a user pool.
UpdateResourceServer Updates a resource server's name and scopes.
DeleteResourceServer Deletes a resource server from a user pool.

Identity Providers

Action Description
CreateIdentityProvider Registers a federated identity provider on a user pool.
DescribeIdentityProvider Returns a registered identity provider.
ListIdentityProviders Lists a user pool's providers as name/type/date summaries.
UpdateIdentityProvider Updates a provider's details, attribute mapping or identifiers.
DeleteIdentityProvider Deletes an identity provider from a user pool.

Providers can be used for generic OIDC authorization-code sign-in. Floci routes the AWS-shaped /oauth2/authorize and /oauth2/idpresponse endpoints to the configured local OIDC provider, exchanges the returned code, provisions or reconciles the federated user, and issues a Cognito authorization code for the registered callback.

Only ProviderType=OIDC is supported by this flow. Floci does not implement social providers such as Google, Facebook, Login with Amazon or SignInWithApple. The pool's own users sign in on the hosted login page instead (see Well-Known And OAuth Endpoints below).

Two deliberate divergences from AWS, both consequences of not calling out to a third party:

  • No create-time validation of ProviderDetails. AWS resolves an OIDC provider's oidc_issuer discovery document while handling CreateIdentityProvider, and rejects the call when it is unreachable. Floci stores ProviderDetails opaquely and makes no outbound request, so it also does not enforce the per-provider-type required keys.
  • No injected provider defaults. AWS adds keys such as attributes_url_add_attributes to an OIDC provider's stored details; Floci returns only what was supplied.

AttributeMapping and IdpIdentifiers follow AWS's update semantics: a member the request omits is left unchanged, and an explicitly empty map or list is what clears it. IdpIdentifiers is echoed by CreateIdentityProvider and UpdateIdentityProvider only when the request supplied it, whatever the stored value, while DescribeIdentityProvider always returns it.

User Pool Domains

Action Description
CreateUserPoolDomain Creates a Cognito prefix domain, or a custom domain when CustomDomainConfig.CertificateArn is given. As on AWS, the certificate must exist in ACM in us-east-1 with status ISSUED; the domain is listed under the certificate's InUseBy until it is deleted.
DescribeUserPoolDomain Returns a domain's description, including CloudFrontDistribution for custom domains.
UpdateUserPoolDomain Replaces a custom domain's certificate or changes the managed login version in place. The domain keeps its CloudFrontDistribution, and its InUseBy entry moves to the new certificate.
DeleteUserPoolDomain Deletes a domain from its user pool.

Custom domains

A domain created with CustomDomainConfig answers the OAuth endpoints on that host, as on AWS:

POST https://auth.example.localhost.floci.io/oauth2/token
GET  https://auth.example.localhost.floci.io/oauth2/userInfo

Requests are matched on the Host header, so the name must resolve to Floci (any *.localhost.floci.io does) and, for https, TLS must be enabled. The domain pins its user pool and the account that created it, since these requests carry no AWS credential: a client_id from another pool is refused with invalid_client, and an access token issued by another pool with invalid_token. Domain names are unique across all accounts, as on AWS. The pool's openid-configuration advertises the custom-domain URLs when one exists. Prefix domains (<prefix>.auth.<region>.amazoncognito.com) are stored but not routed, since that hostname never reaches Floci. /login and /logout are not served on any host.

With TLS enabled, a custom domain (CustomDomainConfig set) is added to Floci's server certificate as soon as it is created, so https://<domain> verifies without a restart; see TLS for the accepted suffixes. A prefix domain is served under amazoncognito.com on AWS, not by Floci, and is left alone.

Log Delivery

Action Description
SetLogDeliveryConfiguration Replaces a user pool's log delivery configuration.
GetLogDeliveryConfiguration Returns a user pool's log delivery configuration.

LogLevel accepts ERROR or INFO, and EventSource accepts userNotification or userAuthEvents. LogConfigurations holds at most 2 entries, and an event source may appear only once across them. Each entry must name a destination: CloudWatchLogsConfiguration, FirehoseConfiguration or S3Configuration, and a request that omits one is rejected the way AWS rejects it. Set replaces the whole list rather than merging, so an empty LogConfigurations is what clears it, and Get always returns the member, as [] when nothing is configured.

The length and enum checks run before the pool is looked up, so an oversized or malformed request naming a pool that does not exist reports the request problem rather than ResourceNotFoundException, and every violation of them is reported in one message.

Floci stores the configuration and never delivers anything to the destination: the log group, delivery stream or bucket is not written to, and is not required to exist. Two further divergences, both deliberate:

  • No pricing-tier gate. AWS refuses userAuthEvents on a pool in the ESSENTIALS tier with FeatureUnavailableInTierException; Floci accepts either event source whatever UserPoolTier says.
  • No destination validation. AWS checks the ARN it is handed; Floci stores it as given.

Admin User Management

Action Description
AdminCreateUser Creates or resends setup for a user in a user pool.
AdminGetUser Returns a user's stored attributes and status.
AdminDeleteUser Deletes a user from a user pool.
AdminSetUserPassword Sets a user's password and permanent-password status.
AdminUpdateUserAttributes Updates attributes for a user in a user pool.
AdminLinkProviderForUser Links an external IdP identity to an existing user's identities attribute.

User Operations

Action Description
SignUp Creates a self-service user for an app client.
ConfirmSignUp Confirms a pending self-service signup.
GetUser Returns attributes for the authenticated access-token user.
GetUserAttributeVerificationCode Issues a verification code for the authenticated user's email or phone_number attribute.
VerifyUserAttribute Verifies an email or phone_number attribute with its issued verification code.
UpdateUserAttributes Updates attributes for the authenticated access-token user.
ChangePassword Changes the authenticated user's password.
ForgotPassword Starts the local forgot-password flow for a user.
ConfirmForgotPassword Completes the forgot-password flow by setting a replacement password.

Authentication

Action Description
InitiateAuth Authenticates app-client users through supported user-password and SRP-style flows.
AdminInitiateAuth Starts an admin authentication flow for a user pool user.
RespondToAuthChallenge Responds to supported Cognito auth challenges.

User Listing

Action Description
ListUsers Lists users stored in a user pool.

Supported AuthFlow Values

InitiateAuth accepts USER_PASSWORD_AUTH, USER_SRP_AUTH, CUSTOM_AUTH, USER_AUTH, REFRESH_TOKEN_AUTH and REFRESH_TOKEN. AdminInitiateAuth accepts ADMIN_USER_PASSWORD_AUTH, ADMIN_NO_SRP_AUTH, USER_SRP_AUTH, USER_PASSWORD_AUTH, CUSTOM_AUTH, USER_AUTH, REFRESH_TOKEN_AUTH and REFRESH_TOKEN.

USER_AUTH is the choice-based flow: with no PREFERRED_CHALLENGE it returns ChallengeName=SELECT_CHALLENGE and an AvailableChallenges list drawn from what the user has configured (PASSWORD, PASSWORD_SRP, EMAIL_OTP, SMS_OTP); with one, it goes straight to that challenge. It requires the user pool's tier to be Essentials or higher. WEB_AUTHN and the ConfirmSignUp session as a first-factor shortcut are not implemented yet.

Any other AuthFlow value is rejected with InvalidParameterException and no tokens are issued.

An app client only accepts the flows in its ExplicitAuthFlows: ALLOW_USER_PASSWORD_AUTH, ALLOW_USER_SRP_AUTH, ALLOW_CUSTOM_AUTH, ALLOW_USER_AUTH, ALLOW_ADMIN_USER_PASSWORD_AUTH and ALLOW_REFRESH_TOKEN_AUTH, or the legacy USER_PASSWORD_AUTH, ADMIN_NO_SRP_AUTH and CUSTOM_AUTH_FLOW_ONLY. Any other flow fails with InvalidParameterException.

A client created without ExplicitAuthFlows, or with it cleared to an empty list, stores and describes an empty list, matching AWS. It is enforced as if it had ALLOW_REFRESH_TOKEN_AUTH, ALLOW_USER_SRP_AUTH and ALLOW_CUSTOM_AUTH, the default AWS documents for such a client: only the enforcement uses that default, not the stored or returned value. A client that signs in with USER_PASSWORD_AUTH, ADMIN_USER_PASSWORD_AUTH or USER_AUTH must list the matching ALLOW_ value.

User Attribute Update Verification

CreateUserPool, UpdateUserPool, and DescribeUserPool support UserAttributeUpdateSettings.AttributesRequireVerificationBeforeUpdate for email and phone_number.

For attributes listed in this setting, UpdateUserAttributes keeps the existing verified value and sign-in alias active while the new value is pending. It sends a verification code to the pending destination and returns the corresponding entry in CodeDeliveryDetailsList. A successful VerifyUserAttribute promotes the pending value, switches the alias, and sets the matching *_verified attribute to true.

Without the setting, UpdateUserAttributes replaces the value immediately and sets the matching *_verified attribute to false until verification succeeds. A verification code is still sent to the replacement value, but Cognito no longer retains or exposes the old value. For alias attributes, the old alias is removed immediately and the replacement becomes usable for sign-in only after successful verification. Incorrect or expired codes don't promote a pending value or change its verified state.

Groups

Action Description
CreateGroup Creates a group in a user pool.
GetGroup Returns a user-pool group.
UpdateGroup Updates a user-pool group's stored settings.
ListGroups Lists groups in a user pool.
ListUsersInGroup Lists users assigned to a group.
DeleteGroup Deletes a group from a user pool.
AdminAddUserToGroup Adds a user to a group.
AdminRemoveUserFromGroup Removes a user from a group.
AdminListGroupsForUser Lists the groups assigned to a user.

Managed Login Branding

Action Description
CreateManagedLoginBranding Creates the branding for an app client.
DescribeManagedLoginBranding Returns a branding by its id.
DescribeManagedLoginBrandingByClient Returns the branding attached to an app client.
UpdateManagedLoginBranding Updates a branding's settings, assets or provided-values flag.
DeleteManagedLoginBranding Deletes a branding from its app client.

CreateManagedLoginBranding must name either UseCognitoProvidedValues or Settings; a request with neither is rejected. One branding per app client: a second CreateManagedLoginBranding for the same client is rejected with ManagedLoginBrandingExistsException. ManagedLoginBrandingId must be a version 4 UUID, and a malformed one is rejected before the lookup, as AWS does. Assets holds at most 40 entries on create and on update. Settings is omitted from the response when the caller supplied none, while Assets is always returned. Members an update omits are left unchanged.

The asset-count and branding-id checks run before the pool, client or branding is looked up, so an oversized request naming something that does not exist reports the request problem rather than ResourceNotFoundException, and an update violating both reports them in one message with the asset list first.

Branding is presentation for the hosted UI. Floci's hosted login page does not apply it, so it is stored and returned rather than rendered. Two divergences follow from that:

  • Settings is stored opaquely. AWS validates it against a deep schema, rejecting unknown properties with Invalid settings provided. Validation errors: [{property: $.components...., errorType: UnknownProperty}]. That schema is not published, so Floci accepts any object.
  • A wrongly typed Settings returns a client error. AWS answers that particular input with InternalErrorException and a 500; Floci returns SerializationException: Unexpected field type, which is what AWS returns for a wrongly typed Assets. Reproducing someone else's 500 seemed worse than being consistent.
  • ReturnMergedResources is not honoured. Against AWS it merges Cognito's own default settings and assets into the response: on a pool with 8 configured assets it returns 38. Reproducing that needs Cognito's default corpus, so Floci returns the stored branding either way.

Well-Known And OAuth Endpoints

Endpoint Description
GET /{userPoolId}/.well-known/openid-configuration OpenID discovery document
GET /{userPoolId}/.well-known/jwks.json JSON Web Key Set for JWT validation
GET /cognito-idp/oauth2/authorize OIDC authorization-code start endpoint
GET /cognito-idp/oauth2/idpresponse OIDC provider callback endpoint
GET, POST /cognito-idp/login Hosted login page for the pool's own users
GET /cognito-idp/logout Ends the hosted login session
POST /cognito-idp/oauth2/token OAuth authorization-code, refresh-token and client-credentials token endpoint

The OAuth endpoints support browser-style OIDC authorization-code sign-in as well as the emulator-friendly client-credentials flow:

  • GET /cognito-idp/oauth2/authorize validates the app client and callback. With identity_provider naming an OIDC provider it redirects there with an opaque state and nonce. Without identity_provider, or with identity_provider=COGNITO, the pool's own users sign in on the hosted login page; the client must list COGNITO in SupportedIdentityProviders (the default when none are given).
  • GET /cognito-idp/login shows a user name (or email) and password form when the client lists COGNITO, and a "Continue with " button for each of its other SupportedIdentityProviders, which starts the federated sign-in above with the same authorize parameters. A correct password redirects to the registered callback with a one-time authorization code and state, and starts a one-hour browser session (the cognito cookie), so the next authorize request returns a code without the form unless it sends prompt=login. Wrong passwords, unknown users and unconfirmed users get the same answer until the password is right. Users with a temporary password, MFA or a pending reset are told so; those steps are not served on the page yet.
  • GET /cognito-idp/logout?client_id=...&logout_uri=... ends the session and redirects to logout_uri, which must be one of the client's LogoutURLs. With redirect_uri and response_type=code instead, it ends the session and shows the login page again.
  • PKCE: send code_challenge with code_challenge_method=S256 to authorize, and the matching code_verifier when redeeming the code. nonce is copied into the ID token.
  • Codes from the hosted login and from federated sign-in fire the Pre Token Generation trigger with triggerSource=TokenGeneration_HostedAuth, and the hosted login fires Pre and Post Authentication.
  • GET /cognito-idp/oauth2/idpresponse consumes the provider state, exchanges the provider code and redirects to the registered callback with a one-time Cognito authorization code.
  • POST /cognito-idp/oauth2/token redeems that authorization code once, exchanges a refresh_token for new access and ID tokens, or issues a machine token for grant_type=client_credentials.

POST /cognito-idp/oauth2/token is intentionally emulator-friendly rather than full Cognito parity:

  • It requires an existing client_id.
  • It accepts client_id and client_secret from the form body or Basic auth.
  • Client-credentials requires a confidential app client created with GenerateSecret=true.
  • Authorization-code redemption validates the client, callback URI and one-time code binding.
  • It requires AllowedOAuthFlowsUserPoolClient=true and AllowedOAuthFlows=["client_credentials"].
  • It doesn't require a Cognito domain.
  • Client-credentials returns only access_token, token_type, and expires_in; authorization-code redemption returns the Cognito access, ID and refresh token set.
  • It validates requested OAuth scopes against the app client's AllowedOAuthScopes and the pool's registered resource-server scopes.
  • It advertises the prefixed token endpoint in /{userPoolId}/.well-known/openid-configuration, or https://<domain>/oauth2/token when the pool has a custom domain (see Custom domains above).

Sign-in Identifiers (UsernameAttributes)

Floci follows real Cognito semantics for pools created with UsernameAttributes (e.g. --username-attributes email):

  • AdminCreateUser/SignUp accept the email (or phone number) as the sign-in value, but the canonical Username is an auto-generated, immutable UUID equal to sub. The supplied email is stored as a mutable alias attribute.
  • ListUsers, AdminGetUser and Lambda trigger event.userName all report the UUID, never the email; the email lives in the email attribute and in event.request.userAttributes.email.
  • Sign-in resolves by the current email alias or the UUID. SECRET_HASH is validated against the exact USERNAME value sent: Base64(HMAC-SHA256(USERNAME + clientId, clientSecret)).
  • In CUSTOM_AUTH / SRP flows, ChallengeParameters.USERNAME echoes the UUID (as real AWS does); the RespondToAuthChallenge SECRET_HASH is validated against the USERNAME sent that round. That USERNAME must still resolve to the session's user — the UUID or any of its current aliases — and a value naming a different user is rejected with NotAuthorizedException.
  • AdminUpdateUserAttributes can change the email; afterwards sign-in works with the new email and fails with the old one, while Username/sub stay fixed.
  • Duplicate email/phone on AdminCreateUser: UsernameExistsException when the incoming alias is unverified, AliasExistsException when it is verified (email_verified=true); pass ForceAliasCreation=true to migrate a verified alias off the previous owner. On AdminUpdateUserAttributes, changing to an in-use alias throws AliasExistsException.

Pools without UsernameAttributes (classic pools, and pools using AliasAttributes) keep the literal Username you supply, unchanged.

Token claims

Floci mirrors AWS's access-token / ID-token split:

  • Access token: sub, username (the UUID), scope (aws.cognito.signin.user.admin for API sign-in), client_id, cognito:groups, jti/origin_jti. It does not carry cognito:username or user attributes like email.
  • ID token: sub, cognito:username, aud, and readable user attributes (email, email_verified, phone_number, custom:*, ...). Attribute claims are filtered by the app client's ReadAttributes (an unset/empty list means all attributes are readable).

Not-found errors (ResourceNotFoundException, UserNotFoundException) return HTTP 400, matching the Cognito JSON protocol.

Configuration

Variable Default Description
FLOCI_SERVICES_COGNITO_ENABLED true Enable or disable the service

Examples

export AWS_ENDPOINT_URL=http://localhost:4566

# Create a user pool
POOL_ID=$(aws cognito-idp create-user-pool \
  --pool-name MyApp \
  --query UserPool.Id --output text \
  --endpoint-url $AWS_ENDPOINT_URL)

# Create an app client
CLIENT_ID=$(aws cognito-idp create-user-pool-client \
  --user-pool-id $POOL_ID \
  --client-name my-client \
  --generate-secret \
  --allowed-o-auth-flows-user-pool-client \
  --allowed-o-auth-flows client_credentials \
  --allowed-o-auth-scopes notes/read notes/write \
  --query UserPoolClient.ClientId --output text \
  --endpoint-url $AWS_ENDPOINT_URL)

# Retrieve the generated client secret
CLIENT_SECRET=$(aws cognito-idp describe-user-pool-client \
  --user-pool-id $POOL_ID \
  --client-id $CLIENT_ID \
  --query UserPoolClient.ClientSecret --output text \
  --endpoint-url $AWS_ENDPOINT_URL)

# Register a resource server and scopes
aws cognito-idp create-resource-server \
  --user-pool-id $POOL_ID \
  --identifier notes \
  --name "Notes API" \
  --scopes ScopeName=read,ScopeDescription="Read notes" ScopeName=write,ScopeDescription="Write notes" \
  --endpoint-url $AWS_ENDPOINT_URL

# Create a user
aws cognito-idp admin-create-user \
  --user-pool-id $POOL_ID \
  --username alice@example.com \
  --temporary-password Temp1234! \
  --endpoint-url $AWS_ENDPOINT_URL

# Set a permanent password
aws cognito-idp admin-set-user-password \
  --user-pool-id $POOL_ID \
  --username alice@example.com \
  --password Perm1234! \
  --permanent \
  --endpoint-url $AWS_ENDPOINT_URL

# Authenticate
aws cognito-idp initiate-auth \
  --auth-flow USER_PASSWORD_AUTH \
  --client-id $CLIENT_ID \
  --auth-parameters USERNAME=alice@example.com,PASSWORD=Perm1234! \
  --endpoint-url $AWS_ENDPOINT_URL

# Create a group
aws cognito-idp create-group \
  --user-pool-id $POOL_ID \
  --group-name admin \
  --description "Admin group" \
  --endpoint-url $AWS_ENDPOINT_URL

# Add user to group
aws cognito-idp admin-add-user-to-group \
  --user-pool-id $POOL_ID \
  --group-name admin \
  --username alice@example.com \
  --endpoint-url $AWS_ENDPOINT_URL

# List groups for user
aws cognito-idp admin-list-groups-for-user \
  --user-pool-id $POOL_ID \
  --username alice@example.com \
  --endpoint-url $AWS_ENDPOINT_URL

# Fetch the pool discovery document
curl -s "$AWS_ENDPOINT_URL/$POOL_ID/.well-known/openid-configuration"

# Get a machine access token from the OAuth endpoint
curl -s \
  -X POST "$AWS_ENDPOINT_URL/cognito-idp/oauth2/token" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -u "$CLIENT_ID:$CLIENT_SECRET" \
  --data-urlencode "grant_type=client_credentials" \
  --data-urlencode "scope=notes/read notes/write"

JWT Validation

Tokens issued by Floci can be validated using the discovery and JWKS endpoints:

http://localhost:4566/$POOL_ID/.well-known/openid-configuration
http://localhost:4566/$POOL_ID/.well-known/jwks.json

Tokens include the cognito:groups claim as a JSON array when the authenticated user belongs to one or more groups.

Tokens issued by Cognito auth flows and the OAuth token endpoint use the emulator base URL plus the pool id:

http://localhost:4566/$POOL_ID

This keeps the issuer, discovery document, JWKS URL, and token endpoint internally consistent for local JWT validation while supporting LocalStack-style confidential clients and resource-server-backed scopes.