Class: Masquerade::HandoffToken

Inherits:
Object
  • Object
show all
Defined in:
app/services/masquerade/handoff_token.rb

Overview

Signed, single-use, short-lived token used to hand off a masquerade session
from the CRM server to the www server without exposing the customer's
long-lived authentication_token.

The token carries:

  • masquerading_account_id - the employee performing the masquerade
  • masqueraded_account_id - the customer being masqueraded as
  • nonce - unique identifier tagged onto the AuthTrail
    LoginActivity row written when the WWW side
    consumes the token (see config/initializers/authtrail.rb)

Security properties:

  • Signed with Rails.application.message_verifier - tamper-evident
  • TTL of TOKEN_TTL seconds - stale links are rejected
  • Single-use via Rails.cache SETNX on the nonce - replay-rejected

Defined Under Namespace

Classes: Error, ExpiredTokenError, InvalidTokenError, ReplayError

Constant Summary collapse

VERIFIER_PURPOSE =
:masquerade_handoff
TOKEN_TTL =
60.seconds
NONCE_TTL =
10.minutes
NONCE_CACHE_PREFIX =
'masquerade:handoff:consumed:'

Class Method Summary collapse

Class Method Details

.decode_and_consume!(token) ⇒ Object

Verifies signature + TTL, then atomically consumes the nonce so the
token cannot be replayed. Raises on any failure; returns the payload
hash on success.



51
52
53
54
55
56
57
58
59
60
61
# File 'app/services/masquerade/handoff_token.rb', line 51

def decode_and_consume!(token)
  raise InvalidTokenError, 'Missing token' if token.blank?

  payload = verifier.verify(token)
  validate_payload!(payload)
  validate_freshness!(payload)
  consume_nonce!(payload[:nonce])
  payload
rescue ActiveSupport::MessageVerifier::InvalidSignature
  raise InvalidTokenError, 'Invalid or tampered token'
end

.encode(masquerading_account_id:, masqueraded_account_id:, nonce:) ⇒ Object



31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'app/services/masquerade/handoff_token.rb', line 31

def encode(masquerading_account_id:, masqueraded_account_id:, nonce:)
  # We carry our own `iat` (issued-at) and check expiration ourselves
  # rather than relying on MessageVerifier's `expires_in` option, because
  # Rails 7.0's MessageVerifier raises the same InvalidSignature class
  # for both tampered AND expired tokens, making it impossible to give
  # the user a precise error message ("expired" vs "invalid"). With an
  # explicit iat we can cleanly distinguish the two failure modes.
  verifier.generate(
    {
      masquerading_account_id: ,
      masqueraded_account_id:  ,
      nonce:                   nonce,
      iat:                     Time.current.to_i
    }
  )
end