Esempio n. 1
0
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))
Esempio n. 2
0
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='/'))
Esempio n. 3
0
    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
Esempio n. 5
0
    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")
Esempio n. 6
0
 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")
Esempio n. 9
0
def raven_logout(request):
    logout(request)
    return redirect(setting('UCAMWEBAUTH_LOGOUT_REDIRECT', default='/'))
Esempio n. 10
0
def raven_logout(request):
    logout(request)
    return redirect(setting('UCAMWEBAUTH_LOGOUT_REDIRECT', default='/'))