JWT-Secured Authorization Request (JAR)#

В Blitz Identity Provider реализован режим JWT-Secured Authorization Request (JAR), расширение OAuth 2.0, которое упаковывает параметры запроса на аутентификацию в единый подписанный и зашифрованный JWT-токен. Данный режим используется для повышения безопасности, предотвращения несанкционированного доступа и дополнительной проверки источника запроса.

Механизм работы JAR#

Примечание

Шифрование запроса осуществляется по спецификации JWE.

Для реализации JAR необходимо передавать параметры запроса на /ae в поле request в виде зашифрованного json-объекта. При использовании JAR в query-параметрах запроса должны передаваться только параметр request и client_id. Остальные query-параметры (при их наличии) будут проигнорированы. Query-параметр client_id должен совпадать со значением client_id внутри объекта request.

Шаги формирования query-параметра request:

  1. Формируется json-строка из параметров запроса (JWS_Payload).

  2. Формируется строка в формате JWS c header равным {"alg":"none"}, payload с json-параметрами и пустым блоком подписи:

    BASE64URL(UTF8(JWS_Header)) || '.' ||BASE64URL(JWS_Payload)||'.'
    
  3. Формируется строка в формате JWE c header и 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").

Пример формирования запроса на аутентификацию#

  1. Сформировать 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"
    }
    
  2. Сформировать jws c header - {"alg":"none"}.

  3. Сформировать jwe c header - {"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":"yXQellqlTy7fzJbw5PDCdeTWTILS0yokltrlf_G3Eg3MikrgkdLMRmRco4ZVOr3gfWZW1yBkRyuo05nOwQRL7myTgesJJOH6pamzxFgF9TebIY3ce2QazCxtO2FoUrw9jHmz3XlzdD82PhshFOadjVWDCYq-WbI5wme3RxT_8uwgzDJ-pFi4lcnhglLnf5a8ExROVPwFZJOqz_DWvKipwW7kvy-7H-_bB1Q6uZF7-KJEx2ulZwCRmSsQbMYOOkjwJQZC8xVwyU7Q-F00ypKDVmyhrW_LUv1jzqUp-Aywc2fj0ogoPATRWOlyAUraI_KshuWw_SWOdguH0Dbi4krsEQ","e":"AQAB","use":"sig","alg":"RS256","kid":"default","x5c":["MIIDBTCCAe2gAwIBAgIEZ9qZdzANBgkqhkiG9w0BAQsFADAzMREwDwYDVQQDEwhCbGl0eklkUDEeMBwGA1UEAwwVandzX3JzMjU2X3JzYV9kZWZhdWx0MB4XDTE4MDcxNzE1MzQ0OFoXDTI4MDcxNDE1MzQ0OFowMzERMA8GA1UEAxMIQmxpdHpJZFAxHjAcBgNVBAMMFWp3c19yczI1Nl9yc2FfZGVmYXVsdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMl0HpZapU8u38yW8OTwwnXk1kyC0tMqJJba5X/xtxINzIpK4JHSzEZkXKOGVTq94H1mVtcgZEcrqNOZzsEES+5sk4HrCSTh+qWps8RYBfU3myGN3HtkGswsbTthaFK8PYx5s915c3Q/Nj4bIRTmnY1VgwmKvlmyOcJnt0cU//LsIMwyfqRYuJXJ4YJS53+WvBMUTlT8BWSTqs/w1ryoqcFu5L8vux/v2wdUOrmRe/iiRMdrpWcAkZkrEGzGDjpI8CUGQvMVcMlO0PhdNMqSg1Zsoa1vy1L9Y86lKfgMsHNn49KIKDwE0VjpcgFK2iPyrIblsP0ljnYLh9A24uJK7BECAwEAAaMhMB8wHQYDVR0OBBYEFHwmZuN1l8F6XzSRQSCN1l4R00g4MA0GCSqGSIb3DQEBCwUAA4IBAQC9YDK5xHrDP+7mTVSWP6Vbe5vqDSHs7AuEW2lblwdHCPjXjzmlzCQpAu+8GQwWMYOgB1WLn79iEi6LUt0nx1s13l3GC1QTIuONWwx9TskiUPKcA9/iEDGSpOWdPTbrHmGtlLX1usjQgJ/3U9UR8TolSz7/+mXLBiLpFD74yW/pMJMMAgjdYm3xtRNM1BcBjC+9gg7xbgbJZ3y1+HQTEwhVfQcLIMh6E3HEpS/udp/z3G+wbSHSmcQ/K2Kr0rb8XBHN7s28gAx60/hxy0W4935AplWtw9ajjolMT3Dj0cZ/1Oz9nIfI9KW5FJzqkEfYawKY2EHzcE3QEMUodivZ6Oxe"],"x5t":"Tm1DhOA551N6YufvOtjBWZPEoy4"},{"kty":"RSA","n":"yQMnKuKI_vl-XImHrM8TT1DuqEpqBlMh9Gcoa_Q48fMhM7QbKjaeRHRfnVqSGrJzNlr55lAMjnX-rFK2uMVpmRpgkciVNUtyQZs2Kwwe4U4P1XYumgaQV6yd5t1BVLb5pO0R5KaTPddSoRP--fv4JBZ0j0v8VHDc9q_vvkEw7Nt9ZYx6NrO9l6nu1R-hUmpeN0mpAzt7M4zCzLckI7WtgMxgiM0v8BQCdmiHbZ8bdlP_d_BZKm8sUGpZ8s8wqS3PUWRAFdnwGvQQeY4oi_-Ehf4md1RDCGFJpebfOT-g9katBOIa1o0mThtljrmRnUA6fpDXWFMn4A6AHcxHbBYcvQ","e":"AQAB","use":"enc","alg":"RSA-OAEP","kid":"kid","x5c":["MIIDGTCCAgGgAwIBAgIENyeT8zANBgkqhkiG9w0BAQsFADA9MQswCQYDVQQGEwJSVTEMMAoGA1UEChMDTU9TMQwwCgYDVQQLEwNESVQxEjAQBgNVBAMTCXN1ZGlyMnJzYTAeFw0yNTA2MDkxMTUxMjhaFw0yNTA5MDcxMTUxMjhaMD0xCzAJBgNVBAYTAlJVMQwwCgYDVQQKEwNNT1MxDDAKBgNVBAsTA0RJVDESMBAGA1UEAxMJc3VkaXIycnNhMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyQMnKuKI/vl+XImHrM8TT1DuqEpqBlMh9Gcoa/Q48fMhM7QbKjaeRHRfnVqSGrJzNlr55lAMjnX+rFK2uMVpmRpgkciVNUtyQZs2Kwwe4U4P1XYumgaQV6yd5t1BVLb5pO0R5KaTPddSoRP++fv4JBZ0j0v8VHDc9q/vvkEw7Nt9ZYx6NrO9l6nu1R+hUmpeN0mpAzt7M4zCzLckI7WtgMxgiM0v8BQCdmiHbZ8bdlP/d/BZKm8sUGpZ8s8wqS3PUWRAFdnwGvQQeY4oi/+Ehf4md1RDCGFJpebfOT+g9katBOIa1o0mThtljrmRnUA6fpDXWFMn4A6AHcxHbBYcvQIDAQABoyEwHzAdBgNVHQ4EFgQUm6MFkHqpFglQgj5uXd3P4ufQoIwwDQYJKoZIhvcNAQELBQADggEBAD62VxaIhqOrRa5d5yLwFfhUgP2ySxzPU6tAZdt2k4GiSPCmzN6mdTjxoH+4FsTxXglE0DDEla4jQYjb/aSKfp9qbgTlAbpDqHlH8ZlnzguGZKm8y0uyPtpF/xRV9wCA5PLgjTOPhFMuU/SGj45myI7NXr7mvxAivzVUayiDqk6FhS8LClXM/kBDTOLEsD+ZN+qADjqWoGhtTNlGyl+ZgSUsZvLxkaIkif/mdA8FH5y6ttLLjLzNtdHUo37fkU0XUCCFbVicCnesMnkxmPDTxCONsXxnsqKYZmfStziNQI96kIOWLtctAbqx+BAnuyMPskB7uIQGRlhYqicJOciOYW0="],"x5t":"Te3zzJF9XmU-9GY22TBun9yXWX4"}]}'
    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=eyJraWQiOiJkZWZhdWx0IiwiYWxnIjoiUlNBLU9BRVAiLCJlbmMiOiJBMjU2R0NNIn0.RYgv7ccmyEPBKAuGmdx5pH7nvG-POxd4UHfVKKXrWWNKKT9gW7qYBTlmm_D3r6aYsST8IkodF4oTK8i-eCVCOZjb7K1C6S-t0cTt516KWwQe6WRsDcZ12YfoD5eyHrswYuOVa7UxyhtA47n2zV-LCaV33N30dFN1DQahjX34UL4CEL2aASDKvQTzo-Z23g6Vs6dD_XbqvE8qWW2__vuZXKVgIFwe0lBg2KJWycZzvS8d8Ut3WpN6Ruh5zccgJrCawPAIWZ6HiAD_Fn_hoEdE4ovhKVqDuECfwLlUkxnFusLKAh1e9-vT5MpvMKRgEQpuTMtqphd4qcUX1ZCG6sgTWA.vHr65WPA0NDzPk8e.s7tAK1l3vqtPrv_w67UUXgcZui50vAn05U_OH6Jqk-OaeziPVIGoTBbK_gpiXl7AYagM_u4fhiG1oJYFHcVObCd5sng33gWmshYliQKL4yxOKU--MY5BshIkRdnHcuurghdK1i7YMCTKj0no0htfnpB7kpK_0lY7cZIsfIHNFgz2P7UECPUCd4R_QqO7hsIZy9MZAPU433izVTUCnHGLeg1hKDgE47JbsxvTg1Ig76xoKIBQ9W7BEklQE9MjXBoITEBTxwwevG1UBPJbeCDqQ_aCQ1xaofUaxV8_teLyXCawUhsTEaJhl6hoPQyXIFWh5QMsq3jYrvi8HAeKa1b9DOLCmssXaPBCr9u2xtmVvYZ5wINY2HwnxFLOPRVbB2BjYR2j1tOBsE4RWes1fZeXQsxSbuNzkeHRfnZ5CUr57bsawSMycWzGCW8.qCD9pfo_tYPFV-fQ9XWJ2g

Возвращаемые 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=Вам необходимо войти под пользователем: {}