Skip to content

Xác thực OpenStack và Token Management

Giới thiệu

Trong bài này, chúng ta sẽ tìm hiểu về OpenStack Keystone authentication và cách quản lý scoped tokens cho các requests đến OpenStack APIs.


OpenStack Keystone Authentication

Keystone là gì?

Keystone là OpenStack Identity Service, cung cấp:

  • Authentication (xác thực người dùng)
  • Authorization (phân quyền)
  • Service catalog (danh sách services và endpoints)
  • Token management (quản lý tokens)

Authentication Flow

1. Client → Keystone: POST /auth/tokens
   Body: {
     auth: {
       identity: { methods: ['password'], password: { user: {...} } },
       scope: { project: { id: projectId } }
     }
   }
   
2. Keystone → Client: Response
   Headers: {
     'X-Subject-Token': '<scoped-token>'
   }
   Body: { token: {...} }
   
3. Client → OpenStack Services (Nova, Glance):
   Headers: {
     'X-Auth-Token': '<scoped-token>'
   }

Scoped Token

Token Types

OpenStack hỗ trợ 2 loại tokens:

  1. Unscoped Token

    • Không gắn với project cụ thể
    • Chỉ dùng để list projects
  2. Scoped Token (Project-scoped)

    • Gắn với một project cụ thể
    • Dùng để access resources trong project đó
    • Chúng ta sử dụng loại này

Scoped Token Structure

typescript
{
  auth: {
    identity: {
      methods: ['password'],
      password: {
        user: {
          name: 'username',
          password: 'password',
          domain: { name: 'Default' }
        }
      }
    },
    scope: {
      project: {
        id: 'project-id'  // hoặc { name: 'project-name', domain: {...} }
      }
    }
  }
}

Implementation trong Portal

Environment Variables

Mỗi location (HCM/HNI) có bộ environment variables riêng:

bash
# HCM (Hồ Chí Minh)
OPENSTACK_ENDPOINT_HCM=https://keystone-hcm.inet.vn:5000
OPENSTACK_USERNAME_HCM=portal-user-hcm
OPENSTACK_PASSWORD_HCM=secret-password-hcm
OPENSTACK_PROJECT_ID_HCM=project-id-hcm
OPENSTACK_USER_DOMAIN_NAME_HCM=Default

# HNI (Hà Nội)
OPENSTACK_ENDPOINT_HN=https://keystone-hni.inet.vn:5000
OPENSTACK_USERNAME_HN=portal-user-hni
OPENSTACK_PASSWORD_HN=secret-password-hni
OPENSTACK_PROJECT_ID_HN=project-id-hni
OPENSTACK_USER_DOMAIN_NAME_HN=Default

OpenStack Environment Configuration

typescript
export type OpenStackEnv = {
  authUrl: string;      // Base endpoint + /identity/v3
  baseEndpoint: string; // Base endpoint (e.g., https://keystone.inet.vn:5000)
  username: string;
  password: string;
  userDomainName?: string;
  projectId?: string;
  projectName?: string;
  projectDomainName?: string;
  region: string;
  interfaceType: 'public' | 'internal' | 'admin';
};

export function getOpenStackEnv(location?: string): OpenStackEnv {
  // Normalize: undefined or empty defaults to HCM
  const normalizedLocation = location && location !== '' ? location : 'HCM';
  const isHanoi = normalizedLocation === 'HNI';
  
  // Get base endpoint from env
  const baseEndpoint = isHanoi 
    ? (process.env.OPENSTACK_ENDPOINT_HN || process.env.OPENSTACK_ENDPOINT || '')
    : (process.env.OPENSTACK_ENDPOINT_HCM || process.env.OPENSTACK_ENDPOINT || '');
  
  // Build authUrl from baseEndpoint
  const authUrl = baseEndpoint ? `${baseEndpoint.replace(/\/$/, '')}/identity/v3` : '';
  
  // ... other configs (username, password, projectId, etc.)
  
  return {
    authUrl,
    baseEndpoint,
    username,
    password,
    // ... other fields
  };
}

Get Scoped Token Function

typescript
async function getScopedToken(location?: string) {
  const env = getOpenStackEnv(location);
  
  const payload: any = {
    auth: {
      identity: {
        methods: ['password'],
        password: {
          user: {
            name: env.username,
            password: env.password,
            domain: env.userDomainName 
              ? { name: env.userDomainName } 
              : { name: 'Default' },
          },
        },
      },
      scope: env.projectId 
        ? { project: { id: env.projectId } }
        : { 
            project: { 
              name: env.projectName, 
              domain: env.projectDomainName 
                ? { name: env.projectDomainName } 
                : undefined 
            } 
          },
    },
  };
  
  const url = `${env.authUrl.replace(/\/$/, '')}/auth/tokens`;
  const client = createAxios();
  const res = await client.post(url, payload);
  
  const token = String(res.headers['x-subject-token'] || res.headers['X-Subject-Token'] || '');
  if (!token) throw new Error('Auth failed: missing X-Subject-Token');
  
  return { token };
}

Token Usage trong API Calls

typescript
export const createServerSnapshotOpenStack = async function (params: {
  serverId: string;
  snapshotName: string;
  location?: string;
}) {
  // 1. Get scoped token
  const { token } = await getScopedToken(location);
  
  // 2. Build endpoint
  const computeBase = buildEndpoint('compute', location);
  
  // 3. Create axios client with token
  const nova = createAxios(computeBase, { 'X-Auth-Token': token });
  
  // 4. Call Nova API
  const actionRes = await nova.post(
    `/servers/${encodeURIComponent(serverId)}/action`,
    {
      createImage: {
        name: snapshotName,
        metadata: { source: 'portal-manual' }
      }
    }
  );
  
  // ...
};

Token Lifecycle

Token Expiration

  • Default expiration: Thường là 1 giờ
  • Không cache tokens: Mỗi request lấy token mới (stateless)
  • Lý do: Đơn giản hóa, không cần refresh logic

Stateless Token Pattern

typescript
// ✅ DO: Get token per request (stateless)
async function callOpenStackAPI(location?: string) {
  const { token } = await getScopedToken(location);
  const client = createAxios(endpoint, { 'X-Auth-Token': token });
  return await client.get('/some-endpoint');
}

// ❌ DON'T: Cache tokens (có thể expired)
let cachedToken: string | null = null;
async function callOpenStackAPI() {
  if (!cachedToken) {
    cachedToken = await getScopedToken();
  }
  // Token có thể đã expired
}

Location-Based Authentication

Normalize Location

typescript
function normalizeLocation(location?: string): string {
  if (!location || location === '') return 'HCM';
  if (location === 'HNI') return 'HNI';
  return 'HCM'; // Default to HCM
}

Location Routing

typescript
export function getOpenStackEnv(location?: string): OpenStackEnv {
  const normalizedLocation = normalizeLocation(location);
  const isHanoi = normalizedLocation === 'HNI';
  
  // Get config based on location
  const baseEndpoint = isHanoi 
    ? process.env.OPENSTACK_ENDPOINT_HN
    : process.env.OPENSTACK_ENDPOINT_HCM;
  
  const username = isHanoi
    ? process.env.OPENSTACK_USERNAME_HN
    : process.env.OPENSTACK_USERNAME_HCM;
  
  // ... other configs
}

Usage Example

typescript
// HCM server
const { token } = await getScopedToken('HCM');
// Uses: OPENSTACK_ENDPOINT_HCM, OPENSTACK_USERNAME_HCM, etc.

// HNI server
const { token } = await getScopedToken('HNI');
// Uses: OPENSTACK_ENDPOINT_HN, OPENSTACK_USERNAME_HN, etc.

// Default to HCM
const { token } = await getScopedToken();
// Uses: OPENSTACK_ENDPOINT_HCM (default)

Error Handling

Common Errors

1. Missing Environment Variables

typescript
if (!baseEndpoint || !username || !password || (!projectId && !projectName)) {
  throw new Error(
    `Missing env for ${isHanoi ? 'Hà Nội' : 'HCM'}: ` +
    `require OPENSTACK_ENDPOINT, OPENSTACK_USERNAME, ` +
    `OPENSTACK_PASSWORD and OPENSTACK_PROJECT_ID or OPENSTACK_PROJECT_NAME`
  );
}

2. Authentication Failed

typescript
const token = String(res.headers['x-subject-token'] || '');
if (!token) {
  throw new Error('Auth failed: missing X-Subject-Token');
}

3. Invalid Credentials

OpenStack sẽ trả về HTTP 401 với error message. Cần log và handle:

typescript
try {
  const res = await client.post(url, payload);
  // ...
} catch (error: any) {
  if (error.response?.status === 401) {
    logger.error('❌ OpenStack authentication failed - invalid credentials');
    throw new Error('Invalid OpenStack credentials');
  }
  throw error;
}

Best Practices

1. Always Normalize Location

typescript
// ✅ DO
const normalizedLocation = normalizeLocation(location);
const { token } = await getScopedToken(normalizedLocation);

// ❌ DON'T
const { token } = await getScopedToken(location); // Location có thể undefined

2. Handle Token Errors Gracefully

typescript
try {
  const { token } = await getScopedToken(location);
  // ...
} catch (error: any) {
  logger.error(`❌ Failed to get OpenStack token: ${error.message}`);
  throw new Error('Failed to authenticate with OpenStack');
}

3. Use Environment Variables Correctly

typescript
// ✅ DO: Use location-specific env vars
const baseEndpoint = isHanoi 
  ? process.env.OPENSTACK_ENDPOINT_HN
  : process.env.OPENSTACK_ENDPOINT_HCM;

// ❌ DON'T: Mix location configs
const baseEndpoint = process.env.OPENSTACK_ENDPOINT; // Generic, no location distinction

4. Log Authentication Attempts

typescript
logger.info(`🔐 Getting OpenStack token for location: ${normalizedLocation}`);
const { token } = await getScopedToken(normalizedLocation);
logger.info(`✅ OpenStack token obtained (length: ${token.length})`);

Testing Authentication

Unit Test Example

typescript
describe('getScopedToken', () => {
  it('should get token for HCM location', async () => {
    process.env.OPENSTACK_ENDPOINT_HCM = 'https://keystone-hcm.inet.vn:5000';
    process.env.OPENSTACK_USERNAME_HCM = 'test-user';
    process.env.OPENSTACK_PASSWORD_HCM = 'test-password';
    process.env.OPENSTACK_PROJECT_ID_HCM = 'test-project-id';
    
    const { token } = await getScopedToken('HCM');
    
    expect(token).toBeDefined();
    expect(token.length).toBeGreaterThan(0);
  });
  
  it('should default to HCM if location is empty', async () => {
    const { token } = await getScopedToken('');
    // Should use HCM config
    expect(token).toBeDefined();
  });
});

Manual Testing

bash
# Test HCM authentication
curl -X POST https://keystone-hcm.inet.vn:5000/identity/v3/auth/tokens \
  -H "Content-Type: application/json" \
  -d '{
    "auth": {
      "identity": {
        "methods": ["password"],
        "password": {
          "user": {
            "name": "username",
            "password": "password",
            "domain": { "name": "Default" }
          }
        }
      },
      "scope": {
        "project": {
          "id": "project-id"
        }
      }
    }
  }'

# Response headers should contain X-Subject-Token

Summary

Key Points

  1. Scoped Token Pattern

    • Mỗi request lấy token mới (stateless)
    • Token trong header X-Auth-Token
    • Token có expiration (thường 1 giờ)
  2. Location-Based Config

    • Mỗi location (HCM/HNI) có env vars riêng
    • Normalize location trước khi dùng
    • Default to HCM nếu không chỉ định
  3. Error Handling

    • Check missing env vars
    • Handle authentication failures
    • Log errors để debug
  4. Best Practices

    • Always normalize location
    • Use location-specific env vars
    • Log authentication attempts
    • Handle errors gracefully

Next Steps

Trong bài tiếp theo, chúng ta sẽ tìm hiểu về:


Last Updated: 2025-01-25
Previous: 01. OpenStack Overview
Next: 03. Snapshot & Backup

Internal documentation for iNET Portal