OAuth 2.0 для API: полное руководство по аутентификации

← Вернуться к статьям

Введение

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

В этом руководстве мы разберем OAuth 2.0 от основ до практической реализации, изучим различные типы flows, научимся использовать токены и рассмотрим best practices для безопасности.

✅ Что вы узнаете:

📋 Содержание

Что такое OAuth 2.0? 🔐

OAuth 2.0 — это фреймворк авторизации, который позволяет третьим сторонам получать ограниченный доступ к HTTP сервисам от имени владельца ресурса. Вместо того, чтобы делиться паролем, OAuth 2.0 использует токены доступа (access tokens).

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

✅ Ключевые принципы OAuth 2.0:

Зачем нужен OAuth 2.0?

Представьте ситуацию: вы хотите использовать приложение, которое должно получить доступ к вашим данным в Google. Без OAuth 2.0 вам пришлось бы передать пароль от Google приложению, что крайне небезопасно.

С OAuth 2.0 вы авторизуетесь напрямую в Google, а приложение получает только токен с ограниченными правами. Если вы передумаете, вы можете отозвать доступ, не меняя пароль.

Компоненты OAuth 2.0 🧩

OAuth 2.0 состоит из нескольких ключевых компонентов:

Компонент Описание Пример
Resource Owner Пользователь, который владеет ресурсом Вы (пользователь)
Client Приложение, которое запрашивает доступ Веб-приложение или мобильное приложение
Authorization Server Сервер, который выдает токены Google OAuth, GitHub OAuth
Resource Server Сервер, который хранит защищенные ресурсы API сервер с данными пользователя
Access Token Токен для доступа к ресурсам JWT или opaque token
Refresh Token Токен для обновления access token Долгоживущий токен
Scope Разрешения, которые запрашивает клиент read:user, write:posts

Authorization Code Flow 🔄

Authorization Code Flow — это самый безопасный и рекомендуемый flow для веб-приложений и мобильных приложений с серверной частью.

Шаги Authorization Code Flow:

Шаг 1: Редирект на Authorization Server

Клиент перенаправляет пользователя на страницу авторизации:

https://authorization-server.com/authorize?client_id=123&redirect_uri=https://app.com/callback&response_type=code&scope=read:user
Шаг 2: Пользователь авторизуется

Пользователь вводит свои учетные данные на странице Authorization Server.

Шаг 3: Authorization Code

Authorization Server перенаправляет пользователя обратно с authorization code:

https://app.com/callback?code=AUTHORIZATION_CODE
Шаг 4: Обмен кода на токен

Клиент обменивает authorization code на access token (на сервере):

POST /token
grant_type=authorization_code&code=AUTHORIZATION_CODE&client_id=123&client_secret=SECRET
Шаг 5: Получение Access Token

Authorization Server возвращает access token и refresh token:

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "refresh_token": "def50200...",
  "expires_in": 3600,
  "token_type": "Bearer"
}
Шаг 6: Использование Access Token

Клиент использует access token для запросов к API:

GET /api/user
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Пример реализации Authorization Code Flow

Node.js (Express)

// server.js
const express = require('express');
const axios = require('axios');
const app = express();

// Шаг 1: Редирект на авторизацию
app.get('/login', (req, res) => {
  const authUrl = `https://authorization-server.com/authorize?` +
    `client_id=${CLIENT_ID}&` +
    `redirect_uri=${encodeURIComponent('http://localhost:3000/callback')}&` +
    `response_type=code&` +
    `scope=read:user`;
  
  res.redirect(authUrl);
});

// Шаг 3-4: Callback и обмен кода на токен
app.get('/callback', async (req, res) => {
  const { code } = req.query;
  
  try {
    // Обмен authorization code на access token
    const tokenResponse = await axios.post('https://authorization-server.com/token', {
      grant_type: 'authorization_code',
      code: code,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      redirect_uri: 'http://localhost:3000/callback'
    });
    
    const { access_token, refresh_token, expires_in } = tokenResponse.data;
    
    // Сохраняем токены (в реальном приложении - в БД или сессии)
    req.session.access_token = access_token;
    req.session.refresh_token = refresh_token;
    
    res.redirect('/dashboard');
  } catch (error) {
    res.status(500).send('Ошибка авторизации');
  }
});

// Шаг 6: Использование access token
app.get('/api/user', async (req, res) => {
  const accessToken = req.session.access_token;
  
  try {
    const userResponse = await axios.get('https://api.example.com/user', {
      headers: {
        'Authorization': `Bearer ${accessToken}`
      }
    });
    
    res.json(userResponse.data);
  } catch (error) {
    res.status(401).send('Недействительный токен');
  }
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

Python (Flask)

# app.py
from flask import Flask, redirect, request, session
import requests

app = Flask(__name__)
app.secret_key = 'your-secret-key'

CLIENT_ID = 'your-client-id'
CLIENT_SECRET = 'your-client-secret'
REDIRECT_URI = 'http://localhost:5000/callback'

# Шаг 1: Редирект на авторизацию
@app.route('/login')
def login():
    auth_url = (
        f'https://authorization-server.com/authorize?'
        f'client_id={CLIENT_ID}&'
        f'redirect_uri={REDIRECT_URI}&'
        f'response_type=code&'
        f'scope=read:user'
    )
    return redirect(auth_url)

# Шаг 3-4: Callback и обмен кода на токен
@app.route('/callback')
def callback():
    code = request.args.get('code')
    
    # Обмен authorization code на access token
    token_data = {
        'grant_type': 'authorization_code',
        'code': code,
        'client_id': CLIENT_ID,
        'client_secret': CLIENT_SECRET,
        'redirect_uri': REDIRECT_URI
    }
    
    token_response = requests.post(
        'https://authorization-server.com/token',
        data=token_data
    )
    
    token_json = token_response.json()
    session['access_token'] = token_json['access_token']
    session['refresh_token'] = token_json['refresh_token']
    
    return redirect('/dashboard')

# Шаг 6: Использование access token
@app.route('/api/user')
def get_user():
    access_token = session.get('access_token')
    
    headers = {'Authorization': f'Bearer {access_token}'}
    user_response = requests.get(
        'https://api.example.com/user',
        headers=headers
    )
    
    return user_response.json()

if __name__ == '__main__':
    app.run(port=5000)

Client Credentials Flow 🔑

Client Credentials Flow используется для сервер-сервер коммуникации, когда приложение запрашивает доступ к своим собственным ресурсам, а не к ресурсам пользователя.

✅ Когда использовать Client Credentials Flow:

Шаги Client Credentials Flow:

Шаг 1: Запрос токена

Клиент отправляет запрос напрямую к Authorization Server:

POST /token
grant_type=client_credentials&client_id=123&client_secret=SECRET
Шаг 2: Получение Access Token

Authorization Server возвращает access token:

{
  "access_token": "eyJhbGciOiJIUzI1NiIs...",
  "expires_in": 3600,
  "token_type": "Bearer"
}
Шаг 3: Использование Access Token

Клиент использует access token для запросов к API:

GET /api/data
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Пример реализации Client Credentials Flow

// client-credentials.js
const axios = require('axios');

async function getAccessToken() {
  try {
    const response = await axios.post('https://authorization-server.com/token', {
      grant_type: 'client_credentials',
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET,
      scope: 'read:data write:data'
    }, {
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded'
      }
    });
    
    return response.data.access_token;
  } catch (error) {
    console.error('Ошибка получения токена:', error);
    throw error;
  }
}

async function callAPI() {
  const accessToken = await getAccessToken();
  
  const response = await axios.get('https://api.example.com/data', {
    headers: {
      'Authorization': `Bearer ${accessToken}`
    }
  });
  
  return response.data;
}

// Использование
callAPI()
  .then(data => console.log('Данные:', data))
  .catch(error => console.error('Ошибка:', error));

Implicit Flow (устаревший) ⚠️

⚠️ Внимание: Implicit Flow устарел

Implicit Flow был рекомендован для SPA (Single Page Applications), но теперь считается небезопасным. OAuth 2.0 Security Best Practices (RFC 8252) рекомендует использовать Authorization Code Flow с PKCE (Proof Key for Code Exchange) для SPA вместо Implicit Flow.

Implicit Flow пропускал шаг обмена authorization code на токен и возвращал access token напрямую в URL, что создавало риски безопасности.

Refresh Tokens 🔄

Refresh Token — это долгоживущий токен, который используется для получения новых access tokens без повторной авторизации пользователя.

✅ Преимущества Refresh Tokens:

Пример обновления токена

// refresh-token.js
const axios = require('axios');

async function refreshAccessToken(refreshToken) {
  try {
    const response = await axios.post('https://authorization-server.com/token', {
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
      client_id: CLIENT_ID,
      client_secret: CLIENT_SECRET
    });
    
    return {
      access_token: response.data.access_token,
      refresh_token: response.data.refresh_token, // Новый refresh token
      expires_in: response.data.expires_in
    };
  } catch (error) {
    console.error('Ошибка обновления токена:', error);
    throw error;
  }
}

// Middleware для автоматического обновления токена
async function apiCallWithRefresh(url, accessToken, refreshToken) {
  try {
    const response = await axios.get(url, {
      headers: {
        'Authorization': `Bearer ${accessToken}`
      }
    });
    
    return response.data;
  } catch (error) {
    if (error.response && error.response.status === 401) {
      // Токен истек, обновляем
      const newTokens = await refreshAccessToken(refreshToken);
      
      // Повторяем запрос с новым токеном
      const retryResponse = await axios.get(url, {
        headers: {
          'Authorization': `Bearer ${newTokens.access_token}`
        }
      });
      
      return retryResponse.data;
    }
    
    throw error;
  }
}

Реализация на разных языках 💻

PHP (Laravel Passport)

// routes/api.php
Route::middleware('auth:api')->get('/user', function (Request $request) {
    return $request->user();
});

// Получение токена
Route::post('/oauth/token', function (Request $request) {
    $response = Http::asForm()->post('http://your-app.com/oauth/token', [
        'grant_type' => 'authorization_code',
        'client_id' => $request->client_id,
        'client_secret' => $request->client_secret,
        'redirect_uri' => $request->redirect_uri,
        'code' => $request->code,
    ]);
    
    return $response->json();
});

// Использование токена
Route::get('/api/data', function (Request $request) {
    $token = $request->bearerToken();
    
    $response = Http::withToken($token)->get('https://api.example.com/data');
    
    return $response->json();
});

Go (golang.org/x/oauth2)

// main.go
package main

import (
    "context"
    "fmt"
    "golang.org/x/oauth2"
    "golang.org/x/oauth2/clientcredentials"
)

func main() {
    // Client Credentials Flow
    config := &clientcredentials.Config{
        ClientID:     "your-client-id",
        ClientSecret: "your-client-secret",
        TokenURL:     "https://authorization-server.com/token",
        Scopes:       []string{"read:data"},
    }
    
    token, err := config.Token(context.Background())
    if err != nil {
        panic(err)
    }
    
    // Использование токена
    client := config.Client(context.Background())
    resp, err := client.Get("https://api.example.com/data")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    
    fmt.Println("Response:", resp.Status)
}

// Authorization Code Flow
func authorizationCodeFlow() {
    config := &oauth2.Config{
        ClientID:     "your-client-id",
        ClientSecret: "your-client-secret",
        RedirectURL:  "http://localhost:8080/callback",
        Scopes:       []string{"read:user"},
        Endpoint: oauth2.Endpoint{
            AuthURL:  "https://authorization-server.com/authorize",
            TokenURL: "https://authorization-server.com/token",
        },
    }
    
    // Шаг 1: Редирект на авторизацию
    url := config.AuthCodeURL("state", oauth2.AccessTypeOffline)
    fmt.Println("Visit:", url)
    
    // Шаг 4: Обмен кода на токен
    token, err := config.Exchange(context.Background(), "authorization-code")
    if err != nil {
        panic(err)
    }
    
    // Использование токена
    client := config.Client(context.Background(), token)
    resp, err := client.Get("https://api.example.com/user")
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
}

Безопасность OAuth 2.0 🛡️

Безопасность — критически важный аспект OAuth 2.0. Вот основные рекомендации:

✅ Best Practices безопасности:

PKCE (Proof Key for Code Exchange)

PKCE — это расширение OAuth 2.0 для публичных клиентов (мобильные приложения, SPA), которое добавляет дополнительный уровень безопасности.

// pkce-example.js
const crypto = require('crypto');

// Генерация code_verifier и code_challenge
function generatePKCE() {
  // Генерируем случайную строку
  const codeVerifier = crypto.randomBytes(32).toString('base64url');
  
  // Создаем SHA256 hash
  const codeChallenge = crypto
    .createHash('sha256')
    .update(codeVerifier)
    .digest('base64url');
  
  return { codeVerifier, codeChallenge };
}

// Шаг 1: Редирект с code_challenge
const { codeVerifier, codeChallenge } = generatePKCE();
const authUrl = `https://authorization-server.com/authorize?` +
  `client_id=${CLIENT_ID}&` +
  `redirect_uri=${REDIRECT_URI}&` +
  `response_type=code&` +
  `code_challenge=${codeChallenge}&` +
  `code_challenge_method=S256`;

// Шаг 4: Обмен кода на токен с code_verifier
const tokenResponse = await axios.post('https://authorization-server.com/token', {
  grant_type: 'authorization_code',
  code: authorizationCode,
  client_id: CLIENT_ID,
  redirect_uri: REDIRECT_URI,
  code_verifier: codeVerifier // Отправляем оригинальный code_verifier
});

Best Practices 📚

✅ Рекомендации по использованию OAuth 2.0:

Распространенные ошибки

❌ Чего избегать:

Заключение

OAuth 2.0 — это мощный и гибкий протокол для безопасной аутентификации в API. Понимание различных flows и best practices критически важно для создания безопасных приложений.

💡 Ключевые выводы:

Создайте Mock API с OAuth 2.0 за 2 минуты

Хотите протестировать OAuth 2.0 интеграцию? Создайте Mock API с помощью LightBox API и попробуйте различные OAuth flows без необходимости настраивать сложный Authorization Server.

Попробовать бесплатно →
← Вернуться к статьям