Class: Keypair
- Inherits:
-
ActiveRecord::Base
- Object
- ActiveRecord::Base
- Keypair
- Defined in:
- lib/keypair.rb
Overview
This class contains functionality needed for signing messages and publishing JWK.
Keypairs are considered valid based on their #not_before, #not_after and #expires_at attributes.
A keypair can be used for signing if:
-
The current time is greater than or equal to #not_before
-
The current time is less than or equal to #not_after
A keypair can be used for validation if:
-
The current time is less than #expires_at.
By default, this means that when a key is created, it can be used for signing for 1 month and can still be used for signature validation 1 month after it is not used for signing (i.e. for 2 months since it started being used for signing).
If you need to sign messages, use the Keypair.current keypair for this. This method performs the rotation of the keypairs if required.
You can also use the jwt_encode
and jwt_decode
methods directly to encode and securely decode your payloads
Constant Summary collapse
- ALGORITHM =
rubocop:disable Metrics/ClassLength
'RS256'
- ROTATION_INTERVAL =
1.month
Instance Attribute Summary collapse
-
#expires_at ⇒ Time
The time after which the keypair may not be used for signature validation.
-
#jwk_kid ⇒ String
The public external id of the key used to find the associated key on decoding.
-
#not_after ⇒ Time
The time after which no payloads may be signed using the keypair.
-
#not_before ⇒ Time
The time before which no payloads may be signed using the keypair.
Class Method Summary collapse
- .cached_jwks(force: false) ⇒ Object
-
.cached_keyset ⇒ Hash
A cached version of the keyset.
-
.current ⇒ Keypair
The keypair used to sign messages and autorotates if it has expired.
-
.jwk_loader_cached ⇒ Object
options will be ‘true` if a matching `kid` was not found github.com/jwt/ruby-jwt/blob/master/lib/jwt/jwk/key_finder.rb#L31.
-
.jwt_decode(id_token, options = {}) ⇒ Hash
Decodes the payload and verifies the signature against the current valid keypairs.
-
.jwt_encode(payload) ⇒ String
Encodes the payload with the current keypair.
-
.jwt_encode_without_nonce(payload) ⇒ String
Encodes the payload with the current keypair.
-
.keyset ⇒ Hash
The JWK Set of our valid keypairs.
-
.valid ⇒ Object
Non-expired keypairs are considered valid and can be used to validate signatures and export public jwks.
Instance Method Summary collapse
-
#jwt_encode(payload, headers = {}, nonce: true) ⇒ Object
JWT encodes the payload with this keypair.
-
#private_key ⇒ OpenSSL::PKey::RSA
OpenSSL::PKey::RSA instance loaded with our keypair.
-
#public_jwk_export ⇒ Object
Public representation of the keypair in the JWK format.
-
#public_key ⇒ OpenSSL::PKey::RSA
OpenSSL::PKey::RSA instance loaded with the public part our keypair.
Instance Attribute Details
#expires_at ⇒ Time
The time after which the keypair may not be used for signature validation.
37 38 39 |
# File 'lib/keypair.rb', line 37 def expires_at @expires_at end |
#jwk_kid ⇒ String
The public external id of the key used to find the associated key on decoding.
37 38 39 |
# File 'lib/keypair.rb', line 37 def jwk_kid @jwk_kid end |
#not_after ⇒ Time
The time after which no payloads may be signed using the keypair.
37 38 39 |
# File 'lib/keypair.rb', line 37 def not_after @not_after end |
#not_before ⇒ Time
The time before which no payloads may be signed using the keypair.
37 38 39 |
# File 'lib/keypair.rb', line 37 def not_before @not_before end |
Class Method Details
.cached_jwks(force: false) ⇒ Object
151 152 153 154 155 |
# File 'lib/keypair.rb', line 151 def self.cached_jwks(force: false) Rails.cache.fetch('keypairs/Keypair/jwks', force: force, skip_nil: true) do keyset end end |
.cached_keyset ⇒ Hash
Returns a cached version of the keyset.
103 104 105 106 107 |
# File 'lib/keypair.rb', line 103 def self.cached_keyset Rails.cache.fetch('keypairs/Keypair/keyset', expires_in: 12.hours) do keyset end end |
.current ⇒ Keypair
Returns the keypair used to sign messages and autorotates if it has expired.
59 60 61 62 63 64 |
# File 'lib/keypair.rb', line 59 def self.current order(not_before: :asc) .where(arel_table[:not_before].lteq(Time.zone.now)) .where(arel_table[:not_after].gteq(Time.zone.now)) .last || create! end |
.jwk_loader_cached ⇒ Object
options will be ‘true` if a matching `kid` was not found github.com/jwt/ruby-jwt/blob/master/lib/jwt/jwk/key_finder.rb#L31
145 146 147 148 149 |
# File 'lib/keypair.rb', line 145 def self.jwk_loader_cached lambda do || cached_jwks(force: [:invalidate]) || {} end end |
.jwt_decode(id_token, options = {}) ⇒ Hash
Decodes the payload and verifies the signature against the current valid keypairs.
130 131 132 133 134 135 136 137 138 139 140 141 |
# File 'lib/keypair.rb', line 130 def self.jwt_decode(id_token, = {}) # Add default decoding options .reverse_merge!( # Change the default algorithm to match the encoding algorithm algorithm: ALGORITHM, # Load our own keyset as valid keys jwks: jwk_loader_cached, # If the `sub` is provided, validate that it matches the payload `sub` verify_sub: true ) JWT.decode(id_token, nil, true, ).first.with_indifferent_access end |
.jwt_encode(payload) ⇒ String
Encodes the payload with the current keypair. It forewards the call to the instance method #jwt_encode.
113 114 115 |
# File 'lib/keypair.rb', line 113 def self.jwt_encode(payload) current.jwt_encode(payload) end |
.jwt_encode_without_nonce(payload) ⇒ String
Encodes the payload with the current keypair. It forewards the call to the instance method #jwt_encode.
121 122 123 |
# File 'lib/keypair.rb', line 121 def self.jwt_encode_without_nonce(payload) current.jwt_encode(payload, {}, nonce: false) end |
.keyset ⇒ Hash
The JWK Set of our valid keypairs.
88 89 90 91 92 93 94 95 96 97 98 99 |
# File 'lib/keypair.rb', line 88 def self.keyset valid_keys = valid.order(not_before: :asc).to_a # If we don't have any keys or if we don't have a future key (i.e. the last key is the current key) while valid_keys.last.nil? || valid_keys.last.not_before <= Time.zone.now # There is an automatic fallback to Time.zone.now if not_before is not set valid_keys << create!(not_before: valid_keys.last&.not_after) end { keys: valid_keys.map(&:public_jwk_export) } end |
.valid ⇒ Object
Non-expired keypairs are considered valid and can be used to validate signatures and export public jwks.
56 |
# File 'lib/keypair.rb', line 56 scope :valid, -> { where(arel_table[:expires_at].gt(Time.zone.now)) } |
Instance Method Details
#jwt_encode(payload, headers = {}, nonce: true) ⇒ Object
JWT encodes the payload with this keypair. It automatically adds the security attributes iat
, exp
and nonce
to the payload. It automatically sets the kid
in the header.
162 163 164 165 166 167 168 169 170 171 172 173 |
# File 'lib/keypair.rb', line 162 def jwt_encode(payload, headers = {}, nonce: true) # Add security claims to payload payload = secure_payload(payload, nonce: nonce) # Add additional info into the headers headers.reverse_merge!( # Set the id of they key kid: jwk_kid ) JWT.encode(payload, private_key, ALGORITHM, headers) end |
#private_key ⇒ OpenSSL::PKey::RSA
Returns OpenSSL::PKey::RSA instance loaded with our keypair.
198 199 200 |
# File 'lib/keypair.rb', line 198 def private_key OpenSSL::PKey::RSA.new(_keypair) end |
#public_jwk_export ⇒ Object
Public representation of the keypair in the JWK format. We append the alg
, and use
parameters to our JWK to indicate that our intended use is to generate signatures using RS256
.
alg
-
This (algorithm) parameter identifies the algorithm intended for use with the key. It is based in the ALGORITHM. The IMS Security framework specifies that the
alg
value SHOULD be the default ofRS256
. Use of this member is OPTIONAL. use
-
This (public key use) parameter identifies the intended use of the public key. Use of this member is OPTIONAL, unless the application requires its presence.
190 191 192 193 194 195 |
# File 'lib/keypair.rb', line 190 def public_jwk_export public_jwk.export.merge( alg: ALGORITHM, use: 'sig' ) end |
#public_key ⇒ OpenSSL::PKey::RSA
Returns OpenSSL::PKey::RSA instance loaded with the public part our keypair.
203 |
# File 'lib/keypair.rb', line 203 delegate :public_key, to: :private_key |