Skip to content

User

The User class represents an authenticated user from AWS Cognito via ALB OIDC headers.

User

Represents an authenticated user from AWS ALB + Cognito.

Source code in src/cognito_auth/user.py
class User:
    """
    Represents an authenticated user from AWS ALB + Cognito.
    """

    def __init__(
        self,
        oidc_data_header: str | None,
        access_token_header: str | None,
        region: str,
        verify_tokens: bool = True,
    ):
        """
        Initialize User from ALB headers.

        Args:
            oidc_data_header: Value of x-amzn-oidc-data header
            access_token_header: Value of x-amzn-oidc-accesstoken header
            region: AWS region (e.g., 'eu-west-2')
            verify_tokens: Whether to verify token signatures (default: True)

        Raises:
            MissingTokenError: If required headers are missing
            InvalidTokenError: If tokens are invalid
            ExpiredTokenError: If tokens have expired
        """
        if not oidc_data_header:
            logger.error("Missing x-amzn-oidc-data header")
            raise MissingTokenError("x-amzn-oidc-data header is required")
        if not access_token_header:
            logger.error("Missing x-amzn-oidc-accesstoken header")
            raise MissingTokenError("x-amzn-oidc-accesstoken header is required")

        logger.debug("Initializing User with verify_tokens=%s", verify_tokens)

        self._region = region
        self._verifier = TokenVerifier(region) if verify_tokens else None

        # Verify and decode tokens
        if verify_tokens:
            logger.debug("Verifying JWT tokens")
            self._oidc_claims = self._verifier.verify_alb_token(oidc_data_header)
            self._access_claims = self._verifier.verify_cognito_token(
                access_token_header
            )
        else:
            # Decode without verification (not recommended for production)
            logger.warning(
                "Decoding tokens without verification - NOT recommended for production"
            )
            self._oidc_claims = jwt.get_unverified_claims(oidc_data_header)
            self._access_claims = jwt.get_unverified_claims(access_token_header)

        self._is_authenticated = True

        logger.info(
            "User authenticated: email=%s, groups=%s, sub=%s",
            self._oidc_claims.get("email", "N/A"),
            self._access_claims.get("cognito:groups", []),
            self._oidc_claims.get("sub", "N/A"),
        )

    @property
    def is_authenticated(self) -> bool:
        """Whether the user is authenticated"""
        return self._is_authenticated

    @property
    def sub(self) -> str:
        """User's subject identifier (unique user ID)"""
        return self._oidc_claims.get("sub", "")

    @property
    def username(self) -> str:
        """User's username"""
        return self._oidc_claims.get("username", "")

    @property
    def email(self) -> str:
        """User's email address"""
        return self._oidc_claims.get("email", "")

    @property
    def email_domain(self) -> str:
        if self.email:
            return self.email.split("@")[-1]
        return ""

    @property
    def groups(self) -> list[str]:
        """User's Cognito groups"""
        return self._access_claims.get("cognito:groups", [])

    @property
    def is_admin(self) -> bool:
        """Whether the user is an admin (member of gds-idea group)"""
        return "gds-idea" in self.groups

    @property
    def email_verified(self) -> bool:
        """Whether the user's email has been verified"""
        verified = self._oidc_claims.get("email_verified", "false")
        return verified == "true" or verified is True

    @property
    def exp(self) -> datetime | None:
        """Token expiration time"""
        exp_timestamp = self._oidc_claims.get("exp")
        if exp_timestamp:
            return datetime.fromtimestamp(exp_timestamp)
        return None

    @property
    def issuer(self) -> str:
        """Token issuer (Cognito User Pool)"""
        return self._oidc_claims.get("iss", "")

    @property
    def oidc_claims(self) -> dict[str, Any]:
        """All claims from x-amzn-oidc-data token"""
        return self._oidc_claims.copy()

    @property
    def access_claims(self) -> dict[str, Any]:
        """All claims from x-amzn-oidc-accesstoken token"""
        return self._access_claims.copy()

    def __repr__(self) -> str:
        return (
            f"User(username='{self.username}', email='{self.email}', sub='{self.sub}')"
        )

    def __str__(self) -> str:
        return self.email

    @classmethod
    def create_mock(
        cls,
        email: str | None = None,
        username: str | None = None,
        sub: str | None = None,
        groups: list[str] | None = None,
        email_verified: bool = True,
        region: str = "eu-west-2",
        **extra_claims: Any,
    ) -> "User":
        """
        Create a mock user for development and testing.

        This method creates a User instance without requiring valid JWT tokens.
        It loads defaults from dev-mock-user.json if present, and falls back
        to sensible defaults.

        Args:
            email: User's email address
            username: User's username
            sub: User's subject identifier (unique ID)
            groups: List of Cognito groups
            email_verified: Whether email is verified
            region: AWS region
            **extra_claims: Additional claims to include in tokens

        Returns:
            User instance with mock data

        Example:
            >>> user = User.create_mock(email="dev@company.com", groups=["admin"])
            >>> user = User.create_mock()  # Uses defaults from JSON or hardcoded
        """
        logger.warning(
            "Creating mock user - should only be used for development/testing"
        )
        warnings.warn(
            "User.create_mock() is being used. This should only be used for "
            "development and testing, never in production.",
            UserWarning,
            stacklevel=2,
        )

        # Load config from JSON if present
        config = cls._load_dev_config()
        logger.debug(
            "Mock user config loaded: %s", config.keys() if config else "empty"
        )

        # Merge provided values with config and defaults
        email = email or config.get("email", "dev@example.com")
        # Generate UUID-style sub/username like real Cognito tokens
        default_sub = "mock-" + "12345678-1234-1234-1234-123456789abc"
        sub = sub or config.get("sub", default_sub)
        # In real tokens, username is the same as sub (a UUID)
        username = username or config.get("username", sub)
        groups = groups if groups is not None else config.get("groups", [])

        # Build OIDC claims (from ALB header)
        oidc_claims = {
            "sub": sub,
            "email": email,
            "username": username,
            "email_verified": email_verified,
            "exp": int(time.time()) + 3600,  # Expires in 1 hour
            "iss": f"https://cognito-idp.{region}.amazonaws.com/mock-pool",
            **extra_claims,
        }

        # Build access token claims (from Cognito)
        # Match real token structure more closely
        current_time = int(time.time())
        access_claims = {
            "sub": sub,
            "cognito:groups": groups,
            "iss": f"https://cognito-idp.{region}.amazonaws.com/mock-pool",
            "version": 2,
            "client_id": "mock-client-id",
            "token_use": "access",
            "scope": "openid",
            "auth_time": current_time,
            "exp": current_time + 3600,
            "iat": current_time,
            "username": username,
            **extra_claims,
        }

        # Create instance without going through __init__
        instance = cls.__new__(cls)
        instance._region = region
        instance._verifier = None
        instance._oidc_claims = oidc_claims
        instance._access_claims = access_claims
        instance._is_authenticated = True

        logger.info(
            "Mock user created: email=%s, groups=%s, sub=%s", email, groups, sub
        )

        return instance

    @staticmethod
    def _load_dev_config() -> dict[str, Any]:
        """Load development config from JSON file."""
        # Check for custom config path via env var
        config_path = os.getenv("COGNITO_AUTH_DEV_CONFIG")
        if config_path:
            path = Path(config_path)
        else:
            # Default to dev-mock-user.json in current directory
            path = Path.cwd() / "dev-mock-user.json"

        if path.exists():
            logger.debug("Loading dev config from: %s", path)
            try:
                with path.open() as f:
                    config = json.load(f)
                    logger.info("Dev config loaded successfully from: %s", path)
                    return config
            except (json.JSONDecodeError, OSError) as e:
                logger.error("Failed to load dev config from %s: %s", path, e)
                warnings.warn(
                    f"Failed to load dev config from {path}: {e}",
                    UserWarning,
                    stacklevel=3,
                )
                return {}
        logger.debug("No dev config file found at: %s", path)
        return {}

Attributes

sub property

User's subject identifier (unique user ID)

username property

User's username

email property

User's email address

email_domain property

groups property

User's Cognito groups

is_authenticated property

Whether the user is authenticated

is_admin property

Whether the user is an admin (member of gds-idea group)

email_verified property

Whether the user's email has been verified

exp property

Token expiration time

issuer property

Token issuer (Cognito User Pool)

oidc_claims property

All claims from x-amzn-oidc-data token

access_claims property

All claims from x-amzn-oidc-accesstoken token

Functions

__init__(oidc_data_header, access_token_header, region, verify_tokens=True)

Initialize User from ALB headers.

Parameters:

Name Type Description Default
oidc_data_header str | None

Value of x-amzn-oidc-data header

required
access_token_header str | None

Value of x-amzn-oidc-accesstoken header

required
region str

AWS region (e.g., 'eu-west-2')

required
verify_tokens bool

Whether to verify token signatures (default: True)

True

Raises:

Type Description
MissingTokenError

If required headers are missing

InvalidTokenError

If tokens are invalid

ExpiredTokenError

If tokens have expired

Source code in src/cognito_auth/user.py
def __init__(
    self,
    oidc_data_header: str | None,
    access_token_header: str | None,
    region: str,
    verify_tokens: bool = True,
):
    """
    Initialize User from ALB headers.

    Args:
        oidc_data_header: Value of x-amzn-oidc-data header
        access_token_header: Value of x-amzn-oidc-accesstoken header
        region: AWS region (e.g., 'eu-west-2')
        verify_tokens: Whether to verify token signatures (default: True)

    Raises:
        MissingTokenError: If required headers are missing
        InvalidTokenError: If tokens are invalid
        ExpiredTokenError: If tokens have expired
    """
    if not oidc_data_header:
        logger.error("Missing x-amzn-oidc-data header")
        raise MissingTokenError("x-amzn-oidc-data header is required")
    if not access_token_header:
        logger.error("Missing x-amzn-oidc-accesstoken header")
        raise MissingTokenError("x-amzn-oidc-accesstoken header is required")

    logger.debug("Initializing User with verify_tokens=%s", verify_tokens)

    self._region = region
    self._verifier = TokenVerifier(region) if verify_tokens else None

    # Verify and decode tokens
    if verify_tokens:
        logger.debug("Verifying JWT tokens")
        self._oidc_claims = self._verifier.verify_alb_token(oidc_data_header)
        self._access_claims = self._verifier.verify_cognito_token(
            access_token_header
        )
    else:
        # Decode without verification (not recommended for production)
        logger.warning(
            "Decoding tokens without verification - NOT recommended for production"
        )
        self._oidc_claims = jwt.get_unverified_claims(oidc_data_header)
        self._access_claims = jwt.get_unverified_claims(access_token_header)

    self._is_authenticated = True

    logger.info(
        "User authenticated: email=%s, groups=%s, sub=%s",
        self._oidc_claims.get("email", "N/A"),
        self._access_claims.get("cognito:groups", []),
        self._oidc_claims.get("sub", "N/A"),
    )

create_mock(email=None, username=None, sub=None, groups=None, email_verified=True, region='eu-west-2', **extra_claims) classmethod

Create a mock user for development and testing.

This method creates a User instance without requiring valid JWT tokens. It loads defaults from dev-mock-user.json if present, and falls back to sensible defaults.

Parameters:

Name Type Description Default
email str | None

User's email address

None
username str | None

User's username

None
sub str | None

User's subject identifier (unique ID)

None
groups list[str] | None

List of Cognito groups

None
email_verified bool

Whether email is verified

True
region str

AWS region

'eu-west-2'
**extra_claims Any

Additional claims to include in tokens

{}

Returns:

Type Description
User

User instance with mock data

Example

user = User.create_mock(email="dev@company.com", groups=["admin"]) user = User.create_mock() # Uses defaults from JSON or hardcoded

Source code in src/cognito_auth/user.py
@classmethod
def create_mock(
    cls,
    email: str | None = None,
    username: str | None = None,
    sub: str | None = None,
    groups: list[str] | None = None,
    email_verified: bool = True,
    region: str = "eu-west-2",
    **extra_claims: Any,
) -> "User":
    """
    Create a mock user for development and testing.

    This method creates a User instance without requiring valid JWT tokens.
    It loads defaults from dev-mock-user.json if present, and falls back
    to sensible defaults.

    Args:
        email: User's email address
        username: User's username
        sub: User's subject identifier (unique ID)
        groups: List of Cognito groups
        email_verified: Whether email is verified
        region: AWS region
        **extra_claims: Additional claims to include in tokens

    Returns:
        User instance with mock data

    Example:
        >>> user = User.create_mock(email="dev@company.com", groups=["admin"])
        >>> user = User.create_mock()  # Uses defaults from JSON or hardcoded
    """
    logger.warning(
        "Creating mock user - should only be used for development/testing"
    )
    warnings.warn(
        "User.create_mock() is being used. This should only be used for "
        "development and testing, never in production.",
        UserWarning,
        stacklevel=2,
    )

    # Load config from JSON if present
    config = cls._load_dev_config()
    logger.debug(
        "Mock user config loaded: %s", config.keys() if config else "empty"
    )

    # Merge provided values with config and defaults
    email = email or config.get("email", "dev@example.com")
    # Generate UUID-style sub/username like real Cognito tokens
    default_sub = "mock-" + "12345678-1234-1234-1234-123456789abc"
    sub = sub or config.get("sub", default_sub)
    # In real tokens, username is the same as sub (a UUID)
    username = username or config.get("username", sub)
    groups = groups if groups is not None else config.get("groups", [])

    # Build OIDC claims (from ALB header)
    oidc_claims = {
        "sub": sub,
        "email": email,
        "username": username,
        "email_verified": email_verified,
        "exp": int(time.time()) + 3600,  # Expires in 1 hour
        "iss": f"https://cognito-idp.{region}.amazonaws.com/mock-pool",
        **extra_claims,
    }

    # Build access token claims (from Cognito)
    # Match real token structure more closely
    current_time = int(time.time())
    access_claims = {
        "sub": sub,
        "cognito:groups": groups,
        "iss": f"https://cognito-idp.{region}.amazonaws.com/mock-pool",
        "version": 2,
        "client_id": "mock-client-id",
        "token_use": "access",
        "scope": "openid",
        "auth_time": current_time,
        "exp": current_time + 3600,
        "iat": current_time,
        "username": username,
        **extra_claims,
    }

    # Create instance without going through __init__
    instance = cls.__new__(cls)
    instance._region = region
    instance._verifier = None
    instance._oidc_claims = oidc_claims
    instance._access_claims = access_claims
    instance._is_authenticated = True

    logger.info(
        "Mock user created: email=%s, groups=%s, sub=%s", email, groups, sub
    )

    return instance

Examples

Creating a User from Headers

In production, the User is automatically created by framework auth classes:

from cognito_auth.streamlit import StreamlitAuth

auth = StreamlitAuth()
user = auth.get_auth_user()  # User created from request headers

print(f"Email: {user.email}")
print(f"Groups: {user.groups}")
print(f"Is Admin: {user.is_admin}")

Creating a Mock User for Testing

For local development and testing:

from cognito_auth import User

# With defaults
user = User.create_mock()

# With custom values
user = User.create_mock(
    email="developer@example.com",
    groups=["developers", "admin"]
)

assert user.is_authenticated is True
assert "developers" in user.groups

Properties

All user properties are read-only and extracted from the JWT tokens.

Authentication Properties

  • is_authenticated: Whether the user is authenticated (tokens are valid)
  • email_verified: Whether the user's email is verified in Cognito

Identity Properties

  • sub: User's unique subject identifier (UUID)
  • username: User's username (typically same as sub)
  • email: User's email address
  • email_domain: Domain portion of email (e.g., "example.com")

Authorisation Properties

  • groups: List of Cognito groups the user belongs to
  • is_admin: Whether user belongs to "gds-idea" admin group

Token Properties

  • exp: Token expiration timestamp
  • issuer: Token issuer URL
  • oidc_claims: All claims from ALB OIDC token
  • access_claims: All claims from Cognito access token