Skip to content

Commit

Permalink
feat: Add option to encode jwt without nonce
Browse files Browse the repository at this point in the history
  • Loading branch information
KoenSengers authored and LuukvH committed Dec 14, 2023
1 parent 32018d3 commit 1f2d58d
Show file tree
Hide file tree
Showing 2 changed files with 94 additions and 9 deletions.
38 changes: 29 additions & 9 deletions lib/keypair.rb
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,14 @@ def self.jwt_encode(payload)
current.jwt_encode(payload)
end

# Encodes the payload with the current keypair.
# It forewards the call to the instance method {Keypair#jwt_encode}.
# @return [String] Encoded JWT token with security credentials.
# @param payload [Hash] Hash which should be encoded.
def self.jwt_encode_without_nonce(payload)
current.jwt_encode_without_nonce(payload, {}, nonce: false)
end

# Decodes the payload and verifies the signature against the current valid keypairs.
# @param id_token [String] A JWT that should be decoded.
# @param options [Hash] options for decoding, passed to {JWT::Decode}.
Expand All @@ -137,16 +145,9 @@ def self.jwt_decode(id_token, options = {})
# It automatically sets the +kid+ in the header.
# @param payload [Hash] you have to provide a hash since the security attributes have to be added.
# @param headers [Hash] you can optionally add additional headers to the JWT.
def jwt_encode(payload, headers = {})
def jwt_encode(payload, headers = {}, nonce: true)
# Add security claims to payload
payload.reverse_merge!(
# Time at which the Issuer generated the JWT (epoch).
iat: Time.now.to_i,

# Expiration time on or after which the tool MUST NOT accept the ID Token for
# processing (epoch). This is mostly used to allow some clock skew.
exp: Time.now.to_i + 5.minutes.to_i
)
payload = secure_payload(payload, nonce: nonce)

# Add additional info into the headers
headers.reverse_merge!(
Expand Down Expand Up @@ -225,4 +226,23 @@ def expires_at_after_not_after

errors.add(:expires_at, 'must be after not after')
end

def secure_payload(payload, nonce: true)
secure_payload = {
# Time at which the Issuer generated the JWT (epoch).
iat: Time.now.to_i,

# Expiration time on or after which the tool MUST NOT accept the ID Token for
# processing (epoch). This is mostly used to allow some clock skew.
exp: Time.now.to_i + 5.minutes.to_i
}

if nonce
# String value used to associate a tool session with an ID Token, and to mitigate replay
# attacks. The nonce value is a case-sensitive string.
secure_payload[:nonce] = SecureRandom.uuid
end

payload.reverse_merge!(secure_payload)
end
end
65 changes: 65 additions & 0 deletions spec/models/keypair_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,71 @@
# Decode the JWT but don't verify
let(:headers) { JWT.decode(subject, nil, false).second.deep_symbolize_keys }

context 'with string payload' do
let(:payload) { SecureRandom.hex }
it 'raises an error' do
expect { subject }.to raise_error NoMethodError
end
end
context 'with hash payload' do
let(:payload) { { hex: SecureRandom.hex, nested: { hex: SecureRandom.hex } } }
it 'returns a JWT with the correct payload' do
expect(decoded).to include payload
end
it 'adds security payloads' do
expect(decoded.keys).to match_array %i[hex nested iat exp nonce]
end
it 'sets iat to now', timecop: :freeze do
expect(decoded[:iat]).to eq Time.current.to_i
end
it 'sets exp to 5 minutes from now', timecop: :freeze do
expect(decoded[:exp]).to eq 5.minutes.from_now.to_i
end
it 'sets a generated nonce' do
allow(SecureRandom).to receive(:uuid).and_return 'my-nonce'
expect(decoded[:nonce]).to eq 'my-nonce'
end
it 'is encoded with the keypair and correct algorithm' do
expect do
JWT.decode(subject, keypair.public_key, true, algorithm: described_class::ALGORITHM)
end.to_not raise_error
end
it 'sets the kid in the headers' do
expect(headers).to eq(
alg: described_class::ALGORITHM,
kid: keypair.jwk_kid
)
end
end
context 'with security overrides' do
let(:payload) { { foo: 'bar', exp: 1.minute.ago.to_i } }

it 'returns a JWT with the correct payload' do
allow(SecureRandom).to receive(:uuid).and_return 'my-nonce'
expect(decoded).to eq(
foo: 'bar',
iat: Time.current.to_i,
exp: 1.minute.ago.to_i,
nonce: 'my-nonce'
)
end
it 'is cannot be decoded' do
expect do
JWT.decode(subject, keypair.public_key, true, algorithm: described_class::ALGORITHM)
end.to raise_error JWT::ExpiredSignature
end
end
end

describe '.jwt_encode_without_nonce' do
let(:payload) { { hex: SecureRandom.hex, nested: { hex: SecureRandom.hex } } }
let(:keypair) { described_class.new }
subject { keypair.jwt_encode(payload, {}, nonce: false) }
# Decode the JWT but don't verify
let(:decoded) { JWT.decode(subject, nil, false).first.deep_symbolize_keys }
# Decode the JWT but don't verify
let(:headers) { JWT.decode(subject, nil, false).second.deep_symbolize_keys }

context 'with string payload' do
let(:payload) { SecureRandom.hex }
it 'raises an error' do
Expand Down

0 comments on commit 1f2d58d

Please sign in to comment.