Class: TokenManager

Inherits:
Object
  • Object
show all
Defined in:
lib/token_manager.rb,
lib/token_manager/version.rb

Defined Under Namespace

Classes: FaradayMiddleware, RetrievePublicKeyError

Constant Summary collapse

ALGO =
'RS256'
VERSION =
'0.1.1'

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(options) ⇒ TokenManager

Returns a new instance of TokenManager.



19
20
21
22
23
24
25
26
# File 'lib/token_manager.rb', line 19

def initialize(options)
  options = options.deep_stringify_keys
  @service_name = options['service_name'] || raise(ArgumentError, '`service_name` is required')
  @trusted_issuers = options['trusted_issuers'] || {}
  @token_ttl = options['token_ttl']
  @public_key_ttl = options['public_key_ttl'] || 1.month
  @old_key_ttl = options['old_key_ttl'] || 1.week
end

Class Method Details

.token_from(headers) ⇒ Object



14
15
16
17
# File 'lib/token_manager.rb', line 14

def self.token_from(headers)
  headers['Authorization']&.match(/Token token="(\S+)"/)&.[](1) ||
    headers['Authorization']&.match(/Bearer (\S+)/)&.[](1)
end

Instance Method Details

#decode(jwt, options = {}) ⇒ Object



40
41
42
43
44
45
46
47
48
49
50
51
52
# File 'lib/token_manager.rb', line 40

def decode(jwt, options = {})
  options = options.merge(
    algorithm: ALGO,
    required_claims: ['exp', 'iss', 'aud'],
    verify_iss: true,
    iss: @trusted_issuers.keys,
    verify_aud: true,
    aud: @service_name
  )
  ::JWT.decode(jwt, nil, true, options) do |header, payload|
    OpenSSL::PKey::RSA.new(issuer_public_key(iss: payload['iss'], kid: header['kid']))
  end
end

#encode(payload) ⇒ Object

Raises:

  • (ArgumentError)


28
29
30
31
32
33
34
35
36
37
38
# File 'lib/token_manager.rb', line 28

def encode(payload)
  payload = payload.stringify_keys
  raise(ArgumentError, '`aud` is required') unless payload.key?('aud')

  raise(ArgumentError, '`exp` claim or `token_ttl` config option is required') if !payload.key?('exp') && !@token_ttl

  payload.reverse_merge!(exp: @token_ttl.seconds.from_now.to_i) if @token_ttl

  payload = payload.merge(iss: @service_name)
  ::JWT.encode(payload, private_key, ALGO, { kid: key_id })
end

#generate_private_key(expire_current_token: true) ⇒ Object



59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/token_manager.rb', line 59

def generate_private_key(expire_current_token: true)
  rsa_private = OpenSSL::PKey::RSA.generate(2048)
  rsa_public = rsa_private.public_key
  next_key_id = SecureRandom.uuid
  with_redis do |redis|
    redis.multi do |multi|
      # set new token
      multi.set(cache_key(:private_key, next_key_id), rsa_private.to_pem)
      multi.set(cache_key(:public_key, next_key_id), rsa_public.to_pem)
      multi.set(cache_key(:key_id), next_key_id)
      if expire_current_token
        # expire current token
        multi.expire(cache_key(:private_key, key_id), @old_key_ttl)
        multi.expire(cache_key(:public_key, key_id), @old_key_ttl)
      end
    end
  end
  # drop memoization
  @key_id = next_key_id
  @private_key = rsa_private

  rsa_private
end

#key_idObject



83
84
85
86
87
88
# File 'lib/token_manager.rb', line 83

def key_id
  @key_id ||= with_redis do |c|
    c.get(cache_key(:key_id)) ||
      generate_private_key(expire_current_token: false) && c.get(cache_key(:key_id))
  end
end

#public_key(kid = key_id) ⇒ Object



54
55
56
57
# File 'lib/token_manager.rb', line 54

def public_key(kid = key_id)
  @public_key ||= {}
  @public_key[kid.to_s] ||= with_redis { |redis| redis.get(cache_key(:public_key, kid)) }
end