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/oauth2/provider.py

671 lines
22 KiB
Python

#!/usr/bin/env python
import traceback
import sys
import urllib
import urlparse
from oic.utils.sdb import AccessCodeUsed, AuthnEvent
__author__ = 'rohe0002'
import base64
import logging
import os
from oic.exception import MissingParameter
from oic.exception import URIError
from oic.exception import RedirectURIError
from oic.exception import ParameterError
from oic.exception import FailedAuthentication
from oic.exception import UnknownClient
from oic.oauth2.message import AccessTokenResponse
from oic.oauth2.message import ErrorResponse
from oic.oauth2.message import AuthorizationErrorResponse
from oic.oauth2.message import AuthorizationRequest
from oic.oauth2.message import add_non_standard
from oic.oauth2.message import AuthorizationResponse
from oic.oauth2.message import NoneResponse
from oic.oauth2.message import by_schema
from oic.oauth2.message import MissingRequiredAttribute
from oic.oauth2.message import TokenErrorResponse
from oic.oauth2.message import AccessTokenRequest
from oic.utils.http_util import BadRequest
from oic.utils.http_util import CookieDealer
from oic.utils.http_util import make_cookie
from oic.utils.http_util import Redirect
from oic.utils.http_util import Response
from oic.utils.authn.user import NoSuchAuthentication
from oic.utils.authn.user import ToOld
from oic.utils.authn.user import TamperAllert
from oic.oauth2 import rndstr
from oic.oauth2 import Server
logger = logging.getLogger(__name__)
LOG_INFO = logger.info
LOG_DEBUG = logger.debug
class Endpoint(object):
etype = ""
def __init__(self, func):
self.func = func
@property
def name(self):
return "%s_endpoint" % self.etype
def __call__(self, *args, **kwargs):
return self.func(*args, **kwargs)
class AuthorizationEndpoint(Endpoint):
etype = "authorization"
class TokenEndpoint(Endpoint):
etype = "token"
def code_response(**kwargs):
_areq = kwargs["areq"]
_scode = kwargs["scode"]
aresp = AuthorizationResponse()
if "state" in _areq:
aresp["state"] = _areq["state"]
aresp["code"] = _scode
add_non_standard(_areq, aresp)
return aresp
def token_response(**kwargs):
_areq = kwargs["areq"]
_scode = kwargs["scode"]
_sdb = kwargs["sdb"]
_dic = _sdb.upgrade_to_token(_scode, issue_refresh=False)
aresp = AccessTokenResponse(**by_schema(AccessTokenResponse, **_dic))
if "state" in _areq:
aresp["state"] = _areq["state"]
return aresp
# noinspection PyUnusedLocal
def none_response(**kwargs):
_areq = kwargs["areq"]
aresp = NoneResponse()
if "state" in _areq:
aresp["state"] = _areq["state"]
return aresp
def location_url(response_type, redirect_uri, query):
if response_type in [["code"], ["token"], ["none"]]:
return "%s?%s" % (redirect_uri, query)
else:
return "%s#%s" % (redirect_uri, query)
class Provider(object):
endp = [AuthorizationEndpoint, TokenEndpoint]
def __init__(self, name, sdb, cdb, authn_broker, authz, client_authn,
symkey="", urlmap=None, iv=0, default_scope="",
ca_bundle=None, verify_ssl=True, default_acr=""):
self.name = name
self.sdb = sdb
self.cdb = cdb
self.server = Server(ca_certs=ca_bundle, verify_ssl=verify_ssl)
self.authn_broker = authn_broker
if authn_broker is None:
# default cookie function
self.cookie_func = CookieDealer(srv=self).create_cookie
else:
self.cookie_func = self.authn_broker[0][0].create_cookie
for item in self.authn_broker:
item.srv = self
self.authz = authz
self.client_authn = client_authn
self.symkey = symkey
self.seed = rndstr()
self.iv = iv or os.urandom(16)
self.cookie_name = "pyoidc"
self.default_scope = default_scope
self.sso_ttl = 0
self.default_acr = default_acr
if urlmap is None:
self.urlmap = {}
else:
self.urlmap = urlmap
self.response_type_map = {
"code": code_response,
"token": token_response,
"none": none_response,
}
def endpoints(self):
for endp in self.endp:
yield endp(None).name
def subset(self, li1, li2):
"""
Verify that all items in li1 appears in li2
:param li1: List 1
:param li2: List 2
:return: True if all items in li1 appears in li2
"""
for item in li1:
try:
assert item in li2
except AssertionError:
return False
return True
def get_client_id(self, req, authn):
"""
Verify the client and return the client id
:param req: The request
:param authn: Authentication information from the HTTP header
:return:
"""
logger.debug("REQ: %s" % req.to_dict())
if authn:
if authn.startswith("Basic "):
logger.debug("Basic auth")
(_id, _secret) = base64.b64decode(authn[6:]).split(":")
if _id not in self.cdb:
logger.debug("Unknown client_id")
raise FailedAuthentication("Unknown client_id")
else:
try:
assert _secret == self.cdb[_id]["client_secret"]
except AssertionError:
logger.debug("Incorrect secret")
raise FailedAuthentication("Incorrect secret")
else:
try:
assert authn[:6].lower() == "bearer"
logger.debug("Bearer auth")
_token = authn[7:]
except AssertionError:
raise FailedAuthentication("AuthZ type I don't know")
try:
_id = self.cdb[_token]
except KeyError:
logger.debug("Unknown access token")
raise FailedAuthentication("Unknown access token")
else:
try:
_id = req["client_id"]
if _id not in self.cdb:
logger.debug("Unknown client_id")
raise FailedAuthentication("Unknown client_id")
except KeyError:
raise FailedAuthentication("Missing client_id")
return _id
def authn_reply(self, areq, aresp, bsid, **kwargs):
"""
:param areq: Authorization Request
:param aresp: Authorization Response
:param bsid: Session id
:param kwargs: Additional keyword args
:return:
"""
if "redirect_uri" in areq:
# TODO verify that the uri is reasonable
redirect_uri = areq["redirect_uri"]
else:
redirect_uri = self.urlmap[areq["client_id"]]
location = location_url(areq["response_type"], redirect_uri,
aresp.to_urlencoded())
LOG_DEBUG("Redirected to: '%s' (%s)" % (location, type(location)))
# set cookie containing session ID
cookie = make_cookie(self.cookie_name, bsid, self.seed)
return Redirect(str(location), headers=[cookie])
def authn_response(self, areq, **kwargs):
"""
:param areq: Authorization request
:param kwargs: Extra keyword arguments
:return:
"""
scode = kwargs["code"]
areq["response_type"].sort()
_rtype = " ".join(areq["response_type"])
return self.response_type_map[_rtype](areq=areq, scode=scode,
sdb=self.sdb)
@staticmethod
def input(query="", post=None):
# Support GET and POST
if query:
return query
elif post:
return post
else:
raise MissingParameter("No input")
@staticmethod
def _error_response(error, descr=None):
logger.error("%s" % error)
response = ErrorResponse(error=error, error_description=descr)
return Response(response.to_json(), content="application/json",
status="400 Bad Request")
@staticmethod
def _error(error, descr=None):
response = ErrorResponse(error=error, error_description=descr)
return Response(response.to_json(), content="application/json",
status="400 Bad Request")
@staticmethod
def _authz_error(error, descr=None):
response = AuthorizationErrorResponse(error=error)
if descr:
response["error_description"] = descr
return Response(response.to_json(), content="application/json",
status="400 Bad Request")
@staticmethod
def _redirect_authz_error(error, redirect_uri, descr=None):
err = ErrorResponse(error=error)
if descr:
err["error_description"] = descr
location = err.request(redirect_uri)
return Redirect(location)
def _verify_redirect_uri(self, areq):
"""
MUST NOT contain a fragment
MAY contain query component
:return: An error response if the redirect URI is faulty otherwise
None
"""
try:
_redirect_uri = urlparse.unquote(areq["redirect_uri"])
part = urlparse.urlparse(_redirect_uri)
if part.fragment:
raise URIError("Contains fragment")
(_base, _query) = urllib.splitquery(_redirect_uri)
if _query:
_query = urlparse.parse_qs(_query)
match = False
for regbase, rquery in self.cdb[areq["client_id"]]["redirect_uris"]:
if _base == regbase or _redirect_uri.startswith(regbase):
# every registered query component must exist in the
# redirect_uri
if rquery:
for key, vals in rquery.items():
assert key in _query
for val in vals:
assert val in _query[key]
match = True
break
if not match:
raise RedirectURIError("Doesn't match any registered uris")
# ignore query components that are not registered
return None
except Exception, err:
logger.error("Faulty redirect_uri: %s" % areq["redirect_uri"])
try:
_cinfo = self.cdb[areq["client_id"]]
except KeyError:
logger.info("Unknown client: %s" % areq["client_id"])
raise UnknownClient(areq["client_id"])
else:
logger.info("Registered redirect_uris: %s" % _cinfo)
raise RedirectURIError("Faulty redirect_uri: %s" % err)
def get_redirect_uri(self, areq):
""" verify that the redirect URI is reasonable
:param areq: The Authorization request
:return: Tuple of (redirect_uri, Response instance)
Response instance is not None of matching redirect_uri failed
"""
if 'redirect_uri' in areq:
self._verify_redirect_uri(areq)
uri = areq["redirect_uri"]
else:
raise ParameterError(
"Missing redirect_uri and more than one or none registered")
return uri
def pick_auth(self, areq, comparision_type=""):
"""
:param areq: AuthorizationRequest instance
:param comparision_type: How to pick the authentication method
:return: An authentication method and its authn class ref
"""
if comparision_type == "any":
return self.authn_broker[0]
try:
if len(self.authn_broker) == 1:
return self.authn_broker[0]
else:
try:
_values = areq["acr_values"]
except KeyError:
_values = self.default_acr
if isinstance(_values, basestring):
_values = [_values]
if not comparision_type:
comparision_type = "exact"
for _acr in _values:
res = self.authn_broker.pick(_acr, comparision_type)
if res:
# Return the best guess by pick.
return res[0]
except KeyError:
pass
# return the best I have
return None, None
def authorization_endpoint(self, request="", cookie="", authn="", **kwargs):
""" The AuthorizationRequest endpoint
:param request: The client request
"""
logger.debug("Request: '%s'" % request)
# Same serialization used for GET and POST
try:
areq = self.server.parse_authorization_request(query=request)
except MissingRequiredAttribute, err:
logger.debug("%s" % err)
return self._error("invalid_request", "%s" % err)
except KeyError:
areq = AuthorizationRequest().deserialize(request, "urlencoded")
# verify the redirect_uri
try:
self.get_redirect_uri(areq)
except (RedirectURIError, ParameterError), err:
return self._error("invalid_request", "%s" % err)
except Exception, err:
message = traceback.format_exception(*sys.exc_info())
logger.error(message)
logger.debug("Bad request: %s (%s)" % (err, err.__class__.__name__))
return BadRequest("%s" % err)
if not areq:
logger.debug("No AuthzRequest")
return self._error("invalid_request", "No parsable AuthzRequest")
logger.debug("AuthzRequest: %s" % (areq.to_dict(),))
try:
redirect_uri = self.get_redirect_uri(areq)
except (RedirectURIError, ParameterError), err:
return self._error("invalid_request", "%s" % err)
except UnknownClient, err:
return self._error("unauthorized_client", "%s" % err)
try:
# verify that the request message is correct
areq.verify()
except (MissingRequiredAttribute, ValueError), err:
return self._redirect_authz_error("invalid_request", redirect_uri,
"%s" % err)
# Check if the person is already authenticated
a_args = {}
if cookie:
logger.debug("Cookie: %s" % cookie)
a_args["cookie"] = cookie
if authn:
try:
a_args["authorization"] = authn
except KeyError:
pass
# Pick authentication method
_authn, acr = self.pick_auth(areq=areq)
if authn is None:
return self._redirect_authz_error("invalid_request", redirect_uri)
try:
identity = _authn.authenticated_as(**a_args)
except (NoSuchAuthentication, ToOld, TamperAllert):
identity = None
authn_args = {"query": request}
cinfo = self.cdb[areq["client_id"]]
for attr in ["policy_uri", "logo_uri"]:
try:
authn_args[attr] = cinfo[attr]
except KeyError:
pass
# To authenticate or Not
if identity is None: # No!
if "prompt" in areq and "none" in areq["prompt"]:
# Need to authenticate but not allowed
return self._redirect_authz_error("login_required",
redirect_uri)
else:
# Do authentication
return _authn(**authn_args)
else:
user = identity["uid"]
aevent = AuthnEvent(user, authn_info=acr)
# If I get this far the person is already authenticated
logger.debug("- authenticated -")
logger.debug("AREQ keys: %s" % areq.keys())
try:
oidc_req = areq["request"]
except KeyError:
oidc_req = None
skey = self.sdb.create_authz_session(aevent, areq, oidreq=oidc_req)
# Now about the authorization step.
try:
permissions = self.authz.permissions(cookie)
if not permissions:
return self.authz(user, skey)
except (ToOld, TamperAllert):
return self.authz(user, areq, skey)
return self.authz_part2(user, areq, skey, permissions, _authn)
def authz_part2(self, user, areq, skey, permission=None, authn=None,
**kwargs):
"""
After the authentication this is where you should end up
:param user:
:param areq: The Authorization Request
:param skey: Session key
:param permission: A permission specification
:param authn: The Authentication Method used
:param kwargs: possible other parameters
:return: A redirect to the redirect_uri of the client
"""
_log_debug = logger.debug
_log_debug("- in authenticated() -")
self.sdb.update(skey, "auz", permission)
_log_debug("response type: %s" % areq["response_type"])
# create the response
aresp = AuthorizationResponse()
try:
aresp["state"] = areq["state"]
except KeyError:
pass
if "response_type" in areq and \
len(areq["response_type"]) == 1 and \
"none" in areq["response_type"]:
pass
else:
# if self.sdb.is_revoked(sinfo):
# return self._error(error="access_denied",
# descr="Token is revoked")
try:
aresp["scope"] = areq["scope"]
except KeyError:
pass
_log_debug("_dic: %s" % self.sdb[skey])
rtype = set(areq["response_type"][:])
if "code" in areq["response_type"]:
#if issue_new_code:
# scode = self.sdb.duplicate(_sinfo)
# _sinfo = self.sdb[scode]
_code = aresp["code"] = self.sdb.get_token(skey)
rtype.remove("code")
else:
_code = self.sdb[skey]["code"]
self.sdb.update(skey, "code", None)
if "token" in rtype:
self.sdb.upgrade_to_token(skey, issue_refresh=False,
access_grant=_code)
atr = AccessTokenResponse(**aresp.to_dict())
aresp = atr
_cont = self.sdb[skey]
_log_debug("_dic: %s" % _cont)
for key, val in _cont.items():
if key in aresp.parameters() and val is not None:
aresp[key] = val
rtype.remove("token")
if len(rtype):
return BadRequest("Unknown response type")
try:
redirect_uri = self.get_redirect_uri(areq)
except (RedirectURIError, ParameterError), err:
return BadRequest("%s" % err)
# self.sdb.store_session(skey)
# so everything went well should set a SSO cookie
headers = [authn.create_cookie(user, typ="sso", ttl=self.sso_ttl)]
location = aresp.request(redirect_uri)
logger.debug("Redirected to: '%s' (%s)" % (location, type(location)))
return Redirect(str(location), headers=headers)
def token_scope_check(self, areq, info):
""" Not implemented here """
# if not self.subset(areq["scope"], _info["scope"]):
# LOG_INFO("Asked for scope which is not subset of previous defined")
# err = TokenErrorResponse(error="invalid_scope")
# return Response(err.to_json(), content="application/json")
return None
def token_endpoint(self, authn="", **kwargs):
"""
This is where clients come to get their access tokens
"""
_sdb = self.sdb
LOG_DEBUG("- token -")
body = kwargs["request"]
LOG_DEBUG("body: %s" % body)
areq = AccessTokenRequest().deserialize(body, "urlencoded")
try:
client = self.client_authn(self, areq, authn)
except FailedAuthentication, err:
err = TokenErrorResponse(error="unauthorized_client",
error_description="%s" % err)
return Response(err.to_json(), content="application/json",
status="401 Unauthorized")
LOG_DEBUG("AccessTokenRequest: %s" % areq)
try:
assert areq["grant_type"] == "authorization_code"
except AssertionError:
err = TokenErrorResponse(error="invalid_request",
error_description="Wrong grant type")
return Response(err.to_json(), content="application/json",
status="401 Unauthorized")
# assert that the code is valid
_info = _sdb[areq["code"]]
resp = self.token_scope_check(areq, _info)
if resp:
return resp
# If redirect_uri was in the initial authorization request
# verify that the one given here is the correct one.
if "redirect_uri" in _info:
assert areq["redirect_uri"] == _info["redirect_uri"]
try:
_tinfo = _sdb.upgrade_to_token(areq["code"])
except AccessCodeUsed:
err = TokenErrorResponse(error="invalid_grant",
error_description="Access grant used")
return Response(err.to_json(), content="application/json",
status="401 Unauthorized")
LOG_DEBUG("_tinfo: %s" % _tinfo)
atr = AccessTokenResponse(**by_schema(AccessTokenResponse, **_tinfo))
LOG_DEBUG("AccessTokenResponse: %s" % atr)
return Response(atr.to_json(), content="application/json")
def verify_endpoint(self, request="", cookie=None, **kwargs):
_req = urlparse.parse_qs(request)
try:
areq = urlparse.parse_qs(_req["query"][0])
except KeyError:
return BadRequest()
authn, acr = self.pick_auth(areq=areq)
kwargs["cookie"] = cookie
return authn.verify(_req, **kwargs)