Chuyển đến nội dung chính

Bài 14: Organizations - Multi-tenancy và CIAM

Bật và cấu hình Organizations feature, tạo/quản lý organizations, organization domains, organization attributes, quản lý members (managed, unmanaged), invitation management (gửi, theo dõi, resend, xóa), liên kết Identity Providers với organizations, authenticating members (identity-first login), mapping organization claims vào tokens và B2B/B2B2C use cases thực tế.

🔒 DevSecOps — Bài 14 Bài 14: Organizations - Multi-tenancy và CIAM

Keycloak từ Cơ bản đến Nâng cao

Phần 4: User Federation, Organizations và Authorization

xdev.asia

1. Organizations — Tổng quan

Keycloak Organizations là tính năng cho phép quản lý multi-tenancy trong một realm duy nhất. Thay vì tạo nhiều realms cho từng tổ chức (tenant), bạn có thể tạo Organizations bên trong một realm để nhóm users, quản lý domains, và kiểm soát truy cập theo tổ chức.

Đây là tính năng quan trọng cho các nền tảng B2B (Business-to-Business) và B2B2C (Business-to-Business-to-Consumer), còn gọi là CIAM (Customer Identity and Access Management).

1.1 Use Cases

ScenarioMô tả
SaaS Multi-tenantMỗi công ty khách hàng là một organization, users thuộc về organization của họ
B2B PortalĐối tác/vendor có organization riêng, nhân viên đối tác truy cập portal qua organization
Enterprise with subsidiariesTập đoàn có nhiều công ty con, mỗi công ty con là một organization
Educational platformMỗi trường/viện là một organization, giáo viên/sinh viên là members

2. Bật Organizations Feature

Organizations là tính năng có sẵn trong Keycloak 25+. Để bật:

2.1 Bật trên Realm

# Qua Admin Console:
# Realm Settings → General → Organizations → Enabled

# Qua kcadm.sh:
kcadm.sh update realms/my-realm \
  -s organizationsEnabled=true

2.2 Kiểm tra trạng thái

# Qua REST API
curl -s "http://localhost:8080/admin/realms/my-realm" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.organizationsEnabled'
# Output: true

3. Tạo và quản lý Organizations

3.1 Tạo Organization qua Admin Console

Vào Admin Console → Organizations → Create organization:

FieldMô tảVí dụ
NameTên organization (bắt buộc)Acme Corporation
AliasAlias duy nhất, tự sinh từ nameacme-corporation
DescriptionMô tả tổ chứcAcme Corp - Enterprise customer
Redirect URLURL redirect sau khi member đăng nhậphttps://app.acme.com

3.2 Tạo Organization qua REST API

# Tạo organization
curl -X POST "http://localhost:8080/admin/realms/my-realm/organizations" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Corporation",
    "alias": "acme-corporation",
    "description": "Enterprise customer - Acme Corp",
    "redirectUrl": "https://app.acme.com",
    "enabled": true,
    "domains": [
      {
        "name": "acme.com",
        "verified": true
      }
    ],
    "attributes": {
      "plan": ["enterprise"],
      "industry": ["technology"],
      "region": ["asia-pacific"]
    }
  }'

# List organizations
curl -s "http://localhost:8080/admin/realms/my-realm/organizations" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.'

# Get organization by ID
ORG_ID="org-uuid-here"
curl -s "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.'

# Update organization
curl -X PUT "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Corporation (Updated)",
    "description": "Updated description",
    "enabled": true
  }'

# Delete organization
curl -X DELETE "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"

4. Organization Domains

Organization domains cho phép tự động liên kết users với organization dựa trên email domain. Khi user đăng ký hoặc đăng nhập với email thuộc domain đã đăng ký, Keycloak có thể tự động gắn user vào organization tương ứng.

# Thêm domain cho organization
curl -X POST "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}/domains" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "acme.com",
    "verified": true
  }'

# List domains
curl -s "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}/domains" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.'

Domain states:

StateMô tả
VerifiedDomain đã xác minh — users với email domain này tự động qualified cho organization
UnverifiedDomain chưa xác minh — chỉ dùng cho matching, cần admin approve

Lưu ý quan trọng:

  • Một domain chỉ thuộc về một organization duy nhất
  • Domain verification giúp đảm bảo organization thực sự sở hữu domain đó
  • Subdomain matching: acme.com sẽ match [email protected] nhưng không match [email protected]

5. Organization Attributes

Custom attributes cho phép lưu metadata bổ sung cho organizations:

# Set attributes khi tạo hoặc update organization
curl -X PUT "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Acme Corporation",
    "attributes": {
      "plan": ["enterprise"],
      "max_users": ["500"],
      "contract_end_date": ["2027-12-31"],
      "sla_tier": ["platinum"],
      "billing_email": ["[email protected]"]
    }
  }'

Attributes có thể được sử dụng trong:

  • Token claims — thêm organization metadata vào access/ID tokens
  • Authorization policies — phân quyền dựa trên attributes
  • Custom logic — xử lý business logic trong application

6. Quản lý Members

6.1 Managed vs Unmanaged Members

TypeMô tảVí dụ
ManagedUser được organization quản lý hoàn toàn — lifecycle bị ràng buộc với organizationNhân viên công ty: khi rời organization, tài khoản bị vô hiệu hóa
UnmanagedUser tự do, chỉ "tham gia" organization — tài khoản tồn tại độc lậpFreelancer, contractor: có thể thuộc nhiều organizations

6.2 Thêm existing users vào Organization

# Thêm user vào organization
USER_ID="user-uuid-here"
curl -X PUT "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}/members/${USER_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"

# List members của organization
curl -s "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}/members" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.[].username'

# Get membership info cho user
curl -s "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}/members/${USER_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.'

# Remove member
curl -X DELETE "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}/members/${USER_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"

6.3 Member Roles

Organization members có thể được gán roles trong context của organization:

RoleMô tả
memberRole mặc định — standard access
adminOrganization admin — có thể quản lý members và settings

7. Invitation Management

Keycloak Organizations hỗ trợ mời users tham gia organization qua email invitations.

7.1 Gửi Invitation

# Gửi invitation qua REST API
curl -X POST "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}/invitations" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{
    "email": "[email protected]",
    "firstName": "John",
    "lastName": "Doe",
    "redirectUrl": "https://app.example.com/welcome"
  }'

7.2 Invitation States

StateMô tả
PendingInvitation đã gửi, chưa được accept
ExpiredInvitation hết hạn (configurable expiration)

7.3 Quản lý Invitations

# List pending invitations
curl -s "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}/invitations" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.'

# Resend invitation
INV_ID="invitation-uuid-here"
curl -X POST "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}/invitations/${INV_ID}/resend" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"

# Delete/cancel invitation
curl -X DELETE "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}/invitations/${INV_ID}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"

Email template: Keycloak gửi email sử dụng template có thể customize tại Realm Settings → Email → Templates. Template mặc định bao gồm link để user accept invitation và tạo tài khoản (nếu chưa có).

8. Liên kết Identity Providers với Organizations

Mỗi organization có thể có Identity Provider riêng, cho phép members đăng nhập qua IdP của tổ chức họ (ví dụ: Acme Corp dùng Google Workspace, Beta Inc dùng Okta).

8.1 Liên kết IdP với Organization

# Liên kết Identity Provider với organization
IDP_ALIAS="acme-google-workspace"
curl -X POST "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}/identity-providers" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" \
  -H "Content-Type: application/json" \
  -d "\"${IDP_ALIAS}\""

# List linked Identity Providers
curl -s "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}/identity-providers" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}" | jq '.'

# Unlink Identity Provider
curl -X DELETE "http://localhost:8080/admin/realms/my-realm/organizations/${ORG_ID}/identity-providers/${IDP_ALIAS}" \
  -H "Authorization: Bearer ${ACCESS_TOKEN}"

8.2 Cách hoạt động

  1. User nhập email trên login page (identity-first login)
  2. Keycloak xác định email domain → tìm organization tương ứng
  3. Nếu organization có linked IdP → redirect user đến IdP của organization đó
  4. User authenticate với IdP của tổ chức
  5. Keycloak nhận response và tự động gắn user vào organization

9. Identity-First Login

Identity-first login là flow mà user nhập email trước, sau đó Keycloak quyết định authentication method phù hợp dựa trên organization membership.

9.1 Cấu hình Identity-First Login

Khi bật Organizations, Keycloak tự động tạo flow "organization" cho browser authentication. Flow này hoạt động như sau:

Organization Browser Flow
├── Cookie (Alternative)
├── Organization Identity-First (Alternative)
│   ├── Username Form (Required)           → User nhập email
│   └── Organization (Conditional)
│       ├── Condition - Organization Member → Kiểm tra user thuộc organization
│       └── Organization Identity Provider  → Redirect đến org's IdP
└── Forms (Alternative)
    ├── Username Password Form (Required)
    └── Conditional OTP (Conditional)

9.2 Cấu hình existing flow cho Organizations

# Bind Organization flow cho Browser
kcadm.sh update realms/my-realm \
  -s 'browserFlow=organization browser'

10. Mapping Organization Claims vào Tokens

Keycloak có thể thêm organization information vào access tokens và ID tokens, cho phép application biết user thuộc organization nào.

10.1 Organization Membership Mapper

Keycloak tự động thêm organization claim khi Organizations feature được bật. Claim format:

{
  "sub": "user-uuid",
  "email": "[email protected]",
  "organization": {
    "acme-corporation": {
      "name": "Acme Corporation",
      "roles": ["member"]
    }
  },
  "iss": "http://localhost:8080/realms/my-realm",
  "aud": "my-app"
}

10.2 Cấu hình Mapper tùy chỉnh

Bạn có thể thêm Organization Membership Protocol Mapper vào client scope để customize claim:

# Thêm Organization Membership Mapper vào client scope
kcadm.sh create clients/${CLIENT_ID}/protocol-mappers/models -r my-realm \
  -s name="organization-membership" \
  -s protocol=openid-connect \
  -s protocolMapper=oidc-organization-membership-mapper \
  -s 'config."claim.name"=organization' \
  -s 'config."id.token.claim"=true' \
  -s 'config."access.token.claim"=true' \
  -s 'config."userinfo.token.claim"=true'

10.3 Sử dụng Claims trong Application

// Ví dụ: Express.js middleware kiểm tra organization
import { Request, Response, NextFunction } from 'express';

interface OrganizationClaim {
  [alias: string]: {
    name: string;
    roles: string[];
  };
}

function requireOrganization(orgAlias: string) {
  return (req: Request, res: Response, next: NextFunction) => {
    const token = req.user; // decoded JWT
    const orgs: OrganizationClaim = token.organization || {};

    if (!orgs[orgAlias]) {
      return res.status(403).json({
        error: `User is not a member of organization: ${orgAlias}`
      });
    }

    // Attach org info to request
    req.organization = orgs[orgAlias];
    next();
  };
}

// Usage
app.get('/api/dashboard',
  requireOrganization('acme-corporation'),
  (req, res) => {
    res.json({ message: `Welcome to ${req.organization.name}` });
  }
);

11. B2B và B2B2C Use Cases

11.1 B2B Partner Portal

Scenario: Platform cung cấp portal cho partners

Realm: platform-realm
├── Organization: "Partner A" (partner-a.com)
│   ├── IdP: Partner A's Okta
│   ├── Members: 50 employees
│   └── Attributes: { plan: "gold", api_quota: "10000" }
├── Organization: "Partner B" (partner-b.com)
│   ├── IdP: Partner B's Azure AD
│   ├── Members: 200 employees
│   └── Attributes: { plan: "platinum", api_quota: "unlimited" }
└── Organization: "Partner C" (partner-c.com)
    ├── IdP: Partner C's Google Workspace
    ├── Members: 30 employees
    └── Attributes: { plan: "silver", api_quota: "5000" }

Flow:
1. Partner employee truy cập portal
2. Nhập email → Keycloak detect organization từ domain
3. Redirect đến IdP của partner
4. Authenticate → token chứa organization claim
5. Application phân quyền dựa trên organization + plan

11.2 B2B2C SaaS Platform

Scenario: SaaS platform bán cho businesses, businesses mời end-users

Realm: saas-realm
├── Organization: "School A"
│   ├── Domain: school-a.edu.vn
│   ├── Members (Managed): Teachers, Admin staff
│   ├── Members (Unmanaged): Students (self-registered)
│   └── IdP: School A's LDAP → federated via Keycloak IdP
├── Organization: "School B"
│   ├── Domain: school-b.edu.vn
│   ├── Members (Managed): Teachers
│   └── Members (Unmanaged): Students
└── Individual users (no organization)
    └── Free tier users

12. Best Practices

  • Dùng Organizations thay vì multi-realm — giảm overhead quản lý, chia sẻ configs
  • Verify domains — đảm bảo organization thực sự sở hữu domain trước khi auto-assign users
  • Phân biệt Managed/Unmanaged — managed cho employees cần lifecycle management, unmanaged cho external users
  • Sử dụng Identity-First Login — UX tốt hơn khi có nhiều organizations với different IdPs
  • Bảo mật Invitation links — set expiration time hợp lý, monitor pending invitations
  • Organization attributes cho authorization — dùng attributes để phân quyền theo plan, tier, region
  • Monitor member count — đặt alert khi organization vượt quá quota