Exemplo n.º 1
0
        def _validate_permissions_object(claim_name):

            access_permission_object = nmos_token_claims.get(claim_name)

            if not access_permission_object:
                raise InvalidClaimError(
                    "{}. Missing from token claims: '{}'.".format(
                        claim_name, list(nmos_token_claims.keys())))

            if request.method in ["GET", "OPTIONS", "HEAD"]:
                access_right = "read"
            elif request.method in ["PUT", "POST", "PATCH", "DELETE"]:
                access_right = "write"
            else:
                abort(405)

            url_access_list = access_permission_object.get(access_right)
            if not url_access_list:
                raise InvalidClaimError(
                    "{}. No entry in permissions object for '{}'".format(
                        claim_name, access_right))

            pattern = re.compile(r'/x-nmos/[a-z]+/v[0-9]+\.[0-9]+/(.*)'
                                 )  # Capture path after namespace
            sub_path = pattern.match(request.path).group(1)
            for wildcard_url in url_access_list:
                if fnmatch(sub_path, wildcard_url):
                    return

            raise InvalidClaimError(
                "{}. No matching paths in token claim: {} for URL path: '{}'".
                format(claim_name, url_access_list, sub_path))
Exemplo n.º 2
0
    def validate_aud(self):
        """The "aud" (audience) claim identifies the recipients that the JWT is
        intended for.  Each principal intended to process the JWT MUST
        identify itself with a value in the audience claim.  If the principal
        processing the claim does not identify itself with a value in the
        "aud" claim when this claim is present, then the JWT MUST be
        rejected.  In the general case, the "aud" value is an array of case-
        sensitive strings, each containing a StringOrURI value.  In the
        special case when the JWT has one audience, the "aud" value MAY be a
        single case-sensitive string containing a StringOrURI value.  The
        interpretation of audience values is generally application specific.
        Use of this claim is OPTIONAL.
        """
        aud_option = self.options.get('aud')
        aud = self.get('aud')
        if not aud_option or not aud:
            return

        aud_values = aud_option.get('values')
        if not aud_values:
            aud_value = aud_option.get('value')
            if aud_value:
                aud_values = [aud_value]

        if not aud_values:
            return

        if isinstance(self['aud'], list):
            aud_list = self['aud']
        else:
            aud_list = [self['aud']]

        if not any([v in aud_list for v in aud_values]):
            raise InvalidClaimError('aud')
Exemplo n.º 3
0
    def _validate_claim_value(self, claim_name):
        option = self.options.get(claim_name)
        if not option:
            return

        value = self.get(claim_name)
        option_value = option.get('value')
        if option_value and value != option_value:
            raise InvalidClaimError(claim_name)

        option_values = option.get('values')
        if option_values and value not in option_values:
            raise InvalidClaimError(claim_name)

        validate = option.get('validate')
        if validate and not validate(self, value):
            raise InvalidClaimError(claim_name)
Exemplo n.º 4
0
 def validate_contacts(self):
     """Array of strings representing ways to contact people responsible
     for this client, typically email addresses.  The authorization
     server MAY make these contact addresses available to end-users for
     support requests for the client.  See Section 6 for information on
     Privacy Considerations.
     """
     if 'contacts' in self and not isinstance(self['contacts'], list):
         raise InvalidClaimError('contacts')
Exemplo n.º 5
0
 def validate_iat(self, now, leeway):
     """The "iat" (issued at) claim identifies the time at which the JWT was
     issued.  This claim can be used to determine the age of the JWT.  Its
     value MUST be a number containing a NumericDate value.  Use of this
     claim is OPTIONAL.
     """
     iat = self.get('iat')
     if iat and not isinstance(iat, int):
         raise InvalidClaimError('iat')
Exemplo n.º 6
0
    def validate_jwks(self):
        """Client's JSON Web Key Set [RFC7517] document value, which contains
        the client's public keys.  The value of this field MUST be a JSON
        object containing a valid JWK Set.  These keys can be used by
        higher-level protocols that use signing or encryption.  This
        parameter is intended to be used by clients that cannot use the
        "jwks_uri" parameter, such as native clients that cannot host
        public URLs.  The "jwks_uri" and "jwks" parameters MUST NOT both
        be present in the same request or response.
        """
        if 'jwks' in self:
            if 'jwks_uri' in self:
                #  The "jwks_uri" and "jwks" parameters MUST NOT both  be present
                raise InvalidClaimError('jwks')

            jwks = self['jwks']
            if isinstance(jwks, dict) and 'keys' in jwks:
                if all([_is_valid_public_jwk(k) for k in jwks['keys']]):
                    return True

            raise InvalidClaimError('jwks')
Exemplo n.º 7
0
    def validate_jwks(self):
        """Client's JSON Web Key Set [RFC7517] document value, which contains
        the client's public keys.  The value of this field MUST be a JSON
        object containing a valid JWK Set.  These keys can be used by
        higher-level protocols that use signing or encryption.  This
        parameter is intended to be used by clients that cannot use the
        "jwks_uri" parameter, such as native clients that cannot host
        public URLs.  The "jwks_uri" and "jwks" parameters MUST NOT both
        be present in the same request or response.
        """
        if 'jwks' in self:
            if 'jwks_uri' in self:
                #  The "jwks_uri" and "jwks" parameters MUST NOT both  be present
                raise InvalidClaimError('jwks')

            jwks = self['jwks']
            try:
                key_set = JsonWebKey.import_key_set(jwks)
                if not key_set:
                    raise InvalidClaimError('jwks')
            except ValueError:
                raise InvalidClaimError('jwks')
Exemplo n.º 8
0
 def validate_amr(self):
     """OPTIONAL. Authentication Methods References. JSON array of strings
     that are identifiers for authentication methods used in the
     authentication. For instance, values might indicate that both password
     and OTP authentication methods were used. The definition of particular
     values to be used in the amr Claim is beyond the scope of this
     specification. Parties using this claim will need to agree upon the
     meanings of the values used, which may be context-specific. The amr
     value is an array of case sensitive strings.
     """
     amr = self.get('amr')
     if amr and not isinstance(self['amr'], list):
         raise InvalidClaimError('amr')
Exemplo n.º 9
0
    def validate_auth_time(self):
        """Time when the End-User authentication occurred. Its value is a JSON
        number representing the number of seconds from 1970-01-01T0:0:0Z as
        measured in UTC until the date/time. When a max_age request is made or
        when auth_time is requested as an Essential Claim, then this Claim is
        REQUIRED; otherwise, its inclusion is OPTIONAL.
        """
        auth_time = self.get('auth_time')
        if self.params.get('max_age') and not auth_time:
            raise MissingClaimError('auth_time')

        if auth_time and not isinstance(auth_time, (int, float)):
            raise InvalidClaimError('auth_time')
Exemplo n.º 10
0
 def validate_at_hash(self):
     """OPTIONAL. Access Token hash value. Its value is the base64url
     encoding of the left-most half of the hash of the octets of the ASCII
     representation of the access_token value, where the hash algorithm
     used is the hash algorithm used in the alg Header Parameter of the
     ID Token's JOSE Header. For instance, if the alg is RS256, hash the
     access_token value with SHA-256, then take the left-most 128 bits and
     base64url encode them. The at_hash value is a case sensitive string.
     """
     access_token = self.params.get('access_token')
     at_hash = self.get('at_hash')
     if at_hash and access_token:
         if not _verify_hash(at_hash, access_token, self.header['alg']):
             raise InvalidClaimError('at_hash')
Exemplo n.º 11
0
    def validate_execute(self, agreement_id, workflow_did, consumer_address):
        keeper = keeper_instance()

        asset_id = keeper.agreement_manager.get_agreement(agreement_id).did
        did = id_to_did(asset_id)
        asset = DIDResolver(keeper.did_registry).resolve(did)

        if not was_compute_triggered(agreement_id, did, consumer_address, keeper):

            agreement = keeper.agreement_manager.get_agreement(agreement_id)
            cond_ids = agreement.condition_ids
            self.check_ddo(did, agreement_id, asset_id, consumer_address, keeper, cond_ids, ServiceTypes.CLOUD_COMPUTE)

            compute_condition_status = keeper.condition_manager.get_condition_state(cond_ids[0])
            lock_condition_status = keeper.condition_manager.get_condition_state(cond_ids[1])
            escrow_condition_status = keeper.condition_manager.get_condition_state(
                cond_ids[2])
            logger.debug('ComputeExecutionCondition: %d' % compute_condition_status)
            logger.debug('LockPaymentCondition: %d' % lock_condition_status)
            logger.debug('EscrowPaymentCondition: %d' % escrow_condition_status)

            if lock_condition_status != ConditionState.Fulfilled.value:
                logger.debug('ServiceAgreement %s was not paid. Forbidden' % agreement_id)
                raise InvalidClaimError(
                    f"ServiceAgreement {agreement_id} was not paid, LockPaymentCondition status is {lock_condition_status}")

            fulfill_compute_condition(keeper, agreement_id, cond_ids, asset_id, consumer_address,
                                      self.provider_account)
            fulfill_escrow_payment_condition(keeper, agreement_id, cond_ids, asset,
                                             self.provider_account,
                                             ServiceTypes.CLOUD_COMPUTE)

            iteration = 0
            access_granted = False
            while iteration < ConfigSections.PING_ITERATIONS:
                iteration = iteration + 1
                logger.debug('Checking if compute was granted. Iteration %d' % iteration)
                if not was_compute_triggered(agreement_id, did, consumer_address, keeper):
                    time.sleep(ConfigSections.PING_SLEEP / 1000)
                else:
                    access_granted = True
                    break

            if not access_granted:
                msg = (
                    'Scheduling the compute execution failed. Either consumer address does not '
                    'have permission to execute this workflow or consumer address and/or service '
                    'agreement id is invalid.')
                logger.warning(msg)
                raise InvalidClientError(msg)
Exemplo n.º 12
0
    def validate_aud(self):
        super(JWTClaimsValidator, self).validate_aud()
        claim_name = "aud"
        fqdn = getfqdn()  # Fully qualified domain name of Resource Server
        actual_claim_value = self.get(claim_name)  # actual claim value in JWT
        if isinstance(actual_claim_value, string_types):
            actual_claim_value = [actual_claim_value]

        for aud in actual_claim_value:
            if fnmatch(fqdn, aud):
                return
        raise InvalidClaimError(
            "Hostname '{}' does not match aud claim value of '{}'".format(
                fqdn, actual_claim_value))
Exemplo n.º 13
0
 def validate_exp(self, now, leeway):
     """The "exp" (expiration time) claim identifies the expiration time on
     or after which the JWT MUST NOT be accepted for processing.  The
     processing of the "exp" claim requires that the current date/time
     MUST be before the expiration date/time listed in the "exp" claim.
     Implementers MAY provide for some small leeway, usually no more than
     a few minutes, to account for clock skew.  Its value MUST be a number
     containing a NumericDate value.  Use of this claim is OPTIONAL.
     """
     if 'exp' in self:
         exp = self['exp']
         if not _validate_numeric_time(exp):
             raise InvalidClaimError('exp')
         if exp < (now - leeway):
             raise ExpiredTokenError()
Exemplo n.º 14
0
 def validate_nbf(self, now, leeway):
     """The "nbf" (not before) claim identifies the time before which the JWT
     MUST NOT be accepted for processing.  The processing of the "nbf"
     claim requires that the current date/time MUST be after or equal to
     the not-before date/time listed in the "nbf" claim.  Implementers MAY
     provide for some small leeway, usually no more than a few minutes, to
     account for clock skew.  Its value MUST be a number containing a
     NumericDate value.  Use of this claim is OPTIONAL.
     """
     if 'nbf' in self:
         nbf = self['nbf']
         if not _validate_numeric_time(nbf):
             raise InvalidClaimError('nbf')
         if nbf > (now + leeway):
             raise InvalidTokenError()
Exemplo n.º 15
0
 def validate_nonce(self):
     """String value used to associate a Client session with an ID Token,
     and to mitigate replay attacks. The value is passed through unmodified
     from the Authentication Request to the ID Token. If present in the ID
     Token, Clients MUST verify that the nonce Claim Value is equal to the
     value of the nonce parameter sent in the Authentication Request. If
     present in the Authentication Request, Authorization Servers MUST
     include a nonce Claim in the ID Token with the Claim Value being the
     nonce value sent in the Authentication Request. Authorization Servers
     SHOULD perform no other processing on nonce values used. The nonce
     value is a case sensitive string.
     """
     nonce_value = self.params.get('nonce')
     if nonce_value:
         if 'nonce' not in self:
             raise MissingClaimError('nonce')
         if nonce_value != self['nonce']:
             raise InvalidClaimError('nonce')
Exemplo n.º 16
0
 def validate_c_hash(self):
     """Code hash value. Its value is the base64url encoding of the
     left-most half of the hash of the octets of the ASCII representation
     of the code value, where the hash algorithm used is the hash algorithm
     used in the alg Header Parameter of the ID Token's JOSE Header. For
     instance, if the alg is HS512, hash the code value with SHA-512, then
     take the left-most 256 bits and base64url encode them. The c_hash
     value is a case sensitive string.
     If the ID Token is issued from the Authorization Endpoint with a code,
     which is the case for the response_type values code id_token and code
     id_token token, this is REQUIRED; otherwise, its inclusion is OPTIONAL.
     """
     code = self.params.get('code')
     c_hash = self.get('c_hash')
     if code:
         if not c_hash:
             raise MissingClaimError('c_hash')
         if not _verify_hash(c_hash, code, self.header['alg']):
             raise InvalidClaimError('c_hash')
Exemplo n.º 17
0
    def validate_iat(self, now, leeway) -> None:
        """
        Overloaded implementation of the 'validate_iat' method in the AuthLib default 'JWTClaims' class.

        Differences include:
        - checking the claim value is after now, to ensure a token has been issued and is 'in force'

        Note: Validating the 'issued at' claim in this way is not required when validating a token, according to
        RFC7519, the JWT RFC. We do so because it makes logical sense with the way our OAuth provider (Azure) works.

        :type now: float
        :param now: current time, in the form of seconds past the Unix Epoch
        :type leeway: float
        :param leeway: a time delta in seconds to allow for clock skew between servers (i.e. a margin of error)
        """
        iat = self.get('iat')
        if iat and not isinstance(iat, int):
            raise InvalidClaimError('iat')
        if iat > (now + leeway):
            raise InvalidTokenError()
Exemplo n.º 18
0
    def validate_exp(self, now: float = None, leeway: float = 0) -> None:
        """
        Overloaded implementation of the 'validate_exp' method in the AuthLib default 'JWTClaims' class.

        Differences include:
        - providing default parameter values for 'now' and 'leeway' to make it easier to call this method directly

        :type now: float
        :param now: current time, in the form of seconds past the Unix Epoch
        :type leeway: float
        :param leeway: a time delta in seconds to allow for clock skew between servers (i.e. a margin of error)
        """
        if now is None:
            now = int(time.time())

        exp = self.get('exp')
        if exp:
            if not isinstance(exp, int):
                raise InvalidClaimError('exp')
            if exp < (now - leeway):
                raise ExpiredTokenError()
Exemplo n.º 19
0
    def validate_azp(self):
        """OPTIONAL. Authorized party - the party to which the ID Token was
        issued. If present, it MUST contain the OAuth 2.0 Client ID of this
        party. This Claim is only needed when the ID Token has a single
        audience value and that audience is different than the authorized
        party. It MAY be included even when the authorized party is the same
        as the sole audience. The azp value is a case sensitive string
        containing a StringOrURI value.
        """
        aud = self.get('aud')
        client_id = self.params.get('client_id')
        required = False
        if aud and client_id:
            if isinstance(aud, list) and len(aud) == 1:
                aud = aud[0]
            if aud != client_id:
                required = True

        azp = self.get('azp')
        if required and not azp:
            raise MissingClaimError('azp')

        if azp and client_id and azp != client_id:
            raise InvalidClaimError('azp')
Exemplo n.º 20
0
 def _validate_uri(self, key, uri=None):
     if uri is None:
         uri = self.get(key)
     if uri and not is_valid_url(uri):
         raise InvalidClaimError(key)
Exemplo n.º 21
0
    async def _do_jwt_login(
            self,
            login_submission: JsonDict,
            should_issue_refresh_token: bool = False) -> LoginResponse:
        token = login_submission.get("token", None)
        if token is None:
            raise LoginError(403,
                             "Token field for JWT is missing",
                             errcode=Codes.FORBIDDEN)

        from authlib.jose import JsonWebToken, JWTClaims
        from authlib.jose.errors import BadSignatureError, InvalidClaimError, JoseError

        jwt = JsonWebToken([self.jwt_algorithm])
        claim_options = {}
        if self.jwt_issuer is not None:
            claim_options["iss"] = {
                "value": self.jwt_issuer,
                "essential": True
            }
        if self.jwt_audiences is not None:
            claim_options["aud"] = {
                "values": self.jwt_audiences,
                "essential": True
            }

        try:
            claims = jwt.decode(
                token,
                key=self.jwt_secret,
                claims_cls=JWTClaims,
                claims_options=claim_options,
            )
        except BadSignatureError:
            # We handle this case separately to provide a better error message
            raise LoginError(
                403,
                "JWT validation failed: Signature verification failed",
                errcode=Codes.FORBIDDEN,
            )
        except JoseError as e:
            # A JWT error occurred, return some info back to the client.
            raise LoginError(
                403,
                "JWT validation failed: %s" % (str(e), ),
                errcode=Codes.FORBIDDEN,
            )

        try:
            claims.validate(leeway=120)  # allows 2 min of clock skew

            # Enforce the old behavior which is rolled out in productive
            # servers: if the JWT contains an 'aud' claim but none is
            # configured, the login attempt will fail
            if claims.get("aud") is not None:
                if self.jwt_audiences is None or len(self.jwt_audiences) == 0:
                    raise InvalidClaimError("aud")
        except JoseError as e:
            raise LoginError(
                403,
                "JWT validation failed: %s" % (str(e), ),
                errcode=Codes.FORBIDDEN,
            )

        user = claims.get(self.jwt_subject_claim, None)
        if user is None:
            raise LoginError(403, "Invalid JWT", errcode=Codes.FORBIDDEN)

        user_id = UserID(user, self.hs.hostname).to_string()
        result = await self._complete_login(
            user_id,
            login_submission,
            create_non_existent_users=True,
            should_issue_refresh_token=should_issue_refresh_token,
        )
        return result