Skip to content

captainthx/webauthn

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

1 Commit
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

WebAuthn Passkey Demo β€” Spring Boot REST API

Spring Boot 4 + Spring Security 7 application demonstrating WebAuthn (Passkey) authentication with a REST API backend, JPA persistence, and PostgreSQL.


πŸ“– ΰΈͺารบัญ


πŸ” ระบบ Authentication ΰΈ—ΰΈ³ΰΈ‡ΰΈ²ΰΈ™ΰΈ’ΰΈ±ΰΈ‡ΰΉ„ΰΈ‡?

1. Cookie-based Session ΰΈ„ΰΈ·ΰΈ­ΰΈ­ΰΈ°ΰΉ„ΰΈ£?

ΰΈ£ΰΈ°ΰΈšΰΈšΰΈ™ΰΈ΅ΰΉ‰ΰΉƒΰΈŠΰΉ‰ Cookie-based Session (ΰΉ„ΰΈ‘ΰΉˆΰΉƒΰΈŠΰΉˆ JWT) ในการฒืนฒันตัวตน ΰΈ‹ΰΈΆΰΉˆΰΈ‡ΰΉ€ΰΈ›ΰΉ‡ΰΈ™ default ΰΈ‚ΰΈ­ΰΈ‡ Spring Security

ΰΈ«ΰΈ₯ΰΈ±ΰΈΰΈΰΈ²ΰΈ£ΰΈ—ΰΈ³ΰΈ‡ΰΈ²ΰΈ™ΰΉΰΈšΰΈšΰΈ‡ΰΉˆΰΈ²ΰΈ’ΰΉ†:

ΰΈ„ΰΈ΄ΰΈ”ΰΉ€ΰΈ«ΰΈ‘ΰΈ·ΰΈ­ΰΈ™ "ΰΈšΰΈ±ΰΈ•ΰΈ£ΰΉ€ΰΈ‚ΰΉ‰ΰΈ²ΰΈ‡ΰΈ²ΰΈ™ Event"

1. ΰΈ„ΰΈΈΰΈ“ΰΉ„ΰΈ›ΰΈ—ΰΈ΅ΰΉˆΰΈ›ΰΈ£ΰΈ°ΰΈ•ΰΈΉ (Login) β†’ ΰΈ’ΰΈ·ΰΉˆΰΈ™ΰΈšΰΈ±ΰΈ•ΰΈ£ΰΈ›ΰΈ£ΰΈ°ΰΈŠΰΈ²ΰΈŠΰΈ™ (Email + Password)
2. ΰΉ€ΰΈˆΰΉ‰ΰΈ²ΰΈ«ΰΈ™ΰΉ‰ΰΈ²ΰΈ—ΰΈ΅ΰΉˆΰΈ•ΰΈ£ΰΈ§ΰΈˆΰΈͺอบ β†’ ถูกต้อง! β†’ ΰΉƒΰΈ«ΰΉ‰ "ΰΈͺΰΈ²ΰΈ’ΰΈ£ΰΈ±ΰΈ”ΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈ·ΰΈ­" (Session Cookie)
3. ΰΈ•ΰΈ₯ΰΈ­ΰΈ”ΰΈ—ΰΈ±ΰΉ‰ΰΈ‡ΰΈ‡ΰΈ²ΰΈ™ β†’ ΰΉΰΈ„ΰΉˆΰΉ‚ΰΈŠΰΈ§ΰΉŒΰΈͺΰΈ²ΰΈ’ΰΈ£ΰΈ±ΰΈ”ΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈ·ΰΈ­ β†’ ΰΉ€ΰΈ‚ΰΉ‰ΰΈ²ΰΉ„ΰΈ”ΰΉ‰ΰΉ€ΰΈ₯ΰΈ’ ΰΉ„ΰΈ‘ΰΉˆΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΈ’ΰΈ·ΰΉˆΰΈ™ΰΈšΰΈ±ΰΈ•ΰΈ£ΰΈ­ΰΈ΅ΰΈ
4. ΰΈ­ΰΈ­ΰΈΰΈˆΰΈ²ΰΈΰΈ‡ΰΈ²ΰΈ™ (Logout) β†’ ΰΈ•ΰΈ±ΰΈ”ΰΈͺΰΈ²ΰΈ’ΰΈ£ΰΈ±ΰΈ”ΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈ·ΰΈ­ΰΈ—ΰΈ΄ΰΉ‰ΰΈ‡ β†’ ΰΉ€ΰΈ‚ΰΉ‰ΰΈ²ΰΉ„ΰΈ‘ΰΉˆΰΉ„ΰΈ”ΰΉ‰ΰΉΰΈ₯ΰΉ‰ΰΈ§

"ΰΈͺΰΈ²ΰΈ’ΰΈ£ΰΈ±ΰΈ”ΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈ·ΰΈ­" = JSESSIONID Cookie
"ΰΈ£ΰΈ²ΰΈ’ΰΈŠΰΈ·ΰΉˆΰΈ­ΰΈœΰΈΉΰΉ‰ΰΉ€ΰΈ‚ΰΉ‰ΰΈ²ΰΈ‡ΰΈ²ΰΈ™" = Session store ΰΈΰΈ±ΰΉˆΰΈ‡ Server (ΰΉ€ΰΈΰΉ‡ΰΈšΰΉƒΰΈ™ Memory)

ΰΉƒΰΈ™ΰΈ—ΰΈ²ΰΈ‡ Technical:

ΰΈͺΰΉˆΰΈ§ΰΈ™ΰΈ›ΰΈ£ΰΈ°ΰΈΰΈ­ΰΈš ΰΈ„ΰΈ³ΰΈ­ΰΈ˜ΰΈ΄ΰΈšΰΈ²ΰΈ’
JSESSIONID Cookie ΰΈ—ΰΈ΅ΰΉˆ Server ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ΰΉƒΰΈ«ΰΉ‰ Browser ΰΉ€ΰΈΰΉ‡ΰΈšΰΉ„ΰΈ§ΰΉ‰ ΰΉ€ΰΈ›ΰΉ‡ΰΈ™ΰΉΰΈ„ΰΉˆ "ID ΰΈͺุ่ฑ" ΰΉ€ΰΈŠΰΉˆΰΈ™ JSESSIONID=A1B2C3D4E5
HttpSession Object ΰΈΰΈ±ΰΉˆΰΈ‡ Server ΰΈ—ΰΈ΅ΰΉˆΰΉ€ΰΈΰΉ‡ΰΈšΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈΉΰΈ₯ User (ΰΉ€ΰΈŠΰΉˆΰΈ™ ΰΉƒΰΈ„ΰΈ£ΰΈ₯็อกอิน, ΰΈ‘ΰΈ΅ Role ΰΈ­ΰΈ°ΰΉ„ΰΈ£)
SecurityContext Object ΰΈ‚ΰΈ­ΰΈ‡ Spring Security ΰΈ—ΰΈ΅ΰΉˆΰΉ€ΰΈΰΉ‡ΰΈš Authentication info ΰΉ„ΰΈ§ΰΉ‰ΰΉƒΰΈ™ Session
Browser                              Server (Spring Boot)
  β”‚                                      β”‚
  β”‚  Request + Cookie:                   β”‚
  β”‚  JSESSIONID=A1B2C3D4E5              β”‚
  β”‚  ─────────────────────────────────►  β”‚
  β”‚                                      β”‚  Server ΰΈ”ΰΈΆΰΈ‡: "A1B2C3D4E5 = user@example.com, ROLE_USER"
  β”‚                                      β”‚  จาก HttpSession (ΰΉ€ΰΈΰΉ‡ΰΈšΰΉƒΰΈ™ Memory/Redis)
  β”‚  ◄─────────────────────────────────  β”‚
  β”‚  Response: 200 OK (ΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈΉΰΈ₯ User)      β”‚

2. Session เกิดขย้นฒังไง?

ΰΉ€ΰΈ‘ΰΈ·ΰΉˆΰΈ­ Login ΰΈͺΰΈ³ΰΉ€ΰΈ£ΰΉ‡ΰΈˆ Spring Security ΰΈˆΰΈ°ΰΈ—ΰΈ³ 3 ΰΈͺΰΈ΄ΰΉˆΰΈ‡ΰΈ™ΰΈ΅ΰΉ‰:

// 1. ΰΈ’ΰΈ·ΰΈ™ΰΈ’ΰΈ±ΰΈ™ΰΈ•ΰΈ±ΰΈ§ΰΈ•ΰΈ™ (Authenticate)
Authentication auth = authenticationManager.authenticate(
    new UsernamePasswordAuthenticationToken(email, password)
);

// 2. ΰΉ€ΰΈΰΉ‡ΰΈšΰΈœΰΈ₯ΰΈ₯ΰΈ±ΰΈžΰΈ˜ΰΉŒΰΉƒΰΈ™ SecurityContext
SecurityContextHolder.getContext().setAuthentication(auth);

// 3. ΰΈšΰΈ±ΰΈ™ΰΈ—ΰΈΆΰΈΰΈ₯ΰΈ‡ HttpSession (ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ Session ΰΉƒΰΈ«ΰΈ‘ΰΉˆΰΈ–ΰΉ‰ΰΈ²ΰΈ’ΰΈ±ΰΈ‡ΰΉ„ΰΈ‘ΰΉˆΰΈ‘ΰΈ΅)
HttpSession session = request.getSession(true);
session.setAttribute("SPRING_SECURITY_CONTEXT", SecurityContextHolder.getContext());

ΰΈ‚ΰΈ±ΰΉ‰ΰΈ™ΰΈ•ΰΈ­ΰΈ™ 3 ΰΈ„ΰΈ·ΰΈ­ΰΈˆΰΈΈΰΈ”ΰΈͺำคัญ! β€” Server ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ Session ID ΰΈͺΰΈΈΰΉˆΰΈ‘ΰΈ‚ΰΈΆΰΉ‰ΰΈ™ΰΈ‘ΰΈ² แΰΈ₯ΰΉ‰ΰΈ§ΰΉ€ΰΈΰΉ‡ΰΈš Authentication object ΰΉ„ΰΈ§ΰΉ‰ΰΉƒΰΈ™ΰΈ™ΰΈ±ΰΉ‰ΰΈ™ ΰΈˆΰΈ²ΰΈΰΈ™ΰΈ±ΰΉ‰ΰΈ™ΰΈͺΰΉˆΰΈ‡ Session ID กΰΈ₯ΰΈ±ΰΈšΰΉ„ΰΈ›ΰΉ€ΰΈ›ΰΉ‡ΰΈ™ Cookie

3. Cookie ΰΈ‘ΰΈ²ΰΈˆΰΈ²ΰΈΰΉ„ΰΈ«ΰΈ™?

Cookie ถูกΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ΰΉ‚ΰΈ”ΰΈ’ Tomcat (Servlet Container ΰΈ—ΰΈ΅ΰΉˆ Spring Boot ΰΉƒΰΈŠΰΉ‰) ΰΈ­ΰΈ±ΰΈ•ΰΉ‚ΰΈ™ΰΈ‘ΰΈ±ΰΈ•ΰΈ΄:

Login ΰΈͺΰΈ³ΰΉ€ΰΈ£ΰΉ‡ΰΈˆ:

Server Response Headers:
  HTTP/1.1 200 OK
  Set-Cookie: JSESSIONID=A1B2C3D4E5F6; Path=/; HttpOnly    ← ΰΈ•ΰΈ±ΰΈ§ΰΈ™ΰΈ΅ΰΉ‰!
  Content-Type: application/json

Browser ΰΉ€ΰΈ«ΰΉ‡ΰΈ™ Header "Set-Cookie" β†’ ΰΉ€ΰΈΰΉ‡ΰΈš Cookie ΰΉ„ΰΈ§ΰΉ‰ΰΈ­ΰΈ±ΰΈ•ΰΉ‚ΰΈ™ΰΈ‘ΰΈ±ΰΈ•ΰΈ΄
β†’ ทุก Request ΰΈ•ΰΉˆΰΈ­ΰΉ„ΰΈ›ΰΈ—ΰΈ΅ΰΉˆΰΈͺΰΉˆΰΈ‡ΰΉ„ΰΈ›ΰΈ’ΰΈ±ΰΈ‡ Server เดมฒวกัน ΰΈˆΰΈ°ΰΉΰΈ™ΰΈš Cookie ΰΈ™ΰΈ΅ΰΉ‰ΰΉ„ΰΈ›ΰΈ”ΰΉ‰ΰΈ§ΰΈ’

ΰΉƒΰΈ™ΰΉ‚ΰΈ›ΰΈ£ΰΉ€ΰΈˆΰΉ‡ΰΈΰΈ•ΰΉŒΰΈ™ΰΈ΅ΰΉ‰ΰΈ‘ΰΈ΅ Cookie 2 ΰΈ•ΰΈ±ΰΈ§:

Cookie ΰΈ«ΰΈ™ΰΉ‰ΰΈ²ΰΈ—ΰΈ΅ΰΉˆ ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ΰΉ‚ΰΈ”ΰΈ’ΰΉƒΰΈ„ΰΈ£
JSESSIONID Session ID β€” ΰΉƒΰΈŠΰΉ‰ΰΈ£ΰΈ°ΰΈšΰΈΈΰΈ•ΰΈ±ΰΈ§ΰΈ•ΰΈ™ User Tomcat (Servlet Container) ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ΰΈ­ΰΈ±ΰΈ•ΰΉ‚ΰΈ™ΰΈ‘ΰΈ±ΰΈ•ΰΈ΄
XSRF-TOKEN CSRF Protection Token β€” ΰΈ›ΰΉ‰ΰΈ­ΰΈ‡ΰΈΰΈ±ΰΈ™ΰΈΰΈ²ΰΈ£ΰΉ‚ΰΈˆΰΈ‘ΰΈ•ΰΈ΅ Cross-Site Request Forgery Spring Security (CookieCsrfTokenRepository)

JSESSIONID ΰΈͺΰΈ³ΰΈ„ΰΈ±ΰΈΰΈ—ΰΈ΅ΰΉˆΰΈͺΰΈΈΰΈ” β€” ΰΈ–ΰΉ‰ΰΈ²ΰΉ„ΰΈ‘ΰΉˆΰΈ‘ΰΈ΅ Cookie ΰΈ™ΰΈ΅ΰΉ‰ Server ΰΈˆΰΈ°ΰΉ„ΰΈ‘ΰΉˆΰΈ£ΰΈΉΰΉ‰ΰΈ§ΰΉˆΰΈ²ΰΈ„ΰΈΈΰΈ“ΰΉ€ΰΈ›ΰΉ‡ΰΈ™ΰΉƒΰΈ„ΰΈ£ β†’ ΰΈͺΰΉˆΰΈ‡ 401 Unauthorized กΰΈ₯ับฑา

XSRF-TOKEN ΰΉƒΰΈŠΰΉ‰ΰΉ€ΰΈ‰ΰΈžΰΈ²ΰΈ°ΰΈΰΈ±ΰΈš POST/PUT/DELETE β€” ΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΈ­ΰΉˆΰΈ²ΰΈ™ Cookie แΰΈ₯ΰΉ‰ΰΈ§ΰΈͺΰΉˆΰΈ‡ΰΈΰΈ₯ΰΈ±ΰΈšΰΉ„ΰΈ›ΰΉƒΰΈ™ Header X-XSRF-TOKEN ΰΉ€ΰΈžΰΈ·ΰΉˆΰΈ­ΰΈžΰΈ΄ΰΈͺΰΈΉΰΈˆΰΈ™ΰΉŒΰΈ§ΰΉˆΰΈ² Request ΰΈ‘ΰΈ²ΰΈˆΰΈ²ΰΈΰΈ«ΰΈ™ΰΉ‰ΰΈ²ΰΉ€ΰΈ§ΰΉ‡ΰΈšΰΈˆΰΈ£ΰΈ΄ΰΈ‡ΰΉ† ΰΉ„ΰΈ‘ΰΉˆΰΉƒΰΈŠΰΉˆΰΈˆΰΈ²ΰΈΰΉ€ΰΈ§ΰΉ‡ΰΈšΰΈ­ΰΈ·ΰΉˆΰΈ™ΰΈ›ΰΈ₯ΰΈ­ΰΈ‘ΰΈ‘ΰΈ²

// ΰΈ•ΰΈ±ΰΈ§ΰΈ­ΰΈ’ΰΉˆΰΈ²ΰΈ‡ΰΈΰΈ²ΰΈ£ΰΈ­ΰΉˆΰΈ²ΰΈ™ XSRF-TOKEN แΰΈ₯ΰΉ‰ΰΈ§ΰΈͺΰΉˆΰΈ‡ΰΈΰΈ₯ΰΈ±ΰΈšΰΉƒΰΈ™ Header
const xsrf = document.cookie
    .split("; ")
    .find(c => c.startsWith("XSRF-TOKEN="))
    ?.split("=")[1];

fetch("/login/webauthn", {
    method: "POST",
    credentials: "include",   // ← ΰΈͺำคัญ! ΰΈšΰΈ­ΰΈΰΉƒΰΈ«ΰΉ‰ Browser ΰΈͺΰΉˆΰΈ‡ Cookie ΰΉ„ΰΈ›ΰΈ”ΰΉ‰ΰΈ§ΰΈ’
    headers: {
        "Content-Type": "application/json",
        "X-XSRF-TOKEN": decodeURIComponent(xsrf)  // ← ΰΈͺΰΉˆΰΈ‡ CSRF token กΰΈ₯ΰΈ±ΰΈšΰΉ„ΰΈ›
    },
    body: JSON.stringify(data)
});

4. ΰΉ€ΰΈ›ΰΈ£ΰΈ΅ΰΈ’ΰΈšΰΉ€ΰΈ—ΰΈ΅ΰΈ’ΰΈš Cookie Session vs JWT

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    Cookie-based Session (ΰΉ‚ΰΈ›ΰΈ£ΰΉ€ΰΈˆΰΉ‡ΰΈΰΈ•ΰΉŒΰΈ™ΰΈ΅ΰΉ‰)                      β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚   Browser                         Server                                   β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                 β”‚
β”‚   β”‚ Cookie:  β”‚   ── Request ──►   β”‚ Session Store:       β”‚                 β”‚
β”‚   β”‚ JSESSION β”‚                    β”‚  A1B2 β†’ {user: "a"}  β”‚                 β”‚
β”‚   β”‚ =A1B2    β”‚   ◄── Response ──  β”‚  C3D4 β†’ {user: "b"}  β”‚                 β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                    β”‚  E5F6 β†’ {user: "c"}  β”‚                 β”‚
β”‚                                   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β”‚
β”‚   Cookie ΰΉ€ΰΈΰΉ‡ΰΈšΰΉΰΈ„ΰΉˆ "ID"             Server ΰΉ€ΰΈΰΉ‡ΰΈšΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈΉΰΈ₯ User ΰΈ—ΰΈ±ΰΉ‰ΰΈ‡ΰΈ«ΰΈ‘ΰΈ”           β”‚
β”‚   ΰΉ„ΰΈ‘ΰΉˆΰΈ‘ΰΈ΅ΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈΉΰΈ₯ΰΈ­ΰΈ°ΰΉ„ΰΈ£ΰΉ€ΰΈ₯ΰΈ’               ΰΉƒΰΈ™ Memory (ΰΈ«ΰΈ£ΰΈ·ΰΈ­ Redis)                  β”‚
β”‚                                                                             β”‚
β”‚   βœ… ΰΈ‡ΰΉˆΰΈ²ΰΈ’, Spring Security ΰΈ—ΰΈ³ΰΉƒΰΈ«ΰΉ‰ΰΈ­ΰΈ±ΰΈ•ΰΉ‚ΰΈ™ΰΈ‘ΰΈ±ΰΈ•ΰΈ΄                                   β”‚
β”‚   βœ… ΰΉ€ΰΈžΰΈ΄ΰΈΰΈ–ΰΈ­ΰΈ™ (Logout) ΰΉ„ΰΈ”ΰΉ‰ΰΈ—ΰΈ±ΰΈ™ΰΈ—ΰΈ΅ β€” ΰΈ₯บ Session ΰΈΰΈ±ΰΉˆΰΈ‡ Server                    β”‚
β”‚   βœ… ΰΈ›ΰΈ₯ΰΈ­ΰΈ”ΰΈ ΰΈ±ΰΈ’ β€” Cookie HttpOnly ป้องกัน XSS ΰΈ­ΰΉˆΰΈ²ΰΈ™ΰΉ„ΰΈ‘ΰΉˆΰΉ„ΰΈ”ΰΉ‰                      β”‚
β”‚   ❌ ΰΉ„ΰΈ‘ΰΉˆ Stateless β€” Server ΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΈˆΰΈ³ Session ทุกคน                           β”‚
β”‚   ❌ Scale ฒาก β€” ΰΈ–ΰΉ‰ΰΈ²ΰΈ‘ΰΈ΅ΰΈ«ΰΈ₯ΰΈ²ΰΈ’ Server ΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΉΰΈŠΰΈ£ΰΉŒ Session (Redis)                 β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                           JWT (JSON Web Token)                              β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚                                                                             β”‚
β”‚   Browser                         Server                                   β”‚
β”‚   β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”            β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                 β”‚
β”‚   β”‚ Header:          β”‚ ─ Req ──►  β”‚ ΰΉ„ΰΈ‘ΰΉˆΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΉ€ΰΈΰΉ‡ΰΈšΰΈ­ΰΈ°ΰΉ„ΰΈ£!     β”‚                 β”‚
β”‚   β”‚ Authorization:   β”‚            β”‚ ΰΉΰΈ„ΰΉˆ verify signature β”‚                 β”‚
β”‚   β”‚ Bearer eyJhbG... β”‚ ◄─ Res ──  β”‚ ΰΈ”ΰΉ‰ΰΈ§ΰΈ’ Secret Key      β”‚                 β”‚
β”‚   β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                 β”‚
β”‚                                                                             β”‚
β”‚   Token ΰΉ€ΰΈΰΉ‡ΰΈšΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈΉΰΈ₯ User           Server Stateless                         β”‚
β”‚   ΰΉ€ΰΈ‚ΰΉ‰ΰΈ²ΰΈ£ΰΈ«ΰΈ±ΰΈͺ + ΰΈ₯ΰΈ²ΰΈ’ΰΉ€ΰΈ‹ΰΉ‡ΰΈ™              ΰΉ„ΰΈ‘ΰΉˆΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΈˆΰΈ³ΰΉƒΰΈ„ΰΈ£ΰΉ€ΰΈ₯ΰΈ’                           β”‚
β”‚                                                                             β”‚
β”‚   βœ… Stateless β€” Server ΰΉ„ΰΈ‘ΰΉˆΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΉ€ΰΈΰΉ‡ΰΈš Session                                β”‚
β”‚   βœ… Scale ΰΈ‡ΰΉˆΰΈ²ΰΈ’ β€” Server ตัวไหนก็ verify ΰΉ„ΰΈ”ΰΉ‰                                β”‚
β”‚   βœ… ΰΉ€ΰΈ«ΰΈ‘ΰΈ²ΰΈ°ΰΈΰΈ±ΰΈš Mobile App / Microservices                                    β”‚
β”‚   ❌ ΰΉ€ΰΈžΰΈ΄ΰΈΰΈ–ΰΈ­ΰΈ™ΰΈ’ΰΈ²ΰΈ β€” Token ΰΈ’ΰΈ±ΰΈ‡ΰΉƒΰΈŠΰΉ‰ΰΉ„ΰΈ”ΰΉ‰ΰΈˆΰΈ™ΰΈ«ΰΈ‘ΰΈ”ΰΈ­ΰΈ²ΰΈ’ΰΈΈ (ΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΈ—ΰΈ³ Blacklist)              β”‚
β”‚   ❌ Token ΰΉƒΰΈ«ΰΈΰΉˆΰΈΰΈ§ΰΉˆΰΈ² Session ID                                              β”‚
β”‚   ❌ ΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ implement ΰΉ€ΰΈ­ΰΈ‡ β€” Spring Security ΰΉ„ΰΈ‘ΰΉˆΰΉ„ΰΈ”ΰΉ‰ΰΈ—ΰΈ³ΰΉƒΰΈ«ΰΉ‰ΰΈ­ΰΈ±ΰΈ•ΰΉ‚ΰΈ™ΰΈ‘ΰΈ±ΰΈ•ΰΈ΄              β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ”„ Flow การทำงาน

Flow 1: Password Login (ΰΉ€ΰΈ‚ΰΉ‰ΰΈ²ΰΈͺΰΈΉΰΉˆΰΈ£ΰΈ°ΰΈšΰΈšΰΈ”ΰΉ‰ΰΈ§ΰΈ’ Email/Password)

     Browser (login.html)                         Server (Spring Boot)
     ═══════════════════                           ════════════════════

 β”Œβ”€ 1. User กรอก Email + Password แΰΈ₯้วกด Login
 β”‚
 β”‚   POST /api/auth/login
 β”‚   Body: {"email":"a@b.com","password":"1234"}
 β”‚   ──────────────────────────────────────────►
 β”‚                                                 β”Œβ”€ 2. AuthController.login()
 β”‚                                                 β”‚   authenticationManager.authenticate()
 β”‚                                                 β”‚     └─► JpaUserDetailsService.loadUserByUsername("a@b.com")
 β”‚                                                 β”‚           └─► SELECT * FROM app_users WHERE email = 'a@b.com'
 β”‚                                                 β”‚           └─► ΰΉ€ΰΈ›ΰΈ£ΰΈ΅ΰΈ’ΰΈšΰΉ€ΰΈ—ΰΈ΅ΰΈ’ΰΈš password (BCrypt)
 β”‚                                                 β”‚   βœ… ถูกต้อง!
 β”‚                                                 β”‚
 β”‚                                                 β”‚   SecurityContextHolder.getContext().setAuthentication(auth)
 β”‚                                                 β”‚   session.setAttribute("SPRING_SECURITY_CONTEXT", context)
 β”‚                                                 β”‚   ← Tomcat ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ JSESSIONID Cookie ΰΈ­ΰΈ±ΰΈ•ΰΉ‚ΰΈ™ΰΈ‘ΰΈ±ΰΈ•ΰΈ΄
 β”‚                                                 └─
 β”‚
 β”‚   ◄──────────────────────────────────────────
 β”‚   200 OK
 β”‚   Set-Cookie: JSESSIONID=ABC123; Path=/; HttpOnly
 β”‚   Body: {"id":1,"email":"a@b.com","displayName":"John"}
 β”‚
 └─ 3. Browser ΰΉ€ΰΈΰΉ‡ΰΈš JSESSIONID Cookie
    JavaScript: window.location.href = "/index.html"
    β†’ redirect ΰΉ„ΰΈ›ΰΈ«ΰΈ™ΰΉ‰ΰΈ² Profile


 β”Œβ”€ 4. ΰΉ€ΰΈ‚ΰΉ‰ΰΈ²ΰΈ«ΰΈ™ΰΉ‰ΰΈ² index.html β†’ เรมฒก /api/auth/me
 β”‚
 β”‚   GET /api/auth/me
 β”‚   Cookie: JSESSIONID=ABC123    ← Browser ΰΉΰΈ™ΰΈš Cookie ΰΉ„ΰΈ›ΰΈ­ΰΈ±ΰΈ•ΰΉ‚ΰΈ™ΰΈ‘ΰΈ±ΰΈ•ΰΈ΄
 β”‚   ──────────────────────────────────────────►
 β”‚                                                 β”Œβ”€ 5. Spring Security Filter
 β”‚                                                 β”‚   ΰΈ­ΰΉˆΰΈ²ΰΈ™ JSESSIONID=ABC123
 β”‚                                                 β”‚   ΰΈ«ΰΈ² Session β†’ พบ SecurityContext
 β”‚                                                 β”‚   β†’ User "a@b.com" ΰΈ’ΰΈ±ΰΈ‡ΰΈ₯ΰΉ‡ΰΈ­ΰΈΰΈ­ΰΈ΄ΰΈ™ΰΈ­ΰΈ’ΰΈΉΰΉˆ
 β”‚                                                 β”‚   βœ… ΰΈœΰΉˆΰΈ²ΰΈ™!
 β”‚                                                 β”‚
 β”‚                                                 β”‚   AuthController.me()
 β”‚                                                 β”‚   β†’ ΰΈ”ΰΈΆΰΈ‡ Principal จาก SecurityContext
 β”‚                                                 β”‚   β†’ query DB β†’ return User info
 β”‚                                                 └─
 β”‚
 β”‚   ◄──────────────────────────────────────────
 β”‚   200 OK {"id":1,"email":"a@b.com",...}
 β”‚
 └─ 6. แΰΈͺΰΈ”ΰΈ‡ΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈΉΰΈ₯ User ΰΈšΰΈ™ΰΈ«ΰΈ™ΰΉ‰ΰΈ² Profile βœ…

Flow 2: Passkey Registration (ΰΈ₯ΰΈ‡ΰΈ—ΰΈ°ΰΉ€ΰΈšΰΈ΅ΰΈ’ΰΈ™ Passkey)

⚠️ ΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ Login ΰΈ”ΰΉ‰ΰΈ§ΰΈ’ Password ΰΈΰΉˆΰΈ­ΰΈ™ ΰΈ–ΰΈΆΰΈ‡ΰΈˆΰΈ° Register Passkey ΰΉ„ΰΈ”ΰΉ‰

     Browser (index.html)                 Server                    Authenticator
     ════════════════════                 ══════                    (Windows Hello /
                                                                     Touch ID / YubiKey)
                                                                    ═══════════════

 β”Œβ”€ 1. User ΰΈΰΈ”ΰΈ›ΰΈΈΰΉˆΰΈ‘ "Register New Passkey"
 β”‚
 β”‚   POST /webauthn/register/options
 β”‚   Cookie: JSESSIONID=ABC123
 β”‚   ──────────────────────────►
 β”‚                                  β”Œβ”€ 2. Spring Security WebAuthn Filter
 β”‚                                  β”‚   ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ Challenge (ΰΈ„ΰΉˆΰΈ²ΰΈͺุ่ฑ)
 β”‚                                  β”‚   + ΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈΉΰΈ₯ Relying Party (rpId, rpName)
 β”‚                                  β”‚   + ΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈΉΰΈ₯ User (id, name)
 β”‚                                  └─
 β”‚   ◄──────────────────────────
 β”‚   200 OK (PublicKeyCredentialCreationOptions)
 β”‚   {challenge, rp, user, pubKeyCredParams,...}
 β”‚
 β”‚   3. JavaScript เรมฒก Web API:
 β”‚   navigator.credentials.create({publicKey: options})
 β”‚   ────────────────────────────────────────────────────────►
 β”‚                                                              β”Œβ”€ 4. Authenticator
 β”‚                                                              β”‚   แΰΈͺΰΈ”ΰΈ‡ Prompt
 β”‚                                                              β”‚   (ΰΈͺแกนนิ้ว/Face/PIN)
 β”‚                                                              β”‚   βœ… User ΰΈ’ΰΈ·ΰΈ™ΰΈ’ΰΈ±ΰΈ™!
 β”‚                                                              β”‚   ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ Key Pair:
 β”‚                                                              β”‚   - Private Key (ΰΉ€ΰΈΰΉ‡ΰΈšΰΉƒΰΈ™ΰΈ­ΰΈΈΰΈ›ΰΈΰΈ£ΰΈ“ΰΉŒ)
 β”‚                                                              β”‚   - Public Key (ΰΈͺΰΉˆΰΈ‡ΰΈΰΈ₯ับ)
 β”‚                                                              └─
 β”‚   ◄────────────────────────────────────────────────────────
 β”‚   credential (id, publicKey, attestation)
 β”‚
 β”‚   POST /webauthn/register
 β”‚   Body: {credential, label: "My Passkey"}
 β”‚   ──────────────────────────►
 β”‚                                  β”Œβ”€ 5. Spring Security WebAuthn Filter
 β”‚                                  β”‚   ΰΈ•ΰΈ£ΰΈ§ΰΈˆΰΈͺอบ Challenge
 β”‚                                  β”‚   ΰΈ•ΰΈ£ΰΈ§ΰΈˆΰΈͺอบ Attestation
 β”‚                                  β”‚   ΰΈšΰΈ±ΰΈ™ΰΈ—ΰΈΆΰΈ Public Key ΰΈ₯ΰΈ‡ DB
 β”‚                                  β”‚   (table: user_credentials)
 β”‚                                  └─
 β”‚   ◄──────────────────────────
 β”‚   200 OK (Passkey saved!)
 β”‚
 └─ 6. แΰΈͺΰΈ”ΰΈ‡ "Passkey registered! βœ…"

Flow 3: Passkey Login (ΰΉ€ΰΈ‚ΰΉ‰ΰΈ²ΰΈͺΰΈΉΰΉˆΰΈ£ΰΈ°ΰΈšΰΈšΰΈ”ΰΉ‰ΰΈ§ΰΈ’ Passkey)

πŸ”‘ ΰΉ„ΰΈ‘ΰΉˆΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΈΰΈ£ΰΈ­ΰΈ Email/Password ΰΉ€ΰΈ₯ΰΈ’!

     Browser (login.html)                 Server                    Authenticator
     ════════════════════                 ══════                    ═══════════════

 β”Œβ”€ 1. User ΰΈΰΈ”ΰΈ›ΰΈΈΰΉˆΰΈ‘ "Login with Passkey"
 β”‚
 β”‚   POST /webauthn/authenticate/options
 β”‚   Body: {}
 β”‚   ──────────────────────────►
 β”‚                                  β”Œβ”€ 2. Spring Security WebAuthn Filter
 β”‚                                  β”‚   ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ Challenge (ΰΈ„ΰΉˆΰΈ²ΰΈͺΰΈΈΰΉˆΰΈ‘ΰΉƒΰΈ«ΰΈ‘ΰΉˆ)
 β”‚                                  β”‚   + allowCredentials (Passkey ΰΈ—ΰΈ΅ΰΉˆΰΈ₯ΰΈ‡ΰΈ—ΰΈ°ΰΉ€ΰΈšΰΈ΅ΰΈ’ΰΈ™ΰΉ„ΰΈ§ΰΉ‰)
 β”‚                                  └─
 β”‚   ◄──────────────────────────
 β”‚   200 OK (PublicKeyCredentialRequestOptions)
 β”‚   {challenge, allowCredentials,...}
 β”‚
 β”‚   3. JavaScript เรมฒก Web API:
 β”‚   navigator.credentials.get({publicKey: options})
 β”‚   ────────────────────────────────────────────────────────►
 β”‚                                                              β”Œβ”€ 4. Authenticator
 β”‚                                                              β”‚   แΰΈͺΰΈ”ΰΈ‡ Prompt
 β”‚                                                              β”‚   "ΰΉ€ΰΈ₯ือก Passkey"
 β”‚                                                              β”‚   (ΰΈͺแกนนิ้ว/Face/PIN)
 β”‚                                                              β”‚   βœ… User ΰΈ’ΰΈ·ΰΈ™ΰΈ’ΰΈ±ΰΈ™!
 β”‚                                                              β”‚   ΰΉƒΰΈŠΰΉ‰ Private Key ΰΈ—ΰΈ΅ΰΉˆΰΉ€ΰΈΰΉ‡ΰΈšΰΉ„ΰΈ§ΰΉ‰
 β”‚                                                              β”‚   ΰΉ€ΰΈ‹ΰΉ‡ΰΈ™ Challenge β†’ Signature
 β”‚                                                              └─
 β”‚   ◄────────────────────────────────────────────────────────
 β”‚   assertion (id, signature, authenticatorData, clientDataJSON)
 β”‚
 β”‚   POST /login/webauthn
 β”‚   Body: {id, rawId, response: {signature, authenticatorData,...}}
 β”‚   ──────────────────────────►
 β”‚                                  β”Œβ”€ 5. Spring Security WebAuthn Filter
 β”‚                                  β”‚   ΰΈ„ΰΉ‰ΰΈ™ΰΈ«ΰΈ² Credential จาก DB
 β”‚                                  β”‚   ΰΈ”ΰΈΆΰΈ‡ Public Key ΰΈ—ΰΈ΅ΰΉˆΰΉ€ΰΈΰΉ‡ΰΈšΰΉ„ΰΈ§ΰΉ‰
 β”‚                                  β”‚   Verify: signature + challenge + publicKey
 β”‚                                  β”‚   βœ… ถูกต้อง!
 β”‚                                  β”‚
 β”‚                                  β”‚   ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ WebAuthnAuthentication
 β”‚                                  β”‚   Principal = PublicKeyCredentialUserEntity  ← ‼️ ΰΉ„ΰΈ‘ΰΉˆΰΉƒΰΈŠΰΉˆ UserDetails!
 β”‚                                  β”‚   ΰΈšΰΈ±ΰΈ™ΰΈ—ΰΈΆΰΈΰΈ₯ΰΈ‡ Session (ΰΉ€ΰΈ«ΰΈ‘ΰΈ·ΰΈ­ΰΈ™ Password Login)
 β”‚                                  β”‚   ← ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ JSESSIONID Cookie
 β”‚                                  └─
 β”‚   ◄──────────────────────────
 β”‚   200 OK
 β”‚   Set-Cookie: JSESSIONID=XYZ789; Path=/; HttpOnly
 β”‚   Body: {"redirectUrl":"/","authenticated":true}
 β”‚
 └─ 6. JavaScript: window.location.href = "/index.html"
    β†’ redirect ΰΉ„ΰΈ›ΰΈ«ΰΈ™ΰΉ‰ΰΈ² Profile βœ…

⚠️ ΰΈˆΰΈΈΰΈ”ΰΈ—ΰΈ΅ΰΉˆΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΈ£ΰΈ°ΰΈ§ΰΈ±ΰΈ‡ (Bug ΰΈ—ΰΈ΅ΰΉˆΰΉ€ΰΈ£ΰΈ²ΰΉΰΈΰΉ‰ΰΉ„ΰΈ›):

ΰΉ€ΰΈ‘ΰΈ·ΰΉˆΰΈ­ Login ΰΈ”ΰΉ‰ΰΈ§ΰΈ’ Password β†’ Principal ΰΉ€ΰΈ›ΰΉ‡ΰΈ™ UserDetails ΰΉ€ΰΈ‘ΰΈ·ΰΉˆΰΈ­ Login ΰΈ”ΰΉ‰ΰΈ§ΰΈ’ Passkey β†’ Principal ΰΉ€ΰΈ›ΰΉ‡ΰΈ™ PublicKeyCredentialUserEntity

ΰΈ”ΰΈ±ΰΈ‡ΰΈ™ΰΈ±ΰΉ‰ΰΈ™ Controller ΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΈ£ΰΈ­ΰΈ‡ΰΈ£ΰΈ±ΰΈš ΰΈ—ΰΈ±ΰΉ‰ΰΈ‡ 2 ΰΈ›ΰΈ£ΰΈ°ΰΉ€ΰΈ ΰΈ— (ΰΈ”ΰΈΉΰΉƒΰΈ™ AuthController.me() แΰΈ₯ΰΈ° PasskeyController)

Flow 4: ΰΉ€ΰΈ‚ΰΉ‰ΰΈ²ΰΈ«ΰΈ™ΰΉ‰ΰΈ² index.html (ΰΈ«ΰΈ₯ΰΈ±ΰΈ‡ Login ΰΈͺΰΈ³ΰΉ€ΰΈ£ΰΉ‡ΰΈˆ)

     Browser (index.html)                         Server
     ════════════════════                          ══════

 β”Œβ”€ 1. Browser ΰΉ‚ΰΈ«ΰΈ₯ΰΈ” index.html (permitAll β€” ΰΉ„ΰΈ‘ΰΉˆΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ auth)
 β”‚
 β”‚   2. JavaScript ΰΈ—ΰΈ³ΰΈ‡ΰΈ²ΰΈ™:
 β”‚      loadUser()  β†’ GET /api/auth/me + Cookie: JSESSIONID=XYZ789
 β”‚      loadPasskeys() β†’ GET /api/passkeys + Cookie: JSESSIONID=XYZ789
 β”‚      (Browser ΰΈͺΰΉˆΰΈ‡ Cookie ΰΉ„ΰΈ›ΰΈ­ΰΈ±ΰΈ•ΰΉ‚ΰΈ™ΰΈ‘ΰΈ±ΰΈ•ΰΈ΄ΰΉ€ΰΈžΰΈ£ΰΈ²ΰΈ°ΰΉ€ΰΈ›ΰΉ‡ΰΈ™ same-origin)
 β”‚
 β”‚   ──────────────────────────────────────────►
 β”‚                                                β”Œβ”€ 3. Spring Security Filter Chain
 β”‚                                                β”‚   ΰΈ­ΰΉˆΰΈ²ΰΈ™ JSESSIONID=XYZ789
 β”‚                                                β”‚   ΰΈ«ΰΈ² Session β†’ พบ SecurityContext
 β”‚                                                β”‚   β†’ User authenticated βœ…
 β”‚                                                β”‚
 β”‚                                                β”‚   AuthController.me()
 β”‚                                                β”‚   β†’ ΰΈ”ΰΈΆΰΈ‡ Principal (UserDetails ΰΈ«ΰΈ£ΰΈ·ΰΈ­ WebAuthnUser)
 β”‚                                                β”‚   β†’ resolveUsername() β†’ ΰΈ«ΰΈ² email
 β”‚                                                β”‚   β†’ query app_users β†’ return User info
 β”‚                                                β”‚
 β”‚                                                β”‚   PasskeyController.listPasskeys()
 β”‚                                                β”‚   β†’ resolveUsername() β†’ ΰΈ«ΰΈ² email
 β”‚                                                β”‚   β†’ query user_credentials β†’ return passkey list
 β”‚                                                └─
 β”‚   ◄──────────────────────────────────────────
 β”‚   /api/auth/me β†’ 200 OK {email, displayName,...}
 β”‚   /api/passkeys β†’ 200 OK [{credentialId, label,...}]
 β”‚
 └─ 4. แΰΈͺΰΈ”ΰΈ‡ΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈΉΰΈ₯ User + Passkey List ΰΈšΰΈ™ΰΈ«ΰΈ™ΰΉ‰ΰΈ² Profile βœ…

πŸ”„ แนวทางเปΰΈ₯ΰΈ΅ΰΉˆΰΈ’ΰΈ™ΰΈˆΰΈ²ΰΈ Cookie Session ΰΉ„ΰΈ›ΰΉƒΰΈŠΰΉ‰ JWT

ถ้าต้องการเปΰΈ₯ΰΈ΅ΰΉˆΰΈ’ΰΈ™ΰΈˆΰΈ²ΰΈ Cookie-based Session ΰΉ„ΰΈ›ΰΉƒΰΈŠΰΉ‰ JWT (ΰΉ€ΰΈŠΰΉˆΰΈ™ ΰΈͺำหรับ Mobile App ΰΈ«ΰΈ£ΰΈ·ΰΈ­ Microservices) ΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΉ€ΰΈ›ΰΈ₯ΰΈ΅ΰΉˆΰΈ’ΰΈ™ΰΈͺΰΉˆΰΈ§ΰΈ™ΰΈ•ΰΉˆΰΈ²ΰΈ‡ΰΉ† ΰΈ”ΰΈ±ΰΈ‡ΰΈ™ΰΈ΅ΰΉ‰:

ΰΈͺΰΈ΄ΰΉˆΰΈ‡ΰΈ—ΰΈ΅ΰΉˆΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΉ€ΰΈžΰΈ΄ΰΉˆΰΈ‘/ΰΉ€ΰΈ›ΰΈ₯ΰΈ΅ΰΉˆΰΈ’ΰΈ™

1. ΰΉ€ΰΈžΰΈ΄ΰΉˆΰΈ‘ JWT Library (pom.xml)

<!-- ΰΉ€ΰΈžΰΈ΄ΰΉˆΰΈ‘ dependency -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.6</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.6</version>
    <scope>runtime</scope>
</dependency>

2. ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ JWT Utility Class (ΰΉ„ΰΈŸΰΈ₯ΰΉŒΰΉƒΰΈ«ΰΈ‘ΰΉˆ)

πŸ“ ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ΰΉ„ΰΈŸΰΈ₯ΰΉŒΰΉƒΰΈ«ΰΈ‘ΰΉˆ: src/main/java/.../util/JwtUtil.java
// ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ class ΰΈ—ΰΈ΅ΰΉˆΰΈͺΰΈ²ΰΈ‘ΰΈ²ΰΈ£ΰΈ–:
// - generateToken(String email) β†’ ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ JWT Token
// - validateToken(String token) β†’ ΰΈ•ΰΈ£ΰΈ§ΰΈˆΰΈͺอบว่า Token ถูกต้องไหฑ
// - getEmailFromToken(String token) β†’ ΰΈ”ΰΈΆΰΈ‡ email จาก Token

3. ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ JWT Authentication Filter (ΰΉ„ΰΈŸΰΈ₯ΰΉŒΰΉƒΰΈ«ΰΈ‘ΰΉˆ)

πŸ“ ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ΰΉ„ΰΈŸΰΈ₯ΰΉŒΰΉƒΰΈ«ΰΈ‘ΰΉˆ: src/main/java/.../config/JwtAuthenticationFilter.java
// ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ Filter ΰΈ—ΰΈ΅ΰΉˆΰΈ—ΰΈ³ΰΈ‡ΰΈ²ΰΈ™ΰΈ—ΰΈΈΰΈ Request:
// - ΰΈ­ΰΉˆΰΈ²ΰΈ™ Header "Authorization: Bearer <token>"
// - Validate Token
// - ถ้าถูกต้อง β†’ ΰΈ•ΰΈ±ΰΉ‰ΰΈ‡ SecurityContext
//
// extends OncePerRequestFilter

4. แก้ไข SecurityConfig.java ⚠️ (ΰΉ€ΰΈ›ΰΈ₯ΰΈ΅ΰΉˆΰΈ’ΰΈ™ΰΉ€ΰΈ’ΰΈ­ΰΈ°ΰΈ—ΰΈ΅ΰΉˆΰΈͺΰΈΈΰΈ”)

// ΰΉ€ΰΈ›ΰΈ₯ΰΈ΅ΰΉˆΰΈ’ΰΈ™ΰΈˆΰΈ²ΰΈ:
.formLogin(form -> form.loginPage("/login.html")...)

// ΰΉ€ΰΈ›ΰΉ‡ΰΈ™:
.sessionManagement(session ->
    session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)  // ← ΰΉ„ΰΈ‘ΰΉˆΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ Session!
)
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class)
// ΰΈ₯บ .formLogin() ออก
// ΰΈ₯บ .logout() ออก (JWT ΰΉ„ΰΈ‘ΰΉˆΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ logout ΰΈΰΈ±ΰΉˆΰΈ‡ Server)

5. แก้ไข AuthController.java

// ΰΉ€ΰΈ›ΰΈ₯ΰΈ΅ΰΉˆΰΈ’ΰΈ™ login() ΰΉƒΰΈ«ΰΉ‰ return JWT Token แทน Session:

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
    // Authenticate ΰΉ€ΰΈ«ΰΈ‘ΰΈ·ΰΈ­ΰΈ™ΰΉ€ΰΈ”ΰΈ΄ΰΈ‘
    Authentication auth = authenticationManager.authenticate(...);

    // ❌ ΰΈ₯บ: session.setAttribute(...)
    // βœ… ΰΉ€ΰΈžΰΈ΄ΰΉˆΰΈ‘: ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ JWT Token
    String token = jwtUtil.generateToken(request.email());
    return ResponseEntity.ok(Map.of("token", token));
}

// ΰΉ€ΰΈ›ΰΈ₯ΰΈ΅ΰΉˆΰΈ’ΰΈ™ me() ΰΉƒΰΈ«ΰΉ‰ΰΈ­ΰΉˆΰΈ²ΰΈ™ΰΈˆΰΈ²ΰΈ SecurityContext (ΰΉ„ΰΈ‘ΰΉˆΰΉ€ΰΈ›ΰΈ₯ΰΈ΅ΰΉˆΰΈ’ΰΈ™ΰΉ€ΰΈ’ΰΈ­ΰΈ° ΰΉ€ΰΈžΰΈ£ΰΈ²ΰΈ°ΰΉΰΈΰΉ‰ΰΉ„ΰΈ›ΰΉΰΈ₯ΰΉ‰ΰΈ§)

6. แก้ไข Frontend (login.html, index.html)

// ΰΉ€ΰΈ›ΰΈ₯ΰΈ΅ΰΉˆΰΈ’ΰΈ™ΰΈˆΰΈ²ΰΈ:
// (ΰΉ„ΰΈ‘ΰΉˆΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΈ—ΰΈ³ΰΈ­ΰΈ°ΰΉ„ΰΈ£ β€” Cookie ΰΈͺΰΉˆΰΈ‡ΰΈ­ΰΈ±ΰΈ•ΰΉ‚ΰΈ™ΰΈ‘ΰΈ±ΰΈ•ΰΈ΄)

// ΰΉ€ΰΈ›ΰΉ‡ΰΈ™:
// ΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΉ€ΰΈΰΉ‡ΰΈš Token ΰΉƒΰΈ™ localStorage แΰΈ₯ΰΉ‰ΰΈ§ΰΉΰΈ™ΰΈšΰΈ—ΰΈΈΰΈ Request

// Login:
const data = await res.json();
localStorage.setItem("token", data.token);

// ทุก API call:
fetch("/api/auth/me", {
    headers: {
        "Authorization": "Bearer " + localStorage.getItem("token")
    }
});

// Logout:
localStorage.removeItem("token");

7. ⚠️ แก้ไข WebAuthn Authentication (ΰΈͺΰΉˆΰΈ§ΰΈ™ΰΈ—ΰΈ΅ΰΉˆΰΈ’ΰΈ²ΰΈΰΈ—ΰΈ΅ΰΉˆΰΈͺΰΈΈΰΈ”)

ปัญหา: Spring Security's WebAuthn filter ΰΈ–ΰΈΉΰΈΰΈ­ΰΈ­ΰΈΰΉΰΈšΰΈšΰΈ‘ΰΈ²ΰΉƒΰΈ«ΰΉ‰ΰΉƒΰΈŠΰΉ‰ΰΈΰΈ±ΰΈš Session
ΰΉ„ΰΈ‘ΰΉˆΰΉ„ΰΈ”ΰΉ‰ΰΈ­ΰΈ­ΰΈΰΉΰΈšΰΈšΰΈ‘ΰΈ²ΰΉƒΰΈ«ΰΉ‰ΰΈ—ΰΈ³ΰΈ‡ΰΈ²ΰΈ™ΰΈΰΈ±ΰΈš JWT ΰΉ‚ΰΈ”ΰΈ’ΰΈ•ΰΈ£ΰΈ‡

ΰΈ—ΰΈ²ΰΈ‡ΰΉ€ΰΈ₯ือก:
A) ΰΉƒΰΈŠΰΉ‰ "Hybrid" β€” WebAuthn ΰΈ’ΰΈ±ΰΈ‡ΰΉƒΰΈŠΰΉ‰ Session, ΰΉΰΈ•ΰΉˆΰΈ«ΰΈ₯ΰΈ±ΰΈ‡ login ΰΈͺΰΈ³ΰΉ€ΰΈ£ΰΉ‡ΰΈˆ
   ΰΉƒΰΈ«ΰΉ‰ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ JWT Token แΰΈ₯ΰΉ‰ΰΈ§ΰΈͺΰΉˆΰΈ‡ΰΈΰΈ₯ΰΈ±ΰΈšΰΉ„ΰΈ› (แนะนำ)
B) Override WebAuthnAuthenticationSuccessHandler ΰΉƒΰΈ«ΰΉ‰ return JWT
   ΰΉΰΈ—ΰΈ™ΰΈ—ΰΈ΅ΰΉˆΰΈˆΰΈ°ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ Session
C) ΰΉ€ΰΈ‚ΰΈ΅ΰΈ’ΰΈ™ WebAuthn authentication ΰΉ€ΰΈ­ΰΈ‡ ΰΉ„ΰΈ‘ΰΉˆΰΉƒΰΈŠΰΉ‰ built-in filter ΰΈ‚ΰΈ­ΰΈ‡ Spring Security

ΰΈͺΰΈ£ΰΈΈΰΈ›ΰΉ„ΰΈŸΰΈ₯ΰΉŒΰΈ—ΰΈ΅ΰΉˆΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΉΰΈΰΉ‰

 ΰΉ„ΰΈŸΰΈ₯ΰΉŒΰΈ—ΰΈ΅ΰΉˆΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ΰΉΰΈΰΉ‰/ΰΉ€ΰΈžΰΈ΄ΰΉˆΰΈ‘                          ΰΈ£ΰΈ°ΰΈ”ΰΈ±ΰΈšΰΈ„ΰΈ§ΰΈ²ΰΈ‘ΰΈ’ΰΈ²ΰΈ
 ═══════════════════════════════════════════════════════════
 πŸ“„ pom.xml                    β†’ ΰΉ€ΰΈžΰΈ΄ΰΉˆΰΈ‘ jjwt dependency           🟒 ΰΈ‡ΰΉˆΰΈ²ΰΈ’
 πŸ†• JwtUtil.java               β†’ ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ΰΉƒΰΈ«ΰΈ‘ΰΉˆ                       🟑 ปานกΰΈ₯ΰΈ²ΰΈ‡
 πŸ†• JwtAuthenticationFilter.java β†’ ΰΈͺΰΈ£ΰΉ‰ΰΈ²ΰΈ‡ΰΉƒΰΈ«ΰΈ‘ΰΉˆ                     🟑 ปานกΰΈ₯ΰΈ²ΰΈ‡
 πŸ“„ SecurityConfig.java        β†’ ΰΉ€ΰΈ›ΰΈ₯ΰΈ΅ΰΉˆΰΈ’ΰΈ™ΰΉ€ΰΈ’ΰΈ­ΰΈ° (STATELESS + Filter) πŸ”΄ ฒาก
 πŸ“„ AuthController.java        β†’ แก้ login() return token        🟒 ΰΈ‡ΰΉˆΰΈ²ΰΈ’
 πŸ“„ login.html                 β†’ ΰΉ€ΰΈΰΉ‡ΰΈš token + ΰΈͺΰΉˆΰΈ‡ Header          🟑 ปานกΰΈ₯ΰΈ²ΰΈ‡
 πŸ“„ index.html                 β†’ ΰΈͺΰΉˆΰΈ‡ Authorization Header         🟑 ปานกΰΈ₯ΰΈ²ΰΈ‡
 πŸ“„ WebAuthn Integration       β†’ ΰΈ›ΰΈ£ΰΈ±ΰΈš success handler            πŸ”΄ ฒาก

πŸ’‘ คำแนะนำ: ΰΈ–ΰΉ‰ΰΈ²ΰΉ€ΰΈ›ΰΉ‡ΰΈ™ΰΉ€ΰΈ§ΰΉ‡ΰΈš (Browser-based application) ΰΉΰΈ™ΰΈ°ΰΈ™ΰΈ³ΰΉƒΰΈ«ΰΉ‰ΰΉƒΰΈŠΰΉ‰ Cookie-based Session ΰΉ€ΰΈžΰΈ£ΰΈ²ΰΈ°ΰΈ›ΰΈ₯ΰΈ­ΰΈ”ΰΈ ΰΈ±ΰΈ’ΰΈΰΈ§ΰΉˆΰΈ² (Cookie HttpOnly ป้องกัน XSS ΰΉ„ΰΈ”ΰΉ‰) แΰΈ₯ΰΈ° Spring Security ΰΈ—ΰΈ³ΰΉƒΰΈ«ΰΉ‰ΰΈ­ΰΈ±ΰΈ•ΰΉ‚ΰΈ™ΰΈ‘ΰΈ±ΰΈ•ΰΈ΄ ΰΉƒΰΈŠΰΉ‰ JWT ΰΉ€ΰΈ‘ΰΈ·ΰΉˆΰΈ­ΰΈ•ΰΉ‰ΰΈ­ΰΈ‡ support Mobile App ΰΈ«ΰΈ£ΰΈ·ΰΈ­ Microservices ΰΈˆΰΈ£ΰΈ΄ΰΈ‡ΰΉ†


Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  Spring Security WebAuthn                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ PublicKeyCredentialUserEntityRepository (interface)  β”‚ β”‚
β”‚  β”‚ UserCredentialRepository (interface)                 β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                 β”‚ implements                             β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ JpaPublicKeyCredentialUserEntityRepository (@Component)β”‚
β”‚  β”‚ JpaUserCredentialRepository (@Component)            β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                 β”‚ uses                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ UserEntityJpaRepository (Spring Data JPA)           β”‚ β”‚
β”‚  β”‚ CredentialRecordJpaRepository (Spring Data JPA)     β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                 β”‚ maps                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
β”‚  β”‚ UserEntityRecord (@Entity)                          β”‚ β”‚
β”‚  β”‚ CredentialRecordEntity (@Entity)                    β”‚ β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚                 β”‚ Hibernate auto-DDL                      β”‚
β”‚            β”Œβ”€β”€β”€β”€β–Όβ”€β”€β”€β”€β”                                   β”‚
β”‚            β”‚PostgreSQLβ”‚                                   β”‚
β”‚            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Prerequisites

  • Java 21+
  • Docker & Docker Compose
  • Maven (or use the included mvnw wrapper)

πŸš€ Quick Start

# 1. Start PostgreSQL
docker compose up -d

# 2. Run the application
./mvnw spring-boot:run
# Windows: .\mvnw spring-boot:run

# 3. Open test UI
# http://localhost:8080/login.html

Database

Setting Value
Host localhost:5432
Database webauthn
Username webauthn
Password webauthn
JDBC URL jdbc:postgresql://localhost:5432/webauthn

Tables are auto-created by Hibernate (ddl-auto: update):

Table Description
app_users Application users (email/password)
user_entities WebAuthn user entities (passkey owner)
user_credentials WebAuthn credentials (passkeys)

πŸ“‘ REST API

Authentication

Register

curl -X POST http://localhost:8080/api/auth/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "user@example.com",
    "password": "MyPassword123",
    "displayName": "John Doe"
  }'

Response 201 Created:

{
  "id": 1,
  "email": "user@example.com",
  "displayName": "John Doe",
  "createdAt": "2026-02-12T07:00:00Z"
}

Errors:

  • 400 β€” Validation error (missing email, password < 8 chars)
  • 409 β€” Email already registered

Login

curl -X POST http://localhost:8080/api/auth/login \
  -H "Content-Type: application/json" \
  -c cookies.txt \
  -d '{
    "email": "user@example.com",
    "password": "MyPassword123"
  }'

Response 200 OK + Set-Cookie: JSESSIONID=...:

{
  "id": 1,
  "email": "user@example.com",
  "displayName": "John Doe",
  "createdAt": "2026-02-12T07:00:00Z"
}

Error 401:

{ "error": "AUTH_FAILED", "message": "Invalid email or password" }

Get Current User

curl http://localhost:8080/api/auth/me -b cookies.txt

Response 200 OK:

{
  "id": 1,
  "email": "user@example.com",
  "displayName": "John Doe",
  "createdAt": "2026-02-12T07:00:00Z"
}

Logout

curl -X POST http://localhost:8080/api/auth/logout -b cookies.txt

Response 200 OK:

{ "message": "Logged out successfully" }

Passkeys

List Passkeys

curl http://localhost:8080/api/passkeys -b cookies.txt

Response 200 OK:

[
  {
    "credentialId": "NxxppSNYduUBLSESigyfCA",
    "label": "My Passkey",
    "created": "2026-02-12T07:05:00Z",
    "lastUsed": "2026-02-12T07:10:00Z"
  }
]

Delete Passkey

curl -X DELETE http://localhost:8080/api/passkeys/{credentialId} \
  -b cookies.txt \
  -H "X-XSRF-TOKEN: <token>"

Response 200 OK:

{ "message": "Passkey deleted successfully" }

WebAuthn Flows (Spring Security Built-in)

These endpoints are handled by Spring Security's WebAuthn filters.

Passkey Registration Flow

Client                          Server
  β”‚                                β”‚
  β”‚  POST /webauthn/register/options
  β”‚  (authenticated, + XSRF token) β”‚
  β”‚ ──────────────────────────────►│
  β”‚                                β”‚
  β”‚  ◄─── PublicKeyCredentialCreationOptions (challenge, rp, user)
  β”‚                                β”‚
  β”‚  navigator.credentials.create()β”‚
  β”‚  (browser prompts biometric)   β”‚
  β”‚                                β”‚
  β”‚  POST /webauthn/register       β”‚
  β”‚  (credential + XSRF token)    β”‚
  β”‚ ──────────────────────────────►│
  β”‚                                β”‚
  β”‚  ◄─── 200 OK (passkey saved)  β”‚

Passkey Authentication Flow

Client                          Server
  β”‚                                β”‚
  β”‚  POST /webauthn/authenticate/options
  β”‚  (public, no auth needed)      β”‚
  β”‚ ──────────────────────────────►│
  β”‚                                β”‚
  β”‚  ◄─── PublicKeyCredentialRequestOptions (challenge, allowCredentials)
  β”‚                                β”‚
  β”‚  navigator.credentials.get()   β”‚
  β”‚  (browser prompts biometric)   β”‚
  β”‚                                β”‚
  β”‚  POST /login/webauthn          β”‚
  β”‚  (assertion response)         β”‚
  β”‚ ──────────────────────────────►│
  β”‚                                β”‚
  β”‚  ◄─── 200 OK + session cookie β”‚

πŸ“ Project Structure

src/main/java/dev/yutsuki/webauthn/
β”œβ”€β”€ WebauthnApplication.java          # Spring Boot entry point
β”œβ”€β”€ config/
β”‚   └── SecurityConfig.java          # Security: CORS, CSRF, auth, WebAuthn
β”œβ”€β”€ controller/
β”‚   β”œβ”€β”€ AuthController.java          # /api/auth/* (register, login, logout, me)
β”‚   └── PasskeyController.java       # /api/passkeys (list, delete)
β”œβ”€β”€ dto/
β”‚   β”œβ”€β”€ RegisterRequest.java         # { email, password, displayName }
β”‚   β”œβ”€β”€ LoginRequest.java            # { email, password }
β”‚   β”œβ”€β”€ UserResponse.java           # { id, email, displayName, createdAt }
β”‚   β”œβ”€β”€ PasskeyResponse.java        # { credentialId, label, created, lastUsed }
β”‚   └── ApiError.java               # { error, message }
β”œβ”€β”€ entity/
β”‚   β”œβ”€β”€ AppUser.java                 # app_users table
β”‚   β”œβ”€β”€ UserEntityRecord.java        # user_entities table (WebAuthn)
β”‚   └── CredentialRecordEntity.java  # user_credentials table (WebAuthn)
β”œβ”€β”€ repository/
β”‚   β”œβ”€β”€ AppUserRepository.java                          # Spring Data JPA for AppUser
β”‚   β”œβ”€β”€ UserEntityJpaRepository.java                    # Spring Data JPA for UserEntityRecord
β”‚   β”œβ”€β”€ CredentialRecordJpaRepository.java              # Spring Data JPA for CredentialRecordEntity
β”‚   β”œβ”€β”€ JpaPublicKeyCredentialUserEntityRepository.java # Adapter β†’ Spring Security
β”‚   └── JpaUserCredentialRepository.java                # Adapter β†’ Spring Security
└── service/
    └── JpaUserDetailsService.java    # UserDetailsService backed by PostgreSQL

Test UI

Static HTML pages are included at /login.html and /index.html for testing. They call the REST API endpoints β€” they are not required for production use.


πŸ“š แหΰΈ₯ΰΉˆΰΈ‡ΰΈ­ΰΉ‰ΰΈ²ΰΈ‡ΰΈ­ΰΈ΄ΰΈ‡

WebAuthn / Passkeys

  • W3C Web Authentication (WebAuthn) Spec β€” ฑาตรฐาน WebAuthn จาก W3C (spec ΰΉ€ΰΈ•ΰΉ‡ΰΈ‘)
  • MDN Web Docs β€” Web Authentication API β€” ΰΈ„ΰΈ³ΰΈ­ΰΈ˜ΰΈ΄ΰΈšΰΈ²ΰΈ’ WebAuthn API ΰΈ—ΰΈ΅ΰΉˆΰΉ€ΰΈ‚ΰΉ‰ΰΈ²ΰΉƒΰΈˆΰΈ‡ΰΉˆΰΈ²ΰΈ’
  • passkeys.dev β€” แหΰΈ₯ΰΉˆΰΈ‡ΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈΉΰΈ₯ΰΉ€ΰΈΰΈ΅ΰΉˆΰΈ’ΰΈ§ΰΈΰΈ±ΰΈš Passkeys ΰΈ£ΰΈ§ΰΈ‘ΰΈ—ΰΈΈΰΈΰΈ­ΰΈ’ΰΉˆΰΈ²ΰΈ‡
  • FIDO Alliance β€” Passkeys β€” ΰΈ‚ΰΉ‰ΰΈ­ΰΈ‘ΰΈΉΰΈ₯จาก FIDO Alliance ΰΈœΰΈΉΰΉ‰ΰΈΰΈ³ΰΈ«ΰΈ™ΰΈ”ΰΈ‘ΰΈ²ΰΈ•ΰΈ£ΰΈΰΈ²ΰΈ™

Spring Security WebAuthn

Spring Security Session Management

Cookie & CSRF

JWT (ΰΈͺΰΈ³ΰΈ«ΰΈ£ΰΈ±ΰΈšΰΉ€ΰΈ›ΰΈ£ΰΈ΅ΰΈ’ΰΈšΰΉ€ΰΈ—ΰΈ΅ΰΈ’ΰΈš/ΰΉ€ΰΈ›ΰΈ₯ΰΈ΅ΰΉˆΰΈ’ΰΈ™ΰΉ„ΰΈ›ΰΉƒΰΈŠΰΉ‰)

Releases

No releases published

Packages