mirror of
https://github.com/joaovitoriasilva/endurain.git
synced 2026-01-09 15:57:59 -05:00
Implements optional session idle and absolute timeout logic, including new environment variables for configuration. Adds last_activity_at to sessions, enforces timeouts on token refresh, and introduces a scheduler job to clean up idle sessions. Also introduces progressive lockout for failed logins and updates documentation and examples accordingly.
188 lines
5.8 KiB
Python
188 lines
5.8 KiB
Python
"""v0.16.4 migration
|
|
|
|
Revision ID: ef6cd7775aa2
|
|
Revises: 2af2c0629b37
|
|
Create Date: 2025-12-16 12:47:18.298420
|
|
|
|
"""
|
|
|
|
from typing import Sequence, Union
|
|
|
|
from alembic import op
|
|
import sqlalchemy as sa
|
|
|
|
|
|
# revision identifiers, used by Alembic.
|
|
revision: str = "ef6cd7775aa2"
|
|
down_revision: Union[str, None] = "2af2c0629b37"
|
|
branch_labels: Union[str, Sequence[str], None] = None
|
|
depends_on: Union[str, Sequence[str], None] = None
|
|
|
|
|
|
def upgrade() -> None:
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
# Create oauth_states table
|
|
op.create_table(
|
|
"oauth_states",
|
|
sa.Column(
|
|
"id",
|
|
sa.String(length=64),
|
|
nullable=False,
|
|
comment="State parameter itself (secrets.token_urlsafe(32))",
|
|
),
|
|
sa.Column(
|
|
"idp_id", sa.Integer(), nullable=False, comment="Identity provider ID"
|
|
),
|
|
sa.Column(
|
|
"user_id", sa.Integer(), nullable=True, comment="User ID (for link mode)"
|
|
),
|
|
sa.Column(
|
|
"code_challenge",
|
|
sa.String(length=128),
|
|
nullable=True,
|
|
comment="Base64url-encoded SHA256(code_verifier)",
|
|
),
|
|
sa.Column(
|
|
"code_challenge_method",
|
|
sa.String(length=10),
|
|
nullable=True,
|
|
comment="PKCE method (only S256 supported)",
|
|
),
|
|
sa.Column(
|
|
"nonce",
|
|
sa.String(length=64),
|
|
nullable=False,
|
|
comment="OIDC nonce for ID token validation",
|
|
),
|
|
sa.Column(
|
|
"redirect_path",
|
|
sa.String(length=500),
|
|
nullable=True,
|
|
comment="Frontend path after login",
|
|
),
|
|
sa.Column(
|
|
"client_type",
|
|
sa.String(length=10),
|
|
nullable=False,
|
|
comment="Client type: web or mobile",
|
|
),
|
|
sa.Column(
|
|
"ip_address",
|
|
sa.String(length=45),
|
|
nullable=True,
|
|
comment="Client IP address (IPv6 max length)",
|
|
),
|
|
sa.Column(
|
|
"created_at",
|
|
sa.DateTime(),
|
|
server_default=sa.text("now()"),
|
|
nullable=False,
|
|
comment="OAuth state creation timestamp",
|
|
),
|
|
sa.Column(
|
|
"expires_at",
|
|
sa.DateTime(),
|
|
nullable=False,
|
|
comment="Hard expiry at 10 minutes (cleanup marker)",
|
|
),
|
|
sa.Column(
|
|
"used",
|
|
sa.Boolean(),
|
|
nullable=False,
|
|
comment="True when state is consumed (prevents replay)",
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
["idp_id"],
|
|
["identity_providers.id"],
|
|
),
|
|
sa.ForeignKeyConstraint(
|
|
["user_id"],
|
|
["users.id"],
|
|
),
|
|
sa.PrimaryKeyConstraint("id"),
|
|
)
|
|
op.create_index(
|
|
op.f("ix_oauth_states_expires_at"), "oauth_states", ["expires_at"], unique=False
|
|
)
|
|
op.create_index(op.f("ix_oauth_states_id"), "oauth_states", ["id"], unique=False)
|
|
op.create_index(
|
|
op.f("ix_oauth_states_idp_id"), "oauth_states", ["idp_id"], unique=False
|
|
)
|
|
op.create_index(
|
|
op.f("ix_oauth_states_used"), "oauth_states", ["used"], unique=False
|
|
)
|
|
op.create_index(
|
|
op.f("ix_oauth_states_user_id"), "oauth_states", ["user_id"], unique=False
|
|
)
|
|
# Add oauth_state_id and tokens_exchanged to users_sessions table
|
|
op.add_column(
|
|
"users_sessions",
|
|
sa.Column(
|
|
"oauth_state_id",
|
|
sa.String(length=64),
|
|
nullable=True,
|
|
comment="Link to OAuth state for PKCE validation",
|
|
),
|
|
)
|
|
op.add_column(
|
|
"users_sessions",
|
|
sa.Column(
|
|
"tokens_exchanged",
|
|
sa.Boolean(),
|
|
nullable=True,
|
|
comment="Prevents duplicate token exchange for mobile",
|
|
),
|
|
)
|
|
op.execute(
|
|
"""
|
|
UPDATE users_sessions
|
|
SET tokens_exchanged = false
|
|
WHERE tokens_exchanged IS NULL;
|
|
"""
|
|
)
|
|
op.alter_column(
|
|
"users_sessions",
|
|
"tokens_exchanged",
|
|
nullable=False,
|
|
comment="Prevents duplicate token exchange for mobile",
|
|
existing_type=sa.Boolean(),
|
|
)
|
|
op.create_index(
|
|
op.f("ix_users_sessions_oauth_state_id"),
|
|
"users_sessions",
|
|
["oauth_state_id"],
|
|
unique=False,
|
|
)
|
|
op.create_foreign_key(
|
|
None, "users_sessions", "oauth_states", ["oauth_state_id"], ["id"]
|
|
)
|
|
# Add last_activity_at column with default value = created_at
|
|
op.add_column(
|
|
"users_sessions", sa.Column("last_activity_at", sa.DateTime(), nullable=True)
|
|
)
|
|
|
|
# Backfill existing sessions: set last_activity_at = created_at
|
|
op.execute(
|
|
"UPDATE users_sessions SET last_activity_at = created_at WHERE last_activity_at IS NULL"
|
|
)
|
|
|
|
# Make column non-nullable after backfill
|
|
op.alter_column("users_sessions", "last_activity_at", nullable=False)
|
|
# ### end Alembic commands ###
|
|
|
|
|
|
def downgrade() -> None:
|
|
# ### commands auto generated by Alembic - please adjust! ###
|
|
op.drop_column("users_sessions", "last_activity_at")
|
|
op.drop_constraint(None, "users_sessions", type_="foreignkey")
|
|
op.drop_index(op.f("ix_users_sessions_oauth_state_id"), table_name="users_sessions")
|
|
op.drop_column("users_sessions", "tokens_exchanged")
|
|
op.drop_column("users_sessions", "oauth_state_id")
|
|
op.drop_index(op.f("ix_oauth_states_user_id"), table_name="oauth_states")
|
|
op.drop_index(op.f("ix_oauth_states_used"), table_name="oauth_states")
|
|
op.drop_index(op.f("ix_oauth_states_idp_id"), table_name="oauth_states")
|
|
op.drop_index(op.f("ix_oauth_states_id"), table_name="oauth_states")
|
|
op.drop_index(op.f("ix_oauth_states_expires_at"), table_name="oauth_states")
|
|
op.drop_table("oauth_states")
|
|
# ### end Alembic commands ###
|