Giới thiệu về iKame SSO
iKame SSO là một dịch vụ Single Sign-On tập trung, cho phép người dùng đăng nhập một lần và truy cập nhiều service khác nhau trong hệ sinh thái iKame mà không cần đăng nhập lại.
SSO Service là gì?
Single Sign-On (SSO) là một phương thức xác thực cho phép người dùng đăng nhập một lần với một bộ credentials và được tự động đăng nhập vào nhiều ứng dụng hoặc website khác nhau. iKame SSO sử dụng Keycloak và Google OAuth để cung cấp giải pháp SSO an toàn và dễ sử dụng.
Lợi ích của SSO
- Single Sign-On: Đăng nhập một lần, sử dụng nhiều service - Người dùng chỉ cần đăng nhập một lần với Google và có thể truy cập tất cả các service trong hệ sinh thái iKame.
- Bảo mật tập trung: Tất cả việc xác thực được quản lý tập trung qua Keycloak, giảm thiểu rủi ro bảo mật và dễ dàng cập nhật chính sách bảo mật.
- Giảm thiểu quản lý credentials: Không cần lưu trữ và quản lý mật khẩu riêng cho từng service, giảm gánh nặng cho cả người dùng và developers.
- Trải nghiệm người dùng tốt hơn: Người dùng không cần nhớ nhiều tài khoản và mật khẩu, quá trình đăng nhập nhanh chóng và thuận tiện.
- Dễ dàng tích hợp: Chỉ cần redirect user đến SSO endpoint và nhận token từ callback, không cần cấu hình phức tạp.
- Giảm chi phí bảo trì: Quản lý tập trung giúp giảm chi phí bảo trì và cập nhật hệ thống xác thực.
Flow tổng quan
Khi người dùng cần đăng nhập, service sẽ redirect họ đến SSO endpoint. Sau khi đăng nhập thành công với Google qua Keycloak, SSO sẽ redirect về service kèm theo access token để service có thể xác thực người dùng.
Quick Start
Đây là cách nhanh nhất để tích hợp SSO vào service của bạn:
Bước 1: Redirect user đến SSO
Khi user cần đăng nhập, redirect họ đến SSO endpoint với tham số urlDirect:
// Redirect user to SSO login
const serviceUrl = 'https://your-service.com/dashboard';
const ssoUrl = `https://sso.ikameglobal.com/?urlDirect=${encodeURIComponent(serviceUrl)}`;
window.location.href = ssoUrl;
Bước 2: Nhận token từ callback
Sau khi user đăng nhập thành công, SSO sẽ redirect về service của bạn với token trong hash fragment:
// Get ssoToken from URL hash fragment
const hashParams = new URLSearchParams(window.location.hash.substring(1));
const ssoToken = hashParams.get('ssoToken');
if (ssoToken) {
// Store ssoToken and use it for authentication
localStorage.setItem('authToken', ssoToken);
// Remove hash from URL
window.history.replaceState({}, '', window.location.pathname + window.location.search);
// Redirect to your app or make authenticated requests
}
Hướng dẫn tích hợp chi tiết
Hướng dẫn từng bước để tích hợp SSO vào service của bạn:
Bước 1: Redirect user đến SSO endpoint
Khi user cần đăng nhập hoặc chưa được xác thực, redirect họ đến:
https://sso.ikameglobal.com/?urlDirect={your_service_url}
urlDirect là URL mà SSO sẽ redirect về sau khi login thành công. URL này phải được encode đúng cách.
Bước 2: User login với Google
User sẽ được redirect đến trang login SSO, sau đó chọn "Sign in with Google". Quá trình xác thực được xử lý bởi Keycloak và Google OAuth.
Bước 3: Nhận token từ callback URL
Sau khi login thành công, SSO sẽ redirect về urlDirect với token trong hash fragment:
{your_service_url}#ssoToken={access_token}
Bước 4: Validate và sử dụng token
Service của bạn cần validate token (tốt nhất là trên server-side) và sử dụng token để xác thực các request tiếp theo.
Flow diagram chi tiết
Ví dụ code
Dưới đây là các ví dụ code cho các ngôn ngữ và framework phổ biến:
JavaScript / HTML (Client-side)
// Check if user is authenticated
function checkAuth() {
const ssoToken = localStorage.getItem('authToken');
if (!ssoToken) {
// Redirect to SSO login
const currentUrl = window.location.href;
const ssoUrl = `https://sso.ikameglobal.com/?urlDirect=${encodeURIComponent(currentUrl)}`;
window.location.href = ssoUrl;
return false;
}
return true;
}
// Handle callback from SSO
function handleSSOCallback() {
// Read ssoToken from hash fragment
const hashParams = new URLSearchParams(window.location.hash.substring(1));
const ssoToken = hashParams.get('ssoToken');
if (ssoToken) {
// Store ssoToken
localStorage.setItem('authToken', ssoToken);
// Remove hash fragment from URL
window.history.replaceState({}, '', window.location.pathname + window.location.search);
// Continue with authenticated flow
loadUserData(ssoToken);
}
}
// Call on page load
if (window.location.hash.includes('ssoToken=')) {
handleSSOCallback();
} else if (!checkAuth()) {
// Will redirect to SSO
}
PHP (Server-side)
<?php
// Check authentication
session_start();
function checkAuth() {
if (!isset($_SESSION['auth_token'])) {
// Redirect to SSO login
$currentUrl = (isset($_SERVER['HTTPS']) ? "https" : "http") .
"://" . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
$ssoUrl = "https://sso.ikameglobal.com/?urlDirect=" . urlencode($currentUrl);
header("Location: " . $ssoUrl);
exit();
}
return true;
}
// Handle SSO callback via AJAX (called from JavaScript)
if (isset($_POST['ssoToken'])) {
$ssoToken = $_POST['ssoToken'];
// Validate ssoToken (call to your auth service or Keycloak)
if (validateToken($ssoToken)) {
$_SESSION['auth_token'] = $ssoToken;
echo json_encode(['success' => true]);
exit();
} else {
echo json_encode(['success' => false, 'error' => 'Invalid token']);
exit();
}
}
function validateToken($ssoToken) {
// TODO: Implement ssoToken validation
// This should call Keycloak or your auth service to verify the ssoToken
return true; // Placeholder
}
// Use in your protected pages
checkAuth();
?>
<script>
// Read ssoToken from hash fragment and send to server
(function() {
const hashParams = new URLSearchParams(window.location.hash.substring(1));
const ssoToken = hashParams.get('ssoToken');
if (ssoToken) {
// Remove hash from URL
window.history.replaceState({}, '', window.location.pathname + window.location.search);
// Send token to server via AJAX
fetch('', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'ssoToken=' + encodeURIComponent(ssoToken)
}).then(response => response.json())
.then(data => {
if (data.success) {
// Reload page to continue with authenticated session
window.location.reload();
} else {
// Invalid token, redirect to login
window.location.href = '/login';
}
});
}
})();
</script>
Python (Flask)
from flask import Flask, redirect, request, session, url_for, render_template, jsonify
from urllib.parse import urlencode
import requests
app = Flask(__name__)
app.secret_key = 'your-secret-key'
SSO_URL = 'https://sso.ikameglobal.com'
def check_auth():
"""Check if user is authenticated"""
if 'auth_token' not in session:
# Redirect to SSO login
current_url = request.url
sso_url = f"{SSO_URL}/?urlDirect={urlencode({'urlDirect': current_url})}"
return redirect(sso_url)
return None
@app.route('/callback', methods=['POST'])
def sso_callback():
"""Handle SSO callback via AJAX"""
ssoToken = request.form.get('ssoToken')
if ssoToken:
# Validate ssoToken
if validate_token(ssoToken):
session['auth_token'] = ssoToken
return jsonify({'success': True})
else:
return jsonify({'success': False, 'error': 'Invalid token'})
return jsonify({'success': False, 'error': 'No token'})
def validate_token(ssoToken):
"""Validate ssoToken with Keycloak"""
# TODO: Implement ssoToken validation
# Call Keycloak introspection endpoint or verify JWT
return True # Placeholder
@app.route('/dashboard')
def dashboard():
"""Protected route"""
auth_check = check_auth()
if auth_check:
return auth_check
return render_template('dashboard.html')
if __name__ == '__main__':
app.run(debug=True)
// Read ssoToken from hash fragment and send to server
(function() {
const hashParams = new URLSearchParams(window.location.hash.substring(1));
const ssoToken = hashParams.get('ssoToken');
if (ssoToken) {
// Remove hash from URL
window.history.replaceState({}, '', window.location.pathname + window.location.search);
// Send token to server via AJAX
fetch('/callback', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'ssoToken=' + encodeURIComponent(ssoToken)
}).then(response => response.json())
.then(data => {
if (data.success) {
// Reload page to continue with authenticated session
window.location.reload();
} else {
// Invalid token, redirect to login
window.location.href = '/login';
}
});
}
})();
Node.js / Express
const express = require('express');
const session = require('express-session');
const app = express();
const SSO_URL = 'https://sso.ikameglobal.com';
app.use(session({
secret: 'your-secret-key',
resave: false,
saveUninitialized: false
}));
// Middleware to check authentication
function checkAuth(req, res, next) {
if (!req.session.authToken) {
const currentUrl = `${req.protocol}://${req.get('host')}${req.originalUrl}`;
const ssoUrl = `${SSO_URL}/?urlDirect=${encodeURIComponent(currentUrl)}`;
return res.redirect(ssoUrl);
}
next();
}
// Handle SSO callback via POST (called from client-side JavaScript)
app.post('/callback', express.urlencoded({ extended: true }), (req, res) => {
const ssoToken = req.body.ssoToken;
if (ssoToken) {
// Validate ssoToken
if (validateToken(ssoToken)) {
req.session.authToken = ssoToken;
return res.json({ success: true });
}
}
res.json({ success: false, error: 'Invalid token' });
});
function validateToken(ssoToken) {
// TODO: Implement ssoToken validation
// Call Keycloak or verify JWT
return true; // Placeholder
}
// Protected route
app.get('/dashboard', checkAuth, (req, res) => {
res.send(`
<html>
<head><title>Dashboard</title></head>
<body>
<h1>Welcome to Dashboard!</h1>
<script>
// Read ssoToken from hash fragment and send to server
(function() {
const hashParams = new URLSearchParams(window.location.hash.substring(1));
const ssoToken = hashParams.get('ssoToken');
if (ssoToken) {
// Remove hash from URL
window.history.replaceState({}, '', window.location.pathname + window.location.search);
// Send token to server via AJAX
fetch('/callback', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'ssoToken=' + encodeURIComponent(ssoToken)
}).then(response => response.json())
.then(data => {
if (data.success) {
// Reload page to continue with authenticated session
window.location.reload();
} else {
// Invalid token, redirect to login
window.location.href = '/login';
}
});
}
})();
</script>
</body>
</html>
`);
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
React
import React, { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
const SSO_URL = 'https://sso.ikameglobal.com';
function App() {
const [authenticated, setAuthenticated] = useState(false);
const navigate = useNavigate();
useEffect(() => {
// Handle SSO callback - read from hash fragment
const hash = window.location.hash.substring(1);
const hashParams = new URLSearchParams(hash);
const ssoToken = hashParams.get('ssoToken');
if (ssoToken) {
localStorage.setItem('authToken', ssoToken);
setAuthenticated(true);
// Remove hash fragment from URL
navigate(window.location.pathname + window.location.search, { replace: true });
} else {
// Check existing ssoToken
const storedToken = localStorage.getItem('authToken');
if (storedToken) {
setAuthenticated(true);
} else {
// Redirect to SSO
const currentUrl = window.location.href;
const ssoUrl = `${SSO_URL}/?urlDirect=${encodeURIComponent(currentUrl)}`;
window.location.href = ssoUrl;
}
}
}, [navigate]);
if (!authenticated) {
return Redirecting to login...;
}
return (
Welcome!
);
}
export default App;
Xử lý Token
Token được trả về từ SSO là một JWT (JSON Web Token) access token. Dưới đây là cách xử lý token:
Token Format
Token là một JWT có cấu trúc: header.payload.signature
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ...
[header].[payload].[signature]
Validate Token
Bạn nên validate token trên server-side để đảm bảo bảo mật. Có 2 cách chính:
1. Token Introspection (Recommended)
Gọi Keycloak introspection endpoint để verify token:
curl -X POST \
https://keycloak.ikameglobal.com/auth/realms/ikame-platform/protocol/openid-connect/token/introspect \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'client_id=your-client-id&client_secret=your-secret&token=YOUR_SSO_TOKEN'
2. Verify JWT Signature
Decode và verify JWT signature với Keycloak public key:
const jwt = require('jsonwebtoken');
const jwksClient = require('jwks-rsa');
const client = jwksClient({
jwksUri: 'https://keycloak.ikameglobal.com/auth/realms/ikame-platform/protocol/openid-connect/certs'
});
function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
const signingKey = key.publicKey || key.rsaPublicKey;
callback(null, signingKey);
});
}
function verifyToken(ssoToken) {
return new Promise((resolve, reject) => {
jwt.verify(ssoToken, getKey, {
audience: 'ikame-sso',
issuer: 'https://keycloak.ikameglobal.com/auth/realms/ikame-platform'
}, (err, decoded) => {
if (err) {
reject(err);
} else {
resolve(decoded);
}
});
});
}
Token Expiration
Token có thời gian hết hạn. Bạn cần kiểm tra expiration time trong token payload và handle token refresh nếu cần.
Decode Token (Client-side only)
Để decode token trên client-side (chỉ để xem thông tin, không verify):
function decodeToken(ssoToken) {
const base64Url = ssoToken.split('.')[1];
const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/');
const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2);
}).join(''));
return JSON.parse(jsonPayload);
}
const ssoToken = localStorage.getItem('authToken');
const decoded = decodeToken(ssoToken);
console.log('SSO Token expires at:', new Date(decoded.exp * 1000));
Xử lý lỗi
Dưới đây là các lỗi thường gặp và cách xử lý:
Thiếu urlDirect parameter
Nếu không có urlDirect trong query parameter, SSO sẽ hiển thị error message và không thể redirect về service.
https://sso.ikameglobal.com/?urlDirect={your_url}
Không đọc được token từ hash fragment
Token được truyền qua hash fragment, không phải query parameter. Bạn cần đọc bằng JavaScript:
// Correct way to read token from hash fragment
const hashParams = new URLSearchParams(window.location.hash.substring(1));
const ssoToken = hashParams.get('ssoToken');
// Wrong way (will not work):
// const urlParams = new URLSearchParams(window.location.search);
// const ssoToken = urlParams.get('ssoToken');
Login failed
User có thể cancel login hoặc có lỗi trong quá trình xác thực.
// Check for error in callback URL
const urlParams = new URLSearchParams(window.location.search);
const error = urlParams.get('error');
if (error) {
console.error('SSO Error:', error);
// Handle error (show message, redirect to login page, etc.)
alert('Login failed. Please try again.');
}
Invalid token
Token có thể không hợp lệ hoặc đã hết hạn.
async function validateToken(ssoToken) {
try {
const response = await fetch('https://keycloak.ikameglobal.com/auth/realms/ikame-platform/protocol/openid-connect/token/introspect', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams({
client_id: 'your-client-id',
client_secret: 'your-secret',
token: ssoToken
})
});
const data = await response.json();
if (data.active) {
return true; // SSO Token is valid
} else {
// SSO Token is invalid or expired
localStorage.removeItem('authToken');
return false;
}
} catch (error) {
console.error('SSO Token validation error:', error);
return false;
}
}
Network errors
Xử lý các lỗi network khi gọi API:
try {
const ssoToken = localStorage.getItem('authToken');
const response = await fetch('/api/protected-endpoint', {
headers: {
'Authorization': `Bearer ${ssoToken}`
}
});
if (!response.ok) {
if (response.status === 401) {
// SSO Token expired or invalid, redirect to SSO
localStorage.removeItem('authToken');
redirectToSSO();
}
throw new Error('Request failed');
}
} catch (error) {
if (error.name === 'TypeError' && error.message.includes('fetch')) {
// Network error
console.error('Network error:', error);
alert('Network error. Please check your connection.');
}
}
Security Best Practices
Các best practices để đảm bảo bảo mật khi tích hợp SSO:
1. Validate token trên server-side
Luôn validate token trên server-side. Không bao giờ chỉ dựa vào client-side validation.
2. Sử dụng HTTPS only
Đảm bảo tất cả communication đều qua HTTPS để bảo vệ token khỏi man-in-the-middle attacks.
3. Token storage
- Client-side: Sử dụng localStorage hoặc sessionStorage. Không lưu trong cookies không secure.
- Server-side: Lưu trong session hoặc database, không expose trong logs.
- Không lưu token trong URL sau khi đã nhận được (remove từ URL ngay sau khi lưu).
4. CORS considerations
Nếu service của bạn cần gọi API từ browser, đảm bảo CORS được cấu hình đúng trên server.
5. Handle token expiration
Luôn kiểm tra token expiration và redirect user đến SSO để refresh token khi cần.
6. Logout handling
Khi user logout, clear token và có thể gọi Keycloak logout endpoint để invalidate session:
function logout() {
// Clear local ssoToken
localStorage.removeItem('authToken');
// Optional: Call Keycloak logout endpoint to invalidate session
window.location.href = 'https://keycloak.ikameglobal.com/auth/realms/ikame-platform/protocol/openid-connect/logout?redirect_uri=' + encodeURIComponent(window.location.origin);
}
API Reference
SSO Endpoint
| URL | https://sso.ikameglobal.com/ |
|---|---|
| Method | GET |
| Description | SSO login page endpoint |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
urlDirect |
string | Yes | URL to redirect to after successful login. Must be URL encoded. |
Callback Response
After successful login, SSO redirects to urlDirect with token in hash fragment:
{urlDirect}#ssoToken={access_token}
| Parameter | Type | Description |
|---|---|---|
ssoToken |
string | JWT access token for authentication (in hash fragment, not query parameter) |
Keycloak Endpoints
For token validation and other operations:
- Token Introspection:
https://keycloak.ikameglobal.com/auth/realms/ikame-platform/protocol/openid-connect/token/introspect - Public Keys:
https://keycloak.ikameglobal.com/auth/realms/ikame-platform/protocol/openid-connect/certs - Logout:
https://keycloak.ikameglobal.com/auth/realms/ikame-platform/protocol/openid-connect/logout
Troubleshooting
FAQ
Q: Token không hoạt động sau khi nhận được?
A: Đảm bảo bạn đã validate token trên server-side. Token có thể đã hết hạn hoặc không hợp lệ. Kiểm tra token expiration time và validate với Keycloak.
Q: Redirect loop xảy ra?
A: Kiểm tra logic check authentication. Đảm bảo bạn không redirect đến SSO nếu user đã có token hợp lệ. Kiểm tra token validation logic.
Q: CORS errors khi gọi API?
A: Đảm bảo server của bạn cấu hình CORS đúng cách để cho phép requests từ domain của bạn. Kiểm tra CORS headers trên server.
Q: Token bị expose trong URL?
A: Sau khi nhận token từ URL, lưu vào storage và remove token khỏi URL ngay lập tức để tránh expose trong browser history hoặc logs.
Common Issues
- Token expired: Implement token refresh logic hoặc redirect user đến SSO để login lại.
- Invalid redirect URL: Đảm bảo urlDirect được encode đúng cách và là valid URL.
- Session timeout: Keycloak session có thể timeout. User cần login lại.
Support
Nếu bạn gặp vấn đề không được giải quyết ở đây, vui lòng liên hệ support team.