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
typandx5theader claims must be included.the payload claim
iatis insteadnbf
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)