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.
- Войдите в свой аккаунт GitVerse.
- Перейдите в Настройки профиля/Настройки организации → Приложения.
- Нажмите Добавить приложение.
- Заполните форму:
Название:
gitverse-stats-dashboard
URI для перенаправления:https://your-server.ru/callback
Конфиденциальный клиент: ✓ (включено)
Пропускать авторизацию для публичных клиентов после первого доступа: ✗ (выключено)
- Нажмите Создать приложение.
- Сохраните 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.ruScope приложения
Дашборд запрашивает два 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_uri | http://localhost:8000/callback | Callback URL, зарегистрированный для приложения |
response_type | code | Всегда code для Authorization Code Flow |
scope | read:user read:repository | Запрашиваемые области доступа (пробелом разделенный) |
state | Случайная строка | CSRF-защита; возвращается без изменений в callback |
code_challenge | Base64URL(SHA-256(code_verifier)) | PKCE code challenge |
code_challenge_method | S256 | Метод вычисления 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_type | authorization_code | Тип grant — обмен authorization code на токен |
code | gta_xxxxxx | Authorization code из callback |
redirect_uri | http://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 %>
🍴 <%= repo.forks_count %>
🌿 <%= 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.Используемые эндпоинты:
- Получить данные авторизованного пользователя —
GET /user;- Получить список репозиториев текущего пользователя —
GET /user/repos.
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_type | refresh_token | Тип grant — обновление токена |
client_id | Ваш ID клиента | UUID приложения |
client_secret | Ваш Клиентский ключ | Секрет приложения |
refresh_token | Сохраненный refresh_token | Refresh 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
Пользователь может отозвать доступ приложению в любой момент:
- Перейдите в Настройки профиля на вкладку Авторизованные приложения.
- На вкладке Авторизованные приложения найдите
gitverse-stats-dashboard. - Нажмите Отозвать.
Tip
Подробнее об управлении подключенными приложениями см. в разделе Просмотр подключенных приложений.
Полный код приложения
Полный рабочий код приложения gitverse-stats-dashboard доступен в публичном репозитории:
gitverse-stats-dashboard.
Следующие шаги
- Управление OAuth-приложениями — создание и редактирование приложений;
- Авторизация приложений OAuth 2.0 — полное описание процесса авторизации;
- Области доступа (Scope) — описание всех доступных scope;
- Жизненный цикл токенов — сроки жизни и ротация токенов.