Features Cookbook

Requesting Claims

Specific claims can be requested using the Authorization request parameter

from oic.oic.message import ClaimsRequest, Claims

claims_request = ClaimsRequest(
    id_token=Claims(
      email={"essential": None},
      phone_number=None
    ),
    userinfo=Claims(
      given_name={"essential": True},
      family_name={"essential": True}, nickname=None
    )
)

request_args = {
    "redirect_uri": "https://example.com/rp/authz_cb",
    "scope": "openid",
    "response_type": "code",
    "claims": claims_request
}

# client is oic.oic.Client
client.construct_AuthorizationRequest(request_args=request_args)

Client Assertions

Client assertions can be used for authentication at the IdP token endpoint in the OIDC authorization code flow, rather than a client secret. As a challenging example, we’ll use authenticating with a Microsoft Azure AD IdP, as this additionally involves creating a custom client assertion.

Support for client assertions is provided in the do_access_token_request method of the oic.oic.Client class, using keyword arguments to the method:

kwargs = dict(algorithm="RS256", authn_endpoint='token',
              authn_method="private_key_jwt")

The authn_method parameter initiates authentication with a client assertion. The algorithm and authn_endpoint parameters are needed by the oic.utils.authn.client.PrivateKeyJWT class to properly construct the assertion. Also required is a signing key in the client’s keyjar. A way to accomplish this is illustrated by the TestPrivateKeyJWT test in tests/test_client.py

_key = rsa_load(os.path.join(BASE_PATH, "data/keys/rsa.key"))
kc_rsa = KeyBundle([{"key": _key, "kty": "RSA", "use": "ver"},
                    {"key": _key, "kty": "RSA", "use": "sig"}])
client.keyjar[""] = kc_rsa

The test also illustrates how you can directly generate the assertion and inspect it. The JWT payload contents can be understood by examining the assertion_jwt function in oic.utils.authn.client.

So that’s basically all you need to know if your IdP can use pyoidc’s client assertions. If you’re using out-of-band client registration, you would not include a client_secret when constructing the RegistrationResponse, as shown in pyoidc’s relying party documentation.

To use client assertions with Microsoft AzureAD, Microsoft provides this guidance. The differences from the client assertion pyoidc generates are:

  • the typ and x5t header claims must be included.

  • the payload claim iat is instead nbf

pyoidc nicely provides a client_assertion keyword argument to do_access_token_request. This argument’s value is substituted for the client assertion pyoidc would otherwise generate. Note that the other keyword arguments identified above are still required:

kwargs = dict(algorithm="RS256", authn_endpoint='token',
              authn_method="private_key_jwt",
              client_assertion=custom_assertion)

Generating a custom client assertion for Microsoft is illustrated by the function below.

import json
from jwkest.jws import JWSig, SIGNER_ALGS
from jwkest.jwt import b64encode_item
from oic import rndstr
from oic.utils.time_util import utc_time_sans_frac


def make_assertion(client_id, token_endpoint, fingerprint, key, lifetime=60):
    """Creates a JWT for IdP token endpoint auth per Microsoft specs"""
    _alg = "RS256"
    headers = {'alg': _alg, 'typ': "JWT", 'x5t': fingerprint}
    _now = utc_time_sans_frac()
    payload = dict(
        iss=client_id,
        sub=client_id,
        aud=token_endpoint,
        jti=rndstr(32),
        nbf=_now,
        exp=_now + lifetime
    )
    jwt = JWSig(**headers)
    _signer = SIGNER_ALGS[_alg]
    _input = jwt.pack(parts=[json.dumps(payload)])
    sig = _signer.sign(_input.encode("utf-8"),
                       key.get_key(alg=_alg, private=True))
    return ".".join([_input, b64encode_item(sig).decode("utf-8")])

The client_id and token_endpoint arguments to this function should be straightforward. key is a jwkest.jwk.RSAKey. To create one from a certificate private key file at path:

from jwkest.jwk import rsa_load, RSAKey
signing_key = RSAKey(key=rsa_load(path), kty="RSA", use='sig')

fingerprint is a Base64 encoded SHA-1 fingerprint of the X.509 certificate corresponding to the private key. Microsoft Azure AD displays this fingerprint as a hexadecimal string when the certificate is registered during IdP configuration. You can easily convert this string by:

from base64 import b64encode
from binascii import unhexlify
fingerprint = b64encode(unhexlify(hex_string))

If you don’t have the hexadecimal fingerprint, but you have the X.509 certificate file, you can generate the fingerprint, as shown below using the cryptography package:

from base64 import b64encode
from binascii import hexlify
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
with open("cert.pem", "rb") as f:
    cert = x509.load_pem_x509_certificate(f.read(), default_backend())
fingerprint = cert.fingerprint(hashes.SHA1())
hexadecimal_fingerprint = hexlify(fingerprint).upper()
base64_fingerprint = b64encode(fingerprint)

Putting it together

def make_token_request(client, state, code, redirect_uri, scopes, assertion):
    """Retrieves tokens by redeeming an authorization code"""
    request_args = dict(
        client_id=client.client_id,
        code=code,
        redirect_uri=redirect_uri
    )
    kwargs = dict(
        algorithm="RS256",
        authn_endpoint='token',
        authn_method="private_key_jwt",
        client_assertion=assertion,
        request_args=request_args,
        scope=scopes,
        state=state
    )
    return client.do_access_token_request(**kwargs)