Нативное приложение не может хранить секрет клиента, а профиль безопасности fapi2 публичных клиентов не допускает. edge решает это разделением роли клиента. edge аутентифицируется в auth как клиент, по сертификату на mutual-TLS-слушателе. На устройстве лежит только DPoP-ключ, и именно он привязывает токены к отправителю. На странице описаны четыре эндпоинта, которые вызывает приложение. Нужен edge с блоком native (Конфигурация edge) и нативный клиент (Как собрать поток нативного мобильного приложения).
Последовательность
sequenceDiagram
autonumber
participant M as Mobile app
participant E as edge
participant A as auth
participant U as Upstream
M->>E: POST /native/start/{clientId} + DPoP proof (device key)
E->>A: POST /par as the client (PKCE, state, dpop_jkt)
E-->>M: request_uri, sealed blob
note over M,A: System browser opens auth /authorize, user signs in
A-->>M: Redirect to the App Link with code and state
M->>E: POST /native/complete/{clientId} + blob + DPoP proof
E->>A: POST /token (code + PKCE verifier from blob)
A-->>E: DPoP-bound access_token + refresh_token
E-->>M: Token response relayed unchanged
M->>M: Store tokens and device key (Keychain / Keystore)
note over M,E: Session active — app sends DPoP-bound calls
M->>E: GET /resources/api/v1/data + Authorization: DPoP + DPoP proof
E->>E: Check revocation, token binding, permissions and CEL rules
E->>U: Forward + inject Authorization header
U-->>E: 200 OK
E-->>M: 200 OK
note over M,E: Refresh
M->>E: POST /native/token/{clientId} (refresh_token) + DPoP proof
E->>A: POST /token as the client
A-->>E: new tokens
E-->>M: new tokens
| Шаг | Кто | Вызов |
|---|---|---|
| 1. Start | Приложение → edge | POST /native/start/{clientId} |
| 2. Вход | Системный браузер → auth | GET /authorize?client_id=...&request_uri=... |
| 3. Возврат | auth → приложение | Перенаправление на App Link с code, state и iss |
| 4. Complete | Приложение → edge | POST /native/complete/{clientId} |
| 5. Работа | Приложение → edge | /resources/{resourceId}/{path} с Authorization: DPoP |
| 6. Обновление | Приложение → edge | POST /native/token/{clientId} |
| 7. Выход | Приложение → edge | POST /native/revoke/{clientId} |
edge не хранит запись о начатом входе. То, что он сохранил бы, передаётся в запечатанном blob (AES-256-GCM, ключ native.blob-key): в нём ID клиента, PKCE-верификатор, state, отпечаток ключа устройства, redirect URI и срок действия. Токены у edge не лежат: единственное хранилище токенов — auth.
Что хранится на устройстве
- Пара ключей DPoP, созданная один раз и хранящаяся в системном хранилище ключей (Keychain, Android Keystore). Сделайте её неизвлекаемой. Алгоритм должен приниматься сервером:
ES256иPS256входят в метаданные по умолчанию, а клиент может сузить набор черезdpopSigningAlgs. ПредпочтителенES256. - Токены: access token (
token_typeравенDPoP) и refresh token. В access token естьcnf.jkt— отпечаток ключа устройства, — и нет отпечатка сертификата. Без ключа токен злоумышленнику бесполезен. - Blob — только между шагами 1 и 4.
Каждый вызов /native/* несёт один заголовок DPoP: JWT, подписанный ключом устройства, с htm (POST), htu, iat и уникальным jti. htu у разных вызовов разный:
| Вызов | htu |
|---|---|
/native/start/{clientId} | URL этого эндпоинта на edge, построенный из edge-url |
/native/complete/{clientId}, /native/token/{clientId} | token_endpoint из ответа start |
/native/revoke/{clientId} | revocation_endpoint из ответа start |
Последние два — эндпоинты auth на его mutual-TLS-адресе, и edge пересылает им proof байт в байт. Proof для start адресован самому edge, дальше не пересылается и может быть использован один раз.
Если требуется nonce, ответ — 401 с WWW-Authenticate: DPoP error="use_dpop_nonce" и заголовком DPoP-Nonce. Постройте новый proof с этим nonce и повторите запрос один раз. Nonce для proof, адресованного auth, выдаёт auth, а edge передаёт его без изменений.
Требования к клиенту
edge обслуживает клиента на /native/*, только если выполнено всё перечисленное. Иначе ответ 404, причём отсутствующий клиент и неверно настроенный выглядят одинаково.
| Требование | Зачем |
|---|---|
applicationType равен native | Помечает приложение, за которое отвечает edge на стороне устройства |
authMethod равен tls_client_auth, у edge есть сертификат (Клиентские сертификаты) | edge аутентифицируется как клиент на mutual-TLS-слушателе auth |
dpopBoundAccessTokens равен true | Ключ устройства привязывает токены |
certificateBoundAccessTokens равен false | Сертификат edge общий для всех установок, он не может идентифицировать одно устройство |
requirePushedAuthorizationRequests равен true | edge отправляет запрос в /par, поэтому параметры не проходят через браузер |
Redirect URI — App Link на https | Пользовательскую схему может занять любое приложение. Тенант fapi2 их отклоняет |
Тенант клиента назначен этому edge | central синхронизирует только клиентов тенантов этого edge |
PKCE с S256 включён всегда. Подписанные request-объекты не применяются, потому что у распространяемого бинарника нет собственного закрытого ключа. Мастер в консоли Mobile or desktop app выставляет всё это сам.
Вызовы
Все четыре вызова принимают тела application/x-www-form-urlencoded и отвечают с Cache-Control: no-store.
Start
curl -X POST https://edge.example.com/native/start/acme-mobile \
-H "DPoP: <proof for start>" \
-d "scope=openid offline_access" \
-d "redirect_uri=https://app.example.com/callback"
Допустимые параметры тела: redirect_uri (необязателен, если у клиента зарегистрирован ровно один), scope, nonce, acr_values, prompt, login_hint, ui_locales, max_age, claims, resource (можно повторять) и authorization_details. Остальное игнорируется, а повтор любого параметра, кроме resource, даёт invalid_request. edge сам добавляет response_type=code, PKCE-challenge, state и dpop_jkt и отправляет запрос в auth.
{
"client_id": "acme-mobile",
"request_uri": "urn:ietf:params:oauth:request_uri:...",
"expires_in": 600,
"authorization_endpoint": "https://id.example.com/authorize",
"state": "...",
"blob": "...",
"token_endpoint": "https://id.example.com:8083/token",
"revocation_endpoint": "https://id.example.com:8083/revoke"
}
expires_in — меньшее из времени жизни request_uri в auth и native.blob-ttl (по умолчанию 600 s). Откройте системный браузер на authorization_endpoint, передав client_id и request_uri в query. Встроенный web view не используйте.
Complete
Когда браузер вернулся на ваш App Link, прочитайте из него code, state и iss.
curl -X POST https://edge.example.com/native/complete/acme-mobile \
-H "DPoP: <proof for token_endpoint>" \
-d "code=<code>" -d "state=<state>" -d "iss=<iss>" -d "blob=<blob>"
edge вскрывает blob и проверяет, что он его собственный, выдан для этого клиента и не истёк, что state совпадает, что iss — это сервер авторизации, куда был отправлен запрос (RFC 9207), и что proof подписан тем же ключом, с которого начался поток. Затем он обменивает код в auth с PKCE-верификатором из blob и без изменений возвращает токен-ответ auth. Заголовок Idempotency-Key пересылается в auth.
Вызов API
Используйте access token в Authorization: DPoP <access token> и proof для URL edge. Маршрут и правила политики описаны в разделе Политики эндпоинтов.
Refresh
curl -X POST https://edge.example.com/native/token/acme-mobile \
-H "DPoP: <proof for token_endpoint>" \
-H "Idempotency-Key: <unique per rotation>" \
-d "refresh_token=<refresh token>"
Обслуживается только grant refresh_token. grant_type необязателен, любое другое значение даёт unsupported_grant_type. scope и resource пересылаются. Idempotency-Key делает ротацию безопасной для повтора после потерянного ответа.
Revoke
curl -X POST https://edge.example.com/native/revoke/acme-mobile \
-H "DPoP: <proof for revocation_endpoint>" \
-d "token=<refresh token>" -d "token_type_hint=refresh_token"
Это отзыв по RFC 7009, ответ auth возвращается как есть. Отозванный refresh token при следующем обновлении даёт invalid_grant.
Ошибки
| Статус | Тело | Когда |
|---|---|---|
400 | {"error": "invalid_request", "error_description": "..."} | Параметр отсутствует или повторён, redirect_uri не зарегистрирован, тело не в form-кодировке |
400 | invalid_dpop_proof | Proof нет, их больше одного, проверка не прошла или proof для start уже использован |
400 | invalid_grant | Blob выдан не этим edge, для другого клиента или истёк. state или iss не совпали. Ключ proof отличается от ключа, с которого начался поток |
400 | unsupported_grant_type | /native/token с grant, отличным от refresh_token |
401 | нет, WWW-Authenticate: DPoP error="use_dpop_nonce", DPoP-Nonce | Нужен nonce. Повторите один раз |
404 | нет | Неизвестный клиент, клиент, не отвечающий требованиям, или нет блока native |
Объект ошибки соответствует разделу 5.2 RFC 6749. Отказы от auth, например неверный redirect_uri на /par или invalid_grant для использованного кода, возвращаются в том виде, в каком их прислал auth.
Конфигурация
Блок native и его ключи описаны в разделе Конфигурация edge. Ему требуется блок dpop, иначе edge не запустится. Со стороны auth нужен слушатель mutual-tls, в файле trusted-certificates которого есть УЦ, подписывающий сертификат edge.
Ограничения
/native/tokenобслуживает только обновление. Другие grant черезedgeнедоступны.edgeне хранит токены, поэтому не может перечислить или отозвать токены устройства за вас. Используйте просмотр refresh-токенов и сессий пользователя в консоли или отзыв вauth.- Тенант fapi2 не допускает перенаправления на пользовательскую схему. Рассчитывайте на App Links (Android) или Universal Links (iOS) и разместите файл ассоциации на своём домене.
Что дальше
- Как собрать поток нативного мобильного приложения: зарегистрируйте клиента и выполните эти вызовы.
- Клиентские сертификаты edge: сертификат, который
edgeиспользует передauth. - Токены с привязкой к отправителю: DPoP подробно.