By Awa Engineering Team| 29 August 2023
When we set out to build Awa, our roommate coordination app,
one thing was clear from the beginning: security couldn’t be
an afterthought. People would be sharing not just chores and bills,
but also pieces of their daily lives.
We wanted Awa to feel safe, trustworthy,
and private.
In this post,
we’ll take you behind the scenes
of how we designed and implemented
security across Awa, from passwords to tokens
to request filters.
Passwords: One-Way Streets Only
Passwords are one of the oldest attack surfaces in software.
We decided early on that we would never store raw passwords not even temporarily.
Instead, Awa uses BCrypt through Spring Security’s BCryptPasswordEncoder.
We use BCrypt, an industry-standard hashing algorithm, to encode every
password the moment it touches our system. BCrypt is intentionally slow,
making brute-force attacks impractical. In fact, the hashes are salted and one-way:
once encoded, they can’t be reversed.
The only thing we can do is compare a new raw password attempt against the stored hash.
One lesson onemight learn here is that never store or log raw passwords.
Even in development, resist the temptation. Treat them as toxic waste.
Stateless Authentication: JWTs All the Way
We wanted Awa to scale horizontally without worrying about sticky
sessions or server-side state.
That led us to JSON Web Tokens (JWTs).
- On login, we issue both an access token (short-lived, 15 minutes) and a refresh token (long-lived, 30 days).
- Tokens are signed with HMAC SHA-256 using a strong secret key we manage via environment variables.
- Access tokens are meant for API calls. Refresh tokens are only used to get new access tokens.
This two-tier system keeps everyday requests lightweight, while still giving users a way to stay signed in without constantly re-entering passwords.
Guardrails: Our JWT Filter
Every incoming request passes through our custom JwtAuthFilter. Here’s what happens under the hood:
- The filter looks for an Authorization: Bearer <.token> header.
-If no token is present, the request moves on as unauthenticated.
- If a token exists, we validate it with our secret key and extract the user ID.
- We then load the user from MongoDB and wrap them in a UserPrincipal object, which plugs directly into Spring Security.
The result: every request that reaches our controllers already has a verified user identity attached.
Access Control: Roles
Roommates in Awa can have different roles (like OWNER vs MEMBER).
Instead of scattering
“if owner” checks all over the codebase, we lean on Spring Security’s role-based authorities:
ROLE_OWNER ROLE_MEMBER
This allows us to lock down endpoints declaratively (e.g., only an owner can invite new members) while keeping business logic clean.
it is better if roles are modeled early , Retrofitting them later is messy and error-prone.
Refresh Logic: Smooth but Strict
Refresh tokens are powerful, so we’re careful with them. Our AuthenticationService ensures that:
- The refresh token is valid.
- The token type is "refresh".
- The user ID embedded in the token matches the requester.
Only then do we issue new tokens. If any of
these fail, the refresh attempt is rejected.
Security by Default
Awa’s SecurityConfig enforces these principles:
- CSRF disabled (we’re a stateless API).
- Session creation policy set to STATELESS.
- Public endpoints are strictly whitelisted (/api/auth/**, /api/users/register, /avatars/**, etc.)
- Everything else requires authentication.
This way, new endpoints are locked down by default, and only explicitly marked endpoints are public.
What’s Next
We’re exploring:
- Adding rate limiting on auth endpoints to mitigate brute-force attempts.
✨ That’s how we handle security at Awa. If you’d like to dig into the code, check out our GitHub repo. And if you spot ways we can improve, we’d love to hear from you.