Нативные приложения через edge — Versola Docs
VersolaVersola/docs
До 1.00.7.1versola.kzGitHub

Нативные приложения через edge

Как мобильное или десктопное приложение входит через нативные эндпоинты edge с PAR и DPoP-ключом на устройстве, каким должен быть клиент и что возвращает каждый вызов.

Нативное приложение не может хранить секрет клиента, а профиль безопасности 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Приложение → edgePOST /native/start/{clientId}
2. ВходСистемный браузер → authGET /authorize?client_id=...&request_uri=...
3. Возвратauth → приложениеПеренаправление на App Link с code, state и iss
4. CompleteПриложение → edgePOST /native/complete/{clientId}
5. РаботаПриложение → edge/resources/{resourceId}/{path} с Authorization: DPoP
6. ОбновлениеПриложение → edgePOST /native/token/{clientId}
7. ВыходПриложение → edgePOST /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 равен trueedge отправляет запрос в /par, поэтому параметры не проходят через браузер
Redirect URI — App Link на httpsПользовательскую схему может занять любое приложение. Тенант fapi2 их отклоняет
Тенант клиента назначен этому edgecentral синхронизирует только клиентов тенантов этого 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-кодировке
400invalid_dpop_proofProof нет, их больше одного, проверка не прошла или proof для start уже использован
400invalid_grantBlob выдан не этим edge, для другого клиента или истёк. state или iss не совпали. Ключ proof отличается от ключа, с которого начался поток
400unsupported_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) и разместите файл ассоциации на своём домене.

Что дальше