Bloom LIMS uses AWS Cognito for single sign-on (SSO) authentication via the hosted UI. In the 2.0 refactor, Bloom depends on daylily-auth-cognito as the shared auth boundary: browser session handling lives in browser.session, Hosted UI URL and code-exchange helpers live in browser.oauth and browser.google, bearer verification lives in runtime.verifier and runtime.m2m, and Cognito lifecycle stays in daycog via admin.*. Session storage remains token-free.
- Prerequisites
- AWS Cognito Configuration
- Environment Variable Configuration
- Local Development Setup
- Authentication Flow
- Troubleshooting
- Security Considerations
Before setting up Cognito authentication for Bloom LIMS, ensure you have:
| Requirement | Description |
|---|---|
| AWS Account | Active AWS account with billing enabled |
| IAM Permissions | User/role with AmazonCognitoPowerUser policy or equivalent permissions |
| Region Access | Access to your chosen AWS region (e.g., us-east-1, us-west-2) |
Your AWS user/role needs these minimum permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"cognito-idp:CreateUserPool",
"cognito-idp:CreateUserPoolClient",
"cognito-idp:CreateUserPoolDomain",
"cognito-idp:UpdateUserPool",
"cognito-idp:UpdateUserPoolClient",
"cognito-idp:DescribeUserPool",
"cognito-idp:DescribeUserPoolClient",
"cognito-idp:ListUserPools",
"cognito-idp:ListUserPoolClients"
],
"Resource": "*"
}
]
}| Requirement | Version | Purpose |
|---|---|---|
| Python | 3.10+ | Application runtime |
| PyJWT | 2.0+ | JWT token validation |
| python-dotenv | Any | Environment variable loading |
Install required packages:
pip install pyjwt[crypto] python-dotenvIf using Google as an identity provider:
- Google Cloud Project with OAuth 2.0 credentials
- OAuth Consent Screen configured
- OAuth 2.0 Client ID (Web application type)
- Log in to the AWS Console
- Search for "Cognito" in the services search bar
- Click Amazon Cognito
- Click Create user pool
- Authentication providers: Select Cognito user pool
- Cognito user pool sign-in options: Check:
- ☐ User name (optional)
- ☐ Phone number (optional)
- Click Next
-
Password policy:
- Mode: Cognito defaults (recommended) or Custom
- Cognito defaults require: 8+ characters, uppercase, lowercase, number, special character
-
Multi-factor authentication (MFA):
- For development: No MFA
- For production: Optional MFA or Required MFA
-
User account recovery:
- ☑️ Enable self-service account recovery
- Delivery method: Email only
-
Click Next
-
Self-registration: Enable or disable based on your needs
- Enable for open registration
- Disable if you'll create users manually or via federated identity
-
Attribute verification:
- ☑️ Allow Cognito to automatically send messages to verify and confirm
- Attributes to verify: Email
-
Required attributes: Select at minimum:
-
Click Next
-
Email provider:
- For development/testing: Send email with Cognito (50 emails/day limit)
- For production: Send email with Amazon SES (requires SES setup)
-
If using Cognito email:
- FROM email address:
no-reply@verificationemail.com(default)
- FROM email address:
-
Click Next
-
User pool name: Enter a descriptive name (e.g.,
bloom-lims-prodorbloom-lims-dev) -
Hosted authentication pages:
- ☑️ Use the Cognito Hosted UI
-
Domain:
- Domain type: Select Use a Cognito domain
- Cognito domain: Enter a unique prefix (e.g.,
bloom-lims-yourorg) - Full domain will be:
bloom-lims-yourorg.auth.{region}.amazoncognito.com
-
Initial app client:
- App type: Select Public client
- App client name:
bloom-lims-web - Client secret: Select Don't generate a client secret
⚠️ Important: Bloom uses the authorization-code flow and expects the Hosted UI to redirect back to/auth/callback
-
Allowed callback URLs:
https://localhost:8912/auth/callbackAdd your production callback URL(s) here as well (e.g.,
https://bloom.yourcompany.com/auth/callback) -
Allowed sign-out URLs:
https://localhost:8912/ -
Advanced app client settings (expand this section):
-
OAuth 2.0 grant types:
- ☐ Implicit grant
- ☑️ Authorization code grant
-
OpenID Connect scopes:
- ☑️ OpenID
- ☑️ Profile
-
-
Click Next
-
Review all settings
-
Click Create user pool
-
Record these values (you'll need them for Bloom YAML configuration):
Value Where to Find User Pool ID User pool overview → User pool ID (e.g., us-east-1_AbCdEfGhI)Client ID App integration → App clients → Client ID Domain App integration → Domain → Cognito domain Region From User Pool ID prefix (e.g., us-east-1)
If you need to modify the app client after creation:
- Navigate to your User Pool
- Go to App integration tab
- Scroll to App clients and analytics
- Click on your app client name
Click Edit under Hosted UI:
┌─────────────────────────────────────────────────────────────┐
│ Hosted UI Settings │
├─────────────────────────────────────────────────────────────┤
│ Allowed callback URLs: │
│ https://localhost:8912/auth/callback │
│ https://your-production-domain.com/auth/callback │
│ │
│ Allowed sign-out URLs: │
│ https://localhost:8912/ │
│ https://your-production-domain.com/ │
│ │
│ Identity providers: │
│ ☑️ Cognito user pool │
│ ☑️ Google (if configured) │
│ │
│ OAuth 2.0 grant types: │
│ ☑️ Authorization code grant │
│ │
│ OpenID Connect scopes: │
│ ☑️ openid │
│ ☑️ email │
│ ☑️ profile │
└─────────────────────────────────────────────────────────────┘
- Go to Google Cloud Console
- Select or create a project
- Navigate to APIs & Services → Credentials
- Click Create Credentials → OAuth client ID
- Configure the OAuth consent screen if prompted:
- User type: External (or Internal for Google Workspace)
- App name:
Bloom LIMS - User support email: your email
- Authorized domains: Add your domain
- Create OAuth client ID:
- Application type: Web application
- Name:
Bloom LIMS Cognito - Authorized redirect URIs:
Example:
https://YOUR-COGNITO-DOMAIN.auth.REGION.amazoncognito.com/oauth2/idpresponsehttps://bloom-lims-yourorg.auth.us-east-1.amazoncognito.com/oauth2/idpresponse
- Click Create
- Save the Client ID and Client Secret
-
In AWS Console, navigate to your Cognito User Pool
-
Go to Sign-in experience tab
-
Under Federated identity provider sign-in, click Add identity provider
-
Select Google
-
Enter:
- Client ID: From Google Cloud Console
- Client secret: From Google Cloud Console
- Authorized scopes:
openid email profile
-
Attribute mapping (map Google attributes to Cognito):
Google attribute User pool attribute emailemailnamenamesubusername -
Click Add identity provider
- Go to App integration → Your app client
- Click Edit under Hosted UI
- Under Identity providers, check Google
- Save changes
Your Cognito domain is set during User Pool creation, but you can modify it:
- Navigate to App integration tab
- Under Domain, click Actions → Create Cognito domain or Edit
- Format:
https://{your-prefix}.auth.{region}.amazoncognito.com - Example:
https://bloom-lims-dev.auth.us-east-1.amazoncognito.com
For a custom domain like auth.yourcompany.com:
- Create an ACM certificate in us-east-1 region
- In Cognito, go to App integration → Domain
- Click Actions → Create custom domain
- Enter your custom domain
- Select your ACM certificate
- Create a CNAME record in your DNS:
- Name:
auth(or your subdomain) - Value: The CloudFront distribution domain shown in Cognito
- Name:
BLOOM uses a YAML-based configuration system. Configuration is loaded from:
- Environment variables (highest priority) — Use
BLOOM_*prefix with__for nesting - User config file —
~/.config/bloom-<deployment>/bloom-config-<deployment>.yaml - Template defaults —
config/bloom-config-template.yaml
-
Copy the template to your user config directory:
mkdir -p ~/.config/bloom-<deployment> cp config/bloom-config-template.yaml ~/.config/bloom-<deployment>/bloom-config-<deployment>.yaml
-
Edit
~/.config/bloom-<deployment>/bloom-config-<deployment>.yamlwith your Cognito settings:# ----------------------------------------------------------------------------- # Authentication Configuration # ----------------------------------------------------------------------------- auth: # AWS Cognito settings cognito_user_pool_id: "us-east-1_AbCdEfGhI" # Your User Pool ID cognito_client_id: "1abc2defgh3ijklmno4pqrst" # Your App Client ID cognito_region: "us-east-1" # AWS region cognito_domain: "bloom-lims-yourorg.auth.us-east-1.amazoncognito.com" cognito_redirect_uri: "https://localhost:8912/auth/callback" cognito_logout_redirect_uri: "https://localhost:8912/" cognito_scopes: - "openid" - "email" - "profile" cognito_allowed_domains: [] # Empty = block all; list each allowed domain explicitly
Bloom runtime now reads Cognito settings from ~/.config/bloom-<deployment>/bloom-config-<deployment>.yaml.
Do not rely on COGNITO_*, BLOOM_AUTH__COGNITO_*, or daycog env files for service startup.
| YAML Key | Required | Default | Description |
|---|---|---|---|
auth.cognito_region |
✅ Yes | - | AWS region (e.g., us-east-1) |
auth.cognito_user_pool_id |
✅ Yes | - | User Pool ID from AWS Console |
auth.cognito_client_id |
✅ Yes | - | App Client ID (not secret) |
auth.cognito_domain |
✅ Yes | - | Hosted UI domain without https:// |
auth.cognito_redirect_uri |
✅ Yes | - | Post-login redirect URL |
auth.cognito_logout_redirect_uri |
Same as redirect | Post-logout redirect URL | |
auth.cognito_scopes |
❌ No | ["openid", "email", "profile"] |
OAuth scopes to request |
auth.cognito_allowed_domains |
❌ No | [] (block all) |
Allowed email domains |
-
Set up your configuration (see Configuration above)
-
Verify your Cognito callback URLs include your local address:
https://localhost:8912/auth/callbackhttps://localhost:8912/
-
Activate the environment and start the application:
source ./activate <deploy-name> bloom server start
-
Test authentication:
- Navigate to
https://localhost:8912/ - Click "Login with Cognito"
- Complete authentication in the Cognito Hosted UI
- You should be redirected back to Bloom
- Navigate to
For rapid local development, you can bypass Cognito:
# Set environment variable
export BLOOM_OAUTH=no
# Or add to ~/.config/bloom/config.yaml under a top-level key (not nested)
⚠️ Warning: Never useBLOOM_OAUTH=noin production. This creates a fake user session without any authentication.
If you need to test from different hosts (e.g., mobile device on same network):
-
Add the IP/hostname to Cognito callback URLs:
https://192.168.1.100:8912/auth/callback https://192.168.1.100:8912/ -
Update your
~/.config/bloom/config.yaml:auth: cognito_redirect_uri: "https://192.168.1.100:8912/auth/callback" cognito_logout_redirect_uri: "https://192.168.1.100:8912/"
sequenceDiagram
participant U as User Browser
participant B as Bloom LIMS
participant C as Cognito Hosted UI
participant J as Cognito JWKS
U->>B: GET /login
B->>U: Render login page with Cognito URL
U->>C: Click "Login with Cognito"
C->>C: User authenticates (email/password or Google)
C->>U: Redirect to auth.cognito_redirect_uri with authorization code
U->>B: GET /auth/callback?code=...
B->>C: POST /oauth2/token with code
C->>B: Return id_token and access_token
B->>J: Fetch JWKS public keys
J->>B: Return public keys
B->>B: Validate JWT signature and claims
B->>B: Check email domain whitelist
B->>B: Create session with user data
B->>U: Redirect to home page
The authentication is handled by these components:
-
daylily_auth_cognito.browser.session- Shared browser-session contract:CognitoWebSessionConfig: Dataclass holding the Hosted UI session contractstart_cognito_login(): Persists CSRF state and redirects to Cognitocomplete_cognito_callback(): Exchanges the authorization code asynchronously and stores a normalized session principalSessionPrincipal: Normalized browser session identity
-
daylily_auth_cognito.browser.oauth- Cognito URL and token-exchange helpers:build_authorization_url(): Builds the Hosted UI login URLexchange_authorization_code_async(): Async code exchange used by the web callback path
-
daylily_auth_cognito.runtime.verifier- API bearer-token verification:CognitoTokenVerifier: Verification-only JWT boundary for API auth
-
bloom_lims.gui.routes.authandbloom_lims.gui.web_session- Bloom-specific browser glue:/loginrenders the login UI/auth/callbackcompletes the browser login flow/logoutclears the local browser session and redirects to Cognito logout
-
templates/login.html- Login UI:- "Login with Cognito" button triggers redirect
After successful authentication, request.session["user_data"] contains:
{
"email": "user@example.com",
"cognito_username": "google_123456789", # For federated users
"cognito_sub": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"roles": ["READ_WRITE"],
"service_groups": ["API_ACCESS"],
# ... additional user preferences and normalized profile data
}Symptoms: Application fails to start or shows error about missing Cognito YAML settings.
Solutions:
# Check configuration is being loaded
python -c "from bloom_lims.config import get_settings; s = get_settings(); print(s.auth.model_dump())"Checklist:
-
~/.config/bloom/config.yamlexists with auth settings - No typos in YAML keys
- YAML syntax is valid (proper indentation, colons, quotes)
Symptoms: Login appears to work but callback fails with token validation error.
Possible Causes & Solutions:
| Cause | Solution |
|---|---|
| Wrong Client ID | Verify auth.cognito_client_id matches the App Client in AWS |
| Wrong Region | Verify auth.cognito_region matches the User Pool region |
| Clock skew | Ensure server time is accurate (tokens have expiry) |
| Wrong User Pool ID | Verify auth.cognito_user_pool_id is correct |
Debug token issues:
# Decode token without validation to inspect claims
import jwt
token = "your-id-token-here"
decoded = jwt.decode(token, options={"verify_signature": False})
print(f"Issuer: {decoded.get('iss')}")
print(f"Audience: {decoded.get('aud')}")
print(f"Expiry: {decoded.get('exp')}")Symptoms: Cognito shows "redirect_mismatch" error after authentication.
Solutions:
-
Check exact URL match in Cognito Console:
- Navigate to: User Pool → App integration → App client → Hosted UI
- Compare callback URLs character-by-character
-
Common mismatches:
- Trailing slash:
https://localhost:8912/vshttps://localhost:8912 - Protocol:
https://only - Host:
localhostvs127.0.0.1 - Port: Missing or wrong port number
- Trailing slash:
-
Add all variations you might use:
https://localhost:8912/auth/callback https://localhost:8912/
Symptoms: Login succeeds but Bloom rejects the user.
Solutions:
# Check whitelist configuration in YAML
python -c "from bloom_lims.config import get_settings; s = get_settings(); print(s.auth.cognito_allowed_domains)"Symptoms: User clicks logout but remains logged in, or gets an error.
Solutions:
-
Verify sign-out URL is configured in Cognito:
- User Pool → App integration → App client → Hosted UI
- Check "Allowed sign-out URLs"
-
Check Bloom YAML config:
- Verify
auth.cognito_logout_redirect_urimatches a Cognito Allowed sign-out URL
- Verify
-
Clear browser data:
- Cognito sets its own cookies
- Clear cookies for
*.amazoncognito.com
Symptoms: Only email/password login shows, no Google button.
Solutions:
-
Verify Google is enabled for the App Client:
- User Pool → App integration → App client
- Edit Hosted UI → Identity providers → Check "Google"
-
Verify Google IdP is configured:
- User Pool → Sign-in experience → Federated identity providers
- Google should be listed and active
-
Check Google OAuth credentials:
- Authorized redirect URI must be:
https://{your-domain}.auth.{region}.amazoncognito.com/oauth2/idpresponse
- Authorized redirect URI must be:
Symptoms: JavaScript errors about CORS when posting to /oauth_callback.
Solutions:
The CORS middleware in main.py should allow all origins for development:
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)For production, restrict origins appropriately.
Enable detailed logging to troubleshoot issues:
import logging
logging.basicConfig(level=logging.DEBUG)Or set environment variable:
export LOG_LEVEL=DEBUG
python main.py- Never commit
~/.config/bloom/config.yamlto version control - Use secrets management (AWS Secrets Manager, HashiCorp Vault) for sensitive values
- Rotate credentials periodically
- Set
environment: productionin your config
- Use
https://for all redirect URIs in production - Configure TLS/SSL termination
- Set secure cookie flags
# In production, ensure cookies are secure
response.set_cookie(
key="session",
value=token,
httponly=True,
secure=True, # HTTPS only
samesite="lax",
max_age=3600
)- Configure
auth.cognito_allowed_domainsto restrict access - Never use
BLOOM_OAUTH=noin production
# Production example in ~/.config/bloom/config.yaml
auth:
cognito_allowed_domains:
- "yourcompany.com"
- "trustedpartner.org"In AWS Cognito Console:
-
Enable MFA for production:
- User Pool → Sign-in experience → MFA
- Set to "Required" or "Optional"
-
Configure password policy:
- Minimum length: 12+ characters
- Require: uppercase, lowercase, numbers, symbols
-
Enable advanced security:
- User Pool → Sign-in experience → Advanced security
- Enable adaptive authentication
-
Set token expiration:
- User Pool → App integration → App client
- ID token expiration: 1 hour (default)
- Access token expiration: 1 hour (default)
- Refresh token expiration: 30 days (adjust as needed)
Before deploying to production:
# Remove or comment out
# BLOOM_OAUTH=no
# Remove localhost URLs from Cognito callback/logout URLs
# Keep only production URLs- Enable CloudTrail for Cognito API calls
- Set up CloudWatch alarms for:
- Failed authentication attempts
- Unusual sign-in activity
- Review Cognito logs regularly
Bloom validates tokens using these security measures:
- JWKS Verification: Tokens are verified against Cognito's public keys
- Issuer Validation: Token must be from your User Pool
- Audience Validation: Token must be for your App Client
- Expiration Check: Expired tokens are rejected
# From auth/cognito/client.py
def validate_token(self, token: str) -> Dict:
signing_key = self._jwks_client.get_signing_key_from_jwt(token)
return jwt.decode(
token,
signing_key.key,
algorithms=["RS256"],
audience=self.config.client_id, # Validates aud claim
issuer=self.config.issuer, # Validates iss claim
)| Endpoint | Description |
|---|---|
GET /login |
Displays login page with Cognito button |
GET /auth/callback |
Exchanges Cognito authorization code and completes login |
POST /oauth_callback |
Handles legacy fragment-token fallback after Cognito redirect |
GET /logout |
Clears session and redirects to Cognito logout |
GET /user_home |
Shows user profile with Cognito details |
auth:
cognito_region: us-east-1
cognito_user_pool_id: us-east-1_AbCdEfGhI
cognito_client_id: 1abc2defgh3ijklmno4pqrst
cognito_domain: your-domain.auth.us-east-1.amazoncognito.com
cognito_redirect_uri: https://your-app.com/auth/callback
cognito_logout_redirect_uri: https://your-app.com/
cognito_allowed_domains:
- yourcompany.com
cognito_scopes:
- openid
- email
- profile# List User Pools
aws cognito-idp list-user-pools --max-results 10
# Describe User Pool
aws cognito-idp describe-user-pool --user-pool-id us-east-1_AbCdEfGhI
# List App Clients
aws cognito-idp list-user-pool-clients --user-pool-id us-east-1_AbCdEfGhI
# Describe App Client
aws cognito-idp describe-user-pool-client \
--user-pool-id us-east-1_AbCdEfGhI \
--client-id your-client-idIf you're migrating from the previous Supabase authentication:
-
Remove Supabase configuration from
~/.config/bloom/config.yaml:# Remove any supabase-related keys if present -
Add Cognito configuration as documented in the Configuration section above
-
Update any custom integrations that referenced Supabase
-
Test thoroughly before deploying to production
The application automatically detects the auth provider based on configuration.