Skip to content

Commit

Permalink
Add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
anakinj committed Sep 30, 2024
1 parent 142fc0c commit 57ac9d3
Show file tree
Hide file tree
Showing 15 changed files with 106 additions and 34 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ coverage/
*gemfile.lock
.byebug_history
*.gem
doc/
2 changes: 1 addition & 1 deletion lib/jwt/claims.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
require_relative 'claims/decode'

module JWT
module Claims
module Claims # :nodoc:
Error = Struct.new(:message, keyword_init: true)

VERIFIERS = {
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/audience.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module JWT
module Claims
class Audience
class Audience # :nodoc:
def initialize(expected_audience:)
@expected_audience = expected_audience
end
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/decode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module JWT
module Claims
module Decode
module Decode # :nodoc:
VERIFIERS = {
verify_expiration: ->(options) { Claims::Expiration.new(leeway: options[:exp_leeway] || options[:leeway]) },
verify_not_before: ->(options) { Claims::NotBefore.new(leeway: options[:nbf_leeway] || options[:leeway]) },
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/expiration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module JWT
module Claims
class Expiration
class Expiration # :nodoc:
def initialize(leeway:)
@leeway = leeway || 0
end
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/issued_at.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module JWT
module Claims
class IssuedAt
class IssuedAt # :nodoc:
def verify!(context:, **_args)
return unless context.payload.is_a?(Hash)
return unless context.payload.key?('iat')
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/issuer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module JWT
module Claims
class Issuer
class Issuer # :nodoc:
def initialize(issuers:)
@issuers = Array(issuers).map { |item| item.is_a?(Symbol) ? item.to_s : item }
end
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/jwt_id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module JWT
module Claims
class JwtId
class JwtId # :nodoc:
def initialize(validator:)
@validator = validator
end
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/not_before.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module JWT
module Claims
class NotBefore
class NotBefore # :nodoc:
def initialize(leeway:)
@leeway = leeway || 0
end
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/numeric.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module JWT
module Claims
class Numeric
class Numeric # :nodoc:
NUMERIC_CLAIMS = %i[
exp
iat
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/required.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module JWT
module Claims
class Required
class Required # :nodoc:
def initialize(required_claims:)
@required_claims = required_claims
end
Expand Down
2 changes: 1 addition & 1 deletion lib/jwt/claims/subject.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module JWT
module Claims
class Subject
class Subject # :nodoc:
def initialize(expected_subject:)
@expected_subject = expected_subject.to_s
end
Expand Down
7 changes: 4 additions & 3 deletions lib/jwt/decode.rb
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,10 @@ def find_key(&keyfinder)
end

def validate_segment_count!
return if token.segment_count == 3
return if !@verify && token.segment_count == 2 # If no verifying required, the signature is not needed
return if token.segment_count == 2 && none_algorithm?
segment_count = token.jwt.count('.') + 1
return if segment_count == 3
return if !@verify && segment_count == 2 # If no verifying required, the signature is not needed
return if segment_count == 2 && none_algorithm?

raise JWT::DecodeError, 'Not enough or too many segments'
end
Expand Down
106 changes: 90 additions & 16 deletions lib/jwt/token.rb
Original file line number Diff line number Diff line change
@@ -1,66 +1,128 @@
# frozen_string_literal: true

module JWT
# Represents a JWT token
#
# Basic token signed using the HS256 algorithm:
#
# token = JWT::Token.new(payload: {pay: 'load'})
# token.sign!(algorithm: 'HS256', key: 'secret')
# token.jwt # => eyJhb....
#
# Custom headers will be combined with generated headers:
# token = JWT::Token.new(payload: {pay: 'load'}, header: {custom: "value"})
# token.sign!(algorithm: 'HS256', key: 'secret')
# token.header # => {"custom"=>"value", "alg"=>"HS256"}
#
class Token
# Initializes a new Token instance.
#
# @param header [Hash] the header of the JWT token.
# @param payload [Hash] the payload of the JWT token.
def initialize(payload:, header: {})
@header = header&.transform_keys(&:to_s)
@payload = payload
end

def segment_count
jwt.count('.') + 1
end

# Returns the decoded signature of the JWT token.
#
# @return [String] the decoded signature of the JWT token.
def signature
@signature ||= ::JWT::Base64.url_decode(raw_signature || '')
@signature ||= ::JWT::Base64.url_decode(encoded_signature || '')
end

def raw_signature
@raw_signature ||= ::JWT::Base64.url_encode(signature)
# Returns the encoded signature of the JWT token.
#
# @return [String] the encoded signature of the JWT token.
def encoded_signature
@encoded_signature ||= ::JWT::Base64.url_encode(signature)
end

# Returns the decoded header of the JWT token.
#
# @return [Hash] the header of the JWT token.
def header
@header ||= parse_and_decode(raw_header)
@header ||= parse_and_decode(encoded_header)
end

def raw_header
@raw_header ||= ::JWT::Base64.url_encode(JWT::JSON.generate(header))
# Returns the encoded header of the JWT token.
#
# @return [String] the encoded header of the JWT token.
def encoded_header
@encoded_header ||= ::JWT::Base64.url_encode(JWT::JSON.generate(header))
end

# Returns the payload of the JWT token.
#
# @return [Hash] the payload of the JWT token.
def payload
@payload ||= parse_and_decode(raw_payload)
@payload ||= parse_and_decode(encoded_payload)
end

def raw_payload
@raw_payload ||= ::JWT::Base64.url_encode(JWT::JSON.generate(payload))
# Returns the encoded payload of the JWT token.
#
# @return [String] the encoded payload of the JWT token.
def encoded_payload
@encoded_payload ||= ::JWT::Base64.url_encode(JWT::JSON.generate(payload))
end

# Returns the signing input of the JWT token.
#
# @return [String] the signing input of the JWT token.
def signing_input
[raw_header, raw_payload].join('.')
[encoded_header, encoded_payload].join('.')
end

# Returns the JWT token as a string.
#
# @return [String] the JWT token as a string.
# @raise [JWT::EncodeError] if the token is not signed or other encoding issues
def jwt
@jwt ||= (@signature && [raw_header, raw_payload, raw_signature].join('.')) || raise(::JWT::EncodeError, 'Token is not signed')
@jwt ||= (@signature && [encoded_header, encoded_payload, encoded_signature].join('.')) || raise(::JWT::EncodeError, 'Token is not signed')
end

# Verifies the claims of the JWT token.
#
# @param options [Array] the options for verifying the claims.
# @return [void]
# @raise [JWT::DecodeError] if any claim is invalid.
def verify_claims!(*options)
Claims.verify!(self, *options)
end

# Checks if the claims of the JWT token are valid.
#
# @param options [Array] the options for verifying the claims.
# @return [Boolean] true if the claims are valid, false otherwise.
def valid_claims?(*options)
claim_errors(*options).empty?
end

# Returns the errors in the claims of the JWT token.
#
# @param options [Array] the options for verifying the claims.
# @return [Array<JWT::Claims::Error>] the errors in the claims of the JWT token.
def claim_errors(*options)
Claims.errors(self, *options)
end

# Verifies the signature of the JWT token.
#
# @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.
# @return [void]
# @raise [JWT::VerificationError] if the signature verification fails.
def verify_signature!(algorithm:, key:)
return if valid_signature?(algorithm: algorithm, key: key)

raise JWT::VerificationError, 'Signature verification failed'
end

# 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.
# @return [Boolean] true if the signature is valid, false otherwise.
def valid_signature?(algorithm:, key:)
Array(JWA.resolve_and_sort(algorithms: algorithm, preferred_algorithm: header['alg'])).any? do |algo|
Array(key).any? do |one_key|
Expand All @@ -69,6 +131,12 @@ def valid_signature?(algorithm:, key:)
end
end

# Signs the JWT token.
#
# @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:)
raise ::JWT::EncodeError, 'Token already signed' if @signature

Expand All @@ -78,6 +146,9 @@ def sign!(algorithm:, key:)
end
end

# Returns the JWT token as a string.
#
# @return [String] the JWT token as a string.
alias to_s jwt

private
Expand All @@ -92,11 +163,14 @@ def parse_and_decode(segment)
class EncodedToken < Token
attr_reader :jwt

# Initializes a new EncodedToken instance.
#
# @param jwt [String] the encoded JWT token.
def initialize(jwt)
raise ArgumentError 'Provided JWT must be a String' unless jwt.is_a?(String)

@jwt = jwt
@raw_header, @raw_payload, @raw_signature = jwt.split('.')
@encoded_header, @encoded_payload, @encoded_signature = jwt.split('.')

super(header: nil, payload: nil)
end
Expand Down
4 changes: 0 additions & 4 deletions spec/jwt/encoded_token_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,6 @@
it { expect(token.signature).to be_a(String) }
end

describe '#segment_count' do
it { expect(token.segment_count).to eq(3) }
end

describe '#signing_input' do
it { expect(token.signing_input).to eq('eyJhbGciOiJIUzI1NiJ9.eyJwYXkiOiJsb2FkIn0') }
end
Expand Down

0 comments on commit 57ac9d3

Please sign in to comment.