OAuth2 và OpenID Connect: Login with Google, SSO

4 câu hỏi~30 phútTừ khóa: OAuth2, Resource Owner, Authorization Server, Authorization Code, PKCE, state, nonce, Client Credentials, OIDC, ID Token, SSO

Tiếp đi: OAuth2 và OpenID Connect.

Bài này trả lời ba câu hỏi:

  1. Tại sao đăng nhập bằng Google mà không cần đưa password cho ứng dụng?
  2. OAuth2 khác JWT thế nào?
  3. 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 đề

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ĩaVí dụ
Resource OwnerChủ dữ liệuNgười dùng Google
ClientỨng dụng xin quyềnshop-app
Authorization ServerCấp tokenGoogle / Keycloak
Resource ServerAPI chứa tài nguyênGoogle 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:

FlowDùng cho
Authorization Code + PKCEUser đăng nhập qua browser: web app, SPA, mobile
Client CredentialsService gọi service, không có user (machine-to-machine)
Refresh TokenLấ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

6. So sánh: OAuth2, OIDC, JWT, SSO

Khái niệmBản chất
OAuth2Framework ủy quyền truy cập
OIDCLớp authentication trên OAuth2 (ID Token, userinfo)
JWTĐịnh dạng token chứa claims
SSOTrải nghiệm đăng nhập một lần cho nhiều app
KeycloakNền tảng IAM triển khai OAuth2/OIDC/SSO
Spring SecurityFramework 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

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 state và 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èm code_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 state and a code_challenge. After login, the server returns a short-lived code, which the client exchanges for tokens over a back channel together with the code_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


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.