@ts-core/openid-common
v1.0.54
Published
TypeScript library for OpenID Connect authentication with Keycloak support. Provides token validation, role/resource authorization, and automatic token refresh.
Downloads
2,453
Maintainers
Readme
@ts-core/openid-common
TypeScript библиотека для работы с OpenID Connect провайдерами (Keycloak и др.).
Содержание
Установка
npm install @ts-core/openid-commonЗависимости
@ts-core/common(~3.0.57)
Быстрый старт
Инициализация
import { KeycloakService, IKeycloakSettings } from '@ts-core/openid-common';
const settings: IKeycloakSettings = {
url: 'https://keycloak.example.com',
realm: 'my-realm',
clientId: 'my-client',
clientSecret: 'my-secret',
realmPublicKey: '-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----'
};
const service = new KeycloakService(settings);Получение токена по коду авторизации
const tokens = await service.getTokenByCode({
code: 'authorization-code-from-callback',
redirectUri: 'https://your-app.com/callback'
});
console.log('Access Token:', tokens.access);
console.log('Refresh Token:', tokens.refresh);Получение информации о пользователе
// Online (запрос к серверу)
const user = await service.getUserInfo(tokens.access);
// Offline (из токена)
const userOffline = await service.getUserInfo(tokens.access, true);
console.log('User ID:', user.sub);Валидация токена
// Online валидация (через introspection endpoint)
await service.validateToken(accessToken);
// Offline валидация (проверка подписи локально)
await service.validateToken(accessToken, {
publicKey: settings.realmPublicKey,
clientId: settings.clientId,
iss: `${settings.url}/realms/${settings.realm}`
});Проверка ролей
// Роли указываются в формате "type:role"
// realm:admin - роль уровня realm
// client:editor - роль уровня client (resource_access)
// Проверка одной роли
await service.validateRole(accessToken, { role: 'realm:admin' });
// Проверка нескольких ролей (все обязательны)
await service.validateRole(accessToken, { role: ['realm:admin', 'client:editor'] });
// Проверка любой из ролей (isAny: true)
await service.validateRole(accessToken, {
role: ['realm:admin', 'realm:moderator'],
isAny: true
});
// Проверка без исключения (возвращает boolean)
const isAdmin = await service.hasRole(accessToken, { role: 'realm:admin' });Проверка доступа к ресурсам
// Проверка доступа к ресурсу
await service.validateResource(accessToken, { name: 'api-resource' });
// Проверка конкретного scope
await service.validateResource(accessToken, {
name: 'api-resource',
scope: 'read'
});
// Проверка нескольких scopes
await service.validateResource(accessToken, {
name: 'api-resource',
scope: ['read', 'write']
});
// Любой из scopes (isAny: true)
await service.validateResource(accessToken, {
name: 'api-resource',
scope: ['read', 'write', 'delete'],
isAny: true
});
// Проверка без исключения
const hasAccess = await service.hasResourceScope(accessToken, {
name: 'api-resource',
scope: 'write'
});Получение доступных ресурсов
const resources = await service.getResources(accessToken);
// resources - это Map<string, IOpenIdResource>
resources.forEach((resource, name) => {
console.log(`Resource: ${name}`);
console.log(` ID: ${resource.id}`);
console.log(` Scopes: ${resource.scopes.join(', ')}`);
});Обновление токена
const newTokens = await service.getTokenByRefreshToken(tokens.refresh);
console.log('New Access Token:', newTokens.access);Выход из системы
await service.logoutByRefreshToken(tokens.refresh);API Reference
KeycloakService
Основной сервис для работы с Keycloak. Наследует абстрактный класс OpenIdService.
Конструктор
constructor(settings: IKeycloakSettings)Методы
| Метод | Описание |
|-------|----------|
| getUserInfo<T>(token, isOffline?) | Получение информации о пользователе |
| getTokenByCode<T>(code) | Обмен кода авторизации на токены |
| getTokenByRefreshToken<T>(token) | Обновление токенов |
| getResources(token, options?, claim?) | Получение доступных ресурсов |
| logoutByRefreshToken(token) | Завершение сессии |
| validateToken(token, options?) | Валидация токена (online/offline) |
| validateRole(token, options) | Проверка роли с исключением |
| validateResource(token, options) | Проверка ресурса с исключением |
| hasRole(token, options) | Проверка роли (boolean) |
| hasResourceScope(token, options) | Проверка ресурса (boolean) |
KeycloakClient
Низкоуровневый HTTP-клиент для работы с Keycloak API. Наследует DestroyableContainer.
const client = new KeycloakClient(accessToken, settings);
// Получение информации о пользователе
const user = await client.getUserInfo();
const userOffline = await client.getUserInfo(true);
// Токены
const tokens = await client.getTokenByCode(code);
const newTokens = await client.getTokenByRefreshToken(refreshToken);
// Ресурсы
const resources = await client.getResources(options, claim);
// Валидация
await client.validateToken(); // online
await client.validateToken(offlineOptions); // offline
await client.validateResource(options);
// Logout
await client.logoutByRefreshToken(refreshToken);KeycloakTokenManager
Менеджер токенов с автоматическим отслеживанием состояния. Наследует OpenIdTokenRefreshableManager.
import { KeycloakTokenManager } from '@ts-core/openid-common';
const manager = new KeycloakTokenManager();
// Установка токена
manager.value = { access: accessToken, refresh: refreshToken };
// Проверка состояния
console.log('Is Valid:', manager.isValid);
console.log('Is Expired:', manager.isExpired);
// Доступ к распарсенным токенам
const accessTokenParsed = manager.access; // KeycloakAccessToken
const refreshTokenParsed = manager.refresh; // KeycloakToken
// Подписка на изменения
manager.changed.subscribe(token => {
console.log('Token changed:', token);
});
// Очистка
manager.destroy();Свойства
| Свойство | Тип | Описание |
|----------|-----|----------|
| value | IOpenIdTokenRefreshable | Текущие токены (access + refresh) |
| access | KeycloakAccessToken | Распарсенный access token |
| refresh | KeycloakToken | Распарсенный refresh token |
| isValid | boolean | Есть ли валидное значение |
| isExpired | boolean | Истёк ли access token |
| changed | Observable<IOpenIdTokenRefreshable> | События изменения токена |
KeycloakAdministratorTransport
HTTP-транспорт с автоматической аутентификацией для административного API Keycloak.
import { KeycloakAdministratorTransport, IKeycloakAdministratorSettings } from '@ts-core/openid-common';
const settings: IKeycloakAdministratorSettings = {
url: 'https://keycloak.example.com',
realm: 'master',
clientId: 'admin-cli',
clientSecret: 'secret',
realmPublicKey: '...',
scope: 'openid',
login: 'admin',
password: 'admin-password'
};
const transport = new KeycloakAdministratorTransport(logger, settings);
// Токен будет получен автоматически при первом запросе
const users = await transport.call('admin/realms/my-realm/users', {
method: 'get'
});
// Принудительное обновление токена
await transport.refresh(true);OpenIdTokenRefreshableTransport
Базовый класс для создания HTTP-транспортов с автоматическим обновлением токенов.
import { OpenIdTokenRefreshableTransport } from '@ts-core/openid-common';
class MyApiTransport extends OpenIdTokenRefreshableTransport {
protected async getRefreshable(): Promise<IOpenIdTokenRefreshable> {
// Логика обновления токена
const response = await this.refreshTokenFromServer();
return { access: response.access_token, refresh: response.refresh_token };
}
protected isSkipCheckRefreshable(path: string): boolean {
// Пути, которые не требуют авторизации
return path.includes('/public/');
}
}Ключевые методы
| Метод | Описание |
|-------|----------|
| call(path, request?, options?) | HTTP-запрос с автоматической авторизацией |
| refresh(isForce?) | Обновление токена |
| authorization | Геттер для заголовка Bearer {token} |
Интерфейсы
IKeycloakSettings
interface IKeycloakSettings {
url: string; // URL сервера Keycloak
realm: string; // Название realm
clientId: string; // ID клиента
clientSecret: string; // Секрет клиента
realmPublicKey: string; // Публичный ключ realm (для offline валидации)
}IKeycloakAdministratorSettings
interface IKeycloakAdministratorSettings extends IKeycloakSettings {
scope: string; // Scope для токена (например, 'openid')
login: string; // Логин администратора
password: string; // Пароль администратора
}IOpenIdCode
interface IOpenIdCode {
code: string; // Код авторизации
redirectUri: string; // Redirect URI
}IOpenIdTokenRefreshable
interface IOpenIdTokenRefreshable {
access: string; // Access token
refresh: string; // Refresh token
}IOpenIdUser
interface IOpenIdUser {
sub: string; // Subject (ID пользователя)
[key: string]: any; // Дополнительные поля
}IOpenIdResource
interface IOpenIdResource<T = Record<string, any>> {
id: string; // ID ресурса
name: string; // Название ресурса
scopes: Array<string>; // Доступные scopes
type?: string; // Тип ресурса
attributes?: T; // Атрибуты
}
type OpenIdResources = Map<string, IOpenIdResource>;IOpenIdToken
interface IOpenIdToken {
value: string; // Сырое значение токена
readonly isExpired: boolean; // Истёк ли токен
}Опции валидации
// Offline валидация токена
interface IOpenIdOfflineValidationOptions {
iss?: string; // Ожидаемый issuer
type?: string; // Ожидаемый тип токена
notBefore?: number; // Минимальное время создания (iat)
isVerifyAudience?: boolean; // Проверять audience
clientId?: string; // Ожидаемый client ID
publicKey?: string; // Публичный ключ для проверки подписи
}
// Проверка ролей
interface IOpenIdRoleValidationOptions {
role: string | Array<string>; // Роль(и) в формате "type:name"
isAny?: boolean; // Любая из ролей (по умолчанию все)
}
// Проверка ресурсов
interface IOpenIdResourceValidationOptions {
name: string; // Название ресурса
scope?: string | Array<string>; // Требуемые scopes
isAny?: boolean; // Любой из scopes
}
type OpenIdResourceValidationOptions =
| IOpenIdResourceValidationOptions
| Array<IOpenIdResourceValidationOptions>;Обработка ошибок
Все ошибки наследуют базовый класс OpenIdError и содержат HTTP-код статуса.
Классы ошибок
| Класс | Код | HTTP | Описание |
|-------|-----|------|----------|
| OpenIdTokenUndefinedError | TOKEN_UNDEFINED | 401 | Токен не передан |
| OpenIdTokenInvalidError | TOKEN_INVALID | 401 | Невалидный формат токена |
| OpenIdTokenExpiredError | TOKEN_EXPIRED | 401 | Токен истёк |
| OpenIdTokenNotActiveError | TOKEN_NOT_ACTIVE | 401 | Токен неактивен |
| OpenIdTokenStaleError | TOKEN_STALE | 401 | Токен устарел (iat < notBefore) |
| OpenIdTokenNotSignedError | TOKEN_NOT_SIGNED | 403 | Токен не подписан |
| OpenIdTokenSignatureInvalidError | TOKEN_SIGNATURE_INVALID | 401 | Неверная подпись |
| OpenIdTokenSignatureAlgorithmUnknownError | TOKEN_SIGNATURE_ALGORITHM_UNKNOWN | 401 | Неизвестный алгоритм |
| OpenIdTokenWrongIssError | TOKEN_WRONG_ISS | 403 | Неверный issuer |
| OpenIdTokenWrongTypeError | TOKEN_WRONG_TYPE | 403 | Неверный тип токена |
| OpenIdTokenWrongAudienceError | TOKEN_WRONG_AUDIENCE | 403 | Неверный audience |
| OpenIdTokenWrongClientIdError | TOKEN_WRONG_CLIENT_ID | 403 | Неверный client ID |
| OpenIdTokenRoleForbiddenError | TOKEN_ROLE_FORBIDDEN | 403 | Роль отсутствует |
| OpenIdTokenRoleInvalidTypeError | TOKEN_ROLE_INVALID_TYPE | 403 | Неверный тип роли |
| OpenIdTokenResourceForbiddenError | TOKEN_RESOURCE_FORBIDDEN | 403 | Нет доступа к ресурсу |
| OpenIdTokenResourcesUndefinedError | TOKEN_RESOURCES_UNDEFINED | 403 | Ресурсы не определены |
| OpenIdTokenResourceScopeForbiddenError | TOKEN_RESOURCE_SCOPE_FORBIDDEN | 403 | Нет scope для ресурса |
| OpenIdOptionsPublicKeyUndefinedError | OPTIONS_PUBLIC_KEY_UNDEFINED | 403 | Публичный ключ не задан |
| OpenIdAccessDeniedNotAuthorizedError | ACCESS_DENIED_NOT_AUTHORIZED | 403 | Доступ запрещён |
| OpenIdInvalidGrantTokenNotActiveError | INVALID_GRANT_TOKEN_NOT_ACTIVE | 401 | Токен неактивен (Keycloak) |
| OpenIdInvalidGrantSessionNotActiveError | INVALID_GRANT_SESSION_NOT_ACTIVE | 401 | Сессия неактивна (Keycloak) |
Пример обработки
import {
OpenIdErrorCode,
OpenIdTokenExpiredError,
OpenIdTokenRoleForbiddenError
} from '@ts-core/openid-common';
try {
await service.validateRole(token, { role: 'realm:admin' });
} catch (error) {
if (error instanceof OpenIdTokenExpiredError) {
// Обновить токен
const newTokens = await service.getTokenByRefreshToken(refreshToken);
} else if (error instanceof OpenIdTokenRoleForbiddenError) {
// Недостаточно прав
console.log('Missing role:', error.details);
} else if (error.code === OpenIdErrorCode.TOKEN_INVALID) {
// Требуется повторная авторизация
redirectToLogin();
}
}Проверка типа ошибки
import { OpenIdError } from '@ts-core/openid-common';
if (OpenIdError.instanceOf(error)) {
console.log('OpenID Error:', error.code, error.status);
}Примеры
Express.js Middleware
import express from 'express';
import { KeycloakService, OpenIdError, OpenIdErrorCode } from '@ts-core/openid-common';
const app = express();
const authService = new KeycloakService(settings);
// Извлечение токена
function extractToken(req: express.Request): string | null {
const auth = req.headers.authorization;
return auth?.startsWith('Bearer ') ? auth.slice(7) : null;
}
// Middleware аутентификации
async function requireAuth(
req: express.Request,
res: express.Response,
next: express.NextFunction
) {
const token = extractToken(req);
if (!token) {
return res.status(401).json({ error: 'Token required' });
}
try {
await authService.validateToken(token, {
publicKey: settings.realmPublicKey,
clientId: settings.clientId
});
req.user = await authService.getUserInfo(token, true);
next();
} catch (error) {
const status = OpenIdError.instanceOf(error) ? error.status : 500;
res.status(status).json({ error: error.message, code: error.code });
}
}
// Middleware проверки роли
function requireRole(...roles: string[]) {
return async (req: express.Request, res: express.Response, next: express.NextFunction) => {
const token = extractToken(req);
try {
await authService.validateRole(token, { role: roles, isAny: true });
next();
} catch (error) {
res.status(403).json({ error: 'Forbidden' });
}
};
}
// Маршруты
app.get('/api/profile', requireAuth, (req, res) => {
res.json(req.user);
});
app.get('/api/admin', requireAuth, requireRole('realm:admin'), (req, res) => {
res.json({ message: 'Admin area' });
});
app.post('/auth/callback', async (req, res) => {
try {
const tokens = await authService.getTokenByCode({
code: req.body.code,
redirectUri: req.body.redirectUri
});
res.json(tokens);
} catch (error) {
res.status(400).json({ error: 'Authentication failed' });
}
});
app.post('/auth/refresh', async (req, res) => {
try {
const tokens = await authService.getTokenByRefreshToken(req.body.refreshToken);
res.json(tokens);
} catch (error) {
res.status(401).json({ error: 'Refresh failed' });
}
});
app.post('/auth/logout', async (req, res) => {
try {
await authService.logoutByRefreshToken(req.body.refreshToken);
res.json({ success: true });
} catch (error) {
res.status(400).json({ error: 'Logout failed' });
}
});NestJS Guard
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { KeycloakService } from '@ts-core/openid-common';
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private readonly authService: KeycloakService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractToken(request);
if (!token) {
throw new UnauthorizedException('Token required');
}
try {
await this.authService.validateToken(token);
request.user = await this.authService.getUserInfo(token, true);
return true;
} catch (error) {
throw new UnauthorizedException(error.message);
}
}
private extractToken(request: any): string | null {
const auth = request.headers.authorization;
return auth?.startsWith('Bearer ') ? auth.slice(7) : null;
}
}Автоматическое обновление токенов (браузер)
import { KeycloakService, KeycloakTokenManager } from '@ts-core/openid-common';
class AuthManager {
private tokenManager = new KeycloakTokenManager();
private refreshTimeout?: NodeJS.Timeout;
constructor(private service: KeycloakService) {
// Подписка на изменения токена
this.tokenManager.changed.subscribe(() => this.scheduleRefresh());
}
async login(code: string, redirectUri: string): Promise<void> {
const tokens = await this.service.getTokenByCode({ code, redirectUri });
this.tokenManager.value = tokens;
}
async logout(): Promise<void> {
if (this.tokenManager.isValid) {
await this.service.logoutByRefreshToken(this.tokenManager.value.refresh);
}
this.clearRefresh();
this.tokenManager.value = null;
}
get accessToken(): string | null {
return this.tokenManager.access?.value ?? null;
}
get isAuthenticated(): boolean {
return this.tokenManager.isValid && !this.tokenManager.isExpired;
}
private scheduleRefresh(): void {
this.clearRefresh();
if (!this.tokenManager.isValid) return;
// Обновляем за 30 секунд до истечения
const expiresAt = this.tokenManager.access.content.exp * 1000;
const refreshAt = expiresAt - Date.now() - 30000;
if (refreshAt > 0) {
this.refreshTimeout = setTimeout(() => this.refresh(), refreshAt);
}
}
private async refresh(): Promise<void> {
try {
const tokens = await this.service.getTokenByRefreshToken(
this.tokenManager.value.refresh
);
this.tokenManager.value = tokens;
} catch (error) {
console.error('Token refresh failed:', error);
this.tokenManager.value = null;
// Redirect to login
}
}
private clearRefresh(): void {
if (this.refreshTimeout) {
clearTimeout(this.refreshTimeout);
this.refreshTimeout = undefined;
}
}
}Архитектура
Структура проекта
src/
├── error/
│ ├── OpenIdError.ts # Базовый класс ошибок
│ ├── OpenIdErrorCode.ts # Enum кодов ошибок
│ └── index.ts
├── lib/
│ ├── IOpenIdCode.ts # Интерфейс кода авторизации
│ ├── IOpenIdClaim.ts # Интерфейс claims
│ ├── IOpenIdResource.ts # Интерфейс ресурса
│ ├── IOpenIdToken.ts # Базовый класс токена
│ ├── IOpenIdTokenRefreshable.ts # Интерфейс пары токенов
│ ├── IOpenIdTokenRefreshableManager.ts # Менеджер токенов
│ ├── IOpenIdUser.ts # Интерфейс пользователя
│ ├── OpenIdTokenRefreshableTransport.ts # HTTP-транспорт
│ └── index.ts
├── service/
│ ├── IOpenIdOptions.ts # Интерфейсы опций
│ ├── OpenIdService.ts # Абстрактный сервис
│ ├── keycloak/
│ │ ├── IKeycloakSettings.ts # Настройки Keycloak
│ │ ├── KeycloakAccessToken.ts # Access token
│ │ ├── KeycloakAdministratorTransport.ts # Admin API
│ │ ├── KeycloakClient.ts # HTTP-клиент
│ │ ├── KeycloakService.ts # Сервис Keycloak
│ │ ├── KeycloakToken.ts # Базовый токен
│ │ ├── KeycloakTokenManager.ts # Менеджер токенов
│ │ ├── KeycloakUtil.ts # Утилиты
│ │ └── index.ts
│ └── index.ts
└── public-api.ts # Публичный APIДиаграмма классов
OpenIdService (abstract)
└── KeycloakService
└── uses KeycloakClient
OpenIdToken
└── KeycloakToken
└── KeycloakAccessToken
OpenIdTokenRefreshableManager (abstract)
└── KeycloakTokenManager
OpenIdTokenRefreshableTransport (abstract)
└── KeycloakAdministratorTransport
OpenIdError (base)
├── OpenIdTokenExpiredError
├── OpenIdTokenInvalidError
├── OpenIdTokenRoleForbiddenError
└── ... (и другие)Лицензия
ISC
Автор
Renat Gubaev - [email protected]
