Passwordless blog platform built with Spring Boot 4 and Angular 21 using WebAuthn/Passkeys for authentication.
Backend
- Java 21
- Spring Boot 4.0.6
- Spring Security 7
- PostgreSQL 17
- Flyway 11 (migrations + seed data)
- Yubico webauthn-server-core 2.9.0
- Testcontainers 2.0.5
Frontend
- Angular 21
- PrimeNG 21 (Aura theme)
- @simplewebauthn/browser 13
- PrimeFlex
- Inter font
- Passwordless authentication via WebAuthn/Passkeys (Face ID, Touch ID, device PIN)
- Passkey management — register, rename, delete credentials
- Passkey desync handling — graceful errors when credential missing from device or DB
- Account recovery via email OTP — regain access when passkey is lost
- Blog posts — create, read, update, delete
- Comments on posts — create, read, update
- HTTP session-based auth (server-side session after passkey verification)
- Fully reactive Angular UI with signals
- Java 21+
- Node 20+
- Docker + Docker Compose
- A Gmail account with 2FA enabled (for email OTP recovery)
- A browser with WebAuthn support (Chrome, Safari, Firefox — all modern versions)
git clone https://github.com/daniellaera/spring-blog-app.git
cd spring-blog-appcp backend/src/main/resources/application-dev.yml.example \
backend/src/main/resources/application-dev.ymlcp backend/src/main/resources/application-dev.yml.example \
backend/src/main/resources/application-dev.ymlThen open application-dev.yml and replace every placeholder:
YOUR_DB_PASSWORD→ your PostgreSQL passwordYOUR_GMAIL@gmail.com→ your Gmail addressYOUR_16_CHAR_APP_PASSWORD→ your Gmail App Password- Any other
YOUR_*values
application-dev.ymlis gitignored — it will never be committed.application-dev.yml.exampleis the safe template to track in git.
Required for the account recovery email OTP feature:
- Go to myaccount.google.com
- Security → 2-Step Verification (must be enabled first)
- App passwords → Generate
- Or go directly to: https://myaccount.google.com/apppasswords
- Copy the 16-character password into
application-dev.yml
docker-compose -f docker-compose.db.yml up -dcd backend
mvn spring-boot:run -Dspring-boot.run.profiles=devFlyway runs automatically on startup:
- Creates all tables
- Seeds demo users (daniel, alice)
Note: demo users have no passkeys — register one at
/register
cd frontend
npm install
ng serveApp runs at http://localhost:4200
- Enter username on
/register - Backend creates user account and returns a challenge (
POST /register/start) - Browser prompts for Face ID / Touch ID / PIN
- Passkey stored on device; public key verified and stored in DB (
POST /register/verify)
- Enter username on
/login - Backend returns a challenge (
POST /login/start) - Browser prompts for Face ID / Touch ID / PIN
- Backend verifies signature and creates a server-side session (
POST /login/verify) - Session cookie stored in browser; user redirected to app
When a user loses access to their passkey (deleted from device, new phone, etc):
- Go to
/login - Enter username and attempt sign in
- Cancel the passkey prompt
- Recovery dialog appears automatically
- Enter email address
- Receive 6-digit OTP by email (in dev mode: check backend logs)
- Enter OTP code
- Register a new passkey on the current device
- Sign in normally
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /register/start | No | Get registration challenge |
| POST | /register/verify | No | Complete registration |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /login/start | No | Get authentication challenge |
| POST | /login/verify | No | Complete authentication, create session |
| GET | /session/me | Yes | Get current session user |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| POST | /recovery/start | No | Send OTP to user's registered email |
| POST | /recovery/verify | No | Verify OTP and enable passkey re-registration |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/v3/post | No | List all posts |
| GET | /api/v3/post/{id} | No | Get post by id |
| POST | /api/v3/post | Yes | Create post |
| PUT | /api/v3/post/{id} | Yes | Update post |
| DELETE | /api/v3/post/{id} | Yes | Delete post |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/v3/comment/{postId}/comments | No | List comments for a post |
| POST | /api/v3/comment/{postId} | Yes | Add comment to post |
| PATCH | /api/v3/comment/{commentId} | Yes | Update comment |
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /api/user/passkeys | Yes | List user passkeys |
| PATCH | /api/user/passkeys/{id} | Yes | Rename passkey |
| DELETE | /api/user/passkeys/{id} | Yes | Remove passkey |
| DELETE | /api/user/passkeys/orphaned | Yes | Remove stale passkeys (unused 90+ days) |
├── backend/
│ ├── src/main/java/ Spring Boot app
│ ├── src/main/resources/
│ │ └── db/migration/ Flyway SQL migrations (V1–V10)
│ └── pom.xml
└── frontend/
└── src/app/
├── core/ Interceptors, guards, services
├── features/ Auth, posts, account, user
├── layout/ Navbar
└── shared/ Pipes, utils
| File | Committed | Purpose |
|---|---|---|
| application.yml | ✅ Yes | Base config, no secrets |
| application-dev.yml | ❌ No | Local secrets (DB, Gmail) |
| application-dev.yml.example | ✅ Yes | Template for new developers |
- Passkeys are device-bound — no passwords stored anywhere
- This app uses server-side sessions — no JWT tokens
- OTP codes expire after 10 minutes and are single-use
- Gmail credentials are never committed to git
application-dev.ymlis in.gitignore
cd backend && mvn test
cd frontend && ng test --watch=falseTests use Testcontainers to spin up a real PostgreSQL instance — no mocking.
After running migrations, two demo accounts are seeded (V9):
| Username | Notes |
|---|---|
| daniel | Must register a passkey on first login |
| alice | Must register a passkey on first login |
Passkeys are device-bound and cannot be seeded — use
/registerto create your passkey after the account exists.