Hướng dẫn sử dụng FIT LHU SSO

Một tài khoản để đăng nhập mọi ứng dụng của Khoa CNTT. Đăng nhập một lần, các ứng dụng sau tự nhận bạn mà không phải nhập lại mật khẩu.

Dành cho người dùng

Đăng nhập

Khi một ứng dụng chuyển bạn tới trang FIT LHU SSO, trang sẽ ghi “Đăng nhập để tiếp tục vào <tên ứng dụng>”. Bạn có thể đăng nhập bằng:

  • Email + mật khẩu — nhập email, bấm Tiếp tục, rồi nhập mật khẩu.
  • Passkey — bấm Đăng nhập bằng passkey (hoặc chọn passkey trình duyệt gợi ý ngay trong ô email) rồi xác nhận bằng vân tay, khuôn mặt hoặc mã khóa màn hình.
  • Google / GitHub — bấm Tiếp tục với Google hoặc Tiếp tục với GitHub (nếu được bật).

Sau khi đăng nhập, bạn được đưa trở lại ứng dụng. Lần sau mở ứng dụng khác của Khoa, bạn sẽ vào thẳng mà không phải đăng nhập lại (phiên giữ tối đa 30 ngày, hết hạn sau 7 ngày không dùng).

Hệ thống không bao giờ đăng nhập cho bạn bằng mã gửi qua email — mã chỉ dùng để đặt lại mật khẩu và xác minh email.

Tạo tài khoản

  1. Ở trang đăng nhập, bấm Tạo tài khoản (hoặc mở /signup).
  2. Nhập email (chỉ nhận @lhu.edu.vn, @lachong.edu.vn) và bấm Gửi mã xác minh.
  3. Nhập mã 6 chữ số trong email (hiệu lực 10 phút).
  4. Nhập họ tên và mật khẩu, bấm Tạo tài khoản — bạn được đăng nhập ngay.

Cũng có thể tạo tài khoản bằng cách bấm Tiếp tục với Google/GitHub nếu email của tài khoản đó được phép.

Quên mật khẩu / đặt mật khẩu lần đầu

  1. Ở trang đăng nhập, nhập email › Tiếp tục › bấm Quên mật khẩu? (hoặc mở /forgot-password).
  2. Bấm Gửi mã, nhập mã 6 chữ số trong email.
  3. Đặt mật khẩu mới (ít nhất 10 ký tự, không cần ký tự đặc biệt, không trùng email hoặc tên).
  4. Mọi thiết bị đang đăng nhập sẽ bị đăng xuất; đăng nhập lại bằng mật khẩu mới.

Mẹo: dùng một cụm từ dài dễ nhớ, ví dụ “mua ca phe sua da luc 7 gio”, an toàn hơn một từ ngắn nhiều ký tự lạ.

Passkey — đăng nhập không cần mật khẩu

Passkey dùng vân tay, khuôn mặt hoặc mã khóa màn hình của thiết bị thay cho mật khẩu. Không thể bị lộ qua trang lừa đảo và không cần nhớ.

  • Tạo: sau khi đăng nhập, chọn Tạo passkey trên gợi ý hiện ra, hoặc vào Tài khoản › Passkey › Thêm passkey.
  • Dùng trên điện thoại khác máy tính: khi đăng nhập trên máy tính, chọn dùng điện thoại và quét mã QR trình duyệt hiển thị.
  • Đổi tên / xóa: trong Tài khoản › Passkey. Không thể xóa phương thức đăng nhập cuối cùng của bạn.

Liên kết Google / GitHub

Trong Tài khoản › Tài khoản liên kết, bấm Liên kết để thêm Google hoặc GitHub làm cách đăng nhập. Nếu bạn đăng nhập bằng Google/GitHub có cùng email đã xác minh, tài khoản sẽ tự liên kết. GitHub cần có email chính đã xác minh.

Xác minh email

Nếu trang Tài khoản hiện nhãn Chưa xác minh cạnh email, bấm vào dòng Email › Gửi mã xác minh › nhập mã. Ứng dụng nhận được trạng thái email_verified để biết email là thật của bạn.

Phiên đăng nhập và ứng dụng đã cho phép

  • Phiên đăng nhập: xem thiết bị, IP, cách đăng nhập và lần hoạt động cuối; bấm Đăng xuất để đăng xuất từng thiết bị.
  • Ứng dụng đã cho phép: các ứng dụng bạn đã đồng ý chia sẻ thông tin; bấm Thu hồi để ngừng chia sẻ và đăng xuất khỏi ứng dụng đó.
  • Xác thực lại: đổi mật khẩu, thêm/xóa passkey, liên kết/hủy liên kết cần đã đăng nhập trong 10 phút gần nhất; nếu quá, hệ thống sẽ hỏi xác thực lại.

An toàn tài khoản

  • FIT LHU SSO chỉ gửi hai loại email: mã đặt lại mật khẩu và mã xác minh email. Email khác tự xưng là FIT LHU SSO (yêu cầu bấm link, nhập mật khẩu…) là giả mạo.
  • Không đưa mã 6 chữ số cho bất kỳ ai, kể cả người tự xưng là quản trị viên.
  • Chỉ nhập mật khẩu ở địa chỉ auth.fitlhu.com.
  • Thấy phiên lạ trong Tài khoản: đăng xuất phiên đó và đổi mật khẩu ngay.

Câu hỏi thường gặp

Không nhận được mã?
Kiểm tra thư mục Spam/Quảng cáo. Có thể gửi lại sau 60 giây; mỗi email nhận tối đa 5 mã mỗi giờ. Mã chỉ dùng được trên đúng trình duyệt đã yêu cầu.
“Bạn đã nhập sai quá nhiều lần”?
Sau 5 lần sai mật khẩu trong 15 phút, đăng nhập từ máy đó bị tạm dừng 15 phút. Dùng “Quên mật khẩu?” nếu không nhớ.
“Yêu cầu đăng nhập đã hết hạn”?
Yêu cầu đăng nhập chỉ có hiệu lực 10 phút và trên đúng trình duyệt đã mở. Quay lại ứng dụng và bấm đăng nhập lại.
“Hệ thống đang bảo trì, thử lại sau ít giây”?
Hệ thống đang chuyển máy chủ dữ liệu; chờ vài giây rồi thử lại.
Tài khoản bị khóa?
Liên hệ quản trị viên của Khoa.

Dành cho nhà phát triển ứng dụng

Tổng quan

FIT LHU SSO là nhà cung cấp OpenID Connect theo luồng Authorization Code + PKCE. Mọi thư viện OIDC chuẩn đều dùng được (cấu hình bằng discovery URL); ví dụ viết tay không thư viện nằm trong repo ở examples/client-nextjs.

EndpointPhương thứcURL
DiscoveryGEThttps://auth.fitlhu.com/.well-known/openid-configuration
JWKS (khóa công khai)GEThttps://auth.fitlhu.com/.well-known/jwks.json
AuthorizeGEThttps://auth.fitlhu.com/authorize
TokenPOSThttps://auth.fitlhu.com/token
UserInfoGET/POSThttps://auth.fitlhu.com/userinfo
Thu hồi tokenPOSThttps://auth.fitlhu.com/revoke
Đăng xuấtGEThttps://auth.fitlhu.com/logout

1. Đăng ký ứng dụng (client)

Có hai cách:

  • Tự quản lý (khuyến nghị): vào Tài khoản › Developer › Đăng ký làm developer, mô tả ứng dụng của bạn. Khi quản trị viên duyệt, mở Ứng dụng của tôi để tạo, sửa, tắt, tạo lại secret hoặc xóa các ứng dụng do bạn tạo.
  • Nhờ quản trị viên: gửi tên ứng dụng, logo, trang chủ, loại client và danh sách URI.

Bạn nhận client_id và (nếu là confidential) client_secret — secret chỉ hiển thị một lần, hãy lưu ngay.

  • Confidential: có backend giữ được secret (Next.js, Express, Laravel…). Xác thực ở /token bằng client_secret_basic hoặc client_secret_post.
  • Public: SPA hoặc ứng dụng di động, không có secret, chỉ dựa vào PKCE.
  • Redirect URI phải khớp chính xác từng ký tự (kể cả dấu / cuối). Chỉ https://; http:// chỉ cho localhost. Sai redirect URI → SSO hiện trang lỗi, không chuyển hướng.

2. Chuyển người dùng tới /authorize

Sinh ngẫu nhiên state, nonce và code_verifier (43–128 ký tự), lưu ở phía server/cookie HttpOnly. PKCE S256 bắt buộc với mọi client: code_challenge = BASE64URL(SHA256(code_verifier)).

GET https://auth.fitlhu.com/authorize
  ?response_type=code
  &client_id=<client_id>
  &redirect_uri=https://app.fitlhu.com/callback
  &scope=openid profile email roles offline_access
  &state=<ngẫu nhiên>
  &nonce=<ngẫu nhiên>
  &code_challenge=<BASE64URL(SHA256(code_verifier))>
  &code_challenge_method=S256
  [&prompt=login|none|consent] [&max_age=<giây>] [&login_hint=<email>]

Kết quả trả về redirect URI: ?code=…&state=…&iss=https://auth.fitlhu.com. Luôn kiểm tra state và iss. Lỗi (sau khi client và redirect URI hợp lệ) trả về dạng ?error=…&error_description=…&state=…, ví dụ login_required khi dùng prompt=none mà người dùng chưa đăng nhập, access_denied khi người dùng từ chối.

3. Đổi code lấy token

Code sống 60 giây và chỉ dùng một lần (dùng lại → invalid_grant và mọi token cấp từ code đó bị thu hồi).

curl -X POST https://auth.fitlhu.com/token \
  -u "<client_id>:<client_secret>" \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code=<code> \
  -d redirect_uri=https://app.fitlhu.com/callback \
  -d code_verifier=<code_verifier>
{
  "access_token": "eyJ…",      // JWT RS256, 10 phút, typ at+jwt
  "id_token": "eyJ…",          // JWT RS256, 10 phút
  "refresh_token": "…",        // chỉ khi xin scope offline_access, 30 ngày
  "token_type": "Bearer",
  "expires_in": 600,
  "scope": "openid profile email roles offline_access"
}

Client public (SPA, ứng dụng di động — không có secret): bỏ -u, gửi client_id trong body; PKCE là lớp bảo vệ duy nhất nên không bao giờ được bỏ.

curl -X POST https://auth.fitlhu.com/token   -d grant_type=authorization_code   -d client_id=<client_id>   -d code=<code>   -d redirect_uri=https://app.fitlhu.com/callback   -d code_verifier=<code_verifier>

Xác minh id_token: chữ ký RS256 với khóa có kid tương ứng trong JWKS (tải lại JWKS nếu không thấy — khóa được xoay định kỳ), rồi iss = https://auth.fitlhu.com, aud = client_id, exp, nonce.

4. Scope và claim

ScopeClaim
openid (bắt buộc)sub (UUID cố định của người dùng — dùng làm khóa liên kết, không dùng email), auth_time, amr, idp, sid
profilename, picture
emailemail, email_verified
rolesroles (mảng, ví dụ ["user","giangvien"])
offline_accesscấp thêm refresh_token

amr cho biết cách đăng nhập: ["pwd"] mật khẩu, ["pop","user"] passkey, ["fed"] Google/GitHub (khi đó idp là google/github, ngược lại local).

5. UserInfo, làm mới, thu hồi

curl https://auth.fitlhu.com/userinfo -H "Authorization: Bearer <access_token>"

curl -X POST https://auth.fitlhu.com/token -u "<client_id>:<client_secret>" \
  -d grant_type=refresh_token -d refresh_token=<refresh_token>

curl -X POST https://auth.fitlhu.com/revoke -u "<client_id>:<client_secret>" \
  -d token=<refresh_token> -d token_type_hint=refresh_token
  • Refresh token xoay vòng: mỗi lần làm mới nhận refresh_token mới, phải lưu đè. Dùng lại token cũ → cả họ token bị thu hồi (coi như bị đánh cắp).
  • /token giới hạn 30 yêu cầu/phút cho mỗi client.

6. Đăng xuất toàn cục

GET https://auth.fitlhu.com/logout
  ?id_token_hint=<id_token>
  &post_logout_redirect_uri=https://app.fitlhu.com/
  &state=<tùy chọn>

post_logout_redirect_uri phải nằm trong danh sách đã đăng ký. Không có id_token_hint hợp lệ thì SSO hỏi người dùng xác nhận trước khi đăng xuất. Hãy xóa phiên riêng của ứng dụng trước khi chuyển hướng.

Không lưu token trong localStorage; giữ token ở server và chỉ đưa trình duyệt một cookie phiên HttpOnly. Không ghi token vào log.

7. Dùng thư viện OIDC có sẵn

Không cần tự viết như ví dụ trên: mọi thư viện hỗ trợ OpenID Connect đều dùng được. Cấu hình chung:

Issuer / Discovery URLhttps://auth.fitlhu.com (thư viện tự đọc https://auth.fitlhu.com/.well-known/openid-configuration)
Client ID / Client secretlấy ở “Ứng dụng của tôi” hoặc từ quản trị viên
LuồngAuthorization Code (response_type=code)
PKCEbật, phương thức S256 — thiếu PKCE sẽ bị từ chối
Xác thực ở /tokenclient_secret_basic (mặc định của đa số thư viện) hoặc client_secret_post; client public: none
Scopeopenid profile email (+ roles, offline_access nếu cần)
Thuật toán id_tokenRS256

Ví dụ Node.js với thư viện openid-client (v6):

import * as oidc from "openid-client";

const config = await oidc.discovery(new URL("https://auth.fitlhu.com"), process.env.CLIENT_ID, process.env.CLIENT_SECRET);
// Chạy thử với issuer http://localhost: thêm tham số thứ 5 { execute: [oidc.allowInsecureRequests] }

// 1) Bắt đầu đăng nhập — lưu verifier/state/nonce vào phiên của ứng dụng
const code_verifier = oidc.randomPKCECodeVerifier();
const state = oidc.randomState();
const nonce = oidc.randomNonce();
const url = oidc.buildAuthorizationUrl(config, {
  redirect_uri: "https://app.fitlhu.com/callback",
  scope: "openid profile email",
  code_challenge: await oidc.calculatePKCECodeChallenge(code_verifier),
  code_challenge_method: "S256",
  state,
  nonce,
});
// res.redirect(url.href)

// 2) Callback — thư viện tự kiểm tra state, iss, chữ ký và nonce
const tokens = await oidc.authorizationCodeGrant(config, new URL(req.url, "https://app.fitlhu.com"), {
  pkceCodeVerifier: code_verifier,
  expectedState: state,
  expectedNonce: nonce,
});
const claims = tokens.claims(); // sub, email, name, amr, idp…

Next.js (Auth.js), Laravel (Socialite + driver OIDC), Spring Security, ASP.NET Core… cấu hình tương tự bằng discovery URL ở trên. Mẫu viết tay không dùng thư viện (Next.js) và hướng dẫn cho Express nằm trong repo: examples/client-nextjs.

8. Lỗi thường gặp khi tích hợp

Hiện tượng / mã lỗiNguyên nhân và cách sửa
Trang lỗi “Địa chỉ quay lại không hợp lệ”redirect_uri không khớp từng ký tự với URI đã đăng ký (http/https, dấu / cuối, cổng, chữ hoa). Sửa URI trong cấu hình ứng dụng.
Trang lỗi “Ứng dụng không hợp lệ”Sai client_id hoặc ứng dụng đang bị tắt.
invalid_request “code_challenge is required”Chưa bật PKCE hoặc dùng plain — phải là S256.
invalid_scopeXin scope chưa được cho phép trong cấu hình ứng dụng, hoặc thiếu openid.
invalid_client (401)Sai secret, hoặc client public lại gửi secret, hoặc secret vừa được tạo lại.
invalid_grantCode quá 60 giây hoặc đã dùng, code_verifier sai, redirect_uri khác lúc gọi /authorize, hoặc refresh token cũ đã bị dùng lại.
login_requiredGọi với prompt=none khi người dùng chưa đăng nhập SSO — chuyển sang đăng nhập bình thường.
access_deniedNgười dùng bấm “Từ chối” ở màn hình cho phép.
slow_down (429)Quá 30 yêu cầu/phút tới /token cho một client — lưu và dùng lại access token cho tới khi hết hạn.
“Yêu cầu đăng nhập đã hết hạn”Người dùng mở link /authorize quá 10 phút hoặc ở trình duyệt khác — bắt đầu lại từ nút đăng nhập của ứng dụng.

Chạy thử trên máy: đăng ký thêm redirect URI dạng http://localhost:<cổng>/callback (http chỉ được phép với localhost). Khi lên production, đổi sang URI https:// của ứng dụng.

Dành cho quản trị viên

Công việc thường ngày

  • Ứng dụng: Quản trị › Ứng dụng › Tạo ứng dụng; sao chép secret ngay (chỉ hiện một lần). Có thể bật màn hình cho phép (consent) và giới hạn phương thức đăng nhập cho từng ứng dụng.
  • Developer: Quản trị › Developer duyệt hoặc từ chối yêu cầu làm developer. Developer chỉ quản lý ứng dụng của chính họ; mục Ứng dụng hiện cột “Người tạo” và bạn vẫn sửa/tắt/xóa được mọi ứng dụng. Gỡ quyền: sửa vai trò trong mục Người dùng.
  • Người dùng: tạo tài khoản (không gửi email mời — báo người dùng dùng “Quên mật khẩu?” để đặt mật khẩu), khóa/mở, gán vai trò, gỡ passkey/tài khoản liên kết.
  • Cài đặt đăng nhập: bật/tắt mật khẩu, passkey, Google, GitHub; bật tự đăng ký và giới hạn domain email; giới hạn Google Workspace.
  • Email: Quản trị › Email › Tài khoản gửi email: chọn Gmail (App Password hoặc OAuth2), nhập địa chỉ gửi và mật khẩu ứng dụng, bấm Lưu rồi Kiểm tra. Mật khẩu được mã hóa và không hiển thị lại; để trống ô mật khẩu khi lưu nghĩa là giữ nguyên.
  • Khóa ký: Xoay khóa định kỳ; khóa cũ vẫn xác minh được token thêm 24 giờ.
  • Nhật ký: lọc theo hành động, người thực hiện, thời gian.

Cơ sở dữ liệu và sao lưu

  • Dữ liệu nằm trên MariaDB và SQL Server: một DB chính nhận ghi, DB kia được đồng bộ mỗi 24 giờ (tiến trình sso-fitlhu-db-sync phải luôn chạy). Bấm Đồng bộ ngay trước khi bảo trì.
  • DB chính hỏng: Quản trị › Cơ sở dữ liệu › Chuyển DB chính (gõ tên DB để xác nhận). Thay đổi chưa đồng bộ kể từ lần đồng bộ trước sẽ bị mất.
  • DB cũ quay lại sẽ hiện “Lỗi thời”: bấm Đồng bộ lại toàn bộ để sao chép lại từ DB chính.
  • Đồng bộ không thay cho backup: chạy scripts/backup-*.sh hằng đêm và mỗi tháng khôi phục thử (pnpm db:verify-restore). Kiểm tra hai DB khớp bằng pnpm db:verify-sync.