Repository: jpadilla/pyjwt (python)

Issue:
Add InvalidSignature exception
Right now the [`jws.decode()`](https://github.com/jpadilla/pyjwt/blob/e0aa10ee586bf010ce9a99c5582ee8dc9293ba1e/jwt/api_jws.py#L206) method raises a `DecodeError` when the signature verification failed. I would like to propose adding a dedicated `InvalidSignatureError` or `SignatureVerificationError` exception class.

The reasoning behind this: I'd like to implement a key-rotating feature that checks for one key and if the signature verification fails checks for the other key. The claims, algorithm, and whatnot still need to be validated. The only solution I see right now is catching the `DecodeError` and checking for `e.args[0] == 'Signature verification failed'` which feels super dirty.

Please let me know what you think about this. I'm happy to provide the corresponding backward compatible patch.

Pull request description:
Fix #315: Raise InvalidSignatureError over generic DecodeError

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5793d70..68eb2ac 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,8 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 
 - Audience parameter now supports iterables [#205][205]
 
+- An invalid signature now raises an `InvalidSignatureError` instead of `DecodeError` [#315][315]
+
 ### Fixed
 
 ### Added
@@ -204,4 +206,5 @@ rarely used. Users affected by this should upgrade to 3.3+.
 [271]: https://github.com/jpadilla/pyjwt/pull/271
 [277]: https://github.com/jpadilla/pyjwt/pull/277
 [281]: https://github.com/jpadilla/pyjwt/pull/281
+[315]: https://github.com/jpadilla/pyjwt/pull/315
 [7c1e61d]: https://github.com/jpadilla/pyjwt/commit/7c1e61dde27bafe16e7d1bb6e35199e778962742
diff --git a/docs/api.rst b/docs/api.rst
index 4bb4d3b..08b6991 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -19,6 +19,11 @@ Exceptions
 
     Raised when a token cannot be decoded because it failed validation
 
+.. class:: InvalidSignatureError
+
+    Raised when a token's signature doesn't match the one provided as part of
+    the token.
+
 .. class:: ExpiredSignatureError
 
     Raised when a token's ``exp`` claim indicates that it has expired
diff --git a/jwt/api_jws.py b/jwt/api_jws.py
index ad3ff6a..b796fa7 100644
--- a/jwt/api_jws.py
+++ b/jwt/api_jws.py
@@ -7,7 +7,10 @@ from .algorithms import (
     Algorithm, get_default_algorithms, has_crypto, requires_cryptography  # NOQA
 )
 from .compat import binary_type, string_types, text_type
-from .exceptions import DecodeError, InvalidAlgorithmError, InvalidTokenError
+from .exceptions import (
+    DecodeError, InvalidAlgorithmError, InvalidSignatureError,
+    InvalidTokenError
+)
 from .utils import base64url_decode, base64url_encode, force_bytes, merge_dict
 
 
@@ -203,7 +206,7 @@ class PyJWS(object):
             key = alg_obj.prepare_key(key)
 
             if not alg_obj.verify(signing_input, key, signature):
-                raise DecodeError('Signature verification failed')
+                raise InvalidSignatureError('Signature verification failed')
 
         except KeyError:
             raise InvalidAlgorithmError('Algorithm not supported')
diff --git a/jwt/exceptions.py b/jwt/exceptions.py
index 31177a0..b396bf1 100644
--- a/jwt/exceptions.py
+++ b/jwt/exceptions.py
@@ -6,6 +6,10 @@ class DecodeError(InvalidTokenError):
     pass
 
 
+class InvalidSignatureError(DecodeError):
+    pass
+
+
 class ExpiredSignatureError(InvalidTokenError):
     pass
 


Test patch:
diff --git a/tests/test_api_jws.py b/tests/test_api_jws.py
index 60671a2..4bf8d39 100644
--- a/tests/test_api_jws.py
+++ b/tests/test_api_jws.py
@@ -5,7 +5,8 @@ from decimal import Decimal
 from jwt.algorithms import Algorithm
 from jwt.api_jws import PyJWS
 from jwt.exceptions import (
-    DecodeError, InvalidAlgorithmError, InvalidTokenError
+    DecodeError, InvalidAlgorithmError, InvalidSignatureError,
+    InvalidTokenError
 )
 from jwt.utils import base64url_decode, force_bytes, force_unicode
 
@@ -178,8 +179,14 @@ class TestJWS:
         bad_secret = 'bar'
         jws_message = jws.encode(payload, right_secret)
 
-        with pytest.raises(DecodeError):
+        with pytest.raises(DecodeError) as excinfo:
+            # Backward compat for ticket #315
+            jws.decode(jws_message, bad_secret)
+        assert 'Signature verification failed' == str(excinfo.value)
+
+        with pytest.raises(InvalidSignatureError) as excinfo:
             jws.decode(jws_message, bad_secret)
+        assert 'Signature verification failed' == str(excinfo.value)
 
     def test_decodes_valid_jws(self, jws, payload):
         example_secret = 'secret'