Source code for actingweb.auth

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 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