Class: Philiprehberger::SignedPayload::Signer

Inherits:
Object
  • Object
show all
Defined in:
lib/philiprehberger/signed_payload/signer.rb

Constant Summary collapse

ALGORITHMS =
{
  sha256: 'SHA256',
  sha384: 'SHA384',
  sha512: 'SHA512'
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(key:, algorithm: :sha256) ⇒ Signer

Returns a new instance of Signer.



16
17
18
19
# File 'lib/philiprehberger/signed_payload/signer.rb', line 16

def initialize(key:, algorithm: :sha256)
  @key = key
  @algorithm = validate_algorithm!(algorithm)
end

Instance Method Details

#decode(token) ⇒ Object



53
54
55
56
57
58
59
# File 'lib/philiprehberger/signed_payload/signer.rb', line 53

def decode(token)
  encoded, _sig = split_token(token)
  parsed = JSON.parse(Base64.urlsafe_decode64(encoded))
  parsed['data']
rescue JSON::ParserError
  raise MalformedToken, 'invalid payload encoding'
end

#expired?(token) ⇒ Boolean

Check if a token has expired without verifying the signature.

Parameters:

  • token (String)

    the token to check

Returns:

  • (Boolean)

    true if the token has expired or has no expiration



91
92
93
94
95
96
97
98
99
# File 'lib/philiprehberger/signed_payload/signer.rb', line 91

def expired?(token)
  encoded, _sig = split_token(token)
  parsed = JSON.parse(Base64.urlsafe_decode64(encoded))
  return false unless parsed.key?('exp')

  parsed['exp'] <= Time.now.to_i
rescue JSON::ParserError
  raise MalformedToken, 'invalid payload encoding'
end

#peek(token) ⇒ Hash

Inspect token metadata without verifying the signature.

Parameters:

  • token (String)

    the token to inspect

Returns:

  • (Hash)

    with :data, :exp (Integer or nil), and :expired (Boolean)



105
106
107
108
109
110
111
112
113
114
115
116
# File 'lib/philiprehberger/signed_payload/signer.rb', line 105

def peek(token)
  encoded, _sig = split_token(token)
  parsed = JSON.parse(Base64.urlsafe_decode64(encoded))
  exp = parsed['exp']
  {
    data: parsed['data'],
    exp: exp,
    expired: exp ? exp <= Time.now.to_i : false
  }
rescue JSON::ParserError
  raise MalformedToken, 'invalid payload encoding'
end

#refresh(token, expires_in:) ⇒ String

Re-sign a verified token with a new expiration.

Parameters:

  • token (String)

    the token to refresh

  • expires_in (Integer)

    new TTL in seconds

Returns:

  • (String)

    a new token with the same data and a fresh expiration

Raises:



67
68
69
70
# File 'lib/philiprehberger/signed_payload/signer.rb', line 67

def refresh(token, expires_in:)
  data = verify(token)
  sign(data, expires_in: expires_in)
end

#sign(data, expires_in: nil) ⇒ Object



21
22
23
24
25
26
# File 'lib/philiprehberger/signed_payload/signer.rb', line 21

def sign(data, expires_in: nil)
  payload = build_payload(data, expires_in)
  encoded = Base64.urlsafe_encode64(payload)
  signature = compute_signature(encoded)
  "#{encoded}.#{Base64.urlsafe_encode64(signature)}"
end

#sign_with_exp(data, exp:) ⇒ String

Re-sign a verified token’s payload preserving the original expiration timestamp. Used internally by key rotation to avoid shifting expiry during re-signing.

Parameters:

  • data (Object)

    the payload data to sign

  • exp (Integer, nil)

    Unix timestamp to preserve (or nil for no expiry)

Returns:

  • (String)

    a new token with the given data and exp



78
79
80
81
82
83
84
85
# File 'lib/philiprehberger/signed_payload/signer.rb', line 78

def sign_with_exp(data, exp:)
  hash = { 'data' => data }
  hash['exp'] = exp unless exp.nil?
  payload = JSON.generate(hash)
  encoded = Base64.urlsafe_encode64(payload)
  signature = compute_signature(encoded)
  "#{encoded}.#{Base64.urlsafe_encode64(signature)}"
end

#valid?(token) ⇒ Boolean

Returns:

  • (Boolean)


46
47
48
49
50
51
# File 'lib/philiprehberger/signed_payload/signer.rb', line 46

def valid?(token)
  verify(token)
  true
rescue Error
  false
end

#verify(token, keys: nil) ⇒ Object

Verify a token’s signature and decode its payload.

Parameters:

  • token (String)

    the token to verify

  • keys (Array<String>, nil) (defaults to: nil)

    optional list of candidate keys to try for zero-downtime secret rotation. Signature passes if any key validates. When nil (default), the key supplied at construction is used.

Returns:

  • (Object)

    the decoded payload data

Raises:



38
39
40
41
42
43
44
# File 'lib/philiprehberger/signed_payload/signer.rb', line 38

def verify(token, keys: nil)
  raise ArgumentError, 'no keys provided' if keys.is_a?(Array) && keys.empty?

  encoded, sig = split_token(token)
  verify_signature!(encoded, sig, keys: keys)
  decode_payload(encoded)
end