diff --git a/CHANGELOG.md b/CHANGELOG.md
index 00168932..d2e16d3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,8 @@
 # Changelog
 
-## [v3.0.1](https://github.com/jwt/ruby-jwt/tree/v3.0.1) (NEXT)
+## [v3.1.0](https://github.com/jwt/ruby-jwt/tree/v3.1.0) (NEXT)
 
-[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v3.0.0...main)
+[Full Changelog](https://github.com/jwt/ruby-jwt/compare/v3.0.0...v3.1.0)
 
 **Features:**
 
@@ -10,6 +10,7 @@
 - Raise an error if the ECDSA signing or verification key is not an instance of `OpenSSL::PKey::EC` [#688](https://github.com/jwt/ruby-jwt/pull/688) ([@anakinj](https://github.com/anakinj))
 - Allow `OpenSSL::PKey::EC::Point` to be used as the verification key in ECDSA [#689](https://github.com/jwt/ruby-jwt/pull/689) ([@anakinj](https://github.com/anakinj))
 - Require claims to have been verified before accessing the `JWT::EncodedToken#payload` [#690](https://github.com/jwt/ruby-jwt/pull/690) ([@anakinj](https://github.com/anakinj))
+- Support signing and verifying tokens using a JWK [#692](https://github.com/jwt/ruby-jwt/pull/692) ([@anakinj](https://github.com/anakinj))
 - Your contribution here
 
 **Fixes and enhancements:**
diff --git a/README.md b/README.md
index d928b8b2..8b41157d 100644
--- a/README.md
+++ b/README.md
@@ -13,9 +13,9 @@ See [CHANGELOG.md](CHANGELOG.md) for a complete set of changes and [upgrade guid
 
 ## Sponsors
 
-|Logo|Message|
-|----|-------|
-|![auth0 logo](https://user-images.githubusercontent.com/83319/31722733-de95bbde-b3ea-11e7-96bf-4f4e8f915588.png)|If you want to quickly add secure token-based authentication to Ruby projects, feel free to check Auth0's Ruby SDK and free plan at [auth0.com/developers](https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=rubyjwt&utm_content=auth)|
+| Logo                                                                                                             | Message                                                                                                                                                                                                                                                                  |
+| ---------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| ![auth0 logo](https://user-images.githubusercontent.com/83319/31722733-de95bbde-b3ea-11e7-96bf-4f4e8f915588.png) | If you want to quickly add secure token-based authentication to Ruby projects, feel free to check Auth0's Ruby SDK and free plan at [auth0.com/developers](https://auth0.com/developers?utm_source=GHsponsor&utm_medium=GHsponsor&utm_campaign=rubyjwt&utm_content=auth) |
 
 ## Installing
 
@@ -251,6 +251,26 @@ encoded_token.payload # => { 'exp'=>1234, 'jti'=>'1234", 'sub'=>'my-subject' }
 encoded_token.header # {'kid'=>'hmac', 'alg'=>'HS256'}
 ```
 
+A JWK can be used to sign and verify the token if it's possible to derive the signing algorithm from the key.
+
+```ruby
+jwk_json = '{
+ "kty": "oct",
+ "k": "c2VjcmV0",
+ "alg": "HS256",
+ "kid": "hmac"
+}'
+
+jwk = JWT::JWK.import(JSON.parse(jwk_json))
+
+token = JWT::Token.new(payload: payload, header: header)
+
+token.sign!(key: jwk)
+
+encoded_token = JWT::EncodedToken.new(token.jwt)
+encoded_token.verify!(signature: { key: jwk})
+```
+
 #### Using a keyfinder
 
 A keyfinder can be used to verify a signature. A keyfinder is an object responding to the `#call` method. The method expects to receive one argument, which is the token to be verified.
diff --git a/lib/jwt/encoded_token.rb b/lib/jwt/encoded_token.rb
index 2ce88475..3e94edf1 100644
--- a/lib/jwt/encoded_token.rb
+++ b/lib/jwt/encoded_token.rb
@@ -138,12 +138,8 @@ def valid?(signature:, claims: nil)
     # @return [nil]
     # @raise [JWT::VerificationError] if the signature verification fails.
     # @raise [ArgumentError] if neither key nor key_finder is provided, or if both are provided.
-    def verify_signature!(algorithm:, key: nil, key_finder: nil)
-      raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil?
-
-      key ||= key_finder.call(self)
-
-      return if valid_signature?(algorithm: algorithm, key: key)
+    def verify_signature!(algorithm: nil, key: nil, key_finder: nil)
+      return if valid_signature?(algorithm: algorithm, key: key, key_finder: key_finder)
 
       raise JWT::VerificationError, 'Signature verification failed'
     end
@@ -151,15 +147,20 @@ def verify_signature!(algorithm:, key: nil, key_finder: nil)
     # Checks if the signature of the JWT token is valid.
     #
     # @param algorithm [String, Array<String>, Object, Array<Object>] the algorithm(s) to use for verification.
-    # @param key [String, Array<String>] the key(s) to use for verification.
+    # @param key [String, Array<String>, JWT::JWK::KeyBase, Array<JWT::JWK::KeyBase>] the key(s) to use for verification.
+    # @param key_finder [#call] an object responding to `call` to find the key for verification.
     # @return [Boolean] true if the signature is valid, false otherwise.
-    def valid_signature?(algorithm:, key:)
-      valid = Array(JWA.resolve_and_sort(algorithms: algorithm, preferred_algorithm: header['alg'])).any? do |algo|
-        Array(key).any? do |one_key|
-          algo.verify(data: signing_input, signature: signature, verification_key: one_key)
-        end
-      end
+    def valid_signature?(algorithm: nil, key: nil, key_finder: nil)
+      raise ArgumentError, 'Provide either key or key_finder, not both or neither' if key.nil? == key_finder.nil?
+
+      keys      = Array(key || key_finder.call(self))
+      verifiers = JWA.create_verifiers(algorithms: algorithm, keys: keys, preferred_algorithm: header['alg'])
 
+      raise JWT::VerificationError, 'No algorithm provided' if verifiers.empty?
+
+      valid = verifiers.any? do |jwa|
+        jwa.verify(data: signing_input, signature: signature)
+      end
       valid.tap { |verified| @signature_verified = verified }
     end
 
diff --git a/lib/jwt/jwa.rb b/lib/jwt/jwa.rb
index 92df7dd5..d5b0de6a 100644
--- a/lib/jwt/jwa.rb
+++ b/lib/jwt/jwa.rb
@@ -13,11 +13,43 @@
 module JWT
   # The JWA module contains all supported algorithms.
   module JWA
+    # @api private
+    class VerifierContext
+      def initialize(jwa:, keys:)
+        @jwa = jwa
+        @keys = Array(keys)
+      end
+
+      def verify(*args, **kwargs)
+        @keys.any? do |key|
+          @jwa.verify(*args, **kwargs, verification_key: key)
+        end
+      end
+    end
+
+    # @api private
+    class SignerContext
+      def initialize(jwa:, key:)
+        @jwa = jwa
+        @key = key
+      end
+
+      def sign(*args, **kwargs)
+        @jwa.sign(*args, **kwargs, signing_key: @key)
+      end
+
+      def jwa_header
+        @jwa.header
+      end
+    end
+
     class << self
       # @api private
       def resolve(algorithm)
         return find(algorithm) if algorithm.is_a?(String) || algorithm.is_a?(Symbol)
 
+        raise ArgumentError, 'Algorithm must be provided' if algorithm.nil?
+
         raise ArgumentError, 'Custom algorithms are required to include JWT::JWA::SigningAlgorithm' unless algorithm.is_a?(SigningAlgorithm)
 
         algorithm
@@ -25,8 +57,25 @@ def resolve(algorithm)
 
       # @api private
       def resolve_and_sort(algorithms:, preferred_algorithm:)
-        algs = Array(algorithms).map { |alg| JWA.resolve(alg) }
-        algs.partition { |alg| alg.valid_alg?(preferred_algorithm) }.flatten
+        Array(algorithms).map { |alg| JWA.resolve(alg) }
+                         .partition { |alg| alg.valid_alg?(preferred_algorithm) }
+                         .flatten
+      end
+
+      # @api private
+      def create_signer(algorithm:, key:)
+        return key if key.is_a?(JWK::KeyBase)
+
+        SignerContext.new(jwa: resolve(algorithm), key: key)
+      end
+
+      # @api private
+      def create_verifiers(algorithms:, keys:, preferred_algorithm:)
+        jwks, other_keys = keys.partition { |key| key.is_a?(JWK::KeyBase) }
+
+        jwks + resolve_and_sort(algorithms: algorithms,
+                                preferred_algorithm: preferred_algorithm)
+               .map { |jwa| VerifierContext.new(jwa: jwa, keys: other_keys) }
       end
     end
   end
diff --git a/lib/jwt/jwa/ecdsa.rb b/lib/jwt/jwa/ecdsa.rb
index 60ddf6e3..12458c3b 100644
--- a/lib/jwt/jwa/ecdsa.rb
+++ b/lib/jwt/jwa/ecdsa.rb
@@ -64,6 +64,7 @@ def verify(data:, signature:, verification_key:)
         register_algorithm(new(v[:algorithm], v[:digest]))
       end
 
+      # @api private
       def self.curve_by_name(name)
         NAMED_CURVES.fetch(name) do
           raise UnsupportedEcdsaCurve, "The ECDSA curve '#{name}' is not supported"
diff --git a/lib/jwt/jwk/ec.rb b/lib/jwt/jwk/ec.rb
index 68bdad01..4f442b8c 100644
--- a/lib/jwt/jwk/ec.rb
+++ b/lib/jwt/jwk/ec.rb
@@ -73,6 +73,13 @@ def []=(key, value)
 
       private
 
+      def jwa
+        return super if self[:alg]
+
+        curve_name = self.class.to_openssl_curve(self[:crv])
+        JWA.resolve(JWA::Ecdsa.curve_by_name(curve_name)[:algorithm])
+      end
+
       def ec_key
         @ec_key ||= create_ec_key(self[:crv], self[:x], self[:y], self[:d])
       end
diff --git a/lib/jwt/jwk/key_base.rb b/lib/jwt/jwk/key_base.rb
index 4962d112..84c46f8f 100644
--- a/lib/jwt/jwk/key_base.rb
+++ b/lib/jwt/jwk/key_base.rb
@@ -42,6 +42,19 @@ def ==(other)
         other.is_a?(::JWT::JWK::KeyBase) && self[:kid] == other[:kid]
       end
 
+      def verify(**kwargs)
+        jwa.verify(**kwargs, verification_key: verify_key)
+      end
+
+      def sign(**kwargs)
+        jwa.sign(**kwargs, signing_key: signing_key)
+      end
+
+      # @api private
+      def jwa_header
+        jwa.header
+      end
+
       alias eql? ==
 
       def <=>(other)
@@ -52,6 +65,12 @@ def <=>(other)
 
       private
 
+      def jwa
+        raise JWT::JWKError, 'Could not resolve the JWA, the "alg" parameter is missing' unless self[:alg]
+
+        JWA.resolve(self[:alg])
+      end
+
       attr_reader :parameters
     end
   end
diff --git a/lib/jwt/token.rb b/lib/jwt/token.rb
index 7bf5a104..8552e531 100644
--- a/lib/jwt/token.rb
+++ b/lib/jwt/token.rb
@@ -87,16 +87,16 @@ def detach_payload!
 
     # Signs the JWT token.
     #
+    # @param key [String, JWT::JWK::KeyBase] the key to use for signing.
     # @param algorithm [String, Object] the algorithm to use for signing.
-    # @param key [String] the key to use for signing.
     # @return [void]
     # @raise [JWT::EncodeError] if the token is already signed or other problems when signing
-    def sign!(algorithm:, key:)
+    def sign!(key:, algorithm: nil)
       raise ::JWT::EncodeError, 'Token already signed' if @signature
 
-      JWA.resolve(algorithm).tap do |algo|
-        header.merge!(algo.header) { |_key, old, _new| old }
-        @signature = algo.sign(data: signing_input, signing_key: key)
+      JWA.create_signer(algorithm: algorithm, key: key).tap do |signer|
+        header.merge!(signer.jwa_header) { |_key, old, _new| old }
+        @signature = signer.sign(data: signing_input)
       end
 
       nil
diff --git a/lib/jwt/version.rb b/lib/jwt/version.rb
index 290267ef..ffe3375d 100644
--- a/lib/jwt/version.rb
+++ b/lib/jwt/version.rb
@@ -15,8 +15,8 @@ def self.gem_version
   # Version constants
   module VERSION
     MAJOR = 3
-    MINOR = 0
-    TINY  = 1
+    MINOR = 1
+    TINY  = 0
     PRE   = nil
 
     STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
diff --git a/spec/integration/readme_examples_spec.rb b/spec/integration/readme_examples_spec.rb
index 3c8ecc80..a1fd7c33 100644
--- a/spec/integration/readme_examples_spec.rb
+++ b/spec/integration/readme_examples_spec.rb
@@ -456,4 +456,26 @@ def self.verify(data:, signature:, verification_key:)
       expect(header).to include('alg' => 'HS512')
     end
   end
+
+  context 'JWK to verify a signature' do
+    it 'allows to verify a signature with a JWK' do
+      payload = { exp: Time.now.to_i + 60, jti: '1234', sub: 'my-subject' }
+      header = { kid: 'hmac' }
+
+      jwk_json = '{
+                  "kty": "oct",
+                  "k": "c2VjcmV0",
+                  "alg": "HS256",
+                  "kid": "hmac"
+                  }'
+
+      jwk = JWT::JWK.import(JSON.parse(jwk_json))
+
+      token = JWT::Token.new(payload: payload, header: header)
+      token.sign!(key: jwk)
+
+      encoded_token = JWT::EncodedToken.new(token.jwt)
+      expect { encoded_token.verify!(signature: { key: jwk }) }.not_to raise_error
+    end
+  end
 end
diff --git a/spec/jwt/encoded_token_spec.rb b/spec/jwt/encoded_token_spec.rb
index 8d61f833..e239c785 100644
--- a/spec/jwt/encoded_token_spec.rb
+++ b/spec/jwt/encoded_token_spec.rb
@@ -139,6 +139,12 @@
       end
     end
 
+    context 'when algorithm is not given' do
+      it 'raises an error' do
+        expect { token.verify_signature!(key: 'secret') }.to raise_error(JWT::VerificationError, 'No algorithm provided')
+      end
+    end
+
     context 'when header has invalid alg value' do
       let(:header) { { 'alg' => 'HS123' } }
 
@@ -211,6 +217,19 @@
         expect(token.verify_signature!(algorithm: 'RS256', key_finder: key_finder)).to eq(nil)
       end
     end
+
+    context 'when JWK is given as a key' do
+      let(:jwk) { JWT::JWK.new(test_pkey('rsa-2048-private.pem'), alg: 'RS256') }
+      let(:encoded_token) do
+        JWT::Token.new(payload: payload)
+                  .tap { |t| t.sign!(algorithm: 'RS256', key: jwk.signing_key) }
+                  .jwt
+      end
+
+      it 'uses the JWK for verification' do
+        expect(token.verify_signature!(key: jwk)).to eq(nil)
+      end
+    end
   end
 
   describe '#verify_claims!' do
diff --git a/spec/jwt/jwk/ec_spec.rb b/spec/jwt/jwk/ec_spec.rb
index 147c9591..88ef9b76 100644
--- a/spec/jwt/jwk/ec_spec.rb
+++ b/spec/jwt/jwk/ec_spec.rb
@@ -110,6 +110,37 @@
     end
   end
 
+  describe '#verify' do
+    let(:data) { 'data_to_sign' }
+    let(:signature) { jwk.sign(data: data) }
+
+    context 'when jwk is missing the alg parameter' do
+      let(:jwk) { described_class.new(ec_key) }
+
+      context 'when the signature is valid' do
+        it 'returns true' do
+          expect(jwk.verify(data: data, signature: signature)).to be(true)
+        end
+      end
+    end
+
+    context 'when jwk has alg parameter' do
+      let(:jwk) { described_class.new(ec_key, alg: 'ES384') }
+
+      context 'when the signature is valid' do
+        it 'returns true' do
+          expect(jwk.verify(data: data, signature: signature)).to be(true)
+        end
+      end
+
+      context 'when the signature is invalid' do
+        it 'returns false' do
+          expect(jwk.verify(data: data, signature: 'invalid')).to be(false)
+        end
+      end
+    end
+  end
+
   describe '.to_openssl_curve' do
     context 'when a valid curve name is given' do
       it 'returns the corresponding OpenSSL curve name' do
diff --git a/spec/jwt/jwk/rsa_spec.rb b/spec/jwt/jwk/rsa_spec.rb
index 7c574e00..78457454 100644
--- a/spec/jwt/jwk/rsa_spec.rb
+++ b/spec/jwt/jwk/rsa_spec.rb
@@ -93,6 +93,38 @@
     end
   end
 
+  describe '#verify' do
+    let(:rsa) { described_class.new(rsa_key, alg: 'RS256') }
+    let(:data) { 'data_to_sign' }
+    let(:signature) { rsa.sign(data: data) }
+
+    context 'when the signature is valid' do
+      it 'returns true' do
+        expect(rsa.verify(data: data, signature: signature)).to be(true)
+      end
+    end
+
+    context 'when the signature is invalid' do
+      it 'returns false' do
+        expect(rsa.verify(data: data, signature: 'invalid_signature')).to be(false)
+      end
+    end
+
+    context 'when the jwk is missing the alg header' do
+      let(:rsa) { described_class.new(rsa_key) }
+      it 'raises JWT::JWKError' do
+        expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::JWKError, 'Could not resolve the JWA, the "alg" parameter is missing')
+      end
+    end
+
+    context 'when the jwk has an invalid alg header' do
+      let(:rsa) { described_class.new(rsa_key, alg: 'INVALID') }
+      it 'raises JWT::JWKError' do
+        expect { rsa.verify(data: data, signature: 'signature') }.to raise_error(JWT::VerificationError, 'Algorithm not supported')
+      end
+    end
+  end
+
   describe '.common_parameters' do
     context 'when a common parameters hash is given' do
       it 'imports the common parameter' do
diff --git a/spec/jwt/token_spec.rb b/spec/jwt/token_spec.rb
index 63d8efe3..e622dd7f 100644
--- a/spec/jwt/token_spec.rb
+++ b/spec/jwt/token_spec.rb
@@ -22,6 +22,22 @@
         expect { token.sign!(algorithm: 'HS256', key: 'secret') }.to raise_error(JWT::EncodeError)
       end
     end
+
+    context 'when JWK is given as key' do
+      let(:jwk) { JWT::JWK::RSA.new(OpenSSL::PKey::RSA.new(2048), alg: 'RS256') }
+
+      it 'signs the token' do
+        token.sign!(key: jwk)
+
+        expect(JWT::EncodedToken.new(token.jwt).valid_signature?(algorithm: 'RS256', key: jwk.verify_key)).to be(true)
+      end
+    end
+
+    context 'when string key is given but not algorithm' do
+      it 'raises an error' do
+        expect { token.sign!(key: 'secret') }.to raise_error(ArgumentError, 'Algorithm must be provided')
+      end
+    end
   end
 
   describe '#jwt' do