Hiểu đúng flow Redirect 302 của Browser trong OAuth/SSO

Hoang Vu,4 min read

Ngày 31 tháng 3, 2026

Phần lớn lỗi OAuth/SSO trong production không phải do 302 sai, mà do hiểu sai flow redirect của browser.

Bài này tập trung vào luồng chính:

  1. Browser đi qua chuỗi redirect như thế nào trong OAuth/SSO.
  2. Ở callback, cookie/session được set ra sao.
  3. Vì sao popup login xong nhưng tab chính vẫn có thể chưa có session.

1. Redirect 302 trong flow OAuth tiêu chuẩn

Flow quen thuộc:

App -> (302) -> Auth -> (login) -> (302) -> App

OAUTH 302 LOGIN FLOW

1. App initiates /oauth/login
2. 302 -> Auth Server
3. User login on Auth Server
4. 302 -> /oauth/callback
5. App sets session and redirects
6. User lands on success page

Điểm quan trọng nhất: browser không "đoán" redirect, mà làm đúng theo chuẩn HTTP response.

Ví dụ response từ Server A:

HTTP/1.1 302 Found
Location: https://auth.example.com/oauth2/authorize?client_id=...&redirect_uri=...
Cache-Control: no-store

Khi browser nhận response này trong ngữ cảnh điều hướng trang (hoặc popup navigation), nó sẽ:

  1. Đọc status code 302.
  2. Đọc header Location.
  3. Tự tạo request mới đến URL trong Location.
  4. Cập nhật URL hiện tại theo hướng redirect chain.

Đó là lý do bạn thấy flow kiểu App -> Auth -> callback -> success chạy "tự động", dù frontend không cần window.location ở từng hop.

Điều này cũng giải thích vì sao cùng endpoint nhưng gọi bằng fetch thì UI có thể không đổi trang: fetch không phải top-level navigation.

OAuth 302 Sequence Diagram

Browser
App (Server A)
Auth Server (B)
Browser
1) GET /auth/login
App (Server A)
App (Server A)
2) 302 Location: https://auth-b/login
Browser
Browser
3) Navigate to Auth B
Auth Server (B)
Auth Server (B)
4) 302 Location: /oauth/callback?code=...
Browser
Browser
5) GET /oauth/callback
App (Server A)
App (Server A)
6) Set-Cookie + 302 /success
Browser

OAuth 302 Node Diagram

BBrowser context drives navigation

Browser follows each 302 Location and moves to the next node until session is established.

App (A)

Start: /auth/login

302 redirect

Auth Server (B)

Login + consent

302 redirect

App (A)

Callback: set session

302 redirect

App Success

User now signed in

Bên trong callback 302 của Server A thường có gì?

Khi Auth Server redirect về callback của App (/auth/callback), Server A thường làm chuỗi bước sau:

  1. Validate state để chống CSRF login flow.
  2. Đổi authorization_code lấy token ở backend (server-to-server).
  3. Tạo session nội bộ (DB/Redis/JWT session id).
  4. Trả response set cookie + redirect cuối về trang app.

Một response callback điển hình:

HTTP/1.1 302 Found
Set-Cookie: sid=abc123; Path=/; HttpOnly; Secure; SameSite=Lax
Location: /auth/success
Cache-Control: no-store

Browser sẽ lưu cookie (nếu policy hợp lệ), rồi tiếp tục tự điều hướng sang Location kế tiếp. Vì vậy người dùng thấy "login xong là nhảy về app", nhưng thực chất giữa các bước có rất nhiều logic backend diễn ra ở callback.

Điểm mấu chốt:

  1. Top-level page hoặc popup thực sự đổi URL.
  2. Cookie/session thường được thiết lập ngay tại callback response của Server A.
  3. Sau redirect cuối, browser render trang đích mới.

2. SSO popup flow: timeline chi tiết

Ví dụ popup thường gặp:

Popup Browser -> Server A (/auth/login)
Server A -> 302 -> Auth Server (B)
Auth Server login -> 302 -> callback về A
A set cookie/session -> 302 -> page success

POPUP SSO REDIRECT FLOW

1. Popup opens: /auth/login on Server A
2. Server A responds 302 to Auth Server B
3. Auth Server B renders login
4. Auth Server B 302 -> callback on A
5. Server A sets cookie/session
6. Server A 302 -> success page

Trong flow này, popup là nơi chạy redirect chain, không phải tab chính. Vì vậy tab chính cần một cơ chế đồng bộ trạng thái (polling, postMessage, hoặc check session endpoint) sau khi popup hoàn tất.


3. Vì sao là 302 chứ không phải 301?

Trong OAuth/SSO, redirect login là điều hướng tạm thời theo trạng thái phiên, không phải ánh xạ URL cố định.

301 Moved Permanently mang ngữ nghĩa "đã chuyển vĩnh viễn" nên có thể bị cache mạnh bởi browser/CDN. Điều này nguy hiểm cho auth flow vì:

  1. Browser có thể ghi nhớ redirect cũ và bỏ qua một số bước động của phiên đăng nhập.
  2. Callback/login endpoint là endpoint theo ngữ cảnh phiên, không nên bị coi là route chuyển hướng vĩnh viễn.
  3. Khi hạ tầng đổi domain/path callback, cache từ 301 cũ có thể gây lỗi khó debug.

Vì vậy đa số hệ thống dùng 302 (hoặc 303 khi muốn ép method thành GET sau submit form) cho login redirect chain. Tóm lại:

  1. 301: dùng cho SEO/site migration ổn định lâu dài.
  2. 302/303: dùng cho điều hướng tạm thời theo runtime state như OAuth/SSO.

4. Sai lầm phổ biến khi tích hợp OAuth

  1. Gọi endpoint login không theo browser navigation (ví dụ gọi API rồi chờ UI tự chuyển).
  2. Đặt callback xử lý session nhưng quên trả redirect cuối về trang success.
  3. Popup login thành công nhưng không notify tab chính.
  4. Cookie session set ở callback nhưng thuộc tính SameSite hoặc domain/path không đúng.

5. Pattern triển khai khuyến nghị

Với login chính (full page)

window.location.assign('/auth/login');

Với popup SSO

  1. Tab chính mở popup tới /auth/login.
  2. Popup chạy full redirect chain.
  3. Trang success trong popup gửi postMessage('auth:success') về opener.
  4. Tab chính nhận message rồi gọi /auth/session để đồng bộ user state.

6. Checklist debug nhanh khi 302 "có vẻ đúng nhưng vẫn lỗi"

  1. Kiểm tra redirect chain trong DevTools Network (mỗi hop có Location).
  2. Kiểm tra response callback có Set-Cookie đúng domain/path/samesite/secure.
  3. Xác nhận flow chính có chạy bằng browser navigation/popup navigation.
  4. Nếu popup: xác nhận callback đã notify tab chính.
  5. Kiểm tra endpoint success có chạy được trong môi trường hiện tại (localhost/prod).

7. Corner case: khi nào nói về 302 trong fetch?

302 IN BROWSER VS FETCH

Browser navigation

1. Browser requests /oauth/login
2. 302 detected by browser
3. Address bar navigates to IdP
4. User login page is rendered
5. 302 callback updates top-level page

fetch/XHR flow

1. fetch('/oauth/login') runs in JS
2. 302 often auto-followed in fetch stack
3. No top-level navigation by default
4. May be blocked by CORS/opaque redirect
5. You must navigate manually (window.location)

fetch là góc phụ cần biết để tránh debug sai hướng, không phải trục chính của OAuth redirect flow.

Top-level navigation sẽ tự đi theo 302 và đổi trang.

Nhưng fetch('/oauth/login') thường chỉ theo redirect ở mức request nội bộ, không tự điều hướng UI.

  1. Đây là corner case thường gây hiểu nhầm "network OK nhưng UI đứng yên".
  2. Cross-origin redirect trong fetch còn phụ thuộc mode, credentials, CORS và có thể thành opaque.
  3. Với login OAuth/SSO, ưu tiên browser navigation hoặc popup navigation.

Kết luận

302 trong OAuth/SSO trước hết là câu chuyện của browser redirect flow.

Phần fetch chỉ nên xem như corner case để tránh chọn sai cách gọi login endpoint.

Khi nắm chắc flow redirect của browser, phần lớn bug OAuth/SSO khó chịu trong production sẽ trở nên dễ debug hơn rất nhiều.

2026 © @hoag/blog.