Các flow đăng nhập: Auth Code + PKCE, Client Credentials
Tiếp đi: cấu hình flow đăng nhập cho từng client của shop-app trong Keycloak.
Lý thuyết các grant đã có ở 4.4.7, code Spring gọi Client Credentials ở 4.4.8. Bài này nói phần Keycloak: bật công tắc nào, điền ô nào, sai ở đâu.
1. Bài toán
| Client | Ai dùng | Flow cần |
|---|---|---|
shop-web, shop-mobile | Khách hàng | Authorization Code + PKCE |
shop-admin | Nhân viên, có backend | Authorization Code (confidential) |
order-service | Service gọi payment-service | Client Credentials |
| Máy POS ở cửa hàng | Thiết bị không có bàn phím tốt | Device Authorization |
2. Vấn đề
- Bật bừa mọi flow cho mọi client: tăng bề mặt tấn công.
- Redirect URI để
*: kẻ tấn công nhận được authorization code. - Web origins sai: SPA gọi token endpoint bị lỗi CORS.
- Dùng Resource Owner Password (app nhận password rồi gửi Keycloak) cho tiện.
3. Giải pháp: tên công tắc trong Keycloak
| Admin console (Capability config) | Grant OAuth2 |
|---|---|
| Standard flow | Authorization Code |
| Service accounts roles | Client Credentials |
| OAuth 2.0 Device Authorization Grant | Device flow |
| Direct access grants | Resource Owner Password (tắt) |
| Implicit flow | Implicit (tắt) |
Mỗi client chỉ bật đúng flow nó cần.
4. Code từng bước
Bước 1: shop-web: Client authentication Off, Standard flow On, Direct access grants Off. Advanced → PKCE method S256.
Valid redirect URIs: https://shop.example.com/callback
http://localhost:5173/callback (chỉ realm dev)
Valid post logout redirect: https://shop.example.com/
Web origins: https://shop.example.com
Bước 2: shop-mobile: tương tự, redirect URI dạng app link https://shop.example.com/app/callback hoặc custom scheme com.example.shop:/callback.
Bước 3: order-service: Client authentication On, Standard flow Off, Service accounts On. Tab Credentials lấy secret (đưa vào secret manager). Tab Service account roles: gán payment-service: payment:charge.
Bước 4: Test bằng curl
KC=http://localhost:8080/realms/shop/protocol/openid-connect
# Client Credentials
curl -s -X POST $KC/token \
-d grant_type=client_credentials \
-d client_id=order-service \
-d client_secret="$ORDER_SERVICE_SECRET" | jq -r .access_token
# Authorization Code + PKCE (giả lập SPA)
VERIFIER=$(openssl rand -base64 48 | tr -d '=+/\n' | cut -c1-64)
CHALLENGE=$(printf %s "$VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr '+/' '-_' | tr -d '=')
echo "$KC/auth?client_id=shop-web&response_type=code&scope=openid&redirect_uri=http://localhost:5173/callback&code_challenge=$CHALLENGE&code_challenge_method=S256&state=xyz"
# mở URL, login, lấy ?code=... trên thanh địa chỉ
curl -s -X POST $KC/token \
-d grant_type=authorization_code -d client_id=shop-web \
-d code="$CODE" -d redirect_uri=http://localhost:5173/callback \
-d code_verifier="$VERIFIER"
# Device flow
curl -s -X POST $KC/auth/device -d client_id=shop-pos
# → user_code, verification_uri: user mở trên điện thoại, nhập code
curl -s -X POST $KC/token -d grant_type=urn:ietf:params:oauth:grant-type:device_code \
-d client_id=shop-pos -d device_code="$DEVICE_CODE"
5. Cách hoạt động
Authorization Code + PKCE (shop-web)
SPA ──(1) /auth?code_challenge=H(v)──> Keycloak: kiểm redirect_uri khớp whitelist
User login ở trang Keycloak (SPA không thấy password)
Keycloak ──(2) 302 redirect_uri?code=abc──> SPA
SPA ──(3) POST /token code=abc + code_verifier=v──> Keycloak: H(v) == challenge?
(CORS: Origin phải nằm trong Web origins)
Keycloak ──(4) access_token, id_token, refresh_token──> SPA
Client Credentials (order-service)
order-service ──POST /token client_id+secret──> Keycloak
<── access_token (sub = service-account-order-service)
order-service ──Bearer──> payment-service
6. So sánh
| Flow | Có user? | Client | Trạng thái |
|---|---|---|---|
| Auth Code + PKCE | Có | SPA, mobile, web backend | Mặc định nên dùng |
| Client Credentials | Không | Service, job | Dùng |
| Device | Có, trên thiết bị khác | TV, POS, CLI | Dùng khi cần |
| Password (ROPC) | Có | Bất kỳ | Deprecated (OAuth 2.1 bỏ) |
| Implicit | Có | SPA cũ | Deprecated |
7. Khi nào nên dùng / không nên dùng
- Nên: mọi đăng nhập có người → Auth Code + PKCE (kể cả confidential client cũng nên bật PKCE).
- Không nên: ROPC, wildcard redirect URI trên production, Web origins
*.
Mobile app tự làm màn login đẹp rồi gửi username/password lên token endpoint (Direct access grants) thì tiện hơn redirect. Sao lại bị cấm?
Tiện thật, nhưng phá mục đích của IdP:
- App nhìn thấy password. Đúng cái bài 12.1 muốn tránh.
- Không làm được MFA, WebAuthn, social login, "đổi password khi hết hạn": những thứ cần trang của Keycloak.
- Không có SSO: không có cookie session trên trang Keycloak.
- Kẻ tấn công dễ brute force token endpoint.
Muốn giao diện đẹp thì làm theme cho trang login của Keycloak (bài 12.6), và mobile mở bằng system browser (Custom Tabs / ASWebAuthenticationSession), không dùng WebView. ROPC chỉ còn chấp nhận được trong test tự động ở realm dev.
Mình gặp hai lỗi:
- Trang Keycloak báo "Invalid parameter: redirect_uri".
- SPA gọi
/tokenbị lỗi CORS.
Mình phải sửa ở đâu?
Hai lỗi, hai ô khác nhau trong cấu hình client:
| Lỗi | Nguyên nhân hay gặp | Sửa |
|---|---|---|
| Invalid redirect_uri | Lệch một ký tự: http vs https, thiếu port, dư dấu / cuối, sau proxy thì Keycloak thấy host khác | Thêm chính xác URI vào Valid redirect URIs; wildcard chỉ ở cuối path và chỉ ở dev |
CORS ở /token | Web origins trống hoặc khác origin của SPA | Web origins = https://shop.example.com (không có path) hoặc + = lấy theo redirect URIs |
Lưu ý: CORS cho API của order-service là chuyện của service đó (bài 11.5), không phải Keycloak.
Phỏng vấn hỏi cấu hình login flow trong Keycloak thì trả lời sao?
Q1. SPA và mobile dùng flow nào, cấu hình gì trong Keycloak?
🇻🇳 Tiếng Việt:
Authorization Code + PKCE với public client: tắt client authentication, bật Standard flow, PKCE S256, tắt Direct access grants, redirect URI chính xác và Web origins đúng origin của SPA. Mobile mở system browser, không dùng WebView.
🇬🇧 English:
Authorization Code with PKCE on a public client: client authentication off, standard flow on, PKCE S256, direct access grants off, exact redirect URIs and web origins set to the SPA origin. Mobile apps use the system browser, not a WebView.
Q2. Service gọi service thì sao?
🇻🇳 Tiếng Việt:
Confidential client bật Service accounts, dùng Client Credentials. Gán role cho service account theo nguyên tắc least privilege, secret lưu trong secret manager hoặc dùng private key JWT.
🇬🇧 English:
A confidential client with service accounts enabled, using Client Credentials. Its service account gets only the roles it needs, and the secret lives in a secret manager, or we use private key JWT authentication.
Q3. Vì sao Resource Owner Password Credentials bị deprecated?
🇻🇳 Tiếng Việt:
Vì app phải nhận password của user, không hỗ trợ MFA, social login, SSO, và OAuth 2.1 đã bỏ nó. Cần UI đẹp thì custom theme của Keycloak.
🇬🇧 English:
Because the application handles the user's password, it cannot support MFA, social login or SSO, and OAuth 2.1 removes it. If we need a custom look, we customize the Keycloak login theme.
Cách nhớ ngắn gọn
- Standard flow = Auth Code; Service accounts = Client Credentials.
- Có người → PKCE; không người → Client Credentials; không bàn phím → Device.
- Redirect URI chính xác; Web origins cho CORS.
- Direct access grants: tắt.
Tự kiểm tra
Token endpoint của realm shop là gì?
/realms/shop/protocol/openid-connect/token.
PKCE chống tấn công gì?
Kẻ đánh cắp authorization code không đổi được thành token vì không có code_verifier.
Web origins để làm gì?
Cho phép CORS từ origin của SPA khi gọi endpoint của Keycloak như /token.
Token của service account có sub là ai?
User ẩn service-account-<clientId>; role gán ở tab Service account roles.