Автентифікація ADP: Як не прокидатися вночі від 401 помилок
Знайома картина? Синхронізація зарплат працює ідеально, а потім о пів на третю ночі телефон розривається від дзвінків. Інтеграція лежить. Усі API запити падають з 401.
Токен здох. Знов.
Після 17 нічних аварій через токени я нарешті зібрала автентифікацію, яка не підводить. Розповім, як уникнути цього жахіття.
П'ять хвилин і готово 🚀
Схема проста: шлете свої дані через OAuth 2.0, отримуєте токен на годину.
Тільки ось прикол: проста реалізація обов'язково зламається. Покажу, що насправді працює в бойовій.
Бойова версія 💪
Після сотні нічних дзвінків зібрав автентифікацію, що не ламається. З retry логікою, нормальною обробкою помилок та TypeScript:
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios';
import * as https from 'https';
import * as fs from 'fs';
import pRetry from 'p-retry';
interface TokenResponse {
access_token: string;
token_type: string;
expires_in: number;
scope?: string;
}
interface ADPTokenManagerConfig {
clientId: string;
clientSecret: string;
certPath?: string;
keyPath?: string;
environment?: 'production' | 'sandbox';
retryAttempts?: number;
tokenBufferSeconds?: number;
}
class ADPTokenManager {
private clientId: string;
private clientSecret: string;
private certPath?: string;
private keyPath?: string;
private token: string | null = null;
private tokenExpiry: number | null = null;
private refreshPromise: Promise<void> | null = null;
private axiosInstance: AxiosInstance;
private tokenBufferMs: number;
private retryAttempts: number;
private tokenEndpoint: string;
constructor(config: ADPTokenManagerConfig) {
this.clientId = config.clientId;
this.clientSecret = config.clientSecret;
this.certPath = config.certPath;
this.keyPath = config.keyPath;
this.retryAttempts = config.retryAttempts || 3;
this.tokenBufferMs = (config.tokenBufferSeconds || 300) * 1000; // 5 хвилин за замовчуванням
// Встановити кінцеву точку на основі середовища
const baseUrl =
config.environment === 'sandbox'
? 'https://accounts.adp.com'
: 'https://accounts.adp.com';
this.tokenEndpoint = `${baseUrl}/auth/oauth/v2/token`;
// Налаштувати axios з SSL при необхідності
const axiosConfig: AxiosRequestConfig = {
timeout: 30000,
headers: {
'User-Agent': 'ADP-Integration/1.0',
},
};
if(this.certPath && this.keyPath) {
axiosConfig.httpsAgent = new https.Agent({
cert: fs.readFileSync(this.certPath),
key: fs.readFileSync(this.keyPath),
rejectUnauthorized: true,
});
}
this.axiosInstance = axios.create(axiosConfig);
}
async getToken(): Promise<string> {
// Якщо ми вже оновлюємо, чекаємо цього
if(this.refreshPromise) {
await this.refreshPromise;
if (!this.token) throw new Error('Оновлення токена не вдалося');
return this.token;
}
// Перевірити, чи токен ще дійсний (з буфером)
if (
this.token &&
this.tokenExpiry &&
this.tokenExpiry > Date.now() + this.tokenBufferMs
) {
return this.token;
}
// Оновити токен
this.refreshPromise = this.refreshToken();
try {
await this.refreshPromise;
if (!this.token) throw new Error('Оновлення токена не вдалося');
return this.token;
} finally {
this.refreshPromise = null;
}
}
private async refreshToken(): Promise<void> {
const operation = async () => {
console.log('Оновлення токена ADP...');
const response = await this.axiosInstance.post<TokenResponse>(
this.tokenEndpoint,
new URLSearchParams({
grant_type: 'client_credentials',
client_id: this.clientId,
client_secret: this.clientSecret,
}),
{
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
}
);
this.token = response.data.access_token;
// Встановити закінчення з буфером
this.tokenExpiry =
Date.now() + response.data.expires_in * 1000 - this.tokenBufferMs;
console.log(
'Токен успішно оновлено, закінчується:',
new Date(this.tokenExpiry)
);
};
try {
await pRetry(operation, {
retries: this.retryAttempts,
onFailedAttempt: error => {
console.warn(
`Спроба оновлення токена ${error.attemptNumber} не вдалася. Залишилося ${error.retriesLeft} спроб.`,
error.message
);
},
minTimeout: 1000,
maxTimeout: 5000,
randomize: true,
});
} catch(error: any) {
console.error('Оновлення токена не вдалося після всіх спроб:', error.message);
// Логувати детальну інформацію про помилку для відлагодження
if(error.response) {
console.error('Статус відповіді:', error.response.status);
console.error('Дані відповіді:', error.response.data);
}
// Не обнуляти існуючий токен - може ще працювати короткочасно
throw new Error(`Оновлення токена ADP не вдалося: ${error.message}`);
}
}
// Використовуйте це для всіх викликів API
async apiCall<T = any>(
url: string,
options: AxiosRequestConfig = {}
): Promise<T> {
const token = await this.getToken();
const response = await pRetry(
async () => {
return await this.axiosInstance.request<T>({
...options,
url,
headers: {
...options.headers,
Authorization: `Bearer ${token}`,
},
});
},
{
retries: this.retryAttempts,
onFailedAttempt: async error => {
// Якщо отримуємо 401, намагаємося оновити токен
if(error.response?.status <mark>= 401 && error.attemptNumber </mark>= 1) {
console.log('Отримано 401, примусове оновлення токена...');
this.tokenExpiry = 0; // Примусове оновлення
await this.getToken();
}
},
}
);
return response.data;
}
// Утилітарний метод для перевірки дійсності токена
isTokenValid(): boolean {
return !!(
this.token &&
this.tokenExpiry &&
this.tokenExpiry > Date.now() + this.tokenBufferMs
);
}
// Примусове оновлення токена (корисно для тестування)
async forceRefresh(): Promise<void> {
this.tokenExpiry = 0;
await this.getToken();
}
}
Як юзати
Створюєте менеджер з вашими даними та налаштуваннями оточення. Далі він сам стежить за токенами і дає простий метод apiCall для всіх звернень до ADP.
Безпека без параної 🔒
1. Де тримати секрети
У продакшені тільки менеджери секретів (AWS Secrets Manager, HashiCorp Vault). Ніяких паролів у коді! Локально використовуйте .env файли з нормальними правами доступу.
2. Сертифікати
Сертифікати краще тримати в base64 у змінних оточення, а не файлами. У продакшені обов'язково перевіряйте SSL і юзайте TLS 1.2+. Не забувайте про проміжні сертифікати в ланцюжку.
3. Токени
На сервері тримайте токени тільки в пам'яті. У кластері використовуйте зашифрований Redis з нормальними ключами. Токени в логах та БД - табу.
SSL сертифікати і як з ними жити 🔐
Next Gen хоче ПОВНИЙ ланцюжок сертифікатів у PEM форматі - ваш + проміжні + кореневий, все склеєне.
Дорога помилка на $10K
Один клієнт 2 тижні бився з "недійсним сертифікатом". Виявилося, скопіював із PDF і отримав "розумні" лапки замість звичайних. Використовуйте тільки ASCII лапки, ніяких фігурних із Word'а!
Реальна історія. $10K за відлагодження лапок. 🤦♂️
Професійна обробка закінчення токенів
Помилка новачка: Отримання токена на початку тривалої операції. Після 45 хвилин обробки тисяч записів токен закінчується, і кожен наступний виклик завершується помилкою 401.
Перевірений у бойових умовах підхід: Використовуйте менеджер токенів для кожного виклику API. Він автоматично оновлює токени до їх закінчення та обробляє логіку повторних спроб. Обробляйте записи пакетами з правильним обмеженням швидкості між запитами.
Стан гонки, про який ніхто не говорить
Кілька запитів запускаються одночасно? Без захисту ви будете оновлювати токен 10 разів одночасно. Наш паттерн refreshPromise запобігає цьому - всі запити чекають завершення однієї операції оновлення.
Обмеження швидкості та моніторинг у продакшені 📋
Для виробничих середовищ розширте базовий менеджер токенів обмеженням швидкості (максимум 50 запитів/секунду), збором метрик та кінцевими точками перевірки стану. Відстежуйте виклики API, час відповіді та частоту помилок для панелей моніторингу.
Налаштування змінних середовища
Налаштуйте ці змінні середовища: ADP_CLIENT_ID, ADP_CLIENT_SECRET, ADP_ENVIRONMENT (production/sandbox). Для Next Gen додайте ADP_CERT_BASE64 та ADP_KEY_BASE64 для сертифікатів. Встановіть TOKEN_BUFFER_SECONDS=300 та RATE_LIMIT_REQUESTS_PER_SECOND=50 для оптимальної продуктивності.
Поширені помилки автентифікації (та виправлення)
Помилка 1: "Недійсний клієнт"
{
"error": "invalid_client",
"error_description": "Автентифікація клієнта не вдалася"
}
Причини та виправлення:
- Використання виробничих облікових даних проти URL пісочниці (або навпаки)
- Неправильний ID/секрет клієнта (перевірте опечатки, пробіли)
- Облікові дані не закодовані правильно в URL
Помилка 2: "Помилка перевірки сертифіката"
Error: unable to verify the first certificate
Error: self signed certificate in certificate chain
Причини та виправлення:
- Відсутні проміжні сертифікати в ланцюжку
- Неправильний формат сертифіката (потрібен PEM, не DER)
- Невідповідність сертифіката/ключа
- Прострочені сертифікати
Помилка 3: "Токен працює, потім перестає"
// Працює 55 хвилин, потім все ламається
Виправлення: Токени закінчуються ТОЧНО через 1 годину. Реалізуйте правильну логіку оновлення.
Помилка 4: "Перевищено ліміт швидкості"
{
"error": "rate_limit_exceeded",
"message": "Занадто багато запитів"
}
Виправлення: Реалізуйте експоненційну затримку та дотримуйтесь лімітів швидкості (максимум 50 запитів/сек).
Помилка 5: "Помилка SSL handshake"
Error: Client network socket disconnected before secure TLS connection was established
Причини та виправлення:
- Firewall блокує вихідний HTTPS (порт 443)
- Корпоративний проксі перехоплює SSL
- Застаріла версія TLS (використовуйте TLSv1.2+)
Помилка 6: "Таймаут мережі"
Error: timeout of 30000ms exceeded
Виправлення: Збільште таймаут для запитів токенів, реалізуйте логіку повторних спроб.
З цією перевіреною в бойових умовах реалізацією ваша інтеграція буде надійно обробляти виробничий трафік без тих страшних інцидентів о 2 ранку.
Додаткові ресурси
- Портал розробників ADP - Офіційна документація та довідник API
- OAuth 2.0 RFC - Повна специфікація OAuth
- Валідація SSL сертифікатів - Розуміння ланцюжків сертифікатів
Далі: [НЕЗАБАРОМ] Ліміти швидкості ADP: Урок за $50K - Тепер, коли ви автентифіковані, давайте переконаємося, що вас не заблокують.