Принципы работы, влияние на корпоративную безопасность

OAuth 2.0 и OpenID Connect: принципы работы, влияние на корпоративную безопасность. Протоколы аутентификации, потоки авторизации, безопасность OAuth в корпоративной среде.

OAuth 2.0 и OpenID Connect (OIDC)

Что такое OAuth 2.0 и OpenID Connect?

OAuth 2.0 — это протокол авторизации, который позволяет приложениям получать ограниченный доступ к пользовательским аккаунтам. OpenID Connect (OIDC) — это слой идентификации поверх OAuth 2.0, который добавляет аутентификацию к авторизации.

Основные принципы

  • OAuth 2.0 — протокол авторизации
  • OpenID Connect — протокол аутентификации
  • JWT Tokens — токены JWT
  • Identity Provider — поставщик идентификации
  • Relying Party — зависимая сторона

Архитектура OAuth 2.0 и OIDC

1. OAuth 2.0 Core

// Система OAuth 2.0 Core
class OAuth2Core {
  constructor() {
    this.clients = new Map();
    this.authorizationServers = new Map();
    this.resourceServers = new Map();
    this.tokens = new Map();
    this.scopes = new Map();
  }

  // Регистрация клиента
  registerClient(clientData) {
    const client = {
      id: this.generateClientId(),
      secret: this.generateClientSecret(),
      name: clientData.name,
      type: clientData.type, // public, confidential
      redirectUris: clientData.redirectUris || [],
      grantTypes: clientData.grantTypes || ["authorization_code"],
      scopes: clientData.scopes || [],
      status: "ACTIVE",
      createdAt: new Date(),
    };

    // Валидация данных клиента
    const validation = this.validateClientData(client);
    if (!validation.isValid) {
      throw new Error(
        `Client validation failed: ${validation.errors.join(", ")}`
      );
    }

    // Сохранение клиента
    this.clients.set(client.id, client);

    return client;
  }

  // Валидация данных клиента
  validateClientData(client) {
    const errors = [];

    if (!client.name || client.name.trim() === ") {
      errors.push("Client name is required");
    }

    if (!client.type || !["public", "confidential"].includes(client.type)) {
      errors.push("Invalid client type");
    }

    if (client.redirectUris.length === 0) {
      errors.push("At least one redirect URI is required");
    }

    if (client.grantTypes.length === 0) {
      errors.push("At least one grant type is required");
    }

    return {
      isValid: errors.length === 0,
      errors: errors,
    };
  }

  // Генерация client ID
  generateClientId() {
    return (
      "client_" + Date.now() + "_" + Math.random().toString(36).substr(2, 8)
    );
  }

  // Генерация client secret
  generateClientSecret() {
    return (
      "secret_" + Date.now() + "_" + Math.random().toString(36).substr(2, 16)
    );
  }

  // Валидация клиента
  validateClient(clientId, clientSecret) {
    const client = this.clients.get(clientId);
    if (!client) {
      return {
        isValid: false,
        errors: ["Client not found"],
      };
    }

    if (client.status !== "ACTIVE") {
      return {
        isValid: false,
        errors: ["Client is not active"],
      };
    }

    if (client.type === "confidential" && client.secret !== clientSecret) {
      return {
        isValid: false,
        errors: ["Invalid client secret"],
      };
    }

    return {
      isValid: true,
      errors: [],
    };
  }

  // Генерация токенов
  generateTokens(tokenData) {
    const accessToken = this.generateAccessToken(tokenData);
    const refreshToken = this.generateRefreshToken(tokenData);

    const tokens = {
      access_token: accessToken,
      token_type: "Bearer",
      expires_in: 3600, // 1 час
      refresh_token: refreshToken,
      scope: tokenData.scope.join(" "),
    };

    // Сохранение токенов
    this.tokens.set(accessToken, {
      clientId: tokenData.clientId,
      userId: tokenData.userId,
      scope: tokenData.scope,
      issuedAt: new Date(),
      expiresAt: new Date(Date.now() + 3600000), // 1 час
    });

    return tokens;
  }

  // Генерация access token
  generateAccessToken(tokenData) {
    const payload = {
      sub: tokenData.userId,
      client_id: tokenData.clientId,
      scope: tokenData.scope,
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + 3600,
    };

    return this.signJWT(payload);
  }

  // Генерация refresh token
  generateRefreshToken(tokenData) {
    const payload = {
      sub: tokenData.userId,
      client_id: tokenData.clientId,
      type: "refresh",
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + 2592000, // 30 дней
    };

    return this.signJWT(payload);
  }

  // Подписание JWT
  signJWT(payload) {
    const header = {
      alg: "RS256",
      typ: "JWT",
    };

    const encodedHeader = this.base64UrlEncode(JSON.stringify(header));
    const encodedPayload = this.base64UrlEncode(JSON.stringify(payload));

    const signature = this.sign(`${encodedHeader}.${encodedPayload}`);
    const encodedSignature = this.base64UrlEncode(signature);

    return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
  }
}

2. OpenID Connect

// Система OpenID Connect
class OpenIDConnect {
  constructor() {
    this.oidcClients = new Map();
    this.users = new Map();
    this.idTokens = new Map();
    this.authorizationCodes = new Map();
  }

  // Регистрация OIDC клиента
  registerOIDCClient(clientData) {
    const client = {
      id: this.generateClientId(),
      secret: this.generateClientSecret(),
      name: clientData.name,
      type: clientData.type,
      redirectUris: clientData.redirectUris || [],
      grantTypes: clientData.grantTypes || ["authorization_code"],
      scopes: clientData.scopes || ["openid"],
      responseTypes: clientData.responseTypes || ["code"],
      tokenEndpointAuthMethod:
        clientData.tokenEndpointAuthMethod || "client_secret_basic",
      status: "ACTIVE",
      createdAt: new Date(),
    };

    // Валидация OIDC клиента
    const validation = this.validateOIDCClient(client);
    if (!validation.isValid) {
      throw new Error(
        `OIDC client validation failed: ${validation.errors.join(", ")}`
      );
    }

    // Сохранение клиента
    this.oidcClients.set(client.id, client);

    return client;
  }

  // Валидация OIDC клиента
  validateOIDCClient(client) {
    const errors = [];

    if (!client.name || client.name.trim() === ") {
      errors.push("Client name is required");
    }

    if (!client.scopes.includes("openid")) {
      errors.push("OIDC client must include openid scope");
    }

    if (client.redirectUris.length === 0) {
      errors.push("At least one redirect URI is required");
    }

    if (client.grantTypes.length === 0) {
      errors.push("At least one grant type is required");
    }

    return {
      isValid: errors.length === 0,
      errors: errors,
    };
  }

  // Аутентификация пользователя
  authenticateUser(credentials) {
    const user = this.users.get(credentials.username);
    if (!user) {
      return {
        success: false,
        error: "User not found",
      };
    }

    if (user.password !== credentials.password) {
      return {
        success: false,
        error: "Invalid password",
      };
    }

    if (user.status !== "ACTIVE") {
      return {
        success: false,
        error: "User account is not active",
      };
    }

    return {
      success: true,
      user: user,
    };
  }

  // Генерация authorization code
  generateAuthorizationCode(authData) {
    const code = {
      value: this.generateCodeValue(),
      clientId: authData.clientId,
      userId: authData.userId,
      redirectUri: authData.redirectUri,
      scope: authData.scope,
      nonce: authData.nonce,
      issuedAt: new Date(),
      expiresAt: new Date(Date.now() + 600000), // 10 минут
      used: false,
    };

    // Сохранение кода
    this.authorizationCodes.set(code.value, code);

    return code;
  }

  // Генерация ID token
  generateIDToken(tokenData) {
    const payload = {
      iss: this.issuer,
      sub: tokenData.userId,
      aud: tokenData.clientId,
      exp: Math.floor(Date.now() / 1000) + 3600, // 1 час
      iat: Math.floor(Date.now() / 1000),
      nonce: tokenData.nonce,
      auth_time: Math.floor(Date.now() / 1000),
      acr: "1", // Authentication Context Class Reference
    };

    // Добавление claims
    if (tokenData.scope.includes("profile")) {
      payload.name = tokenData.user.name;
      payload.family_name = tokenData.user.familyName;
      payload.given_name = tokenData.user.givenName;
      payload.middle_name = tokenData.user.middleName;
      payload.nickname = tokenData.user.nickname;
      payload.preferred_username = tokenData.user.preferredUsername;
      payload.profile = tokenData.user.profile;
      payload.picture = tokenData.user.picture;
      payload.website = tokenData.user.website;
      payload.gender = tokenData.user.gender;
      payload.birthdate = tokenData.user.birthdate;
      payload.zoneinfo = tokenData.user.zoneinfo;
      payload.locale = tokenData.user.locale;
      payload.updated_at = tokenData.user.updatedAt;
    }

    if (tokenData.scope.includes("email")) {
      payload.email = tokenData.user.email;
      payload.email_verified = tokenData.user.emailVerified;
    }

    if (tokenData.scope.includes("address")) {
      payload.address = tokenData.user.address;
    }

    if (tokenData.scope.includes("phone")) {
      payload.phone_number = tokenData.user.phoneNumber;
      payload.phone_number_verified = tokenData.user.phoneNumberVerified;
    }

    const idToken = this.signJWT(payload);

    // Сохранение ID token
    this.idTokens.set(idToken, {
      clientId: tokenData.clientId,
      userId: tokenData.userId,
      issuedAt: new Date(),
      expiresAt: new Date(Date.now() + 3600000), // 1 час
    });

    return idToken;
  }

  // Валидация ID token
  validateIDToken(idToken, clientId) {
    try {
      const payload = this.verifyJWT(idToken);

      // Проверка issuer
      if (payload.iss !== this.issuer) {
        return {
          isValid: false,
          error: "Invalid issuer",
        };
      }

      // Проверка audience
      if (payload.aud !== clientId) {
        return {
          isValid: false,
          error: "Invalid audience",
        };
      }

      // Проверка срока действия
      if (payload.exp < Math.floor(Date.now() / 1000)) {
        return {
          isValid: false,
          error: "Token expired",
        };
      }

      // Проверка issued at
      if (payload.iat > Math.floor(Date.now() / 1000)) {
        return {
          isValid: false,
          error: "Token issued in the future",
        };
      }

      return {
        isValid: true,
        payload: payload,
      };
    } catch (error) {
      return {
        isValid: false,
        error: "Invalid token format",
      };
    }
  }

  // Генерация значения кода
  generateCodeValue() {
    return (
      "code_" + Date.now() + "_" + Math.random().toString(36).substr(2, 16)
    );
  }
}

3. OIDC Discovery

// Система OIDC Discovery
class OIDCDiscovery {
  constructor() {
    this.wellKnownEndpoints = new Map();
    this.jwks = new Map();
    this.issuers = new Map();
  }

  // Генерация well-known конфигурации
  generateWellKnownConfiguration(issuer) {
    const configuration = {
      issuer: issuer,
      authorization_endpoint: `${issuer}/authorize`,
      token_endpoint: `${issuer}/token`,
      userinfo_endpoint: `${issuer}/userinfo`,
      jwks_uri: `${issuer}/.well-known/jwks.json`,
      registration_endpoint: `${issuer}/register`,
      scopes_supported: [
        "openid",
        "profile",
        "email",
        "address",
        "phone",
        "offline_access",
      ],
      response_types_supported: [
        "code",
        "id_token",
        "code id_token",
        "id_token token",
        "code id_token token",
      ],
      response_modes_supported: ["query", "fragment", "form_post"],
      subject_types_supported: ["public", "pairwise"],
      id_token_signing_alg_values_supported: [
        "RS256",
        "RS384",
        "RS512",
        "ES256",
        "ES384",
        "ES512",
        "HS256",
        "HS384",
        "HS512",
      ],
      token_endpoint_auth_methods_supported: [
        "client_secret_basic",
        "client_secret_post",
        "client_secret_jwt",
        "private_key_jwt",
        "none",
      ],
      claims_supported: [
        "sub",
        "iss",
        "aud",
        "exp",
        "iat",
        "auth_time",
        "nonce",
        "acr",
        "amr",
        "azp",
        "at_hash",
        "c_hash",
        "name",
        "family_name",
        "given_name",
        "middle_name",
        "nickname",
        "preferred_username",
        "profile",
        "picture",
        "website",
        "gender",
        "birthdate",
        "zoneinfo",
        "locale",
        "updated_at",
        "email",
        "email_verified",
        "address",
        "phone_number",
        "phone_number_verified",
      ],
      grant_types_supported: [
        "authorization_code",
        "implicit",
        "refresh_token",
        "client_credentials",
      ],
      code_challenge_methods_supported: ["plain", "S256"],
    };

    return configuration;
  }

  // Генерация JWKS
  generateJWKS() {
    const jwks = {
      keys: [],
    };

    // Добавление ключей подписи
    for (const [kid, key] of this.jwks) {
      jwks.keys.push({
        kty: key.kty,
        kid: kid,
        use: key.use,
        alg: key.alg,
        n: key.n,
        e: key.e,
      });
    }

    return jwks;
  }

  // Добавление ключа подписи
  addSigningKey(keyData) {
    const key = {
      kty: keyData.kty, // Key Type
      kid: keyData.kid, // Key ID
      use: keyData.use, // Public Key Use
      alg: keyData.alg, // Algorithm
      n: keyData.n, // Modulus
      e: keyData.e, // Exponent
    };

    this.jwks.set(keyData.kid, key);
  }

  // Получение ключа подписи
  getSigningKey(kid) {
    return this.jwks.get(kid);
  }
}

Основные компоненты OAuth 2.0 и OIDC

1. OAuth 2.0

  • Authorization Server — сервер авторизации
  • Resource Server — сервер ресурсов
  • Client — клиент
  • Resource Owner — владелец ресурса

2. OpenID Connect

  • Identity Provider — поставщик идентификации
  • Relying Party — зависимая сторона
  • ID Token — токен идентификации
  • UserInfo Endpoint — конечная точка информации о пользователе

3. Discovery

  • Well-Known Configuration — конфигурация well-known
  • JWKS — набор ключей JSON Web Key Set
  • Metadata — метаданные
  • Endpoints — конечные точки

Best Practices

1. Security

  • Use HTTPS — используйте HTTPS
  • Validate Tokens — валидируйте токены
  • Implement PKCE — реализуйте PKCE
  • Secure Storage — безопасное хранение

2. Implementation

  • Follow Standards — следуйте стандартам
  • Handle Errors — обрабатывайте ошибки
  • Implement Logging — реализуйте логирование
  • Regular Updates — регулярные обновления

3. Monitoring

  • Token Usage — использование токенов
  • Error Rates — частота ошибок
  • Performance — производительность
  • Security Events — события безопасности

Заключение

OAuth 2.0 и OpenID Connect — это критически важные протоколы для современной веб-безопасности, которые требуют:

  • Глубокого понимания — протоколов и стандартов
  • Специализированных навыков — в области реализации
  • Правильных инструментов — для разработки и тестирования
  • Системного подхода — к обеспечению безопасности

Помните: OAuth 2.0 и OIDC — это не разовое мероприятие, а постоянный процесс. Регулярно обновляйте реализацию, следите за новыми угрозами и адаптируйте методы защиты.


Совет: Начните с OAuth 2.0, затем добавьте OIDC для аутентификации. Не забывайте о валидации токенов и правильной обработке ошибок!