OAuth 2.0 на примере Node.js-приложения

Info

Это пошаговое руководство проведет вас через весь OAuth 2.0 Authorization Flow с PKCE на примере создания gitverse-stats-dashboard — веб-приложения для просмотра статистики репозиториев пользователя GitVerse.

Каждый шаг строится на предыдущем и включает полный рабочий код на Node.js (Express + EJS + axios).

Теоретическое описание авторизации, параметров и эндпоинтов см. в разделе Авторизация приложений OAuth 2.0.


1. Создание приложения в GitVerse

Перед началом разработки зарегистрируйте OAuth-приложение в GitVerse.

  1. Войдите в свой аккаунт GitVerse.
  2. Перейдите в Настройки профиля/Настройки организации → Приложения.
  3. Нажмите Добавить приложение.
  4. Заполните форму:

Название: gitverse-stats-dashboard
URI для перенаправления: https://your-server.ru/callback
Конфиденциальный клиент: ✓ (включено)
Пропускать авторизацию для публичных клиентов после первого доступа: ✗ (выключено)

  1. Нажмите Создать приложение.
  2. Сохраните ID клиента и Клиентский ключ — Клиентский ключ отображается только один раз.

Warning

redirect_uri должен точно совпадать с одним из callback URL, зарегистрированных для приложения. См. URI для перенаправления и Управление OAuth-приложениями.


2. Подготовка проекта

Создайте Node.js-проект с Express, EJS и axios.

Инициализация проекта

mkdir gitverse-stats-dashboard && cd gitverse-stats-dashboard
npm init -y
npm install express express-session axios dotenv ejs

Настройка шаблонов

Создайте директорию для шаблонов:

mkdir views

Шаблон дашборда (views/dashboard.ejs) будет добавлен на шаге 8.

Ниже — переменные окружения, scope и базовая инициализация сервера.

Файл .env

Переменные окружения:

.env.

# Параметры OAuth-приложения
GITVERSE_CLIENT_ID=ВАШ_CLIENT_ID
GITVERSE_CLIENT_SECRET=ВАШ_CLIENT_SECRET
GITVERSE_REDIRECT_URI=http://localhost:8000/callback
 
# Настройки сервера
PORT=8000
BASE_URL=http://localhost:8000
GITVERSE_BASE_URL=https://gitverse.ru

Scope приложения

Дашборд запрашивает два scope:

  • read:user — чтение профиля пользователя (логин, avatar, публичная информация);
  • read:repository — чтение информации о репозиториях пользователя.

Tip

Приложение запрашивает только те права, которые ему действительно нужны. Подробнее о принципе наименьших привилегий и всех доступных scope см. в разделе Области доступа (scope).

Базовая инициализация Express

Базовая инициализация сервера:

server.js.

const express = require('express');
const session = require('express-session');
const crypto = require('crypto');
const axios = require('axios');
require('dotenv').config();
 
const app = express();
const PORT = process.env.PORT || 8000;
 
// --- Конфигурация ---
const {
  GITVERSE_CLIENT_ID,
  GITVERSE_CLIENT_SECRET,
  GITVERSE_REDIRECT_URI,
  GITVERSE_BASE_URL
} = process.env;
 
// --- Middleware ---
app.set('view engine', 'ejs');
app.use(express.urlencoded({ extended: true }));
 
// Сессия для хранения code_verifier и временных данных
app.use(session({
  secret: crypto.randomBytes(32).toString('hex'),
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false } // В production используйте true (HTTPS)
}));

Note

Сессия (express-session) понадобится для хранения code_verifier между шагами PKCE-flow. В production замените secret на безопасную константу из переменных окружения.


3. Генерация PKCE-параметров

PKCE (Proof Key for Code Exchange) защищает процесс авторизации от перехвата кода. GitVerse требует PKCE для публичных клиентов; для конфиденциальных клиентов он не обязателен, но рекомендуется как дополнительная мера безопасности.

Генерация PKCE-параметров:

server.js.

// Генерация PKCE-параметров
function generatePKCE() {
  // code_verifier: случайная строка 43–128 символов
  const code_verifier = crypto.randomBytes(32).toString('base64url');
 
  // code_challenge: Base64URL(SHA-256(code_verifier))
  const code_challenge = crypto
    .createHash('sha256')
    .update(code_verifier)
    .digest('base64url');
 
  return { code_verifier, code_challenge };
}

Important

code_verifier должен быть сохранен в сессии и передан на шаге обмена кода на токен. Без него GitVerse отклонит запрос с ошибкой unauthorized_client.


4. Инициация авторизации

При нажатии кнопки Авторизовать через GitVerse приложение перенаправляет пользователя на endpoint авторизации GitVerse.

Маршрут инициации авторизации:

server.js.

// Генерация CSRF token (state)
function generateState() {
  return crypto.randomBytes(16).toString('hex');
}
 
// Маршрут: перенаправление на страницу авторизации GitVerse
app.get('/signin', (req, res) => {
  // Генерируем PKCE-параметры и сохраняем verifier в сессии
  const { code_verifier, code_challenge } = generatePKCE();
  req.session.code_verifier = code_verifier;
 
  // Генерируем state для защиты от CSRF
  const state = generateState();
  req.session.state = state;
 
  // Формируем URL авторизации
  // API: GET /signin/oauth/authorize (Авторизация)
  const authUrl = new URL(`${GITVERSE_BASE_URL}/signin/oauth/authorize`);
  authUrl.searchParams.set('client_id', GITVERSE_CLIENT_ID);
  authUrl.searchParams.set('redirect_uri', GITVERSE_REDIRECT_URI);
  authUrl.searchParams.set('response_type', 'code');
  authUrl.searchParams.set('scope', 'read:user read:repository');
  authUrl.searchParams.set('state', state);
  authUrl.searchParams.set('code_challenge', code_challenge);
  authUrl.searchParams.set('code_challenge_method', 'S256');
 
  // Перенаправляем пользователя на GitVerse
  res.redirect(authUrl.toString());
});

Параметры запроса авторизации

Эндпоинт: GET /signin/oauth/authorize — Авторизация

ПараметрЗначениеОписание
client_idВаш ID клиентаUUID зарегистрированного приложения
redirect_urihttp://localhost:8000/callbackCallback URL, зарегистрированный для приложения
response_typecodeВсегда code для Authorization Code Flow
scoperead:user read:repositoryЗапрашиваемые области доступа (пробелом разделенный)
stateСлучайная строкаCSRF-защита; возвращается без изменений в callback
code_challengeBase64URL(SHA-256(code_verifier))PKCE code challenge
code_challenge_methodS256Метод вычисления challenge (SHA-256, рекомендуется)

Tip

Параметр state защищает от CSRF-атак. Всегда проверяйте, что state в callback совпадает с сохраненным в сессии — это базовая мера безопасности.

Пользователь перенаправляется на GitVerse, где видит страницу подтверждения доступа. Далее — шаг 5.


5. Подтверждение доступа пользователем

GitVerse показывает пользователю страницу подтверждения с информацией:

  • имя приложения: gitverse-stats-dashboard;
  • запрашиваемые права: Пользователь: Чтение и Репозитории: Чтение.

Пользователь нажимает Авторизоваться или Отменить.

Warning

Если пользователь нажмет Отменить, GitVerse перенаправит его на redirect_uri с параметром ошибки error=access_denied. Обработайте этот случай в callback.

Подробнее о странице подтверждения см. в разделе Подключение сторонних приложений.


6. Получение authorization code

После подтверждения GitVerse перенаправляет пользователя обратно на redirect_uri:

http://localhost:8000/callback?code=gta_xxxxxx&state=<ваш_state>

Callback-маршрут:

server.js.

// Callback: получение authorization code от GitVerse
app.get('/callback', (req, res) => {
  const { code, state, error } = req.query;
 
  // Обработка ошибки (пользователь отклонил доступ)
  if (error) {
    res.status(400).send(`Ошибка авторизации: ${error}`);
    return;
  }
 
  // Проверка state (защита от CSRF)
  if (state !== req.session.state) {
    res.status(403).send('Ошибка проверки CSRF (state не совпадает)');
    return;
  }
 
  // code_verifier был сохранен в сессии на шаге 3
  const code_verifier = req.session.code_verifier;
 
  // GitVerse перенаправляет пользователя сюда с authorization code
  req.session.authorization_code = code;
  res.redirect('/token');
});

Warning

Authorization code действителен 10 минут и используется один раз. После обмена на токен он становится недействительным.


7. Обмен кода на access_token

Маршрут обмена кода на токен:

server.js.

// Маршрут: обмен authorization code на access_token
app.get('/token', async (req, res) => {
  try {
    // API: POST /login/oauth/access_token (Получение токена)
    const response = await axios.post(
      `${GITVERSE_BASE_URL}/login/oauth/access_token`,
      new URLSearchParams({
        grant_type: 'authorization_code',
        code: req.session.authorization_code,
        redirect_uri: GITVERSE_REDIRECT_URI,
        client_id: GITVERSE_CLIENT_ID,
        client_secret: GITVERSE_CLIENT_SECRET,
        code_verifier: req.session.code_verifier
      }).toString(),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Accept': 'application/json'
        }
      }
    );
 
    // Сохраняем токены в сессии
    const { access_token, refresh_token, expires_in } = response.data;
    req.session.access_token = access_token;
    req.session.refresh_token = refresh_token;
    req.session.token_expires_at = Date.now() + (expires_in * 1000);
 
    // Очистка временных данных
    delete req.session.authorization_code;
    delete req.session.code_verifier;
    delete req.session.state;
 
    res.redirect('/dashboard');
  } catch (error) {
    console.error('Ошибка обмена кода на токен:', error.response?.data || error.message);
    res.status(500).send('Ошибка получения токена');
  }
});

Параметры запроса

Эндпоинт: POST /login/oauth/access_token — Получение токена

ПараметрЗначениеОписание
grant_typeauthorization_codeТип grant — обмен authorization code на токен
codegta_xxxxxxAuthorization code из callback
redirect_urihttp://localhost:8000/callbackДолжен совпадать с redirect_uri при инициации
client_idВаш ID клиентаUUID приложения
client_secretВаш Клиентский ключСекрет приложения (для конфиденциальных клиентов)
code_verifierСтрока из шага 3Исходный PKCE verifier для проверки code_challenge

Успешный ответ

{
  "access_token": "eyJhbGciOiJSUzI1NiIs...",
  "token_type": "bearer",
  "expires_in": 3600,
  "refresh_token": "eyJhbGciOiJSUzI1NiIs..."
}

Important

refresh_token должен быть сохранен — он понадобится для получения нового access_token после истечения срока действия. Подробнее о жизненном цикле токенов см. в разделе Жизненный цикл токенов.


8. Использование access_token

Получив access_token, приложение может обращаться к API GitVerse от имени пользователя. Дашборд отображает профиль пользователя и список его репозиториев.

Шаблон дашборда

Шаблон дашборда:

views/dashboard.ejs.

<!DOCTYPE html>
<html>
<head>
  <title>GitVerse Stats Dashboard</title>
  <style>
    body { font-family: sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
    .repo { padding: 10px; margin: 5px 0; border: 1px solid #ddd; border-radius: 4px; }
    .repo a { text-decoration: none; color: #0366d6; font-weight: bold; }
    .profile { display: flex; align-items: center; gap: 15px; margin-bottom: 30px; }
    .profile img { border-radius: 50%; }
  </style>
</head>
<body>
  <h1>GitVerse Stats Dashboard</h1>
 
  <!-- Профиль пользователя -->
  <div class="profile">
    <img src="<%= user.avatar_url %>" alt="Avatar" width="64" height="64">
    <div>
      <strong><%= user.login %></strong><br>
      <small>ID: <%= user.id %></small>
    </div>
  </div>
 
  <!-- Список репозиториев -->
  <h2>Репозитории (<%= repos.length %>)</h2>
  <% repos.forEach(repo => { %>
    <div class="repo">
      <a href="<%= repo.html_url %>" target="_blank"><%= repo.full_name %></a>
      <p><%= repo.description || 'Нет описания' %></p>
      <small>
        ⭐ <%= repo.stargazers_count %> &nbsp;
        🍴 <%= repo.forks_count %> &nbsp;
        🌿 <%= repo.language || 'Не определен' %>
      </small>
    </div>
  <% }) %>
 
  <p><a href="/logout">Выйти</a></p>
</body>
</html>

Маршрут дашборда

Маршрут дашборда:

server.js.

// Маршрут: дашборд со статистикой
app.get('/dashboard', async (req, res) => {
  const token = req.session.access_token;
 
  if (!token) {
    return res.redirect('/login');
  }
 
  try {
    const apiHeaders = {
      'Authorization': `Bearer ${token}`,
      'Accept': 'application/vnd.gitverse.object+json;version=latest'
    };
 
    // Запрашиваем профиль пользователя
    // API: GET /user — [Получить данные авторизованного пользователя](/developers/public-api/user-management/api-get-user/)
    const userRes = await axios.get(`${GITVERSE_BASE_URL}/user`, {
      headers: apiHeaders
    });
 
    // Запрашиваем репозитории пользователя
    // API: GET /user/repos — [Получить список репозиториев текущего пользователя](/developers/public-api/user-management/api-get-user-repos/)
    const reposRes = await axios.get(`${GITVERSE_BASE_URL}/user/repos`, {
      headers: apiHeaders
    });
 
    res.render('dashboard', {
      user: userRes.data,
      repos: reposRes.data
    });
  } catch (error) {
    if (error.response?.status === 401) {
      // Токен истек — нужно обновить или перезайти
      delete req.session.access_token;
      return res.redirect('/login');
    }
    console.error('Ошибка загрузки дашборда:', error.message);
    res.status(500).send('Ошибка загрузки данных');
  }
});

Note

Обратите внимание на заголовок Accept — он определяет формат ответа API GitVerse. Без него сервер может вернуть HTML вместо JSON.

Используемые эндпоинты:


9. Обновление токена (Refresh Token)

Access token истекает через expires_in секунд (обычно 3600). Для продолжения работы без повторной авторизации используйте refresh_token.

Функция обновления токена:

server.js.

// Обновление access_token с помощью refresh_token
async function refreshAccessToken(refresh_token) {
  try {
    // API: POST /login/oauth/access_token (Обновление токена)
    const response = await axios.post(
      `${GITVERSE_BASE_URL}/login/oauth/access_token`,
      new URLSearchParams({
        grant_type: 'refresh_token',
        client_id: GITVERSE_CLIENT_ID,
        client_secret: GITVERSE_CLIENT_SECRET,
        refresh_token: refresh_token
      }).toString(),
      {
        headers: {
          'Content-Type': 'application/x-www-form-urlencoded',
          'Accept': 'application/json'
        }
      }
    );
 
    return response.data;
  } catch (error) {
    console.error('Ошибка обновления токена:', error.response?.data || error.message);
    return null;
  }
}

Параметры refresh-запроса

Эндпоинт: POST /login/oauth/access_token — Обновление токена

ПараметрЗначениеОписание
grant_typerefresh_tokenТип grant — обновление токена
client_idВаш ID клиентаUUID приложения
client_secretВаш Клиентский ключСекрет приложения
refresh_tokenСохраненный refresh_tokenRefresh token из предыдущего ответа

Успешный ответ

{
  "access_token": "c97d1fe52119f38c7f67f0a14db68d60caa35ddc...",
  "token_type": "bearer",
  "expires_in": 3600,
  "refresh_token": "803c1fd487fec35562c205dac93e9d8e08f9d3...",
  "scope": "read:user read:repository"
}

Important

Новый refresh_token заменяет старый. Старый refresh_token становится недействительным после использования (rotating refresh tokens). Новые scope совпадают со scope предыдущего токена.

Подробнее о политике ротации см. в разделе Жизненный цикл токенов.

Автоматическое обновление токена

Middleware для обновления токена:

server.js.

// Middleware: автоматическое обновление токена при 401
app.use(async (req, res, next) => {
  // Пропускаем маршруты авторизации
  if (['/login', '/callback', '/token', '/logout'].includes(req.path)) {
    return next();
  }
 
  const token = req.session.access_token;
  const expiresAt = req.session.token_expires_at;
 
  // Если токен истек или скоро истечет — обновляем
  if (token && expiresAt && Date.now() >= expiresAt - 60000) {
    const newTokens = await refreshAccessToken(req.session.refresh_token);
    if (newTokens) {
      req.session.access_token = newTokens.access_token;
      req.session.refresh_token = newTokens.refresh_token;
      req.session.token_expires_at = Date.now() + (newTokens.expires_in * 1000);
    }
  }
 
  next();
});

10. Завершение сессии

Пользователь может выйти из приложения или отозвать доступ приложению в настройках GitVerse.

Выход из приложения

// Маршрут: выход из приложения
app.get('/logout', (req, res) => {
  // Очищаем сессию
  req.session.destroy((err) => {
    if (err) {
      console.error('Ошибка удаления сессии:', err);
    }
    res.redirect('/');
  });
});

Note

Уничтожение сессии удаляет токены с сервера, но не отзывает их на стороне GitVerse. Токены останутся действительными до истечения срока. Для принудительного отзыва токена пользователь должен отозвать доступ через GitVerse.

Отзыв доступа через GitVerse

Пользователь может отозвать доступ приложению в любой момент:

  1. Перейдите в Настройки профиля на вкладку Авторизованные приложения.
  2. На вкладке Авторизованные приложения найдите gitverse-stats-dashboard.
  3. Нажмите Отозвать.

Tip

Подробнее об управлении подключенными приложениями см. в разделе Просмотр подключенных приложений.


Полный код приложения

Полный рабочий код приложения gitverse-stats-dashboard доступен в публичном репозитории:
gitverse-stats-dashboard.


Следующие шаги