OAuth2 và OpenID Connect: Login with Google, SSO
Tiếp đi: OAuth2 và OpenID Connect.
Bài này trả lời ba câu hỏi:
- Tại sao đăng nhập bằng Google mà không cần đưa password cho ứng dụng?
- OAuth2 khác JWT thế nào?
- Tại sao nhiều hệ thống dùng Keycloak thay vì tự xây login, JWT, refresh token?
1. Bài toán: công ty có nhiều ứng dụng
shop-app giờ lớn lên, công ty có thêm admin portal và app HR. Nếu mỗi app tự làm authentication:
shop-app └── Login + User DB + JWT
admin-portal └── Login + User DB + JWT
hr-app └── Login + User DB + JWT
2. Vấn đề
- User phải đăng nhập nhiều lần.
- Mỗi app tự quản lý password, tự làm bảo mật.
- Nhân viên nghỉ việc phải thu hồi quyền ở nhiều nơi.
- Khách hàng muốn "Login with Google", không muốn tạo thêm password.
3. Giải pháp: OAuth2 + OIDC với Identity Provider tập trung
OAuth 2.0 là framework ủy quyền truy cập (authorization). Ví dụ app muốn đọc Google Calendar của user: app không cần biết password Google, user cấp quyền qua Google.
Điểm cần nhớ: OAuth2 không phải là JWT. OAuth2 định nghĩa quy trình cấp quyền và token. Access token có thể là JWT hoặc opaque.
| Vai trò | Ý nghĩa | Ví dụ |
|---|---|---|
| Resource Owner | Chủ dữ liệu | Người dùng Google |
| Client | Ứng dụng xin quyền | shop-app |
| Authorization Server | Cấp token | Google / Keycloak |
| Resource Server | API chứa tài nguyên | Google Calendar API, order API |
OAuth2 chỉ trả lời "app được phép truy cập gì". Còn câu hỏi đăng nhập là "người dùng này là ai?". OpenID Connect (OIDC) là lớp định danh xây trên OAuth2, thêm ID Token:
{
"iss": "https://accounts.google.com",
"sub": "google-user-123",
"aud": "your-client-id",
"email": "john@example.com",
"email_verified": true,
"exp": 1791500000
}
Không dùng ID Token để gọi business API. ID Token dành cho client biết user là ai. Resource server cần Access Token dành cho nó.
Các grant type chính:
| Flow | Dùng cho |
|---|---|
| Authorization Code + PKCE | User đăng nhập qua browser: web app, SPA, mobile |
| Client Credentials | Service gọi service, không có user (machine-to-machine) |
| Refresh Token | Lấy access token mới |
| Implicit, Password | Đã lỗi thời, không dùng nữa |
4. Code từng bước: Login with Google
Bước 1: dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
Bước 2: config (client id/secret tạo trong Google Cloud Console, đọc từ biến môi trường, không hardcode)
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT_ID}
client-secret: ${GOOGLE_CLIENT_SECRET}
scope: openid, profile, email
Bước 3: SecurityConfig + controller
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login").permitAll()
.anyRequest().authenticated())
.oauth2Login(Customizer.withDefaults());
return http.build();
}
}
@RestController
public class UserController {
@GetMapping("/me")
public Map<String, Object> me(@AuthenticationPrincipal OidcUser user) {
return Map.of(
"subject", user.getSubject(),
"email", user.getEmail(),
"name", user.getFullName());
}
}
Ở đây là web app, sau OAuth2 login Spring Security giữ đăng nhập bằng session (khác REST API stateless ở bài 4.4.5).
Bước 4: test
# mở trình duyệt (không dùng curl được vì cần user login + consent)
http://localhost:8080/oauth2/authorization/google
# redirect URI phải đăng ký với Google:
http://localhost:8080/login/oauth2/code/google
# sau khi login
curl -b "JSESSIONID=<session>" localhost:8080/me
5. Cách hoạt động: Authorization Code + PKCE
User / Browser
| 1. Click "Login with Google"
v
shop-app
| 2. Redirect to Google:
| client_id, redirect_uri, scope=openid profile email,
| state, nonce, code_challenge
v
Google Authorization Server
| 3. User login + consent
| 4. Redirect về redirect_uri với ?code=...&state=...
v
shop-app
| 5. POST token endpoint: code + code_verifier (+ client secret)
v
Google Token Endpoint
| 6. Trả access_token + id_token (+ refresh_token)
v
shop-app
| 7. Validate ID Token (chữ ký, iss, aud, exp, nonce)
| 8. Tạo phiên đăng nhập trong app
v
User authenticated
- PKCE: client tạo
code_verifierngẫu nhiên, gửicode_challenge = SHA256(verifier)ở bước 2, gửi verifier ở bước 5. Ai trộm đượccodemà không có verifier thì không đổi lấy token được. - state: liên kết response với request ban đầu, chống CSRF trong luồng login.
- nonce: gắn ID Token với đúng request authentication, chống replay.
- Token đi qua kênh back-channel (bước 5–6), không lộ trên URL.
6. So sánh: OAuth2, OIDC, JWT, SSO
| Khái niệm | Bản chất |
|---|---|
| OAuth2 | Framework ủy quyền truy cập |
| OIDC | Lớp authentication trên OAuth2 (ID Token, userinfo) |
| JWT | Định dạng token chứa claims |
| SSO | Trải nghiệm đăng nhập một lần cho nhiều app |
| Keycloak | Nền tảng IAM triển khai OAuth2/OIDC/SSO |
| Spring Security | Framework bảo mật cho ứng dụng Java |
Chúng không thay thế nhau. Một hệ thống dùng tất cả cùng lúc.
7. Khi nào nên dùng / không nên dùng
- Nên: nhiều app cần SSO, cần social login, cần MFA, quản lý user tập trung.
- Nên: SPA và mobile luôn dùng Authorization Code + PKCE.
- Không nên: dùng Implicit hay Password grant; dùng ID Token gọi API.
- Có thể không cần: một app nội bộ nhỏ, một loại user, login + JWT tự làm như bài 4.4.5–4.4.6 là đủ.
Vậy job chạy nền không có user đăng nhập thì dùng flow nào?
Đúng chỗ dùng Client Credentials. Ví dụ order-service có scheduled job retry các payment lỗi. Không có user, không nên dùng JWT của admin hay hardcode token.
order-service Keycloak payment-service
| grant_type=client_credentials | |
| client_id + client_secret --->| |
|<------------ access_token -----| |
| Bearer access_token --------------------------------------->|
| | verify token + scope |
POST /realms/shop/protocol/openid-connect/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials&client_id=order-service&client_secret=${ORDER_SERVICE_SECRET}
Token này đại diện cho service, không phải John. Không có ID Token vì không có user. Code Spring đầy đủ với OAuth2AuthorizedClientManager ở bài 4.4.8.
SSO là mọi app dùng chung một access token à?
Bạn hiểu gần đúng, chỉ cần chỉnh một điểm: cái được chia sẻ là phiên đăng nhập ở IdP, không phải access token.
shop-app --> redirect Keycloak --> user nhập password --> Keycloak SSO session --> về shop-app
admin-app --> redirect Keycloak --> đã có SSO session --> không cần nhập lại --> về admin-app
Mỗi app vẫn chạy Authorization Code flow riêng và nhận token riêng, đúng client_id, aud và quyền của mình. Logout một app không tự logout app khác, trừ khi cấu hình OIDC logout (RP-initiated / back-channel logout).
Phỏng vấn hỏi OAuth2 với OIDC thì trả lời sao?
Q1. OAuth2 khác OpenID Connect thế nào?
🇻🇳 Tiếng Việt:
OAuth2 là framework ủy quyền: cho ứng dụng được quyền truy cập tài nguyên thay mặt user mà không cần password của user. OIDC là lớp định danh xây trên OAuth2, chuẩn hóa việc đăng nhập bằng ID Token và endpoint userinfo, trả lời câu hỏi "user là ai".
🇬🇧 English:
OAuth2 is an authorization framework that lets an application get delegated access to resources without the user's password. OpenID Connect is an identity layer on top of OAuth2 that standardizes login through the ID token and the userinfo endpoint, so it answers "who is the user".
Q2. Access Token khác ID Token thế nào?
🇻🇳 Tiếng Việt:
Access token dùng để gọi resource server, audience là API. ID token cho client biết thông tin user đã đăng nhập, audience là client. Không bao giờ gửi ID token lên API thay cho access token.
🇬🇧 English:
An access token is presented to a resource server and its audience is the API. An ID token tells the client about the authenticated user and its audience is the client. We never send an ID token to an API in place of an access token.
Q3. Authorization Code + PKCE hoạt động thế nào, vì sao cần PKCE?
🇻🇳 Tiếng Việt:
Client redirect user sang authorization server kèm
statevàcode_challenge. User login, server trả về một code ngắn hạn. Client đổi code lấy token qua back-channel, gửi kèmcode_verifier. PKCE đảm bảo ai trộm được code cũng không đổi được token, nên bắt buộc với SPA và mobile vốn không giữ được client secret.
🇬🇧 English:
The client redirects the user to the authorization server with a
stateand acode_challenge. After login, the server returns a short-lived code, which the client exchanges for tokens over a back channel together with thecode_verifier. PKCE ensures a stolen code is useless, so it is required for SPAs and mobile apps, which cannot keep a client secret.
Q4. Khi nào dùng Client Credentials?
🇻🇳 Tiếng Việt:
Khi service gọi service mà không có user, ví dụ batch job. Service dùng client id và secret của chính nó để lấy access token có quyền tối thiểu.
🇬🇧 English:
For machine-to-machine calls with no user involved, such as a batch job. The service authenticates with its own client credentials and gets an access token with the minimum required scope.
Cách nhớ ngắn gọn
- OAuth2 = được làm gì (authorization). OIDC = là ai (authentication, ID Token).
- 4 vai trò: Resource Owner, Client, Authorization Server, Resource Server.
- Có user: Authorization Code + PKCE. Không có user: Client Credentials.
- ID Token cho client, Access Token cho API.
Tự kiểm tra
OAuth2 có bắt buộc access token là JWT không?
Không. Access token có thể là JWT hoặc opaque token (khi đó resource server dùng introspection).
state và code_verifier chống tấn công gì?
state chống CSRF trong luồng login. code_verifier (PKCE) chống trộm authorization code.
Vì sao Implicit flow bị bỏ?
Token trả thẳng trên URL fragment, dễ lộ qua history, log, referrer; không có PKCE hay back-channel.
Có nên gửi ID Token lên order API để xác thực không?
Không. ID Token có aud là client. API cần access token có audience là chính nó.
Scope openid có ý nghĩa gì?
Báo cho server đây là request OIDC, server sẽ trả thêm ID Token.