Skip to main content

vs-security

Authentication and authorization library for FastAPI services in the Viveka Sutra platform. It provides a provider-based authentication model, generic session handling, JWT token management, and route-level guards — all wired together without boilerplate.


Installation

pip install vs-security

With FastAPI guard support (recommended):

pip install "vs-security[fastapi]"

Dependencies: pyjwt, bcrypt, pydantic, vs-common


Architecture Overview

The library is built around three independent concerns that compose together:

Authentication — who are you? A VsAuthProvider receives credentials (username/password, OAuth token, API key — anything) and returns a VsAuthContext confirming the user's identity.

Session — what do we give back, and how do we verify it? A VsSession[T] has two responsibilities: generate() produces a session of type T after login, and validate() verifies an incoming token and returns the VsAuthContext. Both live in one class so the generate and validate logic stays together. VsJWTProvider is the built-in implementation — it returns VsTokenPair.

Authorization — are you allowed? VsSecurity is a FastAPI dependency that calls validate() on every protected request and makes VsAuthContext available inside the handler.

┌──────────────────────────────────────────────────────────┐
│  LOGIN FLOW                                              │
│                                                          │
│  credentials ──► VsAuthProvider ──► VsAuthContext        │
│                                          │               │
│                                   VsSession.generate()   │
│                                          │               │
│                                          T               │
│                                  (VsTokenPair, etc.)     │
└──────────────────────────────────────────────────────────┘

┌──────────────────────────────────────────────────────────┐
│  REQUEST FLOW                                            │
│                                                          │
│  Authorization: Bearer <token>                           │
│         │                                                │
│      VsSecurity (FastAPI dependency/guard)               │
│         │  calls VsSession.validate(token)               │
│         ▼                                                │
│  VsAuthContext ◄── get_auth_context()                    │
│  available anywhere inside the handler                   │
└──────────────────────────────────────────────────────────┘
Class Role Extend?
VsAuthManager Coordinator — routes credentials to the right provider and calls the session No
VsAuthProvider Verifies credentials, returns VsAuthContext Yes — one per auth mechanism
VsSession[T] Abstract base — implement generate() and validate() to define what a session looks like and how it is verified Yes — to change the session format
VsUserLoader Loads a user from your database for username/password auth Yes — required for VsUsernamePasswordAuthProvider
VsJWTProvider Built-in VsSession[VsTokenPair] — mints, verifies, refreshes, and revokes JWTs No — use as-is
VsSecurity FastAPI dependency that guards routes No — use VsSecurity.from_manager()

Quick Start — Complete Example

Step 1 — Implement your user loader

from vs_security.auth.vs_user_loader import VsUserLoader
from vs_security.schema.vs_auth_context import VsAuthContext

class MyUserLoader(VsUserLoader):

    def __init__(self):
        self._repo = UserIdentityRepo()

    async def load(self, username: str):
        identity = await self._repo.find_by_email(username)
        if not identity:
            return None
        context = VsAuthContext(
            user_id=identity.user_id,
            username=identity.email,
            roles=identity.roles,
            provider="email",
        )
        return context, identity.hashed_password

    def verify_password(self, plain: str, stored: str) -> bool:
        import bcrypt
        return bcrypt.checkpw(plain.encode(), stored.encode())

Step 2 — Wire everything at startup

One VsJWTProvider instance is shared between VsAuthManager (for login) and VsSecurity (for request validation). There is no separate factory to initialize.

from vs_security.auth.vs_auth_manager import VsAuthManager
from vs_security.auth.vs_jwt_provider import VsJWTProvider
from vs_security.auth.vs_username_password_provider import VsUsernamePasswordAuthProvider

jwt_provider = VsJWTProvider(config=config)

VsAuthManager.init(session=jwt_provider)
VsAuthManager.register("username_password", VsUsernamePasswordAuthProvider(
    user_loader=MyUserLoader()
))

# Controllers are imported after this point so VsSecurity.from_manager() resolves correctly
from app.controller.auth_controller import AuthController
from app.controller.user_controller import UserController

Step 3 — Add a login endpoint

authenticate() returns whatever T your VsSession produces. With VsJWTProvider, that is VsTokenPair.

from vs_security.auth.vs_auth_manager import VsAuthManager
from vs_security.schema.vs_credentials import VsUsernamePasswordCredentials
from vs_security.schema.vs_token_pair import VsTokenPair
from vs_security.error.vs_auth_error import VsAuthenticationError
from vs_server.decorator.vs_controller_decorator import controller, post

@controller("/auth")
class AuthController:

    @post("/login", response_model=VsTokenPair)
    async def login(self, body: LoginRequest):
        try:
            return await VsAuthManager.get_instance().authenticate(
                "username_password",
                VsUsernamePasswordCredentials(
                    username=body.username,
                    password=body.password,
                ),
            )
        except VsAuthenticationError as e:
            raise HTTPException(status_code=401, detail=str(e))

Step 4 — Protect routes

from vs_security.guard.vs_security import VsSecurity, get_auth_context
from vs_server.decorator.vs_controller_decorator import controller, get

@controller("/users", guards=[VsSecurity.from_manager()])
class UserController:

    @get("/me")
    async def get_me(self):
        context = get_auth_context()
        return {
            "user_id": str(context.user_id),
            "username": context.username,
            "roles": context.roles,
        }

    @get("/admin-only", guards=[VsSecurity.from_manager(roles=["admin"])])
    async def admin_only(self):
        return {"message": "you have admin access"}

Configuration (config.ini)

[auth]
secret_key              = your-very-secret-key-here
algorithm               = HS256
access_expiry_minutes   = 15
refresh_expiry_days     = 7
Key Default Description
auth.secret_key required JWT signing secret
auth.algorithm HS256 JWT signing algorithm
auth.access_expiry_minutes 15 Access token lifetime
auth.refresh_expiry_days 7 Refresh token lifetime

VsAuthManager — In Depth

VsAuthManager is a singleton coordinator. It routes credentials to the correct provider and hands the resulting VsAuthContext to VsSession.generate().

# Initialize once at startup — pass a VsSession[T] implementation
VsAuthManager.init(session=VsJWTProvider(config=config))

# Register providers — call before importing controllers
VsAuthManager.register("username_password", VsUsernamePasswordAuthProvider(...))
VsAuthManager.register("google", GoogleAuthProvider(...))
VsAuthManager.register("api_key", ApiKeyAuthProvider(...))

# Retrieve anywhere in the app
auth_manager = VsAuthManager.get_instance()

# At login time
token_pair = await auth_manager.authenticate(
    "username_password",
    VsUsernamePasswordCredentials(username=..., password=...),
)

VsAuthProvider — In Depth

VsAuthProvider verifies credentials and returns who the user is. It knows nothing about sessions or tokens.

class VsAuthProvider(ABC):

    @abstractmethod
    async def authenticate(self, credentials: VsCredentials) -> VsAuthContext: ...

Rules:

  • Return VsAuthContext on success.
  • Raise VsAuthenticationError (or a subclass) on failure — never return None.

Custom provider example — API key:

from vs_security.auth.vs_auth_provider import VsAuthProvider
from vs_security.schema.vs_auth_context import VsAuthContext
from vs_security.schema.vs_credentials import VsCredentials
from vs_security.error.vs_auth_error import VsInvalidCredentialsError

class ApiKeyCredentials(VsCredentials):
    api_key: str

class ApiKeyAuthProvider(VsAuthProvider):

    def __init__(self, key_repo):
        self._repo = key_repo

    async def authenticate(self, credentials: VsCredentials) -> VsAuthContext:
        if not isinstance(credentials, ApiKeyCredentials):
            raise TypeError("Expected ApiKeyCredentials")

        record = await self._repo.find_by_key(credentials.api_key)
        if not record or not record.is_active:
            raise VsInvalidCredentialsError()

        return VsAuthContext(
            user_id=record.owner_id,
            roles=record.roles,
            provider="api_key",
        )

VsAuthManager.register("api_key", ApiKeyAuthProvider(key_repo=ApiKeyRepo()))

VsSession — In Depth

VsSession[T] is the abstract base for both session generation (login) and token validation (requests). Implement it when you want a session format other than JWT.

class VsSession(ABC, Generic[T]):

    @abstractmethod
    async def generate(self, context: VsAuthContext) -> T:
        """Called after login — produce a session from the authenticated context."""
        ...

    @abstractmethod
    async def validate(self, token: str) -> VsAuthContext:
        """Called on each request — verify the token and return the auth context."""
        ...

Custom example — Redis session store:

from vs_security.schema.vs_session import VsSession
from vs_security.schema.vs_auth_context import VsAuthContext
from pydantic import BaseModel
import uuid

class RedisSessionData(BaseModel):
    session_id: str
    expires_in: int

class RedisSession(VsSession[RedisSessionData]):

    def __init__(self, redis_client, ttl: int = 3600):
        self._redis = redis_client
        self._ttl = ttl

    async def generate(self, context: VsAuthContext) -> RedisSessionData:
        session_id = str(uuid.uuid4())
        await self._redis.setex(f"session:{session_id}", self._ttl, context.model_dump_json())
        return RedisSessionData(session_id=session_id, expires_in=self._ttl)

    async def validate(self, token: str) -> VsAuthContext:
        data = await self._redis.get(f"session:{token}")
        if not data:
            raise VsAuthenticationError("Session not found or expired")
        return VsAuthContext.model_validate_json(data)

# Wire it in
VsAuthManager.init(session=RedisSession(redis_client=redis))

VsJWTProvider — Token Lifecycle

VsJWTProvider is the built-in VsSession[VsTokenPair]. It handles minting, verifying, refreshing, and revoking JWTs.

jwt_provider = VsJWTProvider(config=config, token_store=token_store)
VsAuthManager.init(session=jwt_provider)

token_store is optional. Without it, refresh tokens are stateless (cannot be revoked). With it, they are tracked.

Refresh a token:

@post("/auth/refresh")
async def refresh(body: RefreshRequest):
    try:
        pair = await jwt_provider.refresh_token(body.refresh_token)
        return pair
    except VsAuthenticationError as e:
        raise HTTPException(status_code=401, detail=str(e))

Revoke tokens (logout):

@post("/auth/logout")
async def logout(body: LogoutRequest):
    context = get_auth_context()
    await jwt_provider.revoke_token(context.user_id, body.refresh_token)

@post("/auth/logout-all")
async def logout_all():
    context = get_auth_context()
    await jwt_provider.revoke_all_tokens(context.user_id)

Implementing VsTokenStore:

from vs_security.auth.vs_token_store import VsTokenStore
from uuid import UUID

class RedisTokenStore(VsTokenStore):

    def __init__(self, redis_client):
        self._redis = redis_client

    async def save(self, user_id: UUID, refresh_token: str) -> None:
        await self._redis.sadd(f"tokens:{user_id}", refresh_token)

    async def get_all(self, user_id: UUID) -> list[str]:
        return list(await self._redis.smembers(f"tokens:{user_id}"))

    async def delete(self, user_id: UUID, refresh_token: str) -> None:
        await self._redis.srem(f"tokens:{user_id}", refresh_token)

    async def delete_all(self, user_id: UUID) -> None:
        await self._redis.delete(f"tokens:{user_id}")

VsSecurity — Route Guards

VsSecurity is a FastAPI callable dependency. On every request it:

  1. Reads the Authorization: Bearer <token> header.
  2. Calls VsSession.validate(token) to verify and decode it.
  3. Checks that the user has the required roles (if any).
  4. Stores VsAuthContext in a context variable so get_auth_context() works anywhere in the call stack.

Use VsSecurity.from_manager() to create a guard backed by the initialized VsAuthManager session:

from vs_security.guard.vs_security import VsSecurity

# Any authenticated user
@controller("/users", guards=[VsSecurity.from_manager()])
class UserController: ...

# Only users with "admin" role
@get("/report", guards=[VsSecurity.from_manager(roles=["admin"])])
async def get_report(self): ...

# Multiple roles — user must have at least one
@get("/dashboard", guards=[VsSecurity.from_manager(roles=["admin", "manager"])])
async def dashboard(): ...

Important: Controllers must be imported after VsAuthManager.init() is called, since VsSecurity.from_manager() resolves the session at decoration time.

Accessing the auth context inside a handler:

from vs_security.guard.vs_security import get_auth_context

@get("/me")
async def get_me(self):
    context = get_auth_context()
    return {
        "user_id": str(context.user_id),
        "username": context.username,   # Optional[str]
        "roles": context.roles,
        "provider": context.provider,   # Optional[str]
    }

get_auth_context() uses Python's contextvars — safe in async code and always returns the context for the current request.


VsAuthContext Reference

VsAuthContext represents the authenticated user. Only user_id is required — all other fields are optional since different auth providers may not have them.

context.user_id          # UUID          — required, unique user identifier
context.username         # Optional[str] — display name or email
context.roles            # List[str]     — e.g. ["user", "admin"], defaults to []
context.provider         # Optional[str] — which provider authenticated the user

context.has_role("admin")
context.has_any_role("admin", "moderator")
context.has_all_roles("admin", "moderator")

To attach custom fields, extend VsAuthContext and return the subclass from your VsUserLoader.load():

class AppAuthContext(VsAuthContext):
    tenant_id: str

# In load():
return AppAuthContext(..., tenant_id=user.tenant_id), user.hashed_password

# In a handler:
context: AppAuthContext = get_auth_context()
print(context.tenant_id)

Error Reference

Exception HTTP Status Raised By When
VsInvalidCredentialsError 401 VsUsernamePasswordAuthProvider Wrong username or password
VsAuthenticationError 401 Any provider or session Generic auth failure — base class for all auth errors
VsTokenExpiredError 401 VsJWTProvider JWT has expired
VsTokenRevokedError 401 VsJWTProvider Refresh token was explicitly revoked
VsInsufficientRolesError 403 VsSecurity User authenticated but lacks required roles

VsSecurity catches VsAuthenticationError automatically and converts it to HTTP 401. Only catch these in your login and refresh endpoints:

@post("/auth/login")
async def login(body: LoginRequest):
    try:
        return await VsAuthManager.get_instance().authenticate(...)
    except VsInvalidCredentialsError:
        raise HTTPException(status_code=401, detail="Invalid credentials")
    except VsAuthenticationError as e:
        raise HTTPException(status_code=401, detail=str(e))

Metadata

Release files for vs-security 0.1.8

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for vs-security 0.1.8
File Size Uploaded Provenance
vs_security-0.1.8.tar.gz 16.4 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for vs-security 0.1.8
File Interpreter ABI Platform
vs_security-0.1.8-py3-none-any.whl Python 3 none any Details

Total release size: 32.0 kB

Release files / vs_security-0.1.8.tar.gz

Download URL vs_security-0.1.8.tar.gz
Size 16.4 kB
Tags Source
SHA-256 checksum
How to use checksums
9feb5ece26e42e862ce909ada57a55f824b49b21d0030cfb8c5aafc18924f61f
BLAKE2b-256 checksum
How to use checksums
c8c23e2321ff452e3bb5c64e792dc94e21ec3272e3f4c009d91e1e8e0e946956
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Release files / vs_security-0.1.8-py3-none-any.whl

Download URL vs_security-0.1.8-py3-none-any.whl
Size 15.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5f8ba618310c1084a9a1e08630e81715c2c19edbe5163e6e70d792815c9c4bb3
BLAKE2b-256 checksum
How to use checksums
bb1e1b68fdebbe5a9fb084ba58939bc124d679435d7fc24e0880653a55c03474
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.11.15

Security for vs-security 0.1.8

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page