def raven_login(request): # Get the Raven object and return a redirect to the Raven server login_url = setting('UCAMWEBAUTH_LOGIN_URL') return_url = get_return_url(request) desc = setting('UCAMWEBAUTH_DESC', default='') # aauth is ignored as v3 only supports 'pwd', therefore we do not need it. iact = setting('UCAMWEBAUTH_IACT', default='') msg = setting('UCAMWEBAUTH_MSG', default='') fail = setting('UCAMWEBAUTH_FAIL', default='') next_p = request.GET.get('next', None) if next_p is not None: params = urlencode([('next', next_p)]) msg = urlencode([('ver', 3), ('url', return_url), ('desc', desc), ('iact', iact), ('msg', msg), ('params', params), ('fail', fail)]) else: msg = urlencode([('ver', 3), ('url', return_url), ('desc', desc), ('iact', iact), ('msg', msg), ('fail', fail)]) return HttpResponseSeeOther("%s?%s" % (login_url, msg))
def raven_return(request): try: token = request.GET['WLS-Response'] except KeyError: raise MalformedResponseError("no WLS-Response") # See if this is a valid token user = authenticate(request=request) if user is None: return redirect(setting('UCAMWEBAUTH_LOGOUT_REDIRECT', default='/')) else: login(request, user) # Redirect somewhere sensible redirect_url = get_next_from_wls_response(token) if redirect_url is not None and setting('UCAMWEBAUTH_REDIRECT_AFTER_LOGIN', default=None) is None: return HttpResponseRedirect(redirect_url) else: return HttpResponseRedirect(setting('UCAMWEBAUTH_REDIRECT_AFTER_LOGIN', default='/'))
def authenticate(self, request=None, remote_user=None): """Checks a response from the Raven server and sees if it is valid. If it is, returns the User with the same username as the Raven username. @return User object, or None if authentication failed""" # Check that everything is correct, and return try: response = RavenResponse(request) except Exception as e: logger.error("%s: %s" % (type(e).__name__, e)) raise if not response.validate(): raise OtherStatusCode( "The WLS returned status %d: %s" % (response.status, response.STATUS[response.status])) if (response.ver == 3) and (setting('UCAMWEBAUTH_NOT_CURRENT', default=False) is False) and \ ('current' not in response.ptags): logger.error( "%s: %s" % ("UserNotAuthorised", "Authentication successful but you are not authorised to " "access this site")) raise UserNotAuthorised( "Authentication successful but you are not authorised to access this site" ) if django.VERSION[0] <= 1 and django.VERSION[1] <= 10: user = super(RavenAuthBackend, self).authenticate(response.principal) else: user = super(RavenAuthBackend, self).authenticate(request, response.principal) # creates (if necessary) the UserProfile model and update the raven_for_life property from the RavenResponse if user: profile = UserProfile.objects.get_or_create(user=user)[0] raven_for_life = 'current' not in response.ptags if profile.raven_for_life != raven_for_life: profile.raven_for_life = raven_for_life profile.save() return user
def authenticate(self, request=None, remote_user=None): """Checks a response from the Raven server and sees if it is valid. If it is, returns the User with the same username as the Raven username. @return User object, or None if authentication failed""" # Check that everything is correct, and return try: response = RavenResponse(request) except Exception as e: logger.error("%s: %s" % (type(e).__name__, e)) raise if not response.validate(): raise OtherStatusCode("The WLS returned status %d: %s" % (response.status, response.STATUS[response.status])) if (response.ver == 3) and (setting('UCAMWEBAUTH_NOT_CURRENT', default=False) is False) and \ ('current' not in response.ptags): logger.error("%s: %s" % ("UserNotAuthorised", "Authentication successful but you are not authorised to " "access this site")) raise UserNotAuthorised("Authentication successful but you are not authorised to access this site") if django.VERSION[0] <= 1 and django.VERSION[1] <= 10: user = super(RavenAuthBackend, self).authenticate(response.principal) else: user = super(RavenAuthBackend, self).authenticate(request, response.principal) # creates (if necessary) the UserProfile model and update the raven_for_life property from the RavenResponse if user: profile = UserProfile.objects.get_or_create(user=user)[0] raven_for_life = 'current' not in response.ptags if profile.raven_for_life != raven_for_life: profile.raven_for_life = raven_for_life profile.save() return user
def __init__(self, response_req=None): """Creates a RavenResponse object from the response of the Web login service (WLS) of the University of Cambridge @param response_req The HTTP request that contains the WLS response. """ if response_req is None: raise MalformedResponseError("no request supplied") try: response_str = response_req.GET['WLS-Response'] except KeyError: raise MalformedResponseError("no WLS-Response") # The WLS sends an authentication response message as follows: First a 'encoded response string' is formed by # concatenating the values of the response fields below, in the order shown, using '!' as a separator character. # If the characters '!' or '%' appear in any # field value they MUST be replaced by their %-encoded representation # before concatenation. # Parameters with no relevant value MUST be encoded as the empty string. rawtokens = response_str.split('!') tokens = list(map( unquote, rawtokens)) # return a list for python3 compatibility # ver: The version of the WLS protocol in use. May be the same as the 'ver' parameter # supplied in the request try: self.ver = int(tokens[0]) except ValueError: raise MalformedResponseError( "Version number must be an integer, not %s" % tokens[0]) if not 4 > self.ver > 0: raise MalformedResponseError("Unsupported version: %d" % self.ver) if self.ver == 3: versioni = 0 else: versioni = 1 # Check that the number of parameters in the response is correct if len(tokens) != (14 - versioni): raise MalformedResponseError( "Wrong number of parameters in response: expected %d, got %d" % ((14 - versioni), len(tokens))) # status: A three digit status code indicating the status of the authentication request. The list of possible # statuses can be seen in the STATUS dict of the RavenResponse object. try: self.status = int(tokens[1]) except ValueError: raise MalformedResponseError( "Status code must be an integer, not %s" % tokens[1]) if self.status not in self.STATUS: raise InvalidResponseError("Status returned not known") # msg (optional): A text message further describing the status of the authentication request, # suitable for display to end-user. self.msg = tokens[2] # issue: The date and time that the authentication response was created. try: self.issue = parse_time(tokens[3]) except ValueError: raise MalformedResponseError( "Issue time is not a valid time, got %s" % tokens[3]) # Check that the response is recent by comparing 'issue' with the current time. The WLS MUST and the WAA SHOULD # have their clocks synchronised by NTP or a similar mechanism. Providing the WAA has access to an # NTP-synchronised clock then allowing for a transmission time of 30-60 seconds is probably appropriate. # Otherwise allowance must be made for the maximum expected clock skew. if self.issue > time.time(): raise InvalidResponseError( "The timestamp on the response is in the future") if self.issue < time.time() - setting('UCAMWEBAUTH_TIMEOUT', 30): raise InvalidResponseError( "Response has timed out - issued %s, now %s" % (time.asctime(time.gmtime(self.issue)), time.asctime())) # ident: An identifier for this response. 'ident', combined with 'issue' provides a uid for this response. self.ident = tokens[4] if self.ident == "": raise MalformedResponseError("Empty ID") # url: The value of url supplied in the authentication request and used to form the authentication response. self.url = tokens[5] # Check that 'url' represents the resource currently being # accessed. The request has already been checked against # ALLOWED_HOSTS by Django. if self.url != get_return_url(response_req): raise InvalidResponseError( "The URL in the response does not match the URL expected") # principal: Only present if status == 200, indicates the authenticated identity of the user if self.status == 200: if tokens[6] != "": self.principal = tokens[6] else: raise InvalidResponseError( "The username is not present in the WLS response") else: if tokens[6] != "": raise InvalidResponseError( "The username should not be present if the status code is not 200" ) # ptags (optional): A potentially empty sequence of text tokens separated by ',' indicating attributes # or properties of the identified principal. Possible values of this tag are not standardised and are # a matter for local definition by individual WLS operators (see note below). Web application agent (WAA) # SHOULD ignore values that they do not recognise. if versioni == 0: self.ptags = tokens[7].split(',') # auth (not-empty only if authentication was successfully established by interaction with the user): # This indicates which authentication type was used. v3 only supports 'pwd' self.auth = tokens[8 - versioni] # sso (not-empty only if 'auth' is empty): Authentication must have been established based on previous # successful authentication interaction(s) with the user. This indicates which authentication types were used # on these occasions. This value consists of a sequence of text tokens as described below, separated by ','. self.sso = tokens[9 - versioni].split(',') # life (optional): If the user has established an authenticated 'session' with the WLS, this indicates the # remaining life (in seconds) of that session. If present, a WAA SHOULD use this to establish an upper limit # to the lifetime of any session that it establishes. # TODO https://docs.djangoproject.com/en/dev/topics/http/sessions/#django.contrib.sessions.backends.base.SessionBase.set_expiry if tokens[10 - versioni] != "": try: self.life = int(tokens[10 - versioni]) except ValueError: raise MalformedResponseError( "Life parameter must be an integer, not %s" % tokens[10 - versioni]) # params: a copy of the params parameter from the request try: self.params = parse_qs(tokens[11 - versioni]) except Exception: raise MalformedResponseError( "The params field contains wrong characters: %s" % tokens[11 - versioni]) # REQUIRED to be a copy of the params parameter from the request # if self.params != setting('UCAMWEBAUTH_PARAMS', default=''): # raise InvalidResponseError("The params are not equals to the request ones") # kid (not-empty only if 'sig' is present): A string which identifies the RSA key which was used to form the # signature supplied with the response. Typically these will be small integers. if tokens[12 - versioni] != "": try: self.kid = int(tokens[12 - versioni]) except ValueError: raise MalformedResponseError( "kid parameter must be an integer, not %s" % tokens[12 - versioni]) # sig (not-empty only if 'status' is 200): A public-key signature of the response data constructed from the # entire parameter value except 'kid' and 'sig' (and their separating ':' characters) using the private key # identified by 'kid', the SHA-1 hash algorithm and the 'RSASSA-PKCS1-v1_5' scheme as specified in PKCS #1 v2.1 # [RFC 3447] and the resulting signature encoded using the base64 scheme [RFC 1521] except that the # characters '+', '/', and '=' are replaced by '-', '.' and '_' to reduce the URL-encoding overhead. if tokens[13 - versioni] != "": if self.kid is None: raise InvalidResponseError( "kid must be present if signature is present") self.sig = decode_sig(tokens[13 - versioni]) else: if self.status == 200: raise InvalidResponseError( "Signature must be present if status is 200") # Check that 'kid', corresponds to a key/certificate present in the WAA. Is the only way to check the # signature. The WAA has to use the public key/certificate made available by the WLS. if (self.sig is not None) or (self.status == 200): try: cert = load_certificate(FILETYPE_PEM, settings.UCAMWEBAUTH_CERTS[self.kid]) except Exception: raise PublicKeyNotFoundError( "The server do not have the public key corresponding to the key the web " "login service signed the response with") # Check that the signature matches the data supplied. To check this, the WAA uses the public key identified # by 'kid'. data = '!'.join(rawtokens[0:(12 - versioni)]) # The data string that was signed in the WLS (everything from the WLS-Response except 'kid' and 'sig' try: verify(cert, self.sig, data.encode(), 'sha1') except Exception: raise InvalidResponseError( "The signature for this response is not valid.") if self.status == 200: # Check that 'auth' and/or 'sso' contain values acceptable to the WAA. Simply setting 'aauth' and 'iact' # values in an authentication request is not sufficient since an attacker could construct its own request. # Conversely, the WAA MUST ensure that the values of 'aauth' and/or 'iact' in its authentication requests # correctly reflect its requirement, to prevent the WLS sending it unacceptable responses. UCAMWEBAUTH_IACT = setting('UCAMWEBAUTH_IACT', '') # the authentication was successfully establish by interaction with the user if self.auth != "": # auth only supports 'pwd' in current version, therefore we compare it with 'pwd' only # If more are supported in the future, a setting will be added to specify which ones the WAA wants to # support and check that auth and sso match any element in this list. if self.auth != "pwd": raise InvalidResponseError( "The response used the wrong type of authentication (auth)" ) if UCAMWEBAUTH_IACT == 'no': # We had required a non-interactive authentication, but didn't get one raise InvalidResponseError( "Non-interactive authentication required but not received" ) # authentication was established on a previous interaction(s) with the user else: if self.sso != [""]: if self.sso != ["pwd"]: raise InvalidResponseError( "The response used the wrong type of authentication (sso)" ) if UCAMWEBAUTH_IACT == 'yes': # We had required an interactive authentication, but didn't get one raise InvalidResponseError( "Interactive authentication required but not received" ) else: # Both auth and sso are empty, which is not allowed raise MalformedResponseError( "No authentication types supplied")
def create_unknown_user(self): return setting('UCAMWEBAUTH_CREATE_USER', default=True)
def create_unknown_user(self): return setting('UCAMWEBAUTH_CREATE_USER', default=True)
def __init__(self, response_req=None): """Creates a RavenResponse object from the response of the Web login service (WLS) of the University of Cambridge @param response_req The HTTP request that contains the WLS response. """ if response_req is None: raise MalformedResponseError("no request supplied") try: response_str = response_req.GET['WLS-Response'] except KeyError: raise MalformedResponseError("no WLS-Response") # The WLS sends an authentication response message as follows: First a 'encoded response string' is formed by # concatenating the values of the response fields below, in the order shown, using '!' as a separator character. # If the characters '!' or '%' appear in any # field value they MUST be replaced by their %-encoded representation # before concatenation. # Parameters with no relevant value MUST be encoded as the empty string. rawtokens = response_str.split('!') tokens = list(map(unquote, rawtokens)) # return a list for python3 compatibility # ver: The version of the WLS protocol in use. May be the same as the 'ver' parameter # supplied in the request try: self.ver = int(tokens[0]) except ValueError: raise MalformedResponseError("Version number must be an integer, not %s" % tokens[0]) if not 4 > self.ver > 0: raise MalformedResponseError("Unsupported version: %d" % self.ver) if self.ver == 3: versioni = 0 else: versioni = 1 # Check that the number of parameters in the response is correct if len(tokens) != (14-versioni): raise MalformedResponseError("Wrong number of parameters in response: expected %d, got %d" % ((14-versioni), len(tokens))) # status: A three digit status code indicating the status of the authentication request. The list of possible # statuses can be seen in the STATUS dict of the RavenResponse object. try: self.status = int(tokens[1]) except ValueError: raise MalformedResponseError("Status code must be an integer, not %s" % tokens[1]) if self.status not in self.STATUS: raise InvalidResponseError("Status returned not known") # msg (optional): A text message further describing the status of the authentication request, # suitable for display to end-user. self.msg = tokens[2] # issue: The date and time that the authentication response was created. try: self.issue = parse_time(tokens[3]) except ValueError: raise MalformedResponseError("Issue time is not a valid time, got %s" % tokens[3]) # Check that the response is recent by comparing 'issue' with the current time. The WLS MUST and the WAA SHOULD # have their clocks synchronised by NTP or a similar mechanism. Providing the WAA has access to an # NTP-synchronised clock then allowing for a transmission time of 30-60 seconds is probably appropriate. # Otherwise allowance must be made for the maximum expected clock skew. if self.issue > time.time(): raise InvalidResponseError("The timestamp on the response is in the future") if self.issue < time.time() - setting('UCAMWEBAUTH_TIMEOUT', 30): raise InvalidResponseError("Response has timed out - issued %s, now %s" % (time.asctime(time.gmtime(self.issue)), time.asctime())) # ident: An identifier for this response. 'ident', combined with 'issue' provides a uid for this response. self.ident = tokens[4] if self.ident == "": raise MalformedResponseError("Empty ID") # url: The value of url supplied in the authentication request and used to form the authentication response. self.url = tokens[5] # Check that 'url' represents the resource currently being # accessed. The request has already been checked against # ALLOWED_HOSTS by Django. if self.url != get_return_url(response_req): raise InvalidResponseError("The URL in the response does not match the URL expected") # principal: Only present if status == 200, indicates the authenticated identity of the user if self.status == 200: if tokens[6] != "": self.principal = tokens[6] else: raise InvalidResponseError("The username is not present in the WLS response") else: if tokens[6] != "": raise InvalidResponseError("The username should not be present if the status code is not 200") # ptags (optional): A potentially empty sequence of text tokens separated by ',' indicating attributes # or properties of the identified principal. Possible values of this tag are not standardised and are # a matter for local definition by individual WLS operators (see note below). Web application agent (WAA) # SHOULD ignore values that they do not recognise. if versioni == 0: self.ptags = tokens[7].split(',') # auth (not-empty only if authentication was successfully established by interaction with the user): # This indicates which authentication type was used. v3 only supports 'pwd' self.auth = tokens[8-versioni] # sso (not-empty only if 'auth' is empty): Authentication must have been established based on previous # successful authentication interaction(s) with the user. This indicates which authentication types were used # on these occasions. This value consists of a sequence of text tokens as described below, separated by ','. self.sso = tokens[9-versioni].split(',') # life (optional): If the user has established an authenticated 'session' with the WLS, this indicates the # remaining life (in seconds) of that session. If present, a WAA SHOULD use this to establish an upper limit # to the lifetime of any session that it establishes. # TODO https://docs.djangoproject.com/en/dev/topics/http/sessions/#django.contrib.sessions.backends.base.SessionBase.set_expiry if tokens[10-versioni] != "": try: self.life = int(tokens[10-versioni]) except ValueError: raise MalformedResponseError("Life parameter must be an integer, not %s" % tokens[10-versioni]) # params: a copy of the params parameter from the request try: self.params = parse_qs(tokens[11-versioni]) except Exception: raise MalformedResponseError("The params field contains wrong characters: %s" % tokens[11-versioni]) # REQUIRED to be a copy of the params parameter from the request # if self.params != setting('UCAMWEBAUTH_PARAMS', default=''): # raise InvalidResponseError("The params are not equals to the request ones") # kid (not-empty only if 'sig' is present): A string which identifies the RSA key which was used to form the # signature supplied with the response. Typically these will be small integers. if tokens[12-versioni] != "": try: self.kid = int(tokens[12-versioni]) except ValueError: raise MalformedResponseError("kid parameter must be an integer, not %s" % tokens[12-versioni]) # sig (not-empty only if 'status' is 200): A public-key signature of the response data constructed from the # entire parameter value except 'kid' and 'sig' (and their separating ':' characters) using the private key # identified by 'kid', the SHA-1 hash algorithm and the 'RSASSA-PKCS1-v1_5' scheme as specified in PKCS #1 v2.1 # [RFC 3447] and the resulting signature encoded using the base64 scheme [RFC 1521] except that the # characters '+', '/', and '=' are replaced by '-', '.' and '_' to reduce the URL-encoding overhead. if tokens[13-versioni] != "": if self.kid is None: raise InvalidResponseError("kid must be present if signature is present") self.sig = decode_sig(tokens[13-versioni]) else: if self.status == 200: raise InvalidResponseError("Signature must be present if status is 200") # Check that 'kid', corresponds to a key/certificate present in the WAA. Is the only way to check the # signature. The WAA has to use the public key/certificate made available by the WLS. if (self.sig is not None) or (self.status == 200): try: cert = load_certificate(FILETYPE_PEM, settings.UCAMWEBAUTH_CERTS[self.kid]) except Exception: raise PublicKeyNotFoundError("The server do not have the public key corresponding to the key the web " "login service signed the response with") # Check that the signature matches the data supplied. To check this, the WAA uses the public key identified # by 'kid'. data = '!'.join(rawtokens[0:(12-versioni)]) # The data string that was signed in the WLS (everything from the WLS-Response except 'kid' and 'sig' try: verify(cert, self.sig, data.encode(), 'sha1') except Exception: raise InvalidResponseError("The signature for this response is not valid.") if self.status == 200: # Check that 'auth' and/or 'sso' contain values acceptable to the WAA. Simply setting 'aauth' and 'iact' # values in an authentication request is not sufficient since an attacker could construct its own request. # Conversely, the WAA MUST ensure that the values of 'aauth' and/or 'iact' in its authentication requests # correctly reflect its requirement, to prevent the WLS sending it unacceptable responses. UCAMWEBAUTH_IACT = setting('UCAMWEBAUTH_IACT', '') # the authentication was successfully establish by interaction with the user if self.auth != "": # auth only supports 'pwd' in current version, therefore we compare it with 'pwd' only # If more are supported in the future, a setting will be added to specify which ones the WAA wants to # support and check that auth and sso match any element in this list. if self.auth != "pwd": raise InvalidResponseError("The response used the wrong type of authentication (auth)") if UCAMWEBAUTH_IACT == 'no': # We had required a non-interactive authentication, but didn't get one raise InvalidResponseError("Non-interactive authentication required but not received") # authentication was established on a previous interaction(s) with the user else: if self.sso != [""]: if self.sso != ["pwd"]: raise InvalidResponseError("The response used the wrong type of authentication (sso)") if UCAMWEBAUTH_IACT == 'yes': # We had required an interactive authentication, but didn't get one raise InvalidResponseError("Interactive authentication required but not received") else: # Both auth and sso are empty, which is not allowed raise MalformedResponseError("No authentication types supplied")
def raven_logout(request): logout(request) return redirect(setting('UCAMWEBAUTH_LOGOUT_REDIRECT', default='/'))
def raven_logout(request): logout(request) return redirect(setting('UCAMWEBAUTH_LOGOUT_REDIRECT', default='/'))