gRPC authentication service for PlaceBrain — JWT issuance, OTP email verification, user management.
This service owns the auth_db schema and is the single source of truth for user identity in the PlaceBrain platform. It signs access and refresh tokens that every other service trusts.
PlaceBrain is an open-source IoT platform for smart buildings. See the organization profile for the full architecture.
authis called over gRPC by the gateway for every sign-in, token refresh, profile read, and email-based member lookup.- It does not depend on other PlaceBrain services and does not publish Kafka events.
- The JWT secret it uses to sign access tokens is shared with the gateway, which validates every incoming request against it.
- Python 3.14, uv for dependency management
- gRPC (grpcio)
- Dishka for DI (
APPscope for singletons,REQUESTscope for per-request UoW) - SQLAlchemy 2.0 async + asyncpg, Alembic migrations
- Pydantic Settings with
__-nested environment keys aiosmtplibfor outbound OTP email (fire-and-forget viaasyncio.create_task)- Async bcrypt via
loop.run_in_executor()— hashing never blocks the event loop
| Method | Access | Purpose |
|---|---|---|
Register |
public | Create a user, send first OTP |
SendOtp / VerifyOtp |
public | Email verification flow |
Login |
public | Password login → (access_token, refresh_token) |
RefreshTokens |
public | Rotate refresh token |
Logout |
authenticated | Revoke refresh token |
GetMe |
authenticated | Current user profile |
ValidateToken |
internal | Verify a token signature, return claims |
GetUserByEmail |
internal | Member lookup for the places service |
Proto definitions live in placebrain-contracts (auth.proto).
Full stack (recommended): clone infra and run make dev — auth, its Postgres database, the gateway, and the rest of the platform come up together.
Service-only mode:
uv sync
cp .env.example .env # set JWT__SECRET, DATABASE__URL, SMTP__*
uv run alembic upgrade head
uv run python -m srcYou will need a reachable PostgreSQL instance with a pre-created auth_db database and an SMTP relay for OTP delivery.
All keys use __ as the nested delimiter (e.g. DATABASE__URL → settings.database.url). See .env.example for the complete list — the highlights:
| Variable | Purpose |
|---|---|
DATABASE__URL |
postgresql+asyncpg://... DSN for auth_db |
JWT__SECRET |
HS256 signing key — must match gateway's JWT_SECRET |
JWT__ACCESS_TOKEN_EXPIRE_MINUTES |
default 15 |
JWT__REFRESH_TOKEN_EXPIRE_DAYS |
default 30 |
SMTP__* |
outbound SMTP used for OTP email |
OTP__EXPIRE_MINUTES / OTP__MAX_ATTEMPTS / OTP__RESEND_COOLDOWN_SECONDS |
OTP policy |
src/
├── main.py gRPC server + DI container wiring
├── core/ Settings, DTOs, typed exceptions, JWT & password helpers
├── dependencies/ Dishka providers (config, db/uow, auth, otp)
├── handlers/auth.py gRPC AuthHandler — maps typed exceptions to StatusCode
├── services/{auth,otp}.py Business logic
└── infra/
├── db/ SQLAlchemy helper, UoW, models (User, RefreshToken, OtpRequest), repositories
└── email/sender.py SMTP client, fire-and-forget dispatch
Services raise typed exceptions from core/exceptions.py; the handler maps them deterministically:
| Exception | gRPC StatusCode |
|---|---|
NotFoundError |
NOT_FOUND |
AlreadyExistsError |
ALREADY_EXISTS |
InvalidCredentialsError |
UNAUTHENTICATED |
EmailNotVerifiedError |
FAILED_PRECONDITION |
CooldownError / MaxAttemptsExceededError |
RESOURCE_EXHAUSTED |
InvalidCodeError / ValueError |
INVALID_ARGUMENT |
Apache License 2.0 — see LICENSE.