Class: Railspress::ApiKey

Inherits:
ApplicationRecord show all
Defined in:
app/models/railspress/api_key.rb

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.authenticate(raw_token, ip_address: nil) ⇒ Object



56
57
58
59
60
61
62
63
64
65
66
# File 'app/models/railspress/api_key.rb', line 56

def authenticate(raw_token, ip_address: nil)
  parsed = parse_token(raw_token)
  return nil unless parsed

  candidate = active.where(token_prefix: parsed[:prefix]).recent.first
  return nil unless candidate
  return nil unless secure_digest_match?(candidate.token_digest, digest(parsed[:secret]))

  candidate.touch_usage!(ip_address: ip_address)
  candidate
end

.build_token(prefix, raw_secret) ⇒ Object



68
69
70
# File 'app/models/railspress/api_key.rb', line 68

def build_token(prefix, raw_secret)
  "rp_#{Rails.env}_#{prefix}_#{raw_secret}"
end

.digest(raw_secret) ⇒ Object



72
73
74
# File 'app/models/railspress/api_key.rb', line 72

def digest(raw_secret)
  OpenSSL::HMAC.hexdigest("SHA256", digest_key, raw_secret.to_s)
end

.issue!(name:, actor:, owner: actor, expires_at: nil, rotated_from: nil) ⇒ Object



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
# File 'app/models/railspress/api_key.rb', line 37

def issue!(name:, actor:, owner: actor, expires_at: nil, rotated_from: nil)
  prefix = generate_prefix
  raw_secret = generate_secret
  token = build_token(prefix, raw_secret)

  api_key = create!(
    name: name,
    token_prefix: prefix,
    token_digest: digest(raw_secret),
    secret_ciphertext: raw_secret,
    owner: owner,
    created_by: actor,
    expires_at: expires_at,
    rotated_from: rotated_from
  )

  [ api_key, token ]
end

.parse_token(raw_token) ⇒ Object



76
77
78
79
80
81
82
83
# File 'app/models/railspress/api_key.rb', line 76

def parse_token(raw_token)
  return nil if raw_token.blank?

  match = raw_token.match(/\Arp_[a-z0-9_]+_([a-f0-9]{12})_([a-f0-9]{64})\z/)
  return nil unless match

  { prefix: match[1], secret: match[2] }
end

Instance Method Details

#active?Boolean

Returns:

  • (Boolean)


136
137
138
# File 'app/models/railspress/api_key.rb', line 136

def active?
  revoked_at.nil? && (expires_at.nil? || expires_at > Time.current)
end

#revoke!(actor:, reason: "revoked") ⇒ Object



107
108
109
110
111
112
113
# File 'app/models/railspress/api_key.rb', line 107

def revoke!(actor:, reason: "revoked")
  update!(
    revoked_at: Time.current,
    revoke_reason: reason,
    revoked_by: actor
  )
end

#rotate!(actor:, name: self.name, expires_at: self.expires_at) ⇒ Object



115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'app/models/railspress/api_key.rb', line 115

def rotate!(actor:, name: self.name, expires_at: self.expires_at)
  transaction do
    replacement_key, plain_token = self.class.issue!(
      name: name,
      actor: actor,
      owner: owner,
      expires_at: expires_at,
      rotated_from: self
    )

    update!(
      revoked_at: Time.current,
      revoke_reason: "rotated",
      revoked_by: actor,
      rotated_by: actor
    )

    [ replacement_key, plain_token ]
  end
end

#statusObject



140
141
142
143
144
145
# File 'app/models/railspress/api_key.rb', line 140

def status
  return "revoked" if revoked_at.present?
  return "expired" if expires_at.present? && expires_at <= Time.current

  "active"
end

#touch_usage!(ip_address: nil) ⇒ Object



147
148
149
# File 'app/models/railspress/api_key.rb', line 147

def touch_usage!(ip_address: nil)
  update_columns(last_used_at: Time.current, last_used_ip: ip_address, updated_at: Time.current)
end