Bỏ qua, đến nội dung

FAQ & Troubleshooting

Q: Làm sao thêm phương thức xác thực mới?

Dùng authManager.registerTypes() trong phương thức load() của plugin:

typescript
class MyOIDCAuth extends BaseAuth {
  async validate() {
    // Xác thực qua OIDC provider
    const profile = await this.verifyOIDCToken(ctx);
    return await this.authenticator.findOrCreateUser(profile.sub, {
      displayname: profile.name,
      email: profile.email,
    });
  }
}

// plugin.load()
this.app.authManager.registerTypes('oidc', {
  auth: MyOIDCAuth,
  title: 'OIDC Login',
  getPublicOptions: (options) => ({
    issuer: options.issuer,
    clientId: options.clientId,
  }),
});

Sau khi đăng ký type, admin tạo authenticator mới với authType: 'oidc' qua UI hoặc API authenticators:create. Authenticator cần được enabled: true để hiển thị trên trang login.


Q: Token hết hạn, renew, blacklist hoạt động thế nào?

TokenController quản lý vòng đời JWT theo 3 mốc thời gian:

MốcCấu hìnhMặc địnhÝ nghĩa
Token expirationtokenExpirationTime1 ngàyToken hết hạn, nhưng có thể renew
Renew limitexpiredTokenRenewLimit1 ngàyHết thời gian renew → phải đăng nhập lại
Session expirationsessionExpirationTime7 ngàySession hoàn toàn hết hạn (tính từ lúc signIn)

Luồng:

  1. User đăng nhập → TokenController.add() tạo record issuedTokens với jti, signInTime, issuedTime
  2. Request bình thường → check issuedTime + tokenExpirationTime > now → OK
  3. Token hết hạn → check issuedTime + expiredTokenRenewLimit > now → nếu còn, gọi renew(jti):
    • Tạo JTI mới bằng randomUUID()
    • Update record trong issuedTokens
    • Cache JTI mới 10 giây (xử lý concurrent request)
    • Trả token mới qua header x-new-token
  4. Hết renew limit hoặc session → reject, yêu cầu đăng nhập lại

TokenBlacklistService xử lý revoke:

  • has(token) — kiểm tra qua Bloom filter (nhanh) rồi DB (chính xác)
  • add(token) — thêm vào Bloom filter + DB, đồng thời dọn token hết hạn
  • Bloom filter: capacity 1 triệu token, error rate 0.1%, chiếm khoảng 1.7 MB RAM

Q: Luồng quên mật khẩu hoạt động thế nào?

Yêu cầu cấu hình:

  • Plugin notification-manager phải được cài và có email channel hoạt động
  • Authenticator options cần có: enableResetPassword: true, notificationChannel, emailSubject, emailContentType, emailContentHTML hoặc emailContentText, resetTokenExpiresIn (phút)

Template variables có sẵn:

BiếnMô tả
$userObject user (id, username, email...)
$resetLinkURL reset password đầy đủ
$envEnvironment variables (app.environment.getVariables())
$resetLinkExpirationThời gian hết hạn (phút)
$systemSettingsCài đặt hệ thống (title, logo...)

Q: Sao user đăng nhập bị lỗi 401?

Kiểm tra theo thứ tự:

  1. Authenticator có bật không?

    • Gọi GET /api/authenticators:publicList xem authenticator xuất hiện không
    • Kiểm tra trường enabled: true trong DB
  2. Sai username/email hoặc password?

    • Error message: "The username/email or password is incorrect"
    • BasicAuth.validate() tìm user bằng $or: [{ username }, { email }], sau đó verify password qua PasswordField.verify()
  3. Token policy quá ngắn?

    • Kiểm tra tokenControlConfig trong DB
    • Nếu tokenExpirationTime quá nhỏ, token hết hạn ngay lập tức
    • Nếu sessionExpirationTime quá nhỏ, session hết hạn nhanh
  4. Token bị blacklist?

    • Kiểm tra bảng tokenBlacklist có chứa token không
    • Nếu dùng Redis Bloom filter, kiểm tra service hoạt động bình thường
  5. Header X-Authenticator sai?

    • Client phải gửi header X-Authenticator đúng tên authenticator (ví dụ: basic)
    • Nếu không gửi, server dùng default authenticator (authenticator đầu tiên trong cache)

Q: Có thể dùng nhiều authenticators cùng lúc không?

Có. Hệ thống hỗ trợ nhiều authenticator hoạt động đồng thời:

  • authenticators:publicList trả về tất cả authenticator có enabled: true, sắp xếp theo sort
  • Client hiển thị danh sách trên trang login, mỗi authenticator có UI riêng
  • Khi đăng nhập, client gửi header X-Authenticator: <name> để chỉ định authenticator

Ví dụ cấu hình phổ biến:

  • basic (Email/Password) — luôn bật
  • oidc-google (Google Login) — bật thêm
  • ldap-corp (LDAP nội bộ) — bật cho nhân viên

Lưu ý: Không thể tắt hoặc xóa authenticator cuối cùng. Server kiểm tra ít nhất 1 authenticator bật (checkCount trong authenticators:destroyauthenticators:update).


Q: Environment variables trong authenticator options dùng thế nào?

Storer.renderJsonTemplate() tự động thay thế biến môi trường trong trường options trước khi cache authenticator.

Ví dụ cấu hình OIDC trong DB:

json
{
  "clientId": "$env.OIDC_CLIENT_ID",
  "clientSecret": "$env.OIDC_CLIENT_SECRET",
  "issuer": "$env.OIDC_ISSUER"
}

Khi Storer load authenticator, app.environment.renderJsonTemplate() thay thế $env.* bằng giá trị thực từ environment.

Ngoại lệ: Một số key không được render template để tránh injection. BasicAuth khai báo:

typescript
static readonly optionsKeysNotAllowedInEnv = [
  'emailContentText',
  'emailContentHTML',
  'emailSubject'
];

Các key này được truyền qua tham số omit khi gọi renderJsonTemplate, giữ nguyên nội dung gốc.


Q: Token blacklist dùng Bloom filter có bị false positive không?

Bloom filter được cấu hình với error rate 0.1% (1/1000) và capacity 1 triệu token. Điều này có nghĩa:

  • False positive (khoảng 0.1%): Bloom filter báo token bị block nhưng thực tế không — không ảnh hưởnghas() luôn kiểm tra DB sau Bloom filter
  • False negative (0%): Nếu token thực sự bị block, Bloom filter luôn phát hiện

Nếu Redis không khả dụng, service fallback về chỉ dùng DB query. Bloom filter khởi tạo lúc beforeStart, nạp tất cả token chưa hết hạn từ DB.