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 для предотвра