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.
When configured, PostAuthentication and PreTokenGeneration Lambda triggers must succeed
before authentication or token issuance completes. Function errors and malformed responses
return Cognito Lambda errors instead of issuing tokens without the trigger's claims.
CreateUserPool supports overriding 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.
CreateUserPoolClient and UpdateUserPoolClient store AuthSessionValidity in minutes,
and DescribeUserPoolClient returns it. Values must be integers from 3 through 15.
New clients default to 3 minutes; an update that omits the field retains its stored value.
An explicit JSON null behaves as an omitted field. Wrong JSON types or numbers
outside the 32-bit integer range return SerializationException. Integer durations
outside 3 through 15 return InvalidParameterException with Cognito's constraint-error
format. Validation runs before pool or client lookup.
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
through managed login instead (see Managed login).
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'soidc_issuerdiscovery document while handlingCreateIdentityProvider, and rejects the call when it is unreachable. Floci storesProviderDetailsopaquely 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_attributesto 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:
GET https://auth.example.localhost.floci.io/oauth2/authorize
POST https://auth.example.localhost.floci.io/oauth2/token
GET https://auth.example.localhost.floci.io/oauth2/userInfo
GET https://auth.example.localhost.floci.io/login
GET https://auth.example.localhost.floci.io/logout
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 served at the root only
on a custom-domain host; on Floci's own host they are /cognito-idp/login and /cognito-idp/logout.
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
userAuthEventson a pool in theESSENTIALStier withFeatureUnavailableInTierException; Floci accepts either event source whateverUserPoolTiersays. - 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. |
| GetUserAuthFactors | Returns the authenticated access-token user's sign-in factors: PASSWORD when the user has a password, EMAIL_OTP and SMS_OTP when the email or phone number is verified, whatever the pool's AllowedFirstAuthFactors allows, as on AWS, and SOFTWARE_TOKEN once VerifySoftwareToken has confirmed an authenticator. The access token must carry the aws.cognito.signin.user.admin scope. UserMFASettingList and PreferredMfaSetting report the email MFA preference set with SetUserMFAPreference. WEB_AUTHN and SMS or software-token MFA settings are not reported. |
| 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. |
As on AWS, every operation authorized by the user's access token (GetUser,
GetUserAuthFactors, UpdateUserAttributes, DeleteUserAttributes, ChangePassword,
GetUserAttributeVerificationCode, VerifyUserAttribute, SetUserMFAPreference,
GlobalSignOut, and AssociateSoftwareToken and VerifySoftwareToken with an AccessToken)
requires the token's scope to include aws.cognito.signin.user.admin. Tokens from
InitiateAuth and the other API sign-in flows always carry it; a token from the OAuth token
endpoint carries it only when the authorization request asked for it, or asked for no scope and
the client allows it. Without it the call fails with NotAuthorizedException:
Access Token does not have required scopes, as it does for a token with no scope claim at all,
which a pre token generation trigger leaves when it suppresses every scope.
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, including TOTP setup and software-token MFA. |
| AssociateSoftwareToken | Creates a TOTP secret for a user identified by an MFA setup session or access token. |
| VerifySoftwareToken | Verifies the TOTP code and enables the user's software token. |
With MfaConfiguration=ON and software-token MFA enabled, a successful password or SRP
first factor returns MFA_SETUP instead of tokens for a user without a verified token.
Call AssociateSoftwareToken with that session, then VerifySoftwareToken with the
returned session and a six-digit TOTP code. Finish with RespondToAuthChallenge
(MFA_SETUP) to receive tokens. Later sign-ins return SOFTWARE_TOKEN_MFA, which
requires a fresh code in SOFTWARE_TOKEN_MFA_CODE. Both token-management actions
also accept an access token for an already authenticated user. Sessions expire with
the app client's AuthSessionValidity and cannot be replayed after completion.
This flow currently covers software-token MFA required by a pool. Optional MFA preferences, SMS/email MFA challenges, and managed-login MFA are not emulated.
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. The list keeps only the first factors the pool's
Policies.SignInPolicy.AllowedFirstAuthFactors allows: its PASSWORD covers both PASSWORD and
PASSWORD_SRP, so a pool that allows only EMAIL_OTP offers ["EMAIL_OTP"], and a user with no
password is never offered a password challenge. A pool created without a SignInPolicy gets
{"AllowedFirstAuthFactors": ["PASSWORD"]}, on any tier, as DescribeUserPool reports on AWS, so it
offers password challenges alone. UpdateUserPool keeps the policy when the request omits Policies,
and puts the default back when its Policies has no SignInPolicy. A PREFERRED_CHALLENGE outside
the list, because the policy leaves it out or the user has not set it up, gets SELECT_CHALLENGE and
the list, as on AWS; one that names no challenge Cognito supports fails with
InvalidParameterException. A SELECT_CHALLENGE answer outside the list fails with
InvalidParameterException. 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.
The challenge Session of a USER_SRP_AUTH, CUSTOM_AUTH or USER_AUTH sign-in is valid for 3 minutes
from when it was issued, the AWS default for the client's AuthSessionValidity. Answering the challenge with
an older session fails with NotAuthorizedException (Invalid session for the user, session is expired.)
and the sign-in has to start again. A per-client AuthSessionValidity is not supported yet, and the
NEW_PASSWORD_REQUIRED challenge does not check its session.
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 managed login pages. Floci's sign-in page is deliberately plain, so branding is stored and returned rather than rendered. Two divergences follow from that:
Settingsis stored opaquely. AWS validates it against a deep schema, rejecting unknown properties withInvalid settings provided. Validation errors: [{property: $.components...., errorType: UnknownProperty}]. That schema is not published, so Floci accepts any object.- A wrongly typed
Settingsreturns a client error. AWS answers that particular input withInternalErrorExceptionand a 500; Floci returnsSerializationException: Unexpected field type, which is what AWS returns for a wrongly typedAssets. Reproducing someone else's 500 seemed worse than being consistent. ReturnMergedResourcesis 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 |
Authorization-code start endpoint, for managed login and OIDC |
GET /cognito-idp/oauth2/idpresponse |
OIDC provider callback endpoint |
GET, POST /cognito-idp/login |
Managed login sign-in form |
GET /cognito-idp/logout |
Managed login sign-out |
POST /cognito-idp/oauth2/token |
OAuth authorization-code and client-credentials token endpoint |
The OAuth endpoints support browser-style authorization-code sign-in, for the pool's own users and through a federated OIDC provider, as well as the emulator-friendly client-credentials flow:
GET /cognito-idp/oauth2/authorizevalidates the app client and callback. With noidentity_provider, oridentity_provider=COGNITO, it starts managed login (below); with any other provider name it redirects to that OIDC provider with an opaque state and nonce.GET /cognito-idp/oauth2/idpresponseconsumes the provider state, exchanges the provider code and redirects to the registered callback with a one-time Cognito authorization code.POST /cognito-idp/oauth2/tokenredeems that authorization code once, checking its PKCEcode_verifierwhen the authorization request sent acode_challenge, or issues a machine token forgrant_type=client_credentials.
Managed login
Managed login signs in the pool's own users with authorization code and, optionally, PKCE.
The client needs COGNITO in SupportedIdentityProviders, AllowedOAuthFlows=["code"] and
the callback in CallbackURLs. No domain is needed; on a custom domain the same flow runs at
/oauth2/authorize, /login and /logout.
GET /cognito-idp/oauth2/authorizeredirects to/cognito-idp/loginwith the request's parameters,login_hintincluded. If the browser already has a managed login session in the pool, it skips the form and redirects straight to the callback with a code, as AWS does. Ascopethe client'sAllowedOAuthScopesdoes not include is refused first, as on AWS, with a redirect to the callback carryingerror=invalid_request,error_description=invalid_scopeand thestate.GET /cognito-idp/loginrenders a plain username and password form, with the username filled in fromlogin_hintwhen the request has one. The form carries the request in hidden fields and a CSRF token that must match theXSRF-TOKENcookie set with it. With choice-based sign-in (below), it asks for the username alone.POST /cognito-idp/loginchecks the password asUSER_PASSWORD_AUTHdoes, including sign-in aliases and the pre and post authentication and user migration triggers, but without the client'sExplicitAuthFlows. On success it sets acognitosession cookie (one hour) and redirects to the callback withcodeandstate. A wrong password shows the form again withIncorrect username or password.; an unknown user reads the same.POST /cognito-idp/oauth2/tokenredeems the code. It invokes the pre token generation trigger with triggerSourceTokenGeneration_HostedAuth, as AWS does for a hosted-UI sign-in, so a pool that customises its claims gets the same tokens here as fromInitiateAuth. The access token'sscopeis the scopes the request asked for, or every scope in the client'sAllowedOAuthScopeswhen it asked for none, as on AWS, and the trigger is told the same scopes. The code keeps the scopes granted when it was issued: a scope the client is allowed only afterwards is not added, and one it no longer allows is dropped. A V2 trigger'sscopesToAddandscopesToSuppressapply on top of them. The ID token is issued only when the scopes includeopenid, and it carries the request'snonce, which the trigger cannot override.GET /cognito-idp/logout?client_id=...&logout_uri=...ends the session and redirects tologout_uri, which must be one of the client'sLogoutURLs. Withredirect_uriandresponse_type=codeinstead oflogout_uri, it ends the session and redirects to the sign-in form for that request.
Choice-based sign-in applies when the pool's SignInPolicy.AllowedFirstAuthFactors includes
EMAIL_OTP and the client's ExplicitAuthFlows includes ALLOW_USER_AUTH (on a pool above the
Lite tier). The page then takes one step per POST /cognito-idp/login. Up to the password or the
code, every username gets the same pages, so they do not tell who has an account, which factors
they have, or whether they can sign in:
- The username.
- The factor, as buttons that post
challenge=PASSWORDorchallenge=EMAIL_OTP, when the policy allows both. When it allows onlyEMAIL_OTP, every username goes straight to the code. - For
PASSWORD, the username and password form, where a user without a password fails as a wrong password does. ForEMAIL_OTP, the page sends a code through theUSER_AUTHEMAIL_OTPchallenge, so it arrives in Floci's SES (readable at/_aws/ses), and shows a code field. The username and the challenge'sSessiontravel in hidden fields. A correct code signs the user in as the password does: the session cookie, and a redirect to the callback withcodeandstate. A wrong code shows the code field again withInvalid verification code provided, please try again.and the same session, so the user can try again until the session (the client'sAuthSessionValidity) or the code runs out.
Only a user who can sign in and has a verified email is sent a code. Anyone else, an unknown user
included, gets the same code field, but no message is sent and every code is wrong. Asking again
within 30 seconds, before Floci sends another code, shows the code field for the code already
sent. Why a user cannot sign in (disabled, unconfirmed, a password reset or a new password
required) shows only after a correct code. A correct code uses up the session, so when the user
cannot sign in or the post authentication trigger fails, the page goes back to the username with
the reason. A code signs in once: of several requests that answer with it at once, from one session
or several, one signs in and the others are told the code is wrong, or go back to the username if
their session is already spent. A password posted to a pool whose policy leaves out PASSWORD is
refused. Without choice-based sign-in, the page is the username and password form above, whatever
the policy says.
PKCE follows AWS: code_challenge_method must be S256, and discovery advertises
code_challenge_methods_supported: ["S256"]. A code issued with a code_challenge is
redeemed only with the matching code_verifier, so a public client (no secret) can use it
alone. A code issued without one is refused if a code_verifier is sent, as RFC 9700
recommends. A failed PKCE check spends the code; a request naming the wrong client or
redirect_uri does not. PKCE applies to federated OIDC sign-in too.
Differences from AWS:
- No challenge pages. A user who must change or reset their password, or who is not
confirmed, sees an error on the form instead. Sign-up, forgot-password, MFA and passkey
pages are not served, choice-based sign-in offers no
SMS_OTPorWEB_AUTHNfactor, andprompt,langandidp_identifierare ignored. - Errors are JSON. An authorization request error returns
400with an OAuth error body, even afterredirect_uriis validated, where AWS redirects the error to the callback. The exception is an unallowedscope, which is redirected as on AWS. - Relative redirect. The redirect from
/oauth2/authorizeto the sign-in form has a relativeLocation, where AWS's is absolute. - One session cookie per host. Floci's own host serves every pool, so signing in to a second pool there replaces the first pool's session. Custom domains keep separate sessions, as on AWS. Sessions are held in memory and are lost on restart.
EP=http://localhost:4566
POOL_ID=$(aws --endpoint-url $EP cognito-idp create-user-pool --pool-name web \
--query UserPool.Id --output text)
CLIENT_ID=$(aws --endpoint-url $EP cognito-idp create-user-pool-client --user-pool-id $POOL_ID \
--client-name spa --supported-identity-providers COGNITO \
--allowed-o-auth-flows-user-pool-client --allowed-o-auth-flows code \
--allowed-o-auth-scopes openid email --callback-urls https://app.example.com/cb \
--logout-urls https://app.example.com/ --query UserPoolClient.ClientId --output text)
aws --endpoint-url $EP cognito-idp admin-create-user --user-pool-id $POOL_ID --username alice
aws --endpoint-url $EP cognito-idp admin-set-user-password --user-pool-id $POOL_ID \
--username alice --password 'Perm1234!' --permanent
# PKCE pair: verifier, and its unpadded base64url SHA-256 challenge
VERIFIER=$(openssl rand -base64 48 | tr '+/' '-_' | tr -d '=\n')
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | base64 | tr '+/' '-_' | tr -d '=')
# Open the sign-in form, then post the credentials with its CSRF token
Q="response_type=code&client_id=$CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fcb&scope=openid&state=s1&code_challenge=$CHALLENGE&code_challenge_method=S256"
CSRF=$(curl -s -c jar "$EP/cognito-idp/login?$Q" | sed -n 's/.*name="_csrf" value="\([^"]*\)".*/\1/p')
CODE=$(curl -s -b jar -c jar -o /dev/null -w '%{redirect_url}' "$EP/cognito-idp/login?$Q" \
--data-urlencode "_csrf=$CSRF" --data-urlencode username=alice --data-urlencode 'password=Perm1234!' \
| sed -n 's/.*[?&]code=\([^&]*\).*/\1/p')
# Redeem the code with the verifier
curl -s -X POST "$EP/cognito-idp/oauth2/token" \
--data-urlencode grant_type=authorization_code --data-urlencode client_id=$CLIENT_ID \
--data-urlencode code=$CODE --data-urlencode redirect_uri=https://app.example.com/cb \
--data-urlencode code_verifier=$VERIFIER
# Sign out
curl -s -b jar -o /dev/null -w '%{http_code} %{redirect_url}\n' \
"$EP/cognito-idp/logout?client_id=$CLIENT_ID&logout_uri=https%3A%2F%2Fapp.example.com%2F"
POST /cognito-idp/oauth2/token is intentionally emulator-friendly rather than full Cognito parity:
- It requires an existing
client_id. - It accepts
client_idandclient_secretfrom 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=trueandAllowedOAuthFlows=["client_credentials"]. - It doesn't require a Cognito domain.
- Client-credentials returns only
access_token,token_type, andexpires_in; authorization-code redemption returns the Cognito access and refresh tokens, and an ID token when the granted scopes includeopenid. - It validates requested OAuth scopes against the app client's
AllowedOAuthScopesand the pool's registered resource-server scopes. - It advertises the prefixed token endpoint in
/{userPoolId}/.well-known/openid-configuration, orhttps://<domain>/oauth2/tokenwhen 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/SignUpaccept the email (or phone number) as the sign-in value, but the canonicalUsernameis an auto-generated, immutable UUID equal tosub. The supplied email is stored as a mutable alias attribute.ListUsers,AdminGetUserand Lambda triggerevent.userNameall report the UUID, never the email; the email lives in theemailattribute and inevent.request.userAttributes.email.- Sign-in resolves by the current email alias or the UUID.
SECRET_HASHis validated against the exactUSERNAMEvalue sent:Base64(HMAC-SHA256(USERNAME + clientId, clientSecret)). - In
CUSTOM_AUTH/ SRP flows,ChallengeParameters.USERNAMEechoes the UUID (as real AWS does); theRespondToAuthChallengeSECRET_HASHis validated against theUSERNAMEsent that round. ThatUSERNAMEmust still resolve to the session's user — the UUID or any of its current aliases — and a value naming a different user is rejected withNotAuthorizedException. AdminUpdateUserAttributescan change the email; afterwards sign-in works with the new email and fails with the old one, whileUsername/substay fixed.- Duplicate email/phone on
AdminCreateUser:UsernameExistsExceptionwhen the incoming alias is unverified,AliasExistsExceptionwhen it is verified (email_verified=true); passForceAliasCreation=trueto migrate a verified alias off the previous owner. OnAdminUpdateUserAttributes, changing to an in-use alias throwsAliasExistsException.
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.adminfor API sign-in, the granted OAuth scopes for an authorization code),client_id,cognito:groups,jti/origin_jti. It does not carrycognito:usernameor user attributes likeemail. - ID token:
sub,cognito:username,aud, and readable user attributes (email,email_verified,phone_number,custom:*, ...). Attribute claims are filtered by the app client'sReadAttributes(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:
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:
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.