from __future__ import absolute_import
from builtins import str
from builtins import object
import logging
import time
import base64
import math
from actingweb import (actor, oauth, trust, config as config_class)
# This is where each path and subpath in actingweb is assigned an authentication type
# Fairly simple: /oauth is always oauth, /www can be either basic+trust or
# oauth through config.py, and everything else is basic+trust
[docs]def select_auth_type(path, subpath, config=None):
"""Selects authentication type based on path and subpath.
Currently are only basic and oauth supported. Peer auth is automatic if an Authorization Bearer <token> header is
included in the http request.
"""
if path == 'oauth':
return 'oauth'
if path == 'www':
return config.www_auth
if subpath == 'oauth':
return 'oauth'
return 'basic'
[docs]def add_auth_response(appreq=None, auth_obj=None):
"""Called after init_actingweb() if add_response was set to False, and now responses should be added.
"""
if not appreq or not auth_obj:
return False
logging.debug("add_auth_response: " +
str(auth_obj.response['code']) + ":" + auth_obj.response['text'])
appreq.set_status(auth_obj.response['code'], auth_obj.response['text'])
if auth_obj.response['code'] == 302:
appreq.set_redirect(url=auth_obj.redirect)
elif auth_obj.response['code'] == 401:
appreq.write("Authentication required")
for h, v in list(auth_obj.response['headers'].items()):
appreq.headers[h] = v
return True
[docs]def init_actingweb(appreq=None, actor_id=None, path='', subpath='', add_response=True, config=None):
"""Initialises actingweb by loading a config object, an actor object, and authentication object.
More details about the authentication can be found in the auth object. If add_response is True,
appreq.response
will be changed according to authentication result. If False, appreq will not be touched.
authn_done (bool), response['code'], response['text'], and response['headers'] will indicate results of
the authentication process
and need to be acted upon.
200 is access approved
302 is redirect and auth_obj.redirect contains redirect location where response must be redirected
401 is authentication required, response['headers'] must be added to response
403 is forbidden, text in response['text']
"""
fullpath = '/' + path + '/' + subpath
auth_type = select_auth_type(path=path, subpath=subpath, config=config)
auth_obj = Auth(actor_id, auth_type=auth_type, config=config)
if not auth_obj.actor:
if add_response:
appreq.response.set_status(404, 'Actor not found')
return None, None
auth_obj.check_authentication(appreq=appreq, path=fullpath)
if add_response:
add_auth_response(appreq.response, auth_obj)
# Initialize the on_aw object with auth (.actor) and webobj (.config) for functions in on_aw
appreq.on_aw.aw_init(auth=auth_obj, webobj=appreq)
return auth_obj.actor, auth_obj
[docs]class Auth(object):
""" The auth class handles authentication and authorisation for the various schemes supported.
The helper function init_actingweb() can be used to give you an auth object and do authentication (or can be called
directly).
The check_authentication() function checks the various authentication schemes against the path and does proper
authentication.
There are three types supported: basic (using creator credentials), token (received when trust is created), or
oauth (used to bind
an actor to an oauth-enabled external service, as well as to log into /www path where interactive web functionality
of the actor is available).
The check_authorisation() function validates the authenticated user against the config.py access list.
check_token_auth() can be called from outside the class to do a simple peer/bearer token verification.
The OAuth helper functions are used to:
process_oauth_callback() - process an OAuth callback as part of an OAuth flow and exchange code with a valid token
validate_oauth_token() - validate and, if necessary, refresh a token
set_cookie_on_cookie_redirect() - set a session cookie in the browser to the token value (called AFTER OAuth has
been done!)
The response[], acl[], and authn_done variables are useful outside Auth(). authn_done is set when authentication has
been done and a final authentication status can be found in response[].
self.response = {
"code": 403, # Result code (http)
"text": "Forbidden", # Proposed response text
"headers": [], # Headers to add to response after authentication has been done
}
self.acl = {
"authenticated": False, # Has authentication been verified and passed?
"authorised": False, # Has authorisation been done and appropriate acls set?
"rights": '', # "a", "r" (approve or reject)
"relationship": None, # E.g. creator, friend, admin, etc
"peerid": '', # Peerid if there is a relationship
"approved": False, # True if the peer is approved
}
"""
def __init__(self, actor_id, auth_type='basic', config=None):
if not config:
self.config = config_class.Config()
else:
self.config = config
self.token = None
self.cookie_redirect = None
self.cookie = None
self.type = auth_type
self.trust = None
self.oauth = None
# Proposed response code after check_authentication() or authorise() have been called
self.response = {'code': 403, 'text': "Forbidden", 'headers': {}}
# Whether authentication is complete or not (depends on flow)
self.authn_done = False
# acl stores the actual verified credentials and access rights after
# authentication and authorisation have been done
self.acl = {
"authenticated": False, # Has authentication been verified and passed?
"authorised": False, # Has authorisation been done and appropriate acls set?
"rights": '', # "a", "r" (approve or reject)
"relationship": None, # E.g. creator, friend, admin, etc
"peerid": '', # Peerid if there is a relationship
"approved": False, # True if the peer is approved
}
self.actor = actor.Actor(actor_id, config=self.config)
if not self.actor.id:
self.actor = None
self.oauth = oauth.OAuth(token=None, config=self.config)
self.token = None
self.expiry = None
self.refresh_expiry = None
self.refresh_token = None
return
# We need to initialise oauth for use towards the external oauth service
# Property name used to set self.token
self.oauth_token_property = 'oauth_token'
self.token = self.actor.store.oauth_token
if self.config.migrate_2_5_0 and not self.token:
self.token = self.actor.property.oauth_token
if self.token:
self.actor.store.oauth_token = self.token
self.actor.property.oauth_token = None
self.oauth = oauth.OAuth(token=self.token, config=self.config)
self.expiry = self.actor.store.oauth_token_expiry
self.refresh_expiry = self.actor.store.oauth_refresh_token_expiry
self.refresh_token = self.actor.store.oauth_refresh_token
if self.config.migrate_2_5_0:
if not self.expiry:
self.expiry = self.actor.property.oauth_token_expiry
if self.expiry:
self.actor.store.oauth_token_expiry = self.expiry
self.actor.property.oauth_token_expiry = None
if not self.refresh_expiry:
self.refresh_expiry = self.actor.property.oauth_refresh_token_expiry
if self.refresh_expiry:
self.actor.store.oauth_refresh_token_expiry = self.refresh_expiry
self.actor.property.oauth_refresh_token_expiry = None
if not self.refresh_token:
self.refresh_token = self.actor.property.oauth_refresh_token
if self.refresh_token:
self.actor.store.oauth_refresh_token = self.refresh_token
self.actor.property.oauth_refresh_token = None
if self.type == 'basic':
self.realm = self.config.auth_realm
elif self.type == 'oauth':
if self.oauth.enabled():
self.cookie = 'oauth_token'
redir = self.actor.store.cookie_redirect
if self.config.migrate_2_5_0 and not redir:
redir = self.actor.property.cookie_redirect
if redir:
self.actor.store.cookie_redirect = redir
self.actor.property.cookie_redirect = None
if redir:
self.cookie_redirect = self.config.root + redir
else:
self.cookie_redirect = None
self.redirect = str(self.config.root +
self.actor.id + '/oauth')
else:
self.type = 'none'
def __process_oauth_accept(self, result):
if not result:
return None
if not result['access_token']:
logging.debug('No token in response')
return None
now = time.time()
self.token = result['access_token']
self.actor.store.oauth_token = self.token
self.expiry = str(now + result['expires_in'])
self.actor.store.oauth_token_expiry = self.expiry
if 'refresh_token' in result:
self.refresh_token = result['refresh_token']
if 'refresh_token_expires_in' in result:
self.refresh_expiry = str(
now + result['refresh_token_expires_in'])
else:
# Set a default expiry 12 months ahead
self.refresh_expiry = str(now + (365*24*3600))
self.actor.store.oauth_refresh_token = self.refresh_token
self.actor.store.oauth_refresh_token_expiry = self.refresh_expiry
[docs] def process_oauth_callback(self, code):
""" Called when a callback is received as part of an OAuth flow to exchange code for a bearer token."""
if not code:
return False
if not self.oauth:
logging.warning(
'Call to processOauthCallback() with oauth disabled.')
return False
result = self.oauth.oauth_request_token(code)
if not result or (result and 'access_token' not in result):
logging.warning('No token in response')
return False
self.__process_oauth_accept(result)
return True
[docs] def validate_oauth_token(self, lazy=False):
""" Called to validate the token as part of a web-based flow.
Returns the redirect URI to send back to the browser or empty string.
If lazy is true, refresh_token is used only if < 24h until expiry.
"""
if not self.token or not self.expiry:
return self.oauth.oauth_redirect_uri(state=self.actor.id, creator=self.actor.creator)
now = time.time()
# Is the token still valid?
if now < (float(self.expiry) - 20.0):
return ""
# Has refresh_token expired?
if now > (float(self.refresh_expiry) - 20.0):
return self.oauth.oauth_redirect_uri(state=self.actor.id)
# Do we have more than a day until refresh token expiry?
if lazy and now < (float(self.refresh_expiry) - (3600.0 * 24)):
return ""
# Refresh the token
result = self.oauth.oauth_refresh_token(self.refresh_token)
if not result:
return self.oauth.oauth_redirect_uri(state=self.actor.id)
self.__process_oauth_accept(result)
return ""
[docs] def oauth_get(self, url=None, params=None):
"""Used to call GET from the attached oauth service.
Uses oauth.get_request(), but refreshes token if necessary.
The function fails if token is invalid and no refresh is
possible. For web-based flows, validate_oauth_token() needs
to be used to validate token and get redirect URI for new
authorization flow.
"""
if not url:
return None
ret = self.oauth.get_request(url=url, params=params)
code1 = self.oauth.last_response_code
if (ret and any(ret)) or code1 == 204 or code1 == 201 or code1 == 404:
return ret
if self.actor and self.actor.id and (not ret or code1 == 401 or code1 == 403):
refresh = self.oauth.oauth_refresh_token(
refresh_token=self.refresh_token)
if not refresh:
logging.warning(
'Tried to refresh token and failed for Actor(' + self.actor.id + ')')
else:
self.__process_oauth_accept(refresh)
ret2 = self.oauth.get_request(url=url, params=params)
code2 = self.oauth.last_response_code
if ret2 and any(ret2) or code2 == 204 or code2 == 201:
return ret2
return None
[docs] def oauth_head(self, url=None, params=None):
"""Used to call HEAD from the attached oauth service.
Uses oauth.head_request((), but refreshes token if necessary.
The function fails if token is invalid and no refresh is
possible. For web-based flows, validate_oauth_token() needs
to be used to validate token and get redirect URI for new
authorization flow.
"""
if not url:
return None
ret = self.oauth.head_request(url=url, params=params)
code1 = self.oauth.last_response_code
if (ret and any(ret)) or code1 == 204 or code1 == 201 or code1 == 404:
return ret
if self.actor and self.actor.id and (not ret or code1 == 401 or code1 == 403):
refresh = self.oauth.oauth_refresh_token(
refresh_token=self.refresh_token)
if not refresh:
logging.warning(
'Tried to refresh token and failed for Actor(' + self.actor.id + ')')
else:
self.__process_oauth_accept(refresh)
ret2 = self.oauth.head_request(url=url, params=params)
code2 = self.oauth.last_response_code
if ret2 and any(ret2) or code2 == 204 or code2 == 201:
return ret2
return None
[docs] def oauth_delete(self, url=None):
"""Used to call DELETE from the attached oauth service.
Uses oauth.delete_request((), but refreshes token if necessary.
The function fails if token is invalid and no refresh is
possible. For web-based flows, validate_oauth_token() needs
to be used to validate token and get redirect URI for new
authorization flow.
"""
if not url:
return None
ret = self.oauth.delete_request(url=url)
code1 = self.oauth.last_response_code
if (ret and any(ret)) or code1 == 204 or code1 == 404:
return ret
if self.actor and self.actor.id and (code1 == 401 or code1 == 403):
refresh = self.oauth.oauth_refresh_token(
refresh_token=self.refresh_token)
if not refresh:
logging.warning(
'Tried to refresh token and failed for Actor(' + self.actor.id + ')')
else:
self.__process_oauth_accept(refresh)
ret2 = self.oauth.delete_request(url=url)
code2 = self.oauth.last_response_code
if ret2 and any(ret2) or code2 == 204:
return ret2
return None
[docs] def oauth_post(self, url=None, params=None, urlencode=False):
"""Used to call POST from the attached oauth service.
Uses oauth.post_request(), but refreshes token if necessary.
The function fails if token is invalid and no refresh is
possible. For web-based flows, validate_oauth_token() needs
to be used to validate token and get redirect URI for new
authorization flow.
"""
if not url:
return None
ret = self.oauth.post_request(
url=url, params=params, urlencode=urlencode)
code1 = self.oauth.last_response_code
if (ret and any(ret)) or code1 == 204 or code1 == 201 or code1 == 404:
return ret
if self.actor and self.actor.id and (code1 == 401 or code1 == 403):
refresh = self.oauth.oauth_refresh_token(
refresh_token=self.refresh_token)
if not refresh:
logging.warning(
'Tried to refresh token and failed for Actor(' + self.actor.id + ')')
else:
self.__process_oauth_accept(refresh)
ret2 = self.oauth.post_request(
url=url, params=params, urlencode=urlencode)
code2 = self.oauth.last_response_code
if ret2 and any(ret2) or code2 == 204 or code2 == 201:
return ret2
return None
[docs] def oauth_put(self, url=None, params=None, urlencode=False):
"""Used to call PUT from the attached oauth service.
Uses oauth.put_request(), but refreshes token if necessary.
The function fails if token is invalid and no refresh is
possible. For web-based flows, validate_oauth_token() needs
to be used to validate token and get redirect URI for new
authorization flow.
"""
if not url:
return None
ret = self.oauth.put_request(
url=url, params=params, urlencode=urlencode)
code1 = self.oauth.last_response_code
if (ret and any(ret)) or code1 == 204 or code1 == 201 or code1 == 404:
return ret
if self.actor and self.actor.id and (code1 == 401 or code1 == 403):
refresh = self.oauth.oauth_refresh_token(
refresh_token=self.refresh_token)
if not refresh:
logging.warning(
'Tried to refresh token and failed for Actor(' + self.actor.id + ')')
else:
self.__process_oauth_accept(refresh)
ret2 = self.oauth.put_request(
url=url, params=params, urlencode=urlencode)
code2 = self.oauth.last_response_code
if ret2 and any(ret2) or code2 == 204 or code2 == 201:
return ret2
return None
# Called from a www page (browser access) to verify that a cookie has been
# set to the actor's valid token.
def __check_cookie_auth(self, appreq, path):
if not path:
path = ''
if not self.actor:
return False
if self.token:
now = time.time()
if appreq.request.cookies and self.cookie in appreq.request.cookies:
authz = appreq.request.cookies[self.cookie]
else:
authz = ''
if appreq.request.get('refresh') and appreq.request.get('refresh').lower() == 'true':
# Clear cookie and do a refresh if refresh=True is in GET param
authz = ''
self.actor.store.oauth_token = None
elif authz == self.token and now < (float(self.expiry) - 20.0):
logging.debug(
'Authorization cookie header matches a valid token')
self.acl["relationship"] = "creator"
self.acl["authenticated"] = True
self.response['code'] = 200
self.response['text'] = "Ok"
self.authn_done = True
return True
elif authz != self.token:
logging.debug(
'Authorization cookie header does not match a valid token')
self.response['code'] = 403
self.response['text'] = "Forbidden"
self.authn_done = True
return False
if self.cookie_redirect:
logging.debug('Cookie redirect already set!')
else:
self.actor.store.cookie_redirect = self.actor.id + path
self.cookie_redirect = '/' + self.actor.id + path
self.response['code'] = 302
return False
[docs] def set_cookie_on_cookie_redirect(self, appreq):
""" Called after successful auth to set the cookie with the token value."""
if not self.cookie_redirect:
return False
if not self.token:
logging.warning(
"Trying to set cookie when no token value can be found.")
return False
logging.debug('Setting Authorization cookie: ' + str(self.token))
appreq.response.set_cookie(self.cookie, str(self.token),
max_age=1209600, path='/', secure=True)
appreq.response.set_redirect(str(self.cookie_redirect))
self.actor.store.cookie_redirect = None
return True
def __check_basic_auth_creator(self, appreq):
if self.type != 'basic':
logging.warning(
"Trying to do basic auth when auth type is not basic")
self.response['code'] = 403
self.response['text'] = "Forbidden"
return False
if not self.actor.passphrase:
logging.warning(
"Trying to do basic auth when no passphrase value can be found.")
self.response['code'] = 403
self.response['text'] = "Forbidden"
return False
if 'Authorization' not in appreq.request.headers:
self.response['headers']['WWW-Authenticate'] = 'Basic realm="' + \
self.realm + '"'
self.response['code'] = 401
self.response['text'] = "Authentication required"
return False
authz = appreq.request.headers['Authorization']
(basic, token) = authz.split(' ')
if basic.lower() != "basic":
self.response['code'] = 403
self.response['text'] = "No basic auth in Authorization header"
logging.debug("No basic auth in Authorization header")
return False
self.authn_done = True
au = authz.split(' ')[1]
au = au.encode('utf-8')
au = base64.b64decode(au)
(username, password) = au.split(b':')
password = password.decode('utf-8')
username = username.decode('utf-8')
if username != self.actor.creator:
self.response['code'] = 403
self.response['text'] = "Invalid username or password"
logging.debug("Wrong creator username")
return False
if password != self.actor.passphrase:
self.response['code'] = 403
self.response['text'] = "Invalid username or password"
logging.debug("Wrong creator passphrase(" +
password + ") correct(" + self.actor.passphrase + ")")
return False
self.acl["relationship"] = "creator"
self.acl["authenticated"] = True
self.response['code'] = 200
self.response['text'] = "Ok"
return True
[docs] def check_token_auth(self, appreq):
""" Called with an http request to check the Authorization header and validate if we have a peer with
this token."""
if 'Authorization' not in appreq.request.headers:
return False
auth = appreq.request.headers['Authorization']
(bearer, token) = auth.split(' ')
if bearer.lower() != "bearer":
return False
self.authn_done = True
trustee = self.actor.store.trustee_root
if self.config.migrate_2_5_0 and not trustee:
trustee = self.actor.property.trustee_root
if trustee:
self.actor.property.trustee_root = None
self.actor.store.trustee_root = trustee
# If trustee_root is set, creator name is 'trustee' and
# bit strength of passphrase is > 80, use passphrase as
# token
if trustee and self.actor.creator.lower() == 'trustee':
if math.floor(len(self.actor.passphrase)*math.log(94, 2)) > 80:
if token == self.actor.passphrase:
self.acl["relationship"] = 'trustee'
self.acl["peerid"] = ''
self.acl["approved"] = True
self.acl["authenticated"] = True
self.response['code'] = 200
self.response['text'] = "Ok"
self.token = self.actor.passphrase
return True
else:
logging.warning(
'Attempted trustee bearer token auth with <80 bit strength token.')
tru = trust.Trust(actor_id=self.actor.id,
token=token, config=self.config)
new_trust = tru.get()
if new_trust:
logging.debug('Found trust with token: (' + str(new_trust) + ')')
if new_trust["peerid"] == self.actor.id:
logging.error("Peer == actor!!")
return False
if new_trust and len(new_trust) > 0:
self.acl["relationship"] = new_trust["relationship"]
self.acl["peerid"] = new_trust["peerid"]
self.acl["approved"] = new_trust["approved"]
self.acl["authenticated"] = True
self.response['code'] = 200
self.response['text'] = "Ok"
self.token = new_trust["secret"]
self.trust = new_trust
return True
else:
return False
[docs] def check_authentication(self, appreq, path):
""" Checks authentication in appreq, redirecting back to path if oauth is done."""
logging.debug('Checking authentication, token auth...')
if self.check_token_auth(appreq):
return
elif self.type == 'oauth':
logging.debug('Checking authentication, oauth cookie...')
self.__check_cookie_auth(appreq=appreq, path=path)
return
elif self.type == 'basic':
logging.debug('Checking authentication, basic auth...')
self.__check_basic_auth_creator(appreq=appreq)
return
logging.debug('Authentication done, and failed')
self.authn_done = True
self.response['code'] = 403
self.response['text'] = "Forbidden"
return
[docs] def check_authorisation(self, path='', subpath='', method='', peerid='',
approved=True):
""" Checks if the authenticated user has acl access rights in config.py.
Takes the path, subpath, method, and peerid of the path (if auth user
is different from the peer that owns the path, e.g. creator). If approved
is False, then the trust relationship does not need to be approved for
access"""
if len(self.acl["peerid"]) > 0 and approved and self.acl["approved"] is False:
logging.debug(
'Rejected authorization because trust relationship is not approved.')
return False
if self.acl["relationship"]:
relationship = self.acl["relationship"].lower()
else:
relationship = ''
method = method.upper()
self.acl["authorised"] = True
self.acl["rights"] = "r"
if len(path) == 0:
return False
if not subpath:
subpath = ''
fullpath = path.lower() + '/' + subpath.lower()
# ACLs: ('role', 'path', 'METHODS', 'access')
logging.debug('Testing access for (' + relationship +
' ' + self.acl["peerid"] + ') on (' + fullpath + ' ' +
peerid + ') using method ' + method)
for acl in self.config.access:
if acl[0] == 'any' and not self.acl["authenticated"]:
continue
if len(acl[0]) > 0 and acl[0] != 'any' and acl[0] != relationship and acl[0] != 'owner':
continue # no match on relationship
if acl[0] == relationship or acl[0] == 'any' or \
len(acl[0]) == 0 or (acl[0] == 'owner' and len(peerid) > 0 and
self.acl["peerid"] == peerid):
if fullpath.find(acl[1]) == 0:
if len(acl[2]) == 0 or acl[2].find(method) != -1:
self.acl["rights"] = acl[3]
logging.debug('Granted ' + acl[3] +
' access with ACL:' +
str(acl))
return True
return False