JWT-Secured Authorization Request (JAR)#
В Blitz Identity Provider реализован режим JWT-Secured Authorization Request (JAR), расширение OAuth 2.0, которое упаковывает параметры запроса на аутентификацию в единый подписанный и зашифрованный JWT-токен. Данный режим используется для повышения безопасности, предотвращения несанкционированного доступа и дополнительной проверки источника запроса.
Одно из преимуществ JAR – возможность скрыть содержимое запроса от пользователя и промежуточных узлов. В случае передачи в составе запроса идентификатора пользователя (bip_user_id), он не будет доступен пользователю, не сохранится в логах балансировщика и пр.
Механизм работы JAR#
Примечание
Шифрование запроса осуществляется по спецификации JWE.
Для реализации JAR необходимо передавать параметры запроса на /ae в поле request в виде зашифрованного json-объекта. При использовании JAR в query-параметрах запроса должны передаваться только параметр request и client_id. Остальные query-параметры (при их наличии) будут проигнорированы. Query-параметр client_id должен совпадать со значением client_id внутри объекта request.
Шаги формирования query-параметра request:
Формируется json-строка из параметров запроса (
JWS_Payload).Формируется строка в формате
JWScheaderравным{"alg":"none"},payloadс json-параметрами и пустым блоком подписи:BASE64URL(UTF8(JWS_Header)) || '.' ||BASE64URL(JWS_Payload)||'.'
Формируется строка в формате
JWEcheaderиpayloadравным с полученной на шаге 2 JWS-строкой:BASE64URL(UTF8(header)) ||'.' || BASE64URL(JWE Encrypted Key) || '.' || BASE64URL(JWE Initialization Vector) || '.' || BASE64URL(JWE Ciphertext) || '.' || BASE64URL(JWE Authentication Tag)
Сгенерированный для шифрования данных ключ (ContentEncryption Key(CEK)) должен быть зашифрован на публичном ключе, поддерживающем алгоритм RSA-OAEP (JWE Encrypted Key).
Данные должны быть зашифрованы по одному из алгоритмов A128GCM/A192GCM/A256GCM.
Поддерживаемые алгоритмы шифрования добавлены в сервис OIDC Discovery: /blitz/oauth/.well-known/openid-configuration.
{
"request_object_encryption_alg_values_supported": ["RSA-OAEP"],
"request_object_encryption_enc_values_supported": ["A128GCM","A192GCM","A256GCM"]
}
Важно
Ключ шифрования доступен по адресу: /blitz/oauth/.well-known/jwks (ключ с "use": "enc").
Пример формирования запроса на аутентификацию#
Сформировать
jsonиз параметров. Это набор стандартных параметров и идентификатора пользователя (передается в параметреbip_user_id– опционален):{ "scope": "blitz_api_usec_chg openid", "response_type": "code", "state": "342a2c0c-d9ef-4cd6-b328-b67d9baf6a7f", "client_id": "localhost_test", "nonce": "123456", "redirect_uri": "http://localhost/success", "bip_user_id": "test@test.ru" }
Сформировать
jwscheader-{"alg":"none"}.Сформировать
jwecheader-{"kid":"default","alg":"RSA-OAEP","enc":"A256GCM"}.
Пример скрипта на Python, реализующем формирование параметра request:
# Используемые функции
import json
import base64
import os
from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from cryptography.hazmat.primitives.asymmetric import rsa, padding
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash
from cryptography.hazmat.backends import default_backend
def base64url_encode(data: bytes) -> str:
return base64.urlsafe_b64encode(data).rstrip(b'=').decode('ascii')
def base64url_decode(data: str) -> bytes:
padding = '=' * (4 - len(data) % 4)
return base64.urlsafe_b64decode(data + padding)
def generate_cek(length: int = 256) -> bytes:
return os.urandom(length // 8)
def encrypt_cek(pub_key, cek: bytes) -> bytes:
return pub_key.encrypt(
cek,
padding.OAEP(
mgf=padding.MGF1(algorithm=hashes.SHA1()),
algorithm=hashes.SHA1(),
label=None
)
)
def encrypt_content(cek: bytes, plaintext: bytes, aad: bytes) -> (bytes, bytes, bytes):
iv = os.urandom(12) # 96 bits
aesgcm = AESGCM(cek)
encrypted = aesgcm.encrypt(iv, plaintext, aad)
ciphertext = encrypted[:-16]
tag = encrypted[-16:]
return ciphertext, iv, tag
def parse_jwk_rsa_public_key(jwk: dict):
n = int.from_bytes(base64url_decode(jwk['n']), 'big')
e = int.from_bytes(base64url_decode(jwk['e']), 'big')
pub_numbers = rsa.RSAPublicNumbers(e, n)
return pub_numbers.public_key(default_backend())
def generate_request_object(jwk_json: str, json_params: str) -> str:
jwk = json.loads(jwk_json)
kid = jwk["kid"]
pub_key = parse_jwk_rsa_public_key(jwk)
# Construct JWS header and payload (alg: none)
jws_header = base64url_encode(b'{"alg":"none"}')
jws_payload = base64url_encode(json_params.encode("utf-8"))
jws_token = f"{jws_header}.{jws_payload}."
# Generate CEK and encrypt content
cek = generate_cek()
jwe_header = json.dumps({
"alg": "RSA-OAEP",
"enc": "A256GCM",
"kid": kid
}, separators=(',', ':')).encode("utf-8")
jwe_header_b64 = base64url_encode(jwe_header)
#aad = jwe_header
aad = jwe_header_b64.encode("utf-8")
plaintext = jws_token.encode("utf-8")
ciphertext, iv, tag = encrypt_content(cek, plaintext, aad)
encrypted_cek = encrypt_cek(pub_key, cek)
return f"{jwe_header_b64}.{base64url_encode(encrypted_cek)}.{base64url_encode(iv)}.{base64url_encode(ciphertext)}.{base64url_encode(tag)}"
# Вызов функций
jwk_json = '{"keys":[{"kty":"RSA","n":"yXQellql..EQ","e":"AQAB","use":"sig","alg":"RS256","kid":"default","x5c":["MI..6Oxe"],"x5t":"Tm..oy4"},{"kty":"RSA","n":"yQ..cvQ","e":"AQAB","use":"enc","alg":"RSA-OAEP","kid":"kid","x5c":["MI..W0="],"x5t":"Te..X4"}]}'
jwk_dict = json.loads(jwk_json)
enc_key = next(k for k in jwk_dict["keys"] if k["use"] == "enc")
jwk_json_enc = json.dumps(enc_key)
json_params = json.dumps({
"scope": "blitz_api_usec_chg openid",
"response_type": "code",
"client_id": "pp-my.my.ru",
"redirect_uri": "https://my-tech.my.ru/oauth",
"bip_user_id": "c3a37d2e-8dcd-49d4-943e-d699b6a86907"
})
request_object = generate_request_object(jwk_json_enc, json_params)
print(request_object)
Пример итогового запроса на аутентификацию:
https://login.company.ru/blitz/oauth/ae?client_id=localhost_test&request=eyJ..WJ2g
Возвращаемые OAuth-ошибки#
Возможные OAuth-ошибки:
если параметр
requestнеправильно сформирован илиclient_idпараметр не соответствует зашифрованному значению вrequest:error=invalid_request_object&error_description=Wrong request object signature or encryption
если в параметре
bip_user_idзадан некорректныйsubject_id:error=invalid_request&error_description=Required+user+not+found
имеются методы (вход по электронной подписи, вход через внешний поставщик идентификации, вход по QR-коду), в которых фиксация пользователя не предусмотрена. Если в процессе входа с использованием одного из этих методов пользователь, явно заданный в JAR, будет изменен, то вход завершится с ошибкой
err.selected_subject_not_match. В качестве параметра передаетсяdisplayNameвыбранного пользователя. Текст ошибки по умолчанию:err.selected_subject_not_match=Вам необходимо войти под пользователем: {}
При входе по внешним поставщикам (
externalIdps), когда происходит переключение на режим привязки (extBinding) и выбранная в процессе входа учетная запись отличается от заданного пользователя, то выдается ошибкаlogin.methods.externalIdps.err.frozen_subject_not_match. Текст ошибки по умолчанию:login.methods.externalIdps.err.frozen_subject_not_match=Вам необходимо войти под пользователем: {}