OAuth клиент для TypeScript: авторизация через GitHub, Google и другие провайдеры

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

OAuth клиент для TypeScript: реализация авторизации на образовательном сайте

В современной веб-разработке OAuth (Open Authorization) стал стандартом де-факто для аутентификации пользователей через сторонние сервисы. Для образовательного сайта pythonlib.ru мы подготовили описание универсального OAuth клиента на TypeScript, который позволяет реализовать вход через GitHub, Google и другие популярные провайдеры.

Что такое OAuth клиент?

OAuth клиент — это программный компонент, который управляет процессом получения разрешений от пользователя для доступа к его данным на стороннем сервисе. В контексте TypeScript такой клиент обычно реализует протокол OAuth 2.0 и включает:

  • Инициализацию процесса авторизации
  • Обработку callback-запросов
  • Обмен authorization code на access token
  • Обновление токенов (refresh token)
  • Работу с состоянием сессии

Архитектура OAuth клиента на TypeScript

Наш клиент построен на принципах модульности и типобезопасности. Основные компоненты:

1. Конфигурация провайдера

interface OAuthProviderConfig {
  clientId: string;
  clientSecret: string;
  redirectUri: string;
  authorizationEndpoint: string;
  tokenEndpoint: string;
  scopes: string[];
}

const githubConfig: OAuthProviderConfig = {
  clientId: process.env.GITHUB_CLIENT_ID!,
  clientSecret: process.env.GITHUB_CLIENT_SECRET!,
  redirectUri: 'https://pythonlib.ru/auth/callback/github',
  authorizationEndpoint: 'https://github.com/login/oauth/authorize',
  tokenEndpoint: 'https://github.com/login/oauth/access_token',
  scopes: ['read:user', 'user:email']
};

2. Генерация URL для авторизации

function generateAuthUrl(config: OAuthProviderConfig, state: string): string {
  const params = new URLSearchParams({
    client_id: config.clientId,
    redirect_uri: config.redirectUri,
    scope: config.scopes.join(' '),
    state: state,
    response_type: 'code'
  });
  
  return `${config.authorizationEndpoint}?${params.toString()}`;
}

3. Обмен кода на токен

async function exchangeCodeForToken(
  config: OAuthProviderConfig,
  code: string
): Promise<OAuthTokenResponse> {
  const response = await fetch(config.tokenEndpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'Accept': 'application/json'
    },
    body: JSON.stringify({
      client_id: config.clientId,
      client_secret: config.clientSecret,
      code: code,
      redirect_uri: config.redirectUri
    })
  });

  if (!response.ok) {
    throw new Error(`Token exchange failed: ${response.statusText}`);
  }

  return response.json();
}

Работа с токенами

После получения токенов необходимо обеспечить их безопасное хранение и обновление. Рекомендуется использовать httpOnly cookies или серверное хранилище сессий.

Обновление access token

async function refreshAccessToken(
  config: OAuthProviderConfig,
  refreshToken: string
): Promise<OAuthTokenResponse> {
  const response = await fetch(config.tokenEndpoint, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      client_id: config.clientId,
      client_secret: config.clientSecret,
      refresh_token: refreshToken,
      grant_type: 'refresh_token'
    })
  });

  return response.json();
}

Полный пример реализации для GitHub OAuth

import { randomBytes } from 'crypto';

class GitHubOAuthClient {
  private config: OAuthProviderConfig;
  private stateStore: Map<string, string> = new Map();

  constructor() {
    this.config = {
      clientId: process.env.GITHUB_CLIENT_ID!,
      clientSecret: process.env.GITHUB_CLIENT_SECRET!,
      redirectUri: 'https://pythonlib.ru/auth/callback/github',
      authorizationEndpoint: 'https://github.com/login/oauth/authorize',
      tokenEndpoint: 'https://github.com/login/oauth/access_token',
      scopes: ['read:user', 'user:email']
    };
  }

  getAuthorizationUrl(): string {
    const state = randomBytes(16).toString('hex');
    this.stateStore.set(state, Date.now().toString());
    
    return generateAuthUrl(this.config, state);
  }

  async handleCallback(code: string, state: string): Promise<UserProfile> {
    if (!this.stateStore.has(state)) {
      throw new Error('Invalid state parameter');
    }
    
    const tokenResponse = await exchangeCodeForToken(this.config, code);
    const userProfile = await this.fetchUserProfile(tokenResponse.access_token);
    
    return userProfile;
  }

  private async fetchUserProfile(accessToken: string): Promise<UserProfile> {
    const response = await fetch('https://api.github.com/user', {
      headers: {
        'Authorization': `Bearer ${accessToken}`
      }
    });
    
    return response.json();
  }
}

Безопасность и лучшие практики

  • Защита от CSRF: всегда используйте параметр state для предотвра

Рекомендаций