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
andx5t
header claims must be included.the payload claim
iat
is 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)