This repository has been archived on 2023-02-21. You can view files and clone it, but cannot push or open issues or pull requests.
pyoidc-ozwillo/src/oic/oic/__init__.py

1387 lines
50 KiB
Python

from jwkest.jwe import JWE
from oic.utils.keyio import KeyJar
__author__ = 'rohe0002'
import urlparse
import json
import logging
import os
from oic.oauth2.message import ErrorResponse
from oic.oic.message import IdToken, ClaimsRequest
from oic.oic.message import RegistrationResponse
from oic.oic.message import AuthorizationResponse
from oic.oic.message import AccessTokenResponse
from oic.oic.message import Claims
from oic.oic.message import AccessTokenRequest
from oic.oic.message import RefreshAccessTokenRequest
from oic.oic.message import UserInfoRequest
from oic.oic.message import AuthorizationRequest
from oic.oic.message import OpenIDRequest
from oic.oic.message import RegistrationRequest
from oic.oic.message import RefreshSessionRequest
from oic.oic.message import CheckSessionRequest
from oic.oic.message import CheckIDRequest
from oic.oic.message import EndSessionRequest
from oic.oic.message import OpenIDSchema
from oic.oic.message import ProviderConfigurationResponse
from oic.oic.message import AuthnToken
from oic.oic.message import TokenErrorResponse
from oic.oic.message import ClientRegistrationErrorResponse
from oic.oic.message import UserInfoErrorResponse
from oic.oic.message import AuthorizationErrorResponse
from oic import oauth2
from oic.oauth2 import MissingRequiredAttribute
from oic.oauth2 import HTTP_ARGS
from oic.oauth2 import rndstr
from oic.oauth2.consumer import ConfigurationError
from oic.exception import AccessDenied
from oic.exception import IssuerMismatch
from oic.exception import PyoidcError
from oic.exception import MissingParameter
from oic.utils import time_util
from oic.utils.webfinger import OIC_ISSUER
from oic.utils.webfinger import WebFinger
from jwkest import jws
from jwkest import jwe
from jwkest.jws import JWS
logger = logging.getLogger(__name__)
ENDPOINTS = ["authorization_endpoint", "token_endpoint",
"userinfo_endpoint", "refresh_session_endpoint",
"check_session_endpoint", "end_session_endpoint",
"registration_endpoint", "check_id_endpoint"]
RESPONSE2ERROR = {
"AuthorizationResponse": [AuthorizationErrorResponse,
TokenErrorResponse],
"AccessTokenResponse": [TokenErrorResponse],
"IdToken": [ErrorResponse],
"RegistrationResponse": [ClientRegistrationErrorResponse],
"OpenIDSchema": [UserInfoErrorResponse]
}
REQUEST2ENDPOINT = {
"AuthorizationRequest": "authorization_endpoint",
"OpenIDRequest": "authorization_endpoint",
"AccessTokenRequest": "token_endpoint",
"RefreshAccessTokenRequest": "token_endpoint",
"UserInfoRequest": "userinfo_endpoint",
"CheckSessionRequest": "check_session_endpoint",
"CheckIDRequest": "check_id_endpoint",
"EndSessionRequest": "end_session_endpoint",
"RefreshSessionRequest": "refresh_session_endpoint",
"RegistrationRequest": "registration_endpoint",
"RotateSecret": "registration_endpoint",
# ---
"ResourceRequest": "resource_endpoint"
}
# -----------------------------------------------------------------------------
JWT_BEARER = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
SAML2_BEARER_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:saml2-bearer"
OIDCONF_PATTERN = "%s/.well-known/openid-configuration"
# This should probably be part of the configuration
MAX_AUTHENTICATION_AGE = 86400
DEF_SIGN_ALG = {"id_token": "RS256",
"openid_request_object": "RS256",
"client_secret_jwt": "HS256",
"private_key_jwt": "RS256"}
# -----------------------------------------------------------------------------
ACR_LISTS = [
["0", "1", "2", "3", "4"],
]
def verify_acr_level(req, level):
if req is None:
return level
elif "values" in req:
for _r in req["values"]:
for alist in ACR_LISTS:
try:
if alist.index(_r) <= alist.index(level):
return level
except ValueError:
pass
else: # Required or Optional
return level
raise AccessDenied("", req)
def deser_id_token(inst, txt=""):
if not txt:
return None
else:
return IdToken().from_jwt(txt, keyjar=inst.keyjar)
# -----------------------------------------------------------------------------
def make_openid_request(arq, keys=None, userinfo_claims=None,
idtoken_claims=None, algorithm=None,
**kwargs):
"""
Construct the specification of what I want returned.
The request will be signed
:param arq: The Authorization request
:param keys: Keys to use for signing/encrypting
:param userinfo_claims: UserInfo claims
:param idtoken_claims: IdToken claims
:param algorithm: Which signing/encrypting algorithm to use
:return: JWT encoded OpenID request
"""
oir_args = {}
for prop in OpenIDRequest.c_param.keys():
try:
oir_args[prop] = arq[prop]
except KeyError:
pass
for attr in ["scope", "response_type"]:
if attr in oir_args:
oir_args[attr] = " ".join(oir_args[attr])
c_args = {}
if userinfo_claims is not None:
# UserInfoClaims
c_args["userinfo"] = Claims(**userinfo_claims)
if idtoken_claims is not None:
#IdTokenClaims
c_args["id_token"] = Claims(**idtoken_claims)
if c_args:
oir_args["claims"] = ClaimsRequest(**c_args)
oir = OpenIDRequest(**oir_args)
return oir.to_jwt(key=keys, algorithm=algorithm)
class Token(oauth2.Token):
pass
class Grant(oauth2.Grant):
_authz_resp = AuthorizationResponse
_acc_resp = AccessTokenResponse
_token_class = Token
def add_token(self, resp):
tok = self._token_class(resp)
if tok.access_token:
self.tokens.append(tok)
else:
_tmp = getattr(tok, "id_token", None)
if _tmp:
self.tokens.append(tok)
PREFERENCE2PROVIDER = {
#"require_signed_request_object": "request_object_algs_supported",
"request_object_signing_alg": "request_object_signing_alg_values_supported",
"request_object_encryption_alg":
"request_object_encryption_alg_values_supported",
"request_object_encryption_enc":
"request_object_encryption_enc_values_supported",
"userinfo_signed_response_alg": "userinfo_signing_alg_values_supported",
"userinfo_encrypted_response_alg":
"userinfo_encryption_alg_values_supported",
"userinfo_encrypted_response_enc":
"userinfo_encryption_enc_values_supported",
"id_token_signed_response_alg": "id_token_signing_alg_values_supported",
"id_token_encrypted_response_alg":
"id_token_encryption_alg_values_supported",
"id_token_encrypted_response_enc":
"id_token_encryption_enc_values_supported",
"default_acr_values": "acr_values_supported",
#"require_auth_time":"",
"subject_type": "subject_types_supported",
"token_endpoint_auth_method": "token_endpoint_auth_methods_supported",
"token_endpoint_auth_signing_alg":
"token_endpoint_auth_signing_alg_values_supported",
"response_types": "response_types_supported"
#"request_object_signing_alg": "request_object_signing_alg_values_supported
}
PROVIDER2PREFERENCE = dict([(v,k) for k,v in PREFERENCE2PROVIDER.items()])
PROVIDER_DEFAULT = {
"token_endpoint_auth_method": "client_secret_basic",
"id_token_signed_response_alg": "RS256",
}
#noinspection PyMethodOverriding
class Client(oauth2.Client):
_endpoints = ENDPOINTS
def __init__(self, client_id=None, ca_certs=None,
client_prefs=None, client_authn_method=None, keyjar=None,
verify_ssl=True):
oauth2.Client.__init__(self, client_id, ca_certs,
client_authn_method=client_authn_method,
keyjar=keyjar, verify_ssl=verify_ssl)
self.file_store = "./file/"
self.file_uri = "http://localhost/"
# OpenID connect specific endpoints
for endpoint in ENDPOINTS:
setattr(self, endpoint, "")
self.id_token = None
self.log = None
self.request2endpoint = REQUEST2ENDPOINT
self.response2error = RESPONSE2ERROR
self.grant_class = Grant
self.token_class = Token
self.provider_info = None
self.registration_response = None
self.client_prefs = client_prefs or {}
self.behaviour = {
"request_object_signing_alg":
DEF_SIGN_ALG["openid_request_object"]}
self.wf = WebFinger(OIC_ISSUER)
self.wf.httpd = self
self.allow = {}
self.post_logout_redirect_uris = []
self.registration_expires = 0
self.registration_access_token = None
# Default key by kid for different key types
# For instance {"RSA":"abc"}
self.kid = {"sig": {}, "enc": {}}
def _get_id_token(self, **kwargs):
try:
return kwargs["id_token"]
except KeyError:
grant = self.get_grant(**kwargs)
if grant:
try:
_scope = kwargs["scope"]
except KeyError:
_scope = None
for token in grant.tokens:
if token.scope and _scope:
flag = True
for item in _scope:
try:
assert item in token.scope
except AssertionError:
flag = False
break
if not flag:
break
if token.id_token:
return token.id_token
return None
def request_object_encryption(self, msg, **kwargs):
try:
encalg = self.behaviour["request_object_encryption_alg"]
except KeyError:
return msg
else:
encenc = self.behaviour["request_object_encryption_enc"]
_jwe = JWE(msg, alg=encalg, enc=encenc)
_kty = jwe.alg2keytype(encalg)
try:
_kid = kwargs["enc_kid"]
except KeyError:
try:
_kid = self.kid["enc"][_kty]
except KeyError:
_kid = ""
if _kid:
_jwe["keys"] = self.keyjar.get_encrypt_key(_kty, kid=_kid)
_jwe["kid"] = _kid
else:
_jwe["keys"] = self.keyjar.get_signing_key(_kty)
return _jwe.encrypt(self.keyjar)
def construct_AuthorizationRequest(self, request=AuthorizationRequest,
request_args=None, extra_args=None,
request_param=None, **kwargs):
if request_args is not None:
# if "claims" in request_args:
# kwargs["claims"] = request_args["claims"]
# del request_args["claims"]
if "nonce" not in request_args:
_rt = request_args["response_type"]
if "token" in _rt or "id_token" in _rt:
request_args["nonce"] = rndstr(12)
elif "response_type" in kwargs:
if "token" in kwargs["response_type"]:
request_args = {"nonce": rndstr(12)}
else: # Never wrong to specify a nonce
request_args = {"nonce": rndstr(12)}
if "request_method" in kwargs:
if kwargs["request_method"] == "file":
request_param = "request_uri"
else:
request_param = "request"
del kwargs["request_method"]
areq = oauth2.Client.construct_AuthorizationRequest(self, request,
request_args,
extra_args,
**kwargs)
if request_param:
try:
alg = self.behaviour["request_object_signing_alg"]
except KeyError:
alg = None
else:
if "algorithm" not in kwargs:
kwargs["algorithm"] = alg
if "keys" not in kwargs and alg:
_kty = jws.alg2keytype(alg)
try:
kwargs["keys"] = self.keyjar.get_signing_key(
_kty, kid=self.kid["sig"][_kty])
except KeyError:
kwargs["keys"] = self.keyjar.get_signing_key(_kty)
_req = make_openid_request(areq, **kwargs)
# Should the request be encrypted
_req = self.request_object_encryption(_req, **kwargs)
if request_param == "request":
areq["request"] = _req
else:
_filedir = kwargs["local_dir"]
_webpath = kwargs["base_path"]
_name = rndstr(10) + ".jwt"
filename = os.path.join(_filedir, _name)
while os.path.exists(filename):
_name = rndstr(10)
filename = os.path.join(_filedir, _name)
fid = open(filename, mode="w")
fid.write(_req)
fid.close()
_webname = "%s%s" % (_webpath, _name)
areq["request_uri"] = _webname
return areq
#noinspection PyUnusedLocal
def construct_AccessTokenRequest(self, request=AccessTokenRequest,
request_args=None, extra_args=None,
**kwargs):
return oauth2.Client.construct_AccessTokenRequest(self, request,
request_args,
extra_args, **kwargs)
def construct_RefreshAccessTokenRequest(self,
request=RefreshAccessTokenRequest,
request_args=None, extra_args=None,
**kwargs):
return oauth2.Client.construct_RefreshAccessTokenRequest(self, request,
request_args,
extra_args,
**kwargs)
def construct_UserInfoRequest(self, request=UserInfoRequest,
request_args=None, extra_args=None,
**kwargs):
if request_args is None:
request_args = {}
if "access_token" in request_args:
pass
else:
if "scope" not in kwargs:
kwargs["scope"] = "openid"
token = self.get_token(**kwargs)
if token is None:
raise PyoidcError("No valid token available")
request_args["access_token"] = token.access_token
return self.construct_request(request, request_args, extra_args)
#noinspection PyUnusedLocal
def construct_RegistrationRequest(self, request=RegistrationRequest,
request_args=None, extra_args=None,
**kwargs):
return self.construct_request(request, request_args, extra_args)
#noinspection PyUnusedLocal
def construct_RefreshSessionRequest(self,
request=RefreshSessionRequest,
request_args=None, extra_args=None,
**kwargs):
return self.construct_request(request, request_args, extra_args)
def _id_token_based(self, request, request_args=None, extra_args=None,
**kwargs):
if request_args is None:
request_args = {}
try:
_prop = kwargs["prop"]
except KeyError:
_prop = "id_token"
if _prop in request_args:
pass
else:
id_token = self._get_id_token(**kwargs)
if id_token is None:
raise PyoidcError("No valid id token available")
request_args[_prop] = id_token
return self.construct_request(request, request_args, extra_args)
def construct_CheckSessionRequest(self, request=CheckSessionRequest,
request_args=None, extra_args=None,
**kwargs):
return self._id_token_based(request, request_args, extra_args, **kwargs)
def construct_CheckIDRequest(self, request=CheckIDRequest,
request_args=None,
extra_args=None, **kwargs):
# access_token is where the id_token will be placed
return self._id_token_based(request, request_args, extra_args,
prop="access_token", **kwargs)
def construct_EndSessionRequest(self, request=EndSessionRequest,
request_args=None, extra_args=None,
**kwargs):
if request_args is None:
request_args = {}
if "state" in kwargs:
request_args["state"] = kwargs["state"]
elif "state" in request_args:
kwargs["state"] = request_args["state"]
# if "redirect_url" not in request_args:
# request_args["redirect_url"] = self.redirect_url
return self._id_token_based(request, request_args, extra_args,
**kwargs)
# ------------------------------------------------------------------------
def authorization_request_info(self, request_args=None, extra_args=None,
**kwargs):
return self.request_info(AuthorizationRequest, "GET",
request_args, extra_args, **kwargs)
# ------------------------------------------------------------------------
def do_authorization_request(self, request=AuthorizationRequest,
state="", body_type="", method="GET",
request_args=None, extra_args=None,
http_args=None,
response_cls=AuthorizationResponse):
return oauth2.Client.do_authorization_request(self, request, state,
body_type, method,
request_args,
extra_args, http_args,
response_cls)
def do_access_token_request(self, request=AccessTokenRequest,
scope="", state="", body_type="json",
method="POST", request_args=None,
extra_args=None, http_args=None,
response_cls=AccessTokenResponse,
authn_method="", **kwargs):
return oauth2.Client.do_access_token_request(self, request, scope,
state, body_type, method,
request_args, extra_args,
http_args, response_cls,
authn_method, **kwargs)
def do_access_token_refresh(self, request=RefreshAccessTokenRequest,
state="", body_type="json", method="POST",
request_args=None, extra_args=None,
http_args=None,
response_cls=AccessTokenResponse,
**kwargs):
return oauth2.Client.do_access_token_refresh(self, request, state,
body_type, method,
request_args,
extra_args, http_args,
response_cls, **kwargs)
def do_registration_request(self, request=RegistrationRequest,
scope="", state="", body_type="json",
method="POST", request_args=None,
extra_args=None, http_args=None,
response_cls=None):
url, body, ht_args, csi = self.request_info(request, method=method,
request_args=request_args,
extra_args=extra_args,
scope=scope, state=state)
if http_args is None:
http_args = ht_args
else:
http_args.update(http_args)
if response_cls is None:
response_cls = RegistrationResponse
response = self.request_and_return(url, response_cls, method, body,
body_type, state=state,
http_args=http_args)
return response
def do_check_session_request(self, request=CheckSessionRequest,
scope="",
state="", body_type="json", method="GET",
request_args=None, extra_args=None,
http_args=None,
response_cls=IdToken):
url, body, ht_args, csi = self.request_info(request, method=method,
request_args=request_args,
extra_args=extra_args,
scope=scope, state=state)
if http_args is None:
http_args = ht_args
else:
http_args.update(http_args)
return self.request_and_return(url, response_cls, method, body,
body_type, state=state,
http_args=http_args)
def do_check_id_request(self, request=CheckIDRequest, scope="",
state="", body_type="json", method="GET",
request_args=None, extra_args=None,
http_args=None,
response_cls=IdToken):
url, body, ht_args, csi = self.request_info(request, method=method,
request_args=request_args,
extra_args=extra_args,
scope=scope, state=state)
if http_args is None:
http_args = ht_args
else:
http_args.update(http_args)
return self.request_and_return(url, response_cls, method, body,
body_type, state=state,
http_args=http_args)
def do_end_session_request(self, request=EndSessionRequest, scope="",
state="", body_type="", method="GET",
request_args=None, extra_args=None,
http_args=None, response_cls=None):
url, body, ht_args, csi = self.request_info(request, method=method,
request_args=request_args,
extra_args=extra_args,
scope=scope, state=state)
if http_args is None:
http_args = ht_args
else:
http_args.update(http_args)
return self.request_and_return(url, response_cls, method, body,
body_type, state=state,
http_args=http_args)
def user_info_request(self, method="GET", state="", scope="", **kwargs):
uir = UserInfoRequest()
logger.debug("[user_info_request]: kwargs:%s" % (kwargs,))
if "token" in kwargs:
if kwargs["token"]:
uir["access_token"] = kwargs["token"]
token = Token()
token.token_type = "Bearer"
token.access_token = kwargs["token"]
kwargs["behavior"] = "use_authorization_header"
else:
# What to do ? Need a callback
token = None
elif "access_token" in kwargs and kwargs["access_token"]:
uir["access_token"] = kwargs["access_token"]
del kwargs["access_token"]
token = None
else:
token = self.grant[state].get_token(scope)
if token.is_valid():
uir["access_token"] = token.access_token
if token.token_type == "Bearer" and method == "GET":
kwargs["behavior"] = "use_authorization_header"
else:
# raise oauth2.OldAccessToken
if self.log:
self.log.info("do access token refresh")
try:
self.do_access_token_refresh(token=token)
token = self.grant[state].get_token(scope)
uir["access_token"] = token.access_token
except Exception:
raise
uri = self._endpoint("userinfo_endpoint", **kwargs)
# If access token is a bearer token it might be sent in the
# authorization header
# 3-ways of sending the access_token:
# - POST with token in authorization header
# - POST with token in message body
# - GET with token in authorization header
if "behavior" in kwargs:
_behav = kwargs["behavior"]
_token = uir["access_token"]
try:
_ttype = kwargs["token_type"]
except KeyError:
try:
_ttype = token.token_type
except AttributeError:
raise MissingParameter("Unspecified token type")
# use_authorization_header, token_in_message_body
if "use_authorization_header" in _behav and _ttype == "Bearer":
bh = "Bearer %s" % _token
if "headers" in kwargs:
kwargs["headers"].update({"Authorization": bh})
else:
kwargs["headers"] = {"Authorization": bh}
if not "token_in_message_body" in _behav:
# remove the token from the request
del uir["access_token"]
path, body, kwargs = self.get_or_post(uri, method, uir, **kwargs)
h_args = dict([(k, v) for k, v in kwargs.items() if k in HTTP_ARGS])
return path, body, method, h_args
def do_user_info_request(self, method="POST", state="", scope="openid",
request="openid", **kwargs):
kwargs["request"] = request
path, body, method, h_args = self.user_info_request(method, state,
scope, **kwargs)
logger.debug("[do_user_info_request] PATH:%s BODY:%s H_ARGS: %s" % (
path, body, h_args))
try:
resp = self.http_request(path, method, data=body, **h_args)
except oauth2.MissingRequiredAttribute:
raise
if resp.status_code == 200:
try:
assert "application/json" in resp.headers["content-type"]
sformat = "json"
except AssertionError:
assert "application/jwt" in resp.headers["content-type"]
sformat = "jwt"
elif resp.status_code == 500:
raise PyoidcError("ERROR: Something went wrong: %s" % resp.text)
else:
raise PyoidcError("ERROR: Something went wrong [%s]: %s" % (
resp.status_code, resp.text))
try:
_schema = kwargs["user_info_schema"]
except KeyError:
_schema = OpenIDSchema
logger.debug("Reponse text: '%s'" % resp.text)
if sformat == "json":
return _schema().from_json(txt=resp.text)
else:
algo = self.client_prefs["userinfo_signed_response_alg"]
_kty = jws.alg2keytype(algo)
# Keys of the OP ?
try:
args = {"kid": self.kid["sig"][_kty]}
except KeyError:
args = {}
owner = self.endpoint2issuer(path, "userinfo_endpoint")
keys = self.keyjar.get_signing_key(_kty, owner, **args)
return _schema().from_jwt(resp.text, keys)
def get_userinfo_claims(self, access_token, endpoint, method="POST",
schema_class=OpenIDSchema, **kwargs):
uir = UserInfoRequest(access_token=access_token)
h_args = dict([(k, v) for k, v in kwargs.items() if k in HTTP_ARGS])
if "authn_method" in kwargs:
http_args = self.init_authentication_method(**kwargs)
else:
# If nothing defined this is the default
http_args = self.init_authentication_method(uir, "bearer_header",
**kwargs)
h_args.update(http_args)
path, body, kwargs = self.get_or_post(endpoint, method, uir, **kwargs)
try:
resp = self.http_request(path, method, data=body, **h_args)
except oauth2.MissingRequiredAttribute:
raise
if resp.status_code == 200:
assert "application/json" in resp.headers["content-type"]
elif resp.status_code == 500:
raise PyoidcError("ERROR: Something went wrong: %s" % resp.text)
else:
raise PyoidcError(
"ERROR: Something went wrong [%s]" % resp.status_code)
return schema_class().from_json(txt=resp.text)
def handle_provider_config(self, pcr, issuer, keys=True, endpoints=True):
"""
Deal with Provider Config Response
:param pcr: The ProviderConfigResponse instance
:param issuer: The one I thought should be the issuer of the config
:param keys: Should I deal with keys
:param endpoints: Should I deal with endpoints, that is store them
as attributes in self.
"""
if "issuer" in pcr:
_pcr_issuer = pcr["issuer"]
if pcr["issuer"].endswith("/"):
if issuer.endswith("/"):
_issuer = issuer
else:
_issuer = issuer + "/"
else:
if issuer.endswith("/"):
_issuer = issuer[:-1]
else:
_issuer = issuer
try:
_ = self.allow["issuer_mismatch"]
except KeyError:
try:
assert _issuer == _pcr_issuer
except AssertionError:
raise IssuerMismatch("'%s' != '%s'" % (_issuer,
_pcr_issuer),
pcr)
self.provider_info = pcr
else:
_pcr_issuer = issuer
if endpoints:
for key, val in pcr.items():
if key.endswith("_endpoint"):
setattr(self, key, val)
if keys:
if self.keyjar is None:
self.keyjar = KeyJar(verify_ssl=self.verify_ssl)
self.keyjar.load_keys(pcr, _pcr_issuer)
def provider_config(self, issuer, keys=True, endpoints=True,
response_cls=ProviderConfigurationResponse,
serv_pattern=OIDCONF_PATTERN):
if issuer.endswith("/"):
_issuer = issuer[:-1]
else:
_issuer = issuer
url = serv_pattern % _issuer
pcr = None
r = self.http_request(url)
if r.status_code == 200:
pcr = response_cls().from_json(r.text)
elif r.status_code == 302:
while r.status_code == 302:
r = self.http_request(r.headers["location"])
if r.status_code == 200:
pcr = response_cls().from_json(r.text)
break
#logger.debug("Provider info: %s" % pcr)
if pcr is None:
raise PyoidcError("Trying '%s', status %s" % (url, r.status_code))
self.handle_provider_config(pcr, issuer, keys, endpoints)
return pcr
def unpack_aggregated_claims(self, userinfo):
if userinfo._claim_sources:
for csrc, spec in userinfo._claim_sources.items():
if "JWT" in spec:
if not csrc in self.keyjar:
self.provider_config(csrc, endpoints=False)
keycol = self.keyjar.get_verify_key(owner=csrc)
for typ, keyl in self.keyjar.get_verify_key().items():
try:
keycol[typ].extend(keyl)
except KeyError:
keycol[typ] = keyl
info = json.loads(JWS().verify(str(spec["JWT"]), keycol))
attr = [n for n, s in
userinfo._claim_names.items() if s == csrc]
assert attr == info.keys()
for key, vals in info.items():
userinfo[key] = vals
return userinfo
def fetch_distributed_claims(self, userinfo, callback=None):
for csrc, spec in userinfo._claim_sources.items():
if "endpoint" in spec:
#pcr = self.provider_config(csrc, keys=False, endpoints=False)
if "access_token" in spec:
_uinfo = self.do_user_info_request(
token=spec["access_token"],
userinfo_endpoint=spec["endpoint"])
else:
_uinfo = self.do_user_info_request(
token=callback(csrc),
userinfo_endpoint=spec["endpoint"])
attr = [n for n, s in
userinfo._claim_names.items() if s == csrc]
assert attr == _uinfo.keys()
for key, vals in _uinfo.items():
userinfo[key] = vals
return userinfo
def verify_alg_support(self, alg, usage, other):
"""
Verifies that the algorithm to be used are supported by the other side.
:param alg: The algorithm specification
:param usage: In which context the 'alg' will be used.
The following values are supported:
- userinfo
- id_token
- request_object
- token_endpoint_auth
:param other: The identifier for the other side
:return: True or False
"""
try:
_pcr = self.provider_info
supported = _pcr["%s_algs_supported" % usage]
except KeyError:
try:
supported = getattr(self, "%s_algs_supported" % usage)
except AttributeError:
supported = None
if supported is None:
return True
else:
if alg in supported:
return True
else:
return False
def match_preferences(self, pcr=None, issuer=None):
"""
Match the clients preferences against what the provider can do.
:param pcr: Provider configuration response if available
:param issuer: The issuer identifier
"""
if not pcr:
pcr = self.provider_info
regreq = RegistrationRequest
for _pref, _prov in PREFERENCE2PROVIDER.items():
try:
vals = self.client_prefs[_pref]
except KeyError:
continue
try:
_pvals = pcr[_prov]
except KeyError:
try:
self.behaviour[_pref] = PROVIDER_DEFAULT[_pref]
except KeyError:
#self.behaviour[_pref]= vals[0]
if isinstance(pcr.c_param[_prov][0], list):
self.behaviour[_pref] = []
else:
self.behaviour[_pref] = None
continue
if isinstance(vals, basestring):
if vals in _pvals:
self.behaviour[_pref] = vals
else:
vtyp = regreq.c_param[_pref]
if isinstance(vtyp[0], list):
self.behaviour[_pref] = []
for val in vals:
if val in _pvals:
self.behaviour[_pref].append(val)
else:
for val in vals:
if val in _pvals:
self.behaviour[_pref] = val
break
if _pref not in self.behaviour:
raise ConfigurationError(
"OP couldn't match preference:%s" % _pref, pcr)
for key, val in self.client_prefs.items():
if key in self.behaviour:
continue
try:
vtyp = regreq.c_param[key]
if isinstance(vtyp[0], list):
pass
elif isinstance(val, list) and not isinstance(val, basestring):
val = val[0]
except KeyError:
pass
if key not in PREFERENCE2PROVIDER:
self.behaviour[key] = val
def store_registration_info(self, reginfo):
self.registration_response = reginfo
if "token_endpoint_auth_method" not in self.registration_response:
self.registration_response["token_endpoint_auth_method"] = "client_secret_post"
self.client_secret = reginfo["client_secret"]
self.client_id = reginfo["client_id"]
try:
self.registration_expires = reginfo["client_secret_expires_at"]
except KeyError:
pass
try:
self.registration_access_token = reginfo[
"registration_access_token"]
except KeyError:
pass
def handle_registration_info(self, response):
if response.status_code == 200:
resp = RegistrationResponse().deserialize(response.text, "json")
self.store_registration_info(resp)
else:
err = ErrorResponse().deserialize(response.text, "json")
raise PyoidcError("Registration failed: %s" % err.get_json())
return resp
def registration_read(self, url="", registration_access_token=None):
if not url:
url = self.registration_response["registration_client_uri"]
if not registration_access_token:
registration_access_token = self.registration_access_token
headers = [("Authorization", "Bearer %s" % registration_access_token)]
rsp = self.http_request(url, "GET", headers=headers)
return self.handle_registration_info(rsp)
def create_registration_request(self, **kwargs):
"""
Create a registration request
:param kwargs: parameters to the registration request
:return:
"""
req = RegistrationRequest()
for prop in req.parameters():
try:
req[prop] = kwargs[prop]
except KeyError:
try:
req[prop] = self.behaviour[prop]
except KeyError:
pass
if "post_logout_redirect_uris" not in req:
try:
req[
"post_logout_redirect_uris"] = self.post_logout_redirect_uris
except AttributeError:
pass
if "redirect_uris" not in req:
try:
req["redirect_uris"] = self.redirect_uris
except AttributeError:
raise MissingRequiredAttribute("redirect_uris", req)
return req
def register(self, url, **kwargs):
"""
Register the client at an OP
:param url: The OPs registration endpoint
:param kwargs: parameters to the registration request
:return:
"""
req = self.create_registration_request(**kwargs)
headers = {"content-type": "application/json"}
rsp = self.http_request(url, "POST", data=req.to_json(),
headers=headers)
return self.handle_registration_info(rsp)
def normalization(self, principal, idtype="mail"):
if idtype == "mail":
(local, domain) = principal.split("@")
subject = "acct:%s" % principal
elif idtype == "url":
p = urlparse.urlparse(principal)
domain = p.netloc
subject = principal
else:
domain = ""
subject = principal
return subject, domain
def discover(self, principal):
#subject, host = self.normalization(principal)
return self.wf.discovery_query(principal)
def endpoint2issuer(self, url, endpoint=""):
"""
Given that I know which endpoint it's about and which URL was used
which issuer was it.
:param str endpoint: Which endpoint
:param str url: The endpoint url
:return: Issuer identifier if one matched otherwise ""
"""
if endpoint:
for issuer, pi in self.provider_info.items():
try:
if pi[endpoint] == url:
return issuer
except KeyError:
pass
else:
for issuer, pi in self.provider_info.items():
for endpoint in ENDPOINTS:
try:
if pi[endpoint] == url:
return issuer
except KeyError:
pass
return ""
#noinspection PyMethodOverriding
class Server(oauth2.Server):
def __init__(self, keyjar=None, ca_certs=None, verify_ssl=True):
oauth2.Server.__init__(self, keyjar, ca_certs, verify_ssl)
@staticmethod
def _parse_urlencoded(url=None, query=None):
if url:
parts = urlparse.urlparse(url)
scheme, netloc, path, params, query, fragment = parts[:6]
return urlparse.parse_qs(query)
def parse_token_request(self, request=AccessTokenRequest,
body=None):
return oauth2.Server.parse_token_request(self, request, body)
def parse_authorization_request(self, request=AuthorizationRequest,
url=None, query=None, keys=None):
if url:
parts = urlparse.urlparse(url)
scheme, netloc, path, params, query, fragment = parts[:6]
return self._parse_request(request, query, "urlencoded")
def parse_jwt_request(self, request=AuthorizationRequest, txt="",
keys=None, verify=True):
return oauth2.Server.parse_jwt_request(self, request, txt, keys, verify)
def parse_refresh_token_request(self,
request=RefreshAccessTokenRequest,
body=None):
return oauth2.Server.parse_refresh_token_request(self, request, body)
def parse_check_session_request(self, url=None, query=None):
"""
"""
param = self._parse_urlencoded(url, query)
assert "id_token" in param # ignore the rest
return deser_id_token(self, param["id_token"][0])
def parse_check_id_request(self, url=None, query=None):
"""
"""
param = self._parse_urlencoded(url, query)
assert "access_token" in param # ignore the rest
return deser_id_token(self, param["access_token"][0])
def _parse_request(self, request, data, sformat, client_id=None):
if sformat == "json":
request = request().from_json(data)
elif sformat == "jwt":
request = request().from_jwt(data, keyjar=self.keyjar)
elif sformat == "urlencoded":
if '?' in data:
parts = urlparse.urlparse(data)
scheme, netloc, path, params, query, fragment = parts[:6]
else:
query = data
request = request().from_urlencoded(query)
else:
raise PyoidcError("Unknown package format: '%s'" % sformat,
request)
# get the verification keys
if client_id:
keys = self.keyjar.verify_keys(client_id)
else:
try:
keys = self.keyjar.verify_keys(request["client_id"])
except KeyError:
keys = None
request.verify(key=keys, keyjar=self.keyjar)
return request
def parse_open_id_request(self, data, sformat="urlencoded", client_id=None):
return self._parse_request(OpenIDRequest, data, sformat, client_id)
def parse_user_info_request(self, data, sformat="urlencoded"):
return self._parse_request(UserInfoRequest, data, sformat)
def parse_refresh_session_request(self, url=None, query=None):
if url:
parts = urlparse.urlparse(url)
scheme, netloc, path, params, query, fragment = parts[:6]
return RefreshSessionRequest().from_urlencoded(query)
def parse_registration_request(self, data, sformat="urlencoded"):
return self._parse_request(RegistrationRequest, data, sformat)
def parse_end_session_request(self, query, sformat="urlencoded"):
esr = self._parse_request(EndSessionRequest, query,
sformat)
# if there is a id_token in there it is as a string
esr["id_token"] = deser_id_token(self, esr["id_token"])
return esr
# def parse_issuer_request(self, info, sformat="urlencoded"):
# return self._parse_request(IssuerRequest, info, sformat)
@staticmethod
def update_claims(session, where, about, old_claims=None):
"""
:param session:
:param where: Which request
:param about: userinfo or id_token
:param old_claims:
:return: claims or None
"""
if old_claims is None:
old_claims = {}
req = None
if where == "oidreq":
try:
req = OpenIDRequest().deserialize(session[where], "json")
except KeyError:
pass
else: # where == "authzreq"
try:
req = AuthorizationRequest().deserialize(session[where], "json")
except KeyError:
pass
if req:
logger.debug("%s: %s" % (where, req.to_dict()))
try:
_claims = req["claims"][about]
if _claims:
# update with old claims, do not overwrite
for key, val in old_claims.items():
if key not in _claims:
_claims[key] = val
return _claims
except KeyError:
pass
return old_claims
def id_token_claims(self, session):
"""
Pick the IdToken claims from the request
:param session: Session information
:return: The IdToken claims
"""
itc = {}
itc = self.update_claims(session, "authzreq", "id_token", itc)
itc = self.update_claims(session, "oidreq", "id_token", itc)
return itc
def make_id_token(self, session, loa="2", issuer="",
alg="RS256", code=None, access_token=None,
user_info=None, auth_time=0, exp=None):
"""
:param session: Session information
:param loa: Level of Assurance/Authentication context
:param issuer: My identifier
:param alg: Which signing algorithm to use for the IdToken
:param code: Access grant
:param access_token: Access Token
:param user_info: If user info are to be part of the IdToken
:return: IDToken instance
"""
#defaults
if exp is None:
inawhile = {"days": 1}
else:
inawhile = exp
# Handle the idtoken_claims
extra = {}
itc = self.id_token_claims(session)
if itc:
try:
inawhile = {"seconds": itc["max_age"]}
except KeyError:
inawhile = {}
for key, val in itc.items():
if key == "auth_time":
extra["auth_time"] = auth_time
elif key == "acr":
#["2","http://id.incommon.org/assurance/bronze"]
extra["acr"] = verify_acr_level(val, loa)
else:
if auth_time:
extra["auth_time"] = auth_time
if loa:
extra["acr"] = loa
if user_info is None:
_args = {}
else:
try:
_args = user_info.to_dict()
except AttributeError:
_args = user_info
# Make sure that there are no name clashes
for key in ["iss", "sub", "aud", "exp", "acr", "nonce",
"auth_time"]:
try:
del _args[key]
except KeyError:
pass
halg = "HS%s" % alg[-3:]
if code:
_args["c_hash"] = jws.left_hash(code, halg)
if access_token:
_args["at_hash"] = jws.left_hash(access_token, halg)
idt = IdToken(iss=issuer, sub=session["sub"],
aud=session["client_id"],
exp=time_util.epoch_in_a_while(**inawhile), acr=loa,
iat=time_util.utc_now(),
**_args)
for key, val in extra.items():
idt[key] = val
if "nonce" in session:
idt["nonce"] = session["nonce"]
return idt