def verify(self, token, nonce=None, max_age=None): # Verify token presence if not token or not isinstance(token, str): raise TokenValidationError("ID token is required but missing.") # Verify algorithm and signature payload = self._sv.verify_signature(token) # Verify claims self._verify_payload(payload, nonce, max_age)
def get_key(self, key_id): keys = self._fetch_jwks() if keys and key_id in keys: return keys[key_id] if not self._cache_is_fresh: keys = self._fetch_jwks(force=True) if keys and key_id in keys: return keys[key_id] raise TokenValidationError( 'RSA Public Key with ID "{}" was not found.'.format(key_id))
def verify_signature(self, token): try: header = jwt.get_unverified_header(token) except jwt.exceptions.DecodeError: raise TokenValidationError("ID token could not be decoded.") alg = header.get('alg', None) if alg != self._algorithm: raise TokenValidationError( 'Signature algorithm of "{}" is not supported. Expected the ID token ' 'to be signed with "{}"'.format(alg, self._algorithm)) kid = header.get('kid', None) secret_or_certificate = self._fetch_key(key_id=kid) try: decoded = jwt.decode(jwt=token, key=secret_or_certificate, algorithms=[self._algorithm], options=self.DISABLE_JWT_CHECKS) except jwt.exceptions.InvalidSignatureError: raise TokenValidationError("Invalid token signature.") return decoded
def _verify_payload(self, payload, nonce=None, max_age=None): try: # on Python 2.7, 'str' keys as parsed as 'unicode' # But 'unicode' was removed on Python 3.7 # noinspection PyUnresolvedReferences ustr = unicode except NameError: ustr = str # Issuer if 'iss' not in payload or not isinstance(payload['iss'], (str, ustr)): raise TokenValidationError( 'Issuer (iss) claim must be a string present in the ID token') if payload['iss'] != self.iss: raise TokenValidationError( 'Issuer (iss) claim mismatch in the ID token; expected "{}", ' 'found "{}"'.format(self.iss, payload['iss'])) # Subject if 'sub' not in payload or not isinstance(payload['sub'], (str, ustr)): raise TokenValidationError( 'Subject (sub) claim must be a string present in the ID token') # Audience if 'aud' not in payload or not (isinstance(payload['aud'], (str, ustr)) or isinstance(payload['aud'], list)): raise TokenValidationError( 'Audience (aud) claim must be a string or array of strings present in the ID token' ) if isinstance(payload['aud'], list) and not self.aud in payload['aud']: payload_audiences = ", ".join(payload['aud']) raise TokenValidationError( 'Audience (aud) claim mismatch in the ID token; expected "{}" but was ' 'not one of "{}"'.format(self.aud, payload_audiences)) elif isinstance(payload['aud'], (str, ustr)) and payload['aud'] != self.aud: raise TokenValidationError( 'Audience (aud) claim mismatch in the ID token; expected "{}" ' 'but found "{}"'.format(self.aud, payload['aud'])) # --Time validation (epoch)-- now = self._clock or time.time() leeway = self.leeway # Expires at if 'exp' not in payload or not isinstance(payload['exp'], int): raise TokenValidationError( 'Expiration Time (exp) claim must be a number present in the ID token' ) exp_time = payload['exp'] + leeway if now > exp_time: raise TokenValidationError( 'Expiration Time (exp) claim error in the ID token; current time ({}) is ' 'after expiration time ({})'.format(now, exp_time)) # Issued at if 'iat' not in payload or not isinstance(payload['iat'], int): raise TokenValidationError( 'Issued At (iat) claim must be a number present in the ID token' ) # Nonce if nonce: if 'nonce' not in payload or not isinstance( payload['nonce'], (str, ustr)): raise TokenValidationError( 'Nonce (nonce) claim must be a string present in the ID token' ) if payload['nonce'] != nonce: raise TokenValidationError( 'Nonce (nonce) claim mismatch in the ID token; expected "{}", ' 'found "{}"'.format(nonce, payload['nonce'])) # Authorized party if isinstance(payload['aud'], list) and len(payload['aud']) > 1: if 'azp' not in payload or not isinstance(payload['azp'], (str, ustr)): raise TokenValidationError( 'Authorized Party (azp) claim must be a string present in the ID token when ' 'Audience (aud) claim has multiple values') if payload['azp'] != self.aud: raise TokenValidationError( 'Authorized Party (azp) claim mismatch in the ID token; expected "{}", ' 'found "{}"'.format(self.aud, payload['azp'])) # Authentication time if max_age: if 'auth_time' not in payload or not isinstance( payload['auth_time'], int): raise TokenValidationError( 'Authentication Time (auth_time) claim must be a number present in the ID token ' 'when Max Age (max_age) is specified') auth_valid_until = payload['auth_time'] + max_age + leeway if now > auth_valid_until: raise TokenValidationError( 'Authentication Time (auth_time) claim in the ID token indicates that too much ' 'time has passed since the last end-user authentication. Current time ({}) ' 'is after last auth at ({})'.format(now, auth_valid_until))
user_id, name, mocker, authenticator: Auth0Authenticator): mocker.patch("auth0.v3.authentication.token_verifier.TokenVerifier.verify") token = jwt.encode({ "sub": user_id, "name": name }, "secret", algorithm="HS256").decode("utf8") result = (await authenticator.get_verified_user(token)).collapse() assert isinstance(result, User) assert result.id == str(user_id) assert result.name == name @pytest.mark.parametrize("error", [TokenValidationError(), Exception()]) @pytest.mark.parametrize("user_id", [123456, "foo-id-283"]) @pytest.mark.parametrize("name", ["Foo User"]) @pytest.mark.asyncio async def test_get_verified_user_returns_auth_error_on_invalid_token( user_id, name, error, mocker, authenticator: Auth0Authenticator): """ Check that get_verified_user() returns an auth error with INVALID_TOKEN reason whenever token validation fails. """ mocker.patch("auth0.v3.authentication.token_verifier.TokenVerifier.verify" ).side_effect = error token = jwt.encode({ "sub": user_id, "name": name },