614 lines
20 KiB
Python
614 lines
20 KiB
Python
#!/usr/bin/env python
|
|
import traceback
|
|
import sys
|
|
import urllib
|
|
import urlparse
|
|
|
|
__author__ = 'rohe0002'
|
|
|
|
import base64
|
|
import logging
|
|
import os
|
|
|
|
from oic.oauth2.exception import MissingParameter, URIError
|
|
from oic.oauth2.exception import RedirectURIError
|
|
from oic.oauth2.exception import ParameterError
|
|
from oic.oauth2.exception import FailedAuthentication
|
|
from oic.oauth2.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, 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.update_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):
|
|
self.name = name
|
|
self.sdb = sdb
|
|
self.cdb = cdb
|
|
self.server = Server(ca_certs=ca_bundle)
|
|
|
|
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
|
|
|
|
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:
|
|
:param aresp:
|
|
:param bsid:
|
|
:param kwargs:
|
|
: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)
|
|
|
|
def input(self, query="", post=None):
|
|
# Support GET and POST
|
|
if query:
|
|
return query
|
|
elif post:
|
|
return post
|
|
else:
|
|
raise MissingParameter("No input")
|
|
|
|
def _error_response(self, 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")
|
|
|
|
def _error(self, error, descr=None):
|
|
response = ErrorResponse(error=error, error_description=descr)
|
|
return Response(response.to_json(), content="application/json",
|
|
status="400 Bad Request")
|
|
|
|
def _authz_error(self, 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")
|
|
|
|
def _redirect_authz_error(self, 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, **kwargs):
|
|
"""
|
|
Pick authentication method to be used
|
|
:param kwargs: Extra key word arguments
|
|
:return: Authentication method
|
|
"""
|
|
return self.authn_broker[0]
|
|
|
|
def authorization_endpoint(self, request="", cookie=None, **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, UnknownClient), err:
|
|
return self._error("invalid_request", "%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" in kwargs:
|
|
try:
|
|
a_args["authorization"] = kwargs["authn"]
|
|
except KeyError:
|
|
pass
|
|
|
|
# Pick authentication method
|
|
_authn, acr = self.pick_auth(areq=areq)
|
|
|
|
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"]
|
|
|
|
# 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
|
|
|
|
sinfo = self.sdb.create_authz_session(user, areq, oidreq=oidc_req)
|
|
|
|
# Now about the authorization step.
|
|
try:
|
|
permissions = self.authz.permissions(cookie)
|
|
if not permissions:
|
|
return self.authz(user, sinfo)
|
|
except (ToOld, TamperAllert):
|
|
return self.authz(user, areq, sinfo)
|
|
|
|
return self.authz_part2(user, areq, sinfo, permissions, _authn)
|
|
|
|
def authz_part2(self, user, areq, sinfo, permission=None, authn=None,
|
|
**kwargs):
|
|
"""
|
|
After the authentication this is where you should end up
|
|
|
|
:param user:
|
|
:param areq: The Authorization Request
|
|
:param sinfo: Session information
|
|
: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() -")
|
|
|
|
sinfo["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" % sinfo)
|
|
|
|
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(sinfo)
|
|
rtype.remove("code")
|
|
else:
|
|
sinfo["code"] = None
|
|
_code = None
|
|
|
|
if "token" in rtype:
|
|
_dic = self.sdb.upgrade_to_token(sinfo, issue_refresh=False)
|
|
|
|
atr = AccessTokenResponse(**aresp.to_dict())
|
|
aresp = atr
|
|
_log_debug("_dic: %s" % _dic)
|
|
for key, val in _dic.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(sinfo)
|
|
|
|
# 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, auth_header="", **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, auth_header)
|
|
except FailedAuthentication, err:
|
|
err = TokenErrorResponse(error="unathorized_client",
|
|
error_description="%s" % err)
|
|
return Response(err.to_json(), content="application/json",
|
|
status="401 Unauthorized")
|
|
|
|
LOG_DEBUG("AccessTokenRequest: %s" % areq)
|
|
|
|
assert areq["grant_type"] == "authorization_code"
|
|
|
|
# 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"]
|
|
|
|
_tinfo = _sdb.update_to_token(areq["code"])
|
|
|
|
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)
|