diff --git a/AUTHORS b/AUTHORS
index be65bf8..02fbc3b 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -21,3 +21,5 @@ Patches and Suggestions
  - Mark Adams <mark@markadams.me>
 
  - Wouter Bolsterlee <uws@xs4all.nl>
+
+ - Michael Davis <mike.philip.davis@gmail.com> <mike.davis@workiva.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ced0519..1564f5e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 -------------------------------------------------------------------------
 ### Changed
 - Added this CHANGELOG.md file
+- Added flexible and complete verification options. #131
 
 ### Fixed
 - Placeholder
diff --git a/README.md b/README.md
index 167f78e..5ae0b40 100644
--- a/README.md
+++ b/README.md
@@ -62,6 +62,34 @@ except jwt.InvalidTokenError:
     pass  # do something sensible here, e.g. return HTTP 403 status code
 ```
 
+You may also override exception checking via an `options` dictionary.  The default
+options are as follows: 
+
+```python
+options = {
+   'verify_signature': True,
+   'verify_exp': True,
+   'verify_nbf': True,
+   'verify_iat': True,
+   'verify_aud`: True
+}
+```
+
+You can skip individual checks by passing an `options` dictionary with certain keys set to `False`.
+For example, if you want to verify the signature of a JWT that has already expired.
+
+```python
+options = {
+   'verify_exp': True,
+}
+
+jwt.decode('someJWTstring', 'secret', options=options)
+```
+
+**NOTE**: *Changing the default behavior is done at your own risk, and almost certainly will make your
+application less secure.  Doing so should only be done with a very clear understanding of what you
+are doing.*
+
 ## Tests
 
 You can run tests from the project root after cloning with:
diff --git a/jwt/api.py b/jwt/api.py
index 6d39d8d..68d30f6 100644
--- a/jwt/api.py
+++ b/jwt/api.py
@@ -16,7 +16,7 @@ from .utils import base64url_decode, base64url_encode
 
 
 class PyJWT(object):
-    def __init__(self, algorithms=None):
+    def __init__(self, algorithms=None, options=None):
         self._algorithms = get_default_algorithms()
         self._valid_algs = set(algorithms) if algorithms is not None else set(self._algorithms)
 
@@ -25,6 +25,19 @@ class PyJWT(object):
             if key not in self._valid_algs:
                 del self._algorithms[key]
 
+        if not options:
+            options = {}
+
+        self.default_options = {
+            'verify_signature': True,
+            'verify_exp': True,
+            'verify_nbf': True,
+            'verify_iat': True,
+            'verify_aud': True,
+        }
+
+        self.options = self._merge_options(self.default_options, options)
+
     def register_algorithm(self, alg_id, alg_obj):
         """
         Registers a new Algorithm for use when creating and verifying tokens.
@@ -110,14 +123,16 @@ class PyJWT(object):
 
         return b'.'.join(segments)
 
-    def decode(self, jwt, key='', verify=True, algorithms=None, **kwargs):
+    def decode(self, jwt, key='', verify=True, algorithms=None, options=None, **kwargs):
         payload, signing_input, header, signature = self._load(jwt)
 
         if verify:
-            self._verify_signature(payload, signing_input, header, signature,
-                                   key, algorithms)
+            merged_options = self._merge_options(override_options=options)
+            if merged_options.get('verify_signature'):
+                self._verify_signature(payload, signing_input, header, signature,
+                                       key, algorithms)
 
-            self._validate_claims(payload, **kwargs)
+            self._validate_claims(payload, options=merged_options, **kwargs)
 
         return payload
 
@@ -177,8 +192,8 @@ class PyJWT(object):
         except KeyError:
             raise InvalidAlgorithmError('Algorithm not supported')
 
-    def _validate_claims(self, payload, verify_expiration=True, leeway=0,
-                         audience=None, issuer=None):
+    def _validate_claims(self, payload, audience=None, issuer=None, leeway=0,
+                         options=None, **kwargs):
         if isinstance(leeway, timedelta):
             leeway = timedelta_total_seconds(leeway)
 
@@ -187,7 +202,7 @@ class PyJWT(object):
 
         now = timegm(datetime.utcnow().utctimetuple())
 
-        if 'iat' in payload:
+        if 'iat' in payload and options.get('verify_iat'):
             try:
                 iat = int(payload['iat'])
             except ValueError:
@@ -196,7 +211,7 @@ class PyJWT(object):
             if iat > (now + leeway):
                 raise InvalidIssuedAtError('Issued At claim (iat) cannot be in the future.')
 
-        if 'nbf' in payload and verify_expiration:
+        if 'nbf' in payload and options.get('verify_nbf'):
             try:
                 nbf = int(payload['nbf'])
             except ValueError:
@@ -205,7 +220,7 @@ class PyJWT(object):
             if nbf > (now + leeway):
                 raise ImmatureSignatureError('The token is not yet valid (nbf)')
 
-        if 'exp' in payload and verify_expiration:
+        if 'exp' in payload and options.get('verify_exp'):
             try:
                 exp = int(payload['exp'])
             except ValueError:
@@ -214,7 +229,7 @@ class PyJWT(object):
             if exp < (now - leeway):
                 raise ExpiredSignatureError('Signature has expired')
 
-        if 'aud' in payload:
+        if 'aud' in payload and options.get('verify_aud'):
             audience_claims = payload['aud']
             if isinstance(audience_claims, string_types):
                 audience_claims = [audience_claims]
@@ -233,6 +248,21 @@ class PyJWT(object):
             if payload.get('iss') != issuer:
                 raise InvalidIssuerError('Invalid issuer')
 
+    def _merge_options(self, default_options=None, override_options=None):
+        if not default_options:
+            default_options = {}
+
+        if not override_options:
+            override_options = {}
+
+        try:
+            merged_options = self.default_options.copy()
+            merged_options.update(override_options)
+        except (AttributeError, ValueError) as e:
+            raise TypeError('options must be a dictionary: %s' % e)
+
+        return merged_options
+
 
 _jwt_global_obj = PyJWT()
 encode = _jwt_global_obj.encode

Add more flexible and complete verification options
I was thinking that it might be useful for us to implement more flexible verification options.

I propose something like this:

```
def decode(token, secret, options=None)
```

where options is a dict that looks something like this:
```
options = {
   'verify_signature': True,
   'verify_exp': True,
   'verify_nbf': True,
   'verify_iat': True,
   'verify_aud`: True
}
```

This is similar to what [ruby-jwt does](https://github.com/progrium/ruby-jwt/blob/master/lib/jwt.rb#L110)
We could make it where options could be specified globally (at the PyJWT object level) or as an override argument to `decode()`

diff --git a/tests/test_api.py b/tests/test_api.py
index f1734b4..33ccd51 100644
--- a/tests/test_api.py
+++ b/tests/test_api.py
@@ -71,6 +71,27 @@ class TestAPI(unittest.TestCase):
         self.assertNotIn('none', self.jwt.get_algorithms())
         self.assertIn('HS256', self.jwt.get_algorithms())
 
+    def test_default_options(self):
+        self.assertEqual(self.jwt.default_options, self.jwt.options)
+
+    def test_override_options(self):
+        self.jwt = PyJWT(options={'verify_exp': False, 'verify_nbf': False})
+        expected_options = self.jwt.default_options
+        expected_options['verify_exp'] = False
+        expected_options['verify_nbf'] = False
+        self.assertEqual(expected_options, self.jwt.options)
+
+    def test_non_default_options_persist(self):
+        self.jwt = PyJWT(options={'verify_iat': False, 'foobar': False})
+        expected_options = self.jwt.default_options
+        expected_options['verify_iat'] = False
+        expected_options['foobar'] = False
+        self.assertEqual(expected_options, self.jwt.options)
+
+    def test_options_must_be_dict(self):
+        self.assertRaises(TypeError, PyJWT, options=object())
+        self.assertRaises(TypeError, PyJWT, options=('something'))
+
     def test_encode_decode(self):
         secret = 'secret'
         jwt_message = self.jwt.encode(self.payload, secret)
@@ -467,14 +488,14 @@ class TestAPI(unittest.TestCase):
         secret = 'secret'
         jwt_message = self.jwt.encode(self.payload, secret)
 
-        self.jwt.decode(jwt_message, secret, verify_expiration=False)
+        self.jwt.decode(jwt_message, secret, options={'verify_exp': False})
 
     def test_decode_skip_notbefore_verification(self):
         self.payload['nbf'] = time.time() + 10
         secret = 'secret'
         jwt_message = self.jwt.encode(self.payload, secret)
 
-        self.jwt.decode(jwt_message, secret, verify_expiration=False)
+        self.jwt.decode(jwt_message, secret, options={'verify_nbf': False})
 
     def test_decode_with_expiration_with_leeway(self):
         self.payload['exp'] = utc_timestamp() - 2
@@ -765,6 +786,52 @@ class TestAPI(unittest.TestCase):
         with self.assertRaises(InvalidIssuerError):
             self.jwt.decode(token, 'secret', issuer=issuer)
 
+    def test_skip_check_audience(self):
+        payload = {
+            'some': 'payload',
+            'aud': 'urn:me',
+        }
+        token = self.jwt.encode(payload, 'secret')
+        self.jwt.decode(token, 'secret', options={'verify_aud': False})
+
+    def test_skip_check_exp(self):
+        payload = {
+            'some': 'payload',
+            'exp': datetime.utcnow() - timedelta(days=1)
+        }
+        token = self.jwt.encode(payload, 'secret')
+        self.jwt.decode(token, 'secret', options={'verify_exp': False})
+
+    def test_skip_check_signature(self):
+        token = ("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"
+                 ".eyJzb21lIjoicGF5bG9hZCJ9"
+                 ".4twFt5NiznN84AWoo1d7KO1T_yoc0Z6XOpOVswacPZA")
+        self.jwt.decode(token, 'secret', options={'verify_signature': False})
+
+    def test_skip_check_iat(self):
+        payload = {
+            'some': 'payload',
+            'iat': datetime.utcnow() + timedelta(days=1)
+        }
+        token = self.jwt.encode(payload, 'secret')
+        self.jwt.decode(token, 'secret', options={'verify_iat': False})
+
+    def test_skip_check_nbf(self):
+        payload = {
+            'some': 'payload',
+            'nbf': datetime.utcnow() + timedelta(days=1)
+        }
+        token = self.jwt.encode(payload, 'secret')
+        self.jwt.decode(token, 'secret', options={'verify_nbf': False})
+
+    def test_decode_options_must_be_dict(self):
+        payload = {
+            'some': 'payload',
+        }
+        token = self.jwt.encode(payload, 'secret')
+        self.assertRaises(TypeError, self.jwt.decode, token, 'secret', options=object())
+        self.assertRaises(TypeError, self.jwt.decode, token, 'secret', options='something')
+
     def test_custom_json_encoder(self):
 
         class CustomJSONEncoder(json.JSONEncoder):

pip install -e .
parse_log_pytest
pytest
coverage unittest2 pytest
apt-get update apt-get install -y gcc
3.4
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
certifi==2021.5.30
coverage==6.2
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1631916693255/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
linecache2==1.0.0
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
-e git+https://github.com/jpadilla/pyjwt.git@a2601ad46433a99c8777a74abeaf4dfd70630d17#egg=PyJWT
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
six==1.17.0
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
traceback2==1.4.0
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
unittest2==1.1.0
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: pyjwt
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib-metadata=4.8.1=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - argparse==1.4.0
      - coverage==6.2
      - linecache2==1.0.0
      - six==1.17.0
      - traceback2==1.4.0
      - unittest2==1.1.0
prefix: /opt/conda/envs/pyjwt

tests/test_api.py::TestAPI::test_decode_skip_expiration_verification tests/test_api.py::TestAPI::test_decode_skip_notbefore_verification tests/test_api.py::TestAPI::test_default_options tests/test_api.py::TestAPI::test_non_default_options_persist tests/test_api.py::TestAPI::test_override_options tests/test_api.py::TestAPI::test_skip_check_audience tests/test_api.py::TestAPI::test_skip_check_exp tests/test_api.py::TestAPI::test_skip_check_iat tests/test_api.py::TestAPI::test_skip_check_nbf tests/test_api.py::TestAPI::test_skip_check_signature
tests/test_api.py::TestAPI::test_algorithms_parameter_removes_alg_from_algorithms_list tests/test_api.py::TestAPI::test_allow_skip_verification tests/test_api.py::TestAPI::test_bad_secret tests/test_api.py::TestAPI::test_bytes_secret tests/test_api.py::TestAPI::test_check_audience_in_array_when_valid tests/test_api.py::TestAPI::test_check_audience_when_valid tests/test_api.py::TestAPI::test_check_issuer_when_valid tests/test_api.py::TestAPI::test_custom_json_encoder tests/test_api.py::TestAPI::test_decode_algorithm_param_should_be_case_sensitive tests/test_api.py::TestAPI::test_decode_fails_when_alg_is_not_on_method_algorithms_param tests/test_api.py::TestAPI::test_decode_invalid_crypto_padding tests/test_api.py::TestAPI::test_decode_invalid_header_padding tests/test_api.py::TestAPI::test_decode_invalid_header_string tests/test_api.py::TestAPI::test_decode_invalid_payload_padding tests/test_api.py::TestAPI::test_decode_invalid_payload_string tests/test_api.py::TestAPI::test_decode_missing_segments_throws_exception tests/test_api.py::TestAPI::test_decode_options_must_be_dict tests/test_api.py::TestAPI::test_decode_raises_exception_if_exp_is_not_int tests/test_api.py::TestAPI::test_decode_raises_exception_if_iat_in_the_future tests/test_api.py::TestAPI::test_decode_raises_exception_if_iat_is_not_int tests/test_api.py::TestAPI::test_decode_raises_exception_if_nbf_is_not_int tests/test_api.py::TestAPI::test_decode_unicode_value tests/test_api.py::TestAPI::test_decode_with_algo_none_and_verify_false_should_pass tests/test_api.py::TestAPI::test_decode_with_algo_none_should_fail tests/test_api.py::TestAPI::test_decode_with_expiration tests/test_api.py::TestAPI::test_decode_with_expiration_with_leeway tests/test_api.py::TestAPI::test_decode_with_invalid_aud_list_member_throws_exception tests/test_api.py::TestAPI::test_decode_with_invalid_audience_param_throws_exception tests/test_api.py::TestAPI::test_decode_with_non_mapping_header_throws_exception tests/test_api.py::TestAPI::test_decode_with_non_mapping_payload_throws_exception tests/test_api.py::TestAPI::test_decode_with_nonlist_aud_claim_throws_exception tests/test_api.py::TestAPI::test_decode_with_notbefore tests/test_api.py::TestAPI::test_decode_with_notbefore_with_leeway tests/test_api.py::TestAPI::test_decode_works_with_unicode_token tests/test_api.py::TestAPI::test_decodes_valid_jwt tests/test_api.py::TestAPI::test_ecdsa_related_algorithms tests/test_api.py::TestAPI::test_encode_algorithm_param_should_be_case_sensitive tests/test_api.py::TestAPI::test_encode_bad_type tests/test_api.py::TestAPI::test_encode_datetime tests/test_api.py::TestAPI::test_encode_decode tests/test_api.py::TestAPI::test_invalid_crypto_alg tests/test_api.py::TestAPI::test_load_no_verification tests/test_api.py::TestAPI::test_load_verify_valid_jwt tests/test_api.py::TestAPI::test_no_secret tests/test_api.py::TestAPI::test_nonascii_secret tests/test_api.py::TestAPI::test_options_must_be_dict tests/test_api.py::TestAPI::test_raise_exception_invalid_audience tests/test_api.py::TestAPI::test_raise_exception_invalid_audience_in_array tests/test_api.py::TestAPI::test_raise_exception_invalid_issuer tests/test_api.py::TestAPI::test_raise_exception_token_without_audience tests/test_api.py::TestAPI::test_raise_exception_token_without_issuer tests/test_api.py::TestAPI::test_register_algorithm_does_not_allow_duplicate_registration tests/test_api.py::TestAPI::test_register_algorithm_rejects_non_algorithm_obj tests/test_api.py::TestAPI::test_rsa_related_algorithms tests/test_api.py::TestAPI::test_unicode_secret tests/test_api.py::TestAPI::test_unregister_algorithm_removes_algorithm tests/test_api.py::TestAPI::test_unregister_algorithm_throws_error_if_not_registered tests/test_api.py::TestAPI::test_verify_signature_with_no_secret
jwt/api.py README.md CHANGELOG.md AUTHORS
jwt/api.py README.md CHANGELOG.md AUTHORS