Authz Practice
# Задача
Необходимо реализовать схему OAuth2 Authorization Code Grant with PKCE.
Под реализацией подразумевается описание следующих частей системы:
1. REST API, для каждой точки доступа:
* список входных параметров и требований к ним (обязательность, формат и т.п.).
* варианты ответа сервера.
* пошаговый алгоритм обработки запроса.
2. форматов используемых токенов, например, параметров JWT токена и полей в его JSON части.
3. записи в базах данных, создаваемые/обновляемые в алгоритмах из п.1:
* формат записей.
* оценка времени жизни (момента в будущем, когда запись можно безопасно удалять).
* оценка количества записей в зависимости от количества пользователей и максимального количества сессий у каждого.
# Требования
1. Минимизировать количество данных, которые необходимо хранить на стороне сервера авторизации для корректного функционирования системы.
1. Добавить возможность пользователю инвалидировать все токены, полученные в ходе выполнения потока авторизации.
1. Гарантировать, что Refresh Token может быть использован только один раз.
# Решение
Таблицы
application_clients
- client_id
- redirect_uri
auth_codes
- auth_code (unique)
- application_client_id
- valid_till
refresh_token
- user_id
- token (unique)
- valid_till
users
- id
- application_client_id
- secret
- session_invalidated_at
1. Ендпоинт для получения кода доступа
domain.com/api/v1/auth?response_type=code&client_id=CLIENT_ID
domain.com/path - API authorization endpoint
response_type [REQUIRED] - code запрашиваем доступ с помощью кода авторизации.
client_id [REQUIRED] - идентификатор приложения
Валидация
* response_type
-- REQUIRED
-- value = code
* client_id
-- REQUIRED
-- есть в таблице application_clients
При получении запроса
1. Валидация полученых параметров
2. Если запрос невалидный - ответ с ошибкой
3.1. Проверка того, что пользователь выдал права
3.2. Cоздание записи в таблице auth_codes c valid_till=now+1min
3.2. Создаем запись в таблице users
response
В случае успешного получения прав
HTTP/1.1 302 Found
Location: REDIRECT_URL?code=qwerty
В случае неуспешного получения прав
HTTP/1.1 302 Found
Location: REDIRECT_URL?error=access_denied
2. Ендпоинт для получения токена
POST /token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&code=qwerty&client_id=client_id
Валидация
* grant_type
-- REQUIRED
-- value = authorization_code
* code
-- REQUIRED
-- value = authorization_code из таблицы auth_codes с valid_till < now() and client_id = request.client_id
* client_id
-- REQUIRED
-- есть в таблице application_clients
При получении запроса
1. Валидация полученых параметров
2.1. Если данные невалидны - ответ с ошибкой
2.2.1. Если данные валидны
2.2.2. Удаляем запись из таблицы auth_codes
2.2.3. Создаем запись в таблице refresh_tokens
2.2.5. Генерируем jwt token c использованием users.secret [Структура-токена](https://hackmd.io/uud4W9PATJKwchbHp19lYw?both#Структура-токена)
response
В случае неуспешного запроса
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"access_token",
"token_type":"bearer"
"expires_in":3600,
"refresh_token":"refresh_token"
}
В случае успешного запроса
HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"error":"invalid_request"
}
Возможные error
- invalid_request
- unauthorized_client
- access_denied
- unsupported_response_type
- server_error
- temporarily_unavailable
3. Ендпоинт для инвалидации токена
POST /invalidate-token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
refresh_token=refresh_token&access_token=access_token&client_id=client_id
* access_token
-- REQUIRED
-- value = access_token
* refresh_token
-- REQUIRED
-- value = refresh_token
* client_id
-- REQUIRED
-- есть в таблице application_clients
* access_token validation
При получении запроса
1. Валидация полученых параметров
2.1. Если данные невалидны - ответ с ошибкой
2.2.1. Если данные валидны
2.2.2. access_token и refresh_token валидны
2.2.3. По user_id (полученому из токена) удаляем записи в таблице refresh_tokens и users.tokens_invalidated_at=now()
4. Ендпоинт для получения token по refresh_token
POST /token-by-refresh-token HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
refresh_token=refresh_token&client_id=client_id
Валидация
* refresh_token
-- REQUIRED
-- value = refresh_token из таблицы refresh_tokens client_id = request.client_id and user_id = request.user_id
* client_id
-- REQUIRED
-- есть в таблице application_clients
При получении запроса
1. Валидация полученых параметров
2.1. Если данные невалидны - ответ с ошибкой
2.2.1. Если данные валидны
2.2.2. refresh_token валидный
2.2.3. Удаляем найденную запись из refresh_tokens
2.2.4. Создаем запись в таблице refresh_tokens
response
В случае успешного запроса
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"access_token":"access_token",
"token_type":"bearer"
"expires_in":3600,
"refresh_token":"refresh_token"
}
В случае неуспешного запроса
HTTP/1.1 400 Bad Request
Content-Type: application/json;charset=UTF-8
Cache-Control: no-store
Pragma: no-cache
{
"error":"invalid_request"
}
### Структура токена
JWT состоит из трех частей: заголовок header, полезные данные payload и подпись signature.
header = { "alg": "HS256", "typ": "JWT"}
payload = { "userId": "userId", "exp": unixExpTime, "createdAt": "createdAt" }
```
secretKey = users.secret
unsignedToken = base64urlEncode(header) + '.' + base64urlEncode(payload)
signature = HMAC-SHA256(unsignedToken, secretKey)
```
валидация токена
- проверка signature
- проверка unixExpTime (токен активный)
- проверка что userId есть в таблице users
- проверка что createdAt > users.session_ivalidated_at
хранение токенов
- access_token храним в Cookie
- refresh_token храним в Local storage
### Удаление данных
Можна удалять
- Из таблицы auth_codes данные можно удалять когда valid_till > now(). В этой таблице хранятся только актуальные коды
- из таблицы refresh_tokens удаляются после первого использования
### Данные в таблицах
application_clients - сколько приложений столько и записей
auth_codes - если все юзера получают auth code в один момент, то количество записей равно количеству юзеров. Но после использования или протухания auth_code он удаляется из базы.
refresh_token - количество юзеров минус невалидные токены
users - количество юзеров