Tích hợp đăng nhập (SSO)
Cho phép người dùng đã đăng nhập ở hệ thống của bạn vào thẳng khung chat với đúng danh tính, không phải đăng nhập lại.
1. Cách hoạt động
Backend của bạn đứng ra xác nhận danh tính người dùng với RM Platform; nền tảng cấp cho họ một phiên riêng:
- Backend của bạn ký một identity assertion — JWT ngắn hạn theo RFC 7523 — cho người dùng đang đăng nhập.
- Assertion được đổi lấy phiên tại
POST {PLATFORM_URL}/api/oauth2/tokenvớigrant_type=urn:ietf:params:oauth:grant-type:jwt-bearer. Nền tảng xác minh chữ ký bằngclient_secretcủa Ứng dụng OAuth, tìm (hoặc mở) tài khoản khách tương ứng, trảaccess_token+refresh_token(+id_token). - Khung chat dùng phiên này để trò chuyện.
Với khung nhúng sẵn (bong bóng, iframe, live chat), bước 2 và 3 do khung chat tự làm — bạn chỉ đưa assertion cho nó. Với giao diện tự dựng, bạn gọi endpoint ở bước 2.
Nền tảng là một OAuth 2.0 / OpenID Connect provider: GET {PLATFORM_URL}/.well-known/openid-configuration liệt kê mọi endpoint và grant. Assertion theo RFC 7523 nên thư viện JWT nào cũng ký được.
2. Hai URL cần phân biệt
- PLATFORM_URL — nền tảng Simplize, ví dụ
https://app.simplize.dev. Với khung nhúng sẵn, mọi lời gọi phía này do khung chat tự lo. - URL ứng dụng của bạn — backend của bạn, ví dụ
https://your-app.com. Đây là nơi bạn tự viết endpoint phát assertion.
| Endpoint (ví dụ, của bạn) | Công dụng | Bắt buộc |
|---|---|---|
GET /agent-assertion | Ký và trả về identity assertion cho người dùng đang đăng nhập | Có |
Endpoint phát assertion nằm ở backend của bạn. Mọi đường dẫn còn lại nằm ở PLATFORM_URL.
3. Luồng tổng quan
Người dùng (đã đăng nhập ở hệ thống của bạn)
│
▼
[1] Khung chat nhúng sẵn khởi động, hỏi danh tính
│
▼
[2] Site của bạn lấy assertion ──► GET {YOUR_APP_URL}/agent-assertion
│ (backend ký JWT RFC 7523 bằng client_secret)
▼ truyền { jwt, clientId } vào khung chat
┌─────────────────────────────────────────────────────────────┐
│ Khung chat tự làm phần còn lại: │
│ • POST {PLATFORM_URL}/api/oauth2/token (grant jwt-bearer) │
│ • hiển thị hội thoại, làm mới phiên khi cần │
└─────────────────────────────────────────────────────────────┘
client_secret chỉ tồn tại ở server; site chỉ giữ assertion đã ký nên không thể giả mạo danh tính. Access token / channel ID là định danh công khai, không tham gia xác thực.
4. Các bước tích hợp
Tạo Ứng dụng OAuth ở Portal
Vào Cài đặt → Cấu hình chung → Ứng dụng OAuth → Tạo ứng dụng, xác nhận mật khẩu, đặt tên. Hệ thống cấp:
| Trường | Mô tả | Lưu trữ |
|---|---|---|
client_id | Định danh ứng dụng OAuth (công khai, dạng pc_…) | Có thể để ở FE |
client_secret | Khoá ký assertion — hiện một lần; xem lại / đổi phải xác nhận mật khẩu | Chỉ ở server |
Trên môi trường Development, Simplize cấp và gửi cặp client_id / client_secret cho bạn.
Cấu hình ở hệ thống của bạn
Lưu client_secret ở server (biến môi trường hoặc secret manager) — không commit, không đưa xuống FE. Lưu kèm PLATFORM_URL, client_id, và access token / channel ID (công khai).
Phát identity assertion (RFC 7523)
Tạo một endpoint nội bộ (yêu cầu đăng nhập) ký một JWT HS256 bằng client_secret với đúng các claim sau:
| Claim | Giá trị | Bắt buộc |
|---|---|---|
iss | client_id của bạn | Có |
sub | Mã người dùng trong hệ thống của bạn (ổn định, không đổi) | Có |
aud | {PLATFORM_URL}/api/oauth2/token | Có |
jti | Chuỗi ngẫu nhiên duy nhất — nền tảng đốt sau lần dùng đầu | Có |
iat, exp | Phát lúc nào, hết hạn khi nào — khuyến nghị 5 phút, tối đa 1 giờ | Có |
email | Địa chỉ của người dùng — dùng làm tên đăng nhập lần đầu | Nên có |
name | Tên hiển thị | Nên có |
{
"iss": "pc_your_client_id",
"sub": "45",
"aud": "https://app.simplize.dev/api/oauth2/token",
"jti": "5f2c9a1e-…",
"iat": 1700000000,
"exp": 1700000300,
"email": "user@example.com",
"name": "Nguyễn Văn A"
}
- Node.js
- Python
- Java (jjwt)
const jwt = require('jsonwebtoken');
const { randomUUID } = require('crypto');
const PLATFORM_URL = process.env.PLATFORM_URL; // https://app.simplize.dev
app.get('/agent-assertion', requireLogin, (req, res) => {
const token = jwt.sign(
{ sub: String(req.user.id), email: req.user.email, name: req.user.name },
process.env.CLIENT_SECRET,
{
algorithm: 'HS256',
issuer: process.env.CLIENT_ID,
audience: `${PLATFORM_URL}/api/oauth2/token`,
jwtid: randomUUID(),
expiresIn: '5m',
},
);
res.json({ jwt: token, clientId: process.env.CLIENT_ID });
});
import os, time, uuid, jwt
PLATFORM_URL = os.environ["PLATFORM_URL"] # https://app.simplize.dev
@app.get("/agent-assertion")
@login_required
def agent_assertion():
now = int(time.time())
token = jwt.encode(
{"iss": os.environ["CLIENT_ID"], "sub": str(current_user.id),
"aud": f"{PLATFORM_URL}/api/oauth2/token", "jti": uuid.uuid4().hex,
"iat": now, "exp": now + 300,
"email": current_user.email, "name": current_user.name},
os.environ["CLIENT_SECRET"], algorithm="HS256",
)
return {"jwt": token, "clientId": os.environ["CLIENT_ID"]}
@GetMapping("/agent-assertion")
public Map<String, String> assertion(@AuthenticationPrincipal User user) {
String platformUrl = System.getenv("PLATFORM_URL"); // https://app.simplize.dev
String token = Jwts.builder()
.issuer(System.getenv("CLIENT_ID"))
.subject(String.valueOf(user.getId()))
.audience().add(platformUrl + "/api/oauth2/token").and()
.id(UUID.randomUUID().toString())
.claim("email", user.getEmail())
.claim("name", user.getName())
.issuedAt(new Date())
.expiration(new Date(System.currentTimeMillis() + 300_000))
.signWith(Keys.hmacShaKeyFor(System.getenv("CLIENT_SECRET").getBytes(StandardCharsets.UTF_8)))
.compact();
return Map.of("jwt", token, "clientId", System.getenv("CLIENT_ID"));
}
Nếu không muốn chia sẻ bí mật, khai jwks_uri trên Ứng dụng OAuth (phương thức private_key_jwt) và ký assertion RS256 bằng khoá riêng của bạn — nền tảng xác minh bằng khoá công khai bạn công bố. Các claim giữ nguyên.
Truyền assertion vào khung chat
Đưa { jwt, clientId } cho khung chat nhúng sẵn — khung chat tự đổi lấy phiên. Cách truyền cho bong bóng, iframe và live chat xem tại Nhúng vào trang web → Tự động đăng nhập.
5. Tích hợp trực tiếp qua API (giao diện tự dựng)
Nếu bạn không dùng khung nhúng sẵn, đổi assertion lấy phiên rồi gọi API bằng access_token.
5.1 Đổi assertion lấy phiên
POST {PLATFORM_URL}/api/oauth2/token — thân application/x-www-form-urlencoded (JSON cũng được nhận). Assertion là chứng thực của client (RFC 7523 §2.1), không cần gửi client_secret — nên gọi được từ trình duyệt.
curl -X POST https://app.simplize.dev/api/oauth2/token \
-d 'grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer' \
-d 'client_id=pc_your_client_id' \
-d 'assertion=<JWT do backend của bạn ký>' \
-d 'scope=openid email'
Trả về theo RFC 6749 §5.1:
{
"access_token": "eyJ…",
"token_type": "Bearer",
"expires_in": 900,
"refresh_token": "5ca4…",
"scope": "openid email",
"id_token": "eyJ…"
}
access_tokensống 15 phút;refresh_tokenxoay vòng mỗi lần làm mới.id_tokenlà JWT RS256 — xác minh bằng{PLATFORM_URL}/.well-known/jwks.json,aud=client_idcủa bạn,sub= mã người dùng trên nền tảng.- Cùng một
subtrong assertion luôn về cùng một tài khoản; lần đầu thấysubmới, nền tảng mở tài khoản khách vớiemail/nametrong assertion.
// Trình duyệt: lấy assertion từ backend của bạn rồi đổi lấy phiên
const { jwt, clientId } = await fetch('/agent-assertion', { credentials: 'include' }).then((r) => r.json());
const session = await fetch(`${PLATFORM_URL}/api/oauth2/token`, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
client_id: clientId,
assertion: jwt,
scope: 'openid email',
}),
}).then((r) => r.json());
// session.access_token → header Authorization: Bearer …
5.2 Làm mới phiên
Từ trình duyệt (không có client_secret):
curl -X POST https://app.simplize.dev/api/auth/refresh \
-H 'Content-Type: application/json' \
-d '{"refresh_token": "<refresh_token>"}'
Từ server (có client_secret) có thể dùng grant chuẩn: POST /api/oauth2/token với grant_type=refresh_token&client_id=…&client_secret=…&refresh_token=….
Cả hai trả access_token mới kèm refresh_token mới. Refresh token cũ bị thu hồi ngay khi xoay; dùng lại token đã xoay sẽ huỷ mọi phiên của người đó (dấu hiệu bị đánh cắp).
5.3 Người dùng là ai
curl https://app.simplize.dev/api/oauth2/userinfo -H 'Authorization: Bearer <access_token>'
Trả sub, email, name theo scope đã xin.
5.4 Đăng xuất
curl -X POST https://app.simplize.dev/api/auth/logout -H 'Authorization: Bearer <access_token>'
5.5 Gọi khung chat AI bằng phiên
Các endpoint {PLATFORM_URL}/chat-app/{token}/… (parameters, conversations, messages, chat-messages) nhận Authorization: Bearer <access_token> — người dùng vào đúng danh tính của họ. Chi tiết endpoint chat sẽ được bổ sung.
6. Bảo mật
client_secretchỉ ở server — không đưa xuống FE dưới bất kỳ dạng nào.client_id, access token, channel ID là định danh công khai — lộ cũng không thể ký giả assertion.- Assertion:
expngắn (5 phút),jtiduy nhất — nền tảng từ chối dùng lại cùng một assertion;audphải là đúng token endpoint của nền tảng. - Endpoint phát assertion phải yêu cầu đăng nhập — mỗi người chỉ phát assertion cho chính mình;
subphải là mã ổn định, đừng dùng email làmsubnếu người dùng có thể đổi email. - Nền tảng không tự nối một tài khoản có sẵn vào assertion chỉ vì trùng email: nếu
emailđã thuộc một tài khoản đăng ký theo cách khác, lần đầu sẽ bị từ chối — hãy dùng địa chỉ mà chỉ hệ thống của bạn xác nhận.
7. Mã lỗi
Lỗi trả theo RFC 6749 §5.2: {"error": "...", "error_description": "..."}.
| HTTP | error | Ý nghĩa | Cách xử lý |
|---|---|---|---|
| 401 | invalid_client | client_id không tồn tại, bị tắt, hoặc IP không được phép | Kiểm tra client_id; danh sách IP trên Ứng dụng OAuth |
| 400 | invalid_grant | Assertion sai chữ ký, hết hạn, sai aud/iss, jti đã dùng, hoặc sống quá 1 giờ | Ký assertion mới, đúng claim; không dùng lại jti |
| 400 | unsupported_grant_type | grant_type không đúng chuỗi | Dùng urn:ietf:params:oauth:grant-type:jwt-bearer |
| 400 | invalid_scope | Scope xin không nằm trong scope của Ứng dụng OAuth | Xin đúng scope đã khai |
| 404 | — | Ứng dụng chat chưa bật chia sẻ hoặc chưa publish | Bật chia sẻ và publish ứng dụng |