Metadata-Version: 2.4
Name: daylily-cognito
Version: 0.1.15
Summary: Shared Cognito authentication library for FastAPI + Jinja2 web apps
Author-email: Daylily Informatics <daylily@daylilyinformatics.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/Daylily-Informatics/daylily-cognito
Project-URL: Repository, https://github.com/Daylily-Informatics/daylily-cognito
Keywords: aws,cognito,authentication,fastapi,jwt,oauth2
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Framework :: FastAPI
Classifier: Topic :: Security
Classifier: Topic :: Internet :: WWW/HTTP :: Session
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: boto3>=1.26.0
Requires-Dist: fastapi>=0.104.0
Requires-Dist: typer>=0.9.0
Requires-Dist: rich>=13.0.0
Provides-Extra: auth
Requires-Dist: python-jose[cryptography]>=3.3.0; extra == "auth"
Provides-Extra: dev
Requires-Dist: httpx>=0.24.0; extra == "dev"
Requires-Dist: pytest>=7.4.0; extra == "dev"
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Dynamic: license-file

# daylily-cognito

Shared AWS Cognito authentication library for FastAPI + Jinja2 web applications.

## Installation

```bash
# Basic installation
pip install -e .

# With JWT verification support (recommended)
pip install -e ".[auth]"

# With development dependencies
pip install -e ".[dev,auth]"
```

## Configuration

### Option 1: Explicit Constructor

```python
from daylily_cognito import CognitoConfig, CognitoAuth

config = CognitoConfig(
    name="myapp",
    region="us-west-2",
    user_pool_id="us-west-2_XXXXXXXXX",
    app_client_id="XXXXXXXXXXXXXXXXXXXXXXXXXX",
    aws_profile="my-profile",  # optional
)
config.validate()  # raises ValueError if invalid

auth = CognitoAuth(
    region=config.region,
    user_pool_id=config.user_pool_id,
    app_client_id=config.app_client_id,
    app_client_secret=config.app_client_secret,  # optional, for clients with secrets
    profile=config.aws_profile,
)
```

### App Client Secret Support

When a Cognito app client has a client secret enabled, all authentication API calls
require a `SECRET_HASH` parameter. The library automatically computes this when
`app_client_secret` is provided:

```python
# For app clients WITH a secret
auth = CognitoAuth(
    region="us-west-2",
    user_pool_id="us-west-2_pUqKyIM1N",
    app_client_id="your-client-id",
    app_client_secret="your-client-secret",  # Required for clients with secrets
)

# The SECRET_HASH is automatically computed as:
# base64(hmac_sha256(client_secret, username + client_id))
```

**Note:** If your Cognito app client was created with `GenerateSecret=True`, you MUST
provide the `app_client_secret` parameter, otherwise authentication will fail with
"Unable to verify secret hash for client".

### Option 2: Namespaced Environment Variables

For multi-tenant or multi-environment setups:

```bash
export DAYCOG_PROD_REGION=us-west-2
export DAYCOG_PROD_USER_POOL_ID=us-west-2_abc123
export DAYCOG_PROD_APP_CLIENT_ID=client123
export DAYCOG_PROD_AWS_PROFILE=prod-profile  # optional
```

```python
from daylily_cognito import CognitoConfig

config = CognitoConfig.from_env("PROD")
```

### Option 3: Legacy Environment Variables

For backward compatibility with existing deployments:

```bash
export COGNITO_REGION=us-west-2        # or AWS_REGION, defaults to us-west-2
export COGNITO_USER_POOL_ID=us-west-2_abc123
export COGNITO_APP_CLIENT_ID=client123  # or COGNITO_CLIENT_ID
export AWS_PROFILE=my-profile           # optional
```

```python
from daylily_cognito import CognitoConfig

config = CognitoConfig.from_legacy_env()
```

## CLI Usage

The `daycog` CLI is the operational interface for Cognito management in this repo.

### Shell Setup

Use the helper script so the venv/CLI are ready and shell env loading works:

```bash
source ./daycog_activate
```

This script:
- creates/activates `.venv`
- installs this repo editable
- installs shell completion
- defines a shell wrapper so `daycog setup` can export values into your current shell
- loads `~/.config/daycog/default.env` if present

### Core Commands

```bash
# Show CLI help
daycog --help

# Check current Cognito config/status
daycog status

# Create pool + app client
daycog setup --name my-pool --port 8001 --profile my-aws-profile --region us-east-1

# List all pools in a region
daycog list-pools --profile my-aws-profile --region us-east-1

# Delete one pool by name or ID
daycog delete-pool --pool-name my-pool --profile my-aws-profile --region us-east-1 --force
daycog delete-pool --pool-id us-east-1_abc123 --profile my-aws-profile --region us-east-1 --force

# User management
daycog list-users
daycog add-user user@example.com --password Secure1234
daycog set-password --email user@example.com --password NewPass123
daycog delete-user --email user@example.com --force
daycog delete-all-users --force
```

### `setup` Behavior

`daycog setup` resolves AWS context in this order:
1. `--profile`, `--region`
2. `AWS_PROFILE`, `AWS_REGION`

If either value is missing, setup exits with an error.

On success, setup writes/updates:
- `~/.config/daycog/<pool-name>.env`
- `~/.config/daycog/default.env`

with:
- `AWS_PROFILE`
- `AWS_REGION`
- `COGNITO_REGION`
- `COGNITO_USER_POOL_ID`
- `COGNITO_APP_CLIENT_ID`
- `COGNITO_CALLBACK_URL`

If you pass `--print-exports`, setup also prints shell `export ...` lines.

### Config File Commands

```bash
# Print default config file path and contents
daycog config print

# Print specific pool config path and contents
daycog config print --pool-name my-pool

# Create per-pool config from AWS and update default config
daycog config create --pool-name my-pool --profile my-aws-profile --region us-east-1

# Update per-pool config from AWS and update default config
daycog config update --pool-name my-pool --profile my-aws-profile --region us-east-1
```

### Multi-Config CLI Usage

Use `--config NAME` to select a named configuration:

```bash
export DAYCOG_PROD_REGION=us-west-2
export DAYCOG_PROD_USER_POOL_ID=us-west-2_prod
export DAYCOG_PROD_APP_CLIENT_ID=client_prod

export DAYCOG_DEV_REGION=us-east-1
export DAYCOG_DEV_USER_POOL_ID=us-east-1_dev
export DAYCOG_DEV_APP_CLIENT_ID=client_dev

daycog --config PROD status
daycog --config DEV list-users
```

Note: `daycog config create/update` use AWS lookups with `--profile`/`--region` (or `AWS_*`) and are separate from `--config NAME`.
If a pool has multiple app clients, `config create/update` use the first client returned by AWS.

## FastAPI Integration

```python
from fastapi import Depends, FastAPI
from daylily_cognito import CognitoAuth, CognitoConfig, create_auth_dependency

app = FastAPI()

# Load config and create auth handler
config = CognitoConfig.from_legacy_env()
auth = CognitoAuth(
    region=config.region,
    user_pool_id=config.user_pool_id,
    app_client_id=config.app_client_id,
)

# Create dependencies
get_current_user = create_auth_dependency(auth)
get_optional_user = create_auth_dependency(auth, optional=True)

@app.get("/protected")
def protected_route(user: dict = Depends(get_current_user)):
    return {"user": user}

@app.get("/public")
def public_route(user: dict | None = Depends(get_optional_user)):
    return {"user": user}
```

## OAuth2 Helpers

```python
from daylily_cognito import (
    build_authorization_url,
    build_logout_url,
    exchange_authorization_code,
)

# Build authorization URL for login redirect
auth_url = build_authorization_url(
    domain="myapp.auth.us-west-2.amazoncognito.com",
    client_id="abc123",
    redirect_uri="http://localhost:8000/auth/callback",
    state="csrf-token",
)

# Exchange authorization code for tokens
tokens = exchange_authorization_code(
    domain="myapp.auth.us-west-2.amazoncognito.com",
    client_id="abc123",
    code="auth-code-from-callback",
    redirect_uri="http://localhost:8000/auth/callback",
)

# Build logout URL
logout_url = build_logout_url(
    domain="myapp.auth.us-west-2.amazoncognito.com",
    client_id="abc123",
    logout_uri="http://localhost:8000/",
)
```

## Google OAuth Integration

`daylily-cognito` supports standalone Google OAuth2 authentication that auto-creates
users in your Cognito user pool. This hybrid approach lets users sign in with Google
while keeping Cognito as the single user directory.

### Prerequisites

1. Create a Google Cloud project and enable the OAuth consent screen
2. Create OAuth 2.0 credentials in the Google Cloud Console
3. Register `http://localhost:8000/auth/google/callback` as an authorized redirect URI

### Environment Variables

**Namespaced:**

```bash
export DAYCOG_PROD_GOOGLE_CLIENT_ID="your-google-client-id"
export DAYCOG_PROD_GOOGLE_CLIENT_SECRET="your-google-client-secret"
```

**Legacy:**

```bash
export GOOGLE_CLIENT_ID="your-google-client-id"
export GOOGLE_CLIENT_SECRET="your-google-client-secret"
```

Or use the CLI helper:

```bash
daycog setup-google --client-id YOUR_ID --client-secret YOUR_SECRET
```

### Usage

```python
from daylily_cognito import (
    build_google_authorization_url,
    exchange_google_code_for_tokens,
    fetch_google_userinfo,
    auto_create_cognito_user_from_google,
    generate_state_token,
    CognitoAuth,
    CognitoConfig,
)

# 1. Build authorization URL and redirect the user
state = generate_state_token()
auth_url = build_google_authorization_url(
    client_id="your-google-client-id",
    redirect_uri="http://localhost:8000/auth/google/callback",
    state=state,
)

# 2. In your callback handler, exchange the code for tokens
tokens = exchange_google_code_for_tokens(
    client_id="your-google-client-id",
    client_secret="your-google-client-secret",
    code=request.query_params["code"],
    redirect_uri="http://localhost:8000/auth/google/callback",
)

# 3. Fetch the user's Google profile
userinfo = fetch_google_userinfo(tokens["access_token"])
# userinfo contains: sub, email, email_verified, name, given_name,
#                    family_name, picture, locale, hd (if Google Workspace)

# 4. Auto-create or retrieve the Cognito user
config = CognitoConfig.from_legacy_env()
auth = CognitoAuth(
    region=config.region,
    user_pool_id=config.user_pool_id,
    app_client_id=config.app_client_id,
)
result = auto_create_cognito_user_from_google(auth, userinfo)
# result = {"user": {...}, "created": True/False, "google_sub": "...", "email": "..."}
```

### Google Attributes Captured

All attributes available with standard scopes (`openid email profile`) — no extra
permissions required:

| Claim | Description |
|-------|-------------|
| `sub` | Unique Google user ID |
| `email` | Email address |
| `email_verified` | Whether email is verified by Google |
| `name` | Full display name |
| `given_name` | First name |
| `family_name` | Last name |
| `picture` | Profile photo URL |
| `locale` | User locale (BCP 47) |
| `hd` | Hosted domain (Google Workspace only, absent for personal accounts) |

### Cognito Custom Attributes

The user pool must have these custom attributes configured:

- `custom:customer_id` — defaults to Google `sub`
- `custom:google_sub` — Google unique user ID
- `custom:google_hd` — hosted domain (optional, populated when present)

## Development

```bash
# Install with dev dependencies
pip install -e ".[dev,auth]"

# Run tests
pytest -q

# Run tests with coverage
pytest --cov=daylily_cognito

# Lint and format
ruff check daylily_cognito tests
ruff format daylily_cognito tests
```

## License

MIT
