chainlit icon indicating copy to clipboard operation
chainlit copied to clipboard

Use encrypted state token instead of cookie for oauth

Open dokterbob opened this issue 1 year ago • 1 comments
trafficstars

Is your feature request related to a problem? Please describe. When user initiated login attempt in one browser but clicks email or login validation link in another browser, the authentication fails. This fails the user expectation.

Describe the solution you'd like Using a server-encrypted token with a timestamp, we can allow the user to initiate their session whenever they click the link, while still validating that the session was initiated from the same app and within a reasonable timeframe.

Describe alternatives you've considered User friendly messages when rejecting authorisation, explaining to the user they need to engage in the same browser session.

Additional context AI generated code example, based on server.py's contents:

import os
from cryptography.fernet import Fernet
import base64
import json
import time

# Generate a secret key for encryption (store this securely)
SECRET_KEY = Fernet.generate_key()
cipher_suite = Fernet(SECRET_KEY)

def encrypt_state(data):
    data['timestamp'] = int(time.time())
    json_data = json.dumps(data).encode('utf-8')
    encrypted_data = cipher_suite.encrypt(json_data)
    return base64.urlsafe_b64encode(encrypted_data).decode('utf-8')

def decrypt_state(encrypted_state):
    try:
        decoded_data = base64.urlsafe_b64decode(encrypted_state)
        decrypted_data = cipher_suite.decrypt(decoded_data)
        data = json.loads(decrypted_data)
        
        # Check if the state is not too old (e.g., 5 minutes)
        if int(time.time()) - data['timestamp'] > 300:
            return None
        return data
    except Exception:
        return None

@router.get("/auth/oauth/{provider_id}")
async def oauth_login(provider_id: str, request: Request):
    # ... (previous code remains the same)

    random = random_secret(32)
    state_data = {
        "random": random,
        "provider_id": provider_id,
    }
    encrypted_state = encrypt_state(state_data)

    params = urllib.parse.urlencode(
        {
            "client_id": provider.client_id,
            "redirect_uri": f"{get_user_facing_url(request.url)}/callback",
            "state": encrypted_state,
            **provider.authorize_params,
        }
    )
    response = RedirectResponse(
        url=f"{provider.authorize_url}?{params}",
    )
    return response

@router.get("/auth/oauth/{provider_id}/callback")
async def oauth_callback(
    provider_id: str,
    request: Request,
    error: Optional[str] = None,
    code: Optional[str] = None,
    state: Optional[str] = None,
):
    # ... (previous error handling code remains the same)

    if not code or not state:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="Missing code or state",
        )

    # Decrypt and validate the state
    decrypted_state = decrypt_state(state)
    if not decrypted_state or decrypted_state['provider_id'] != provider_id:
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="Invalid or expired state",
        )

    # ... (rest of the code remains the same)

Of course, we should use get_jwt_secret() here, or directly use CHAINLIT_AUTH_SECRET rather than generating one on every run.

dokterbob avatar Jul 11 '24 11:07 dokterbob

I realise now that we should be mindful of replay attacks in this case. The generated link can be re-used as long as it's not expired. A better approach might be to keep a list of used keys in storage, which is quite inconvenient.

In addition, perhaps we don't want to do our own oauth and use a library?

dokterbob avatar Jul 11 '24 11:07 dokterbob

This issue is stale because it has been open for 14 days with no activity.

github-actions[bot] avatar Jul 22 '25 23:07 github-actions[bot]

This issue was closed because it has been inactive for 7 days since being marked as stale.

github-actions[bot] avatar Jul 30 '25 02:07 github-actions[bot]