class FirebaseIdToken::Certificates

Manage download and access of Google's x509 certificates. Keeps certificates on a Redis namespace database.

## Download & Access Certificates

It describes two ways to download it: {.request} and {.request!}. The first will only do something when Redis certificates database is empty, the second one will always request a new download to Google's API and override the database with the response.

It's important to note that when saving a set of certificates, it will also set a Redis expiration time to match Google's API header `expires`. **After this time went out, Redis will automatically delete those certificates.**

*To know how many seconds left until the expiration you can use {.ttl}.*

When comes to accessing it, you can either use {.present?} to check if there's any data inside Redis certificates database or {.all} to obtain an `Array` of current certificates.

@example `.request` will only download once

FirebaseIdToken::Certificates.request # Downloads certificates.
FirebaseIdToken::Certificates.request # Won't do anything.
FirebaseIdToken::Certificates.request # Won't do anything either.

@example `.request!` will download always

FirebaseIdToken::Certificates.request # Downloads certificates.
FirebaseIdToken::Certificates.request! # Downloads certificates.
FirebaseIdToken::Certificates.request! # Downloads certificates.

Constants

URL

Google's x509 certificates API URL.

Attributes

local_certs[R]

Certificates saved in the Redis (JSON `String` or `nil`).

redis[R]

A Redis instance.

Public Class Methods

all() click to toggle source

Returns an array of hashes, each hash is a single `{key => value}` pair containing the certificate KID `String` as key and a `OpenSSL::X509::Certificate` object of the respective certificate as value. Returns a empty `Array` when there's no certificates data on Redis. @return [Array] @example

FirebaseIdToken::Certificates.request
certs = FirebaseIdToken::Certificates.all
certs.first #=> {"1d6d01c7[...]" => #<OpenSSL::X509::Certificate[...]}
# File lib/firebase_id_token/certificates.rb, line 98
def self.all
  new.local_certs.map { |kid, cert|
    { kid => OpenSSL::X509::Certificate.new(cert) } }
end
find(kid, raise_error: false) click to toggle source

Returns a `OpenSSL::X509::Certificate` object of the requested Key ID (KID) if there's one. Returns `nil` otherwise.

It will raise a {Exceptions::NoCertificatesError} if the Redis certificates database is empty. @param [String] kid Key ID @return [nil, OpenSSL::X509::Certificate] @example

FirebaseIdToken::Certificates.request
cert = FirebaseIdToken::Certificates.find "1d6d01f4w7d54c7[...]"
#=> <OpenSSL::X509::Certificate: subject=#<OpenSSL [...]
# File lib/firebase_id_token/certificates.rb, line 114
def self.find(kid, raise_error: false)
  certs = new.local_certs
  raise Exceptions::NoCertificatesError if certs.empty?

  return OpenSSL::X509::Certificate.new certs[kid] if certs[kid]

  return unless raise_error

  raise Exceptions::CertificateNotFound,
    "Unable to find a certificate with `#{kid}`."
end
find!(kid) click to toggle source

Returns a `OpenSSL::X509::Certificate` object of the requested Key ID (KID) if there's one.

@raise {Exceptions::CertificateNotFound} if it cannot be found.

@raise {Exceptions::NoCertificatesError} if the Redis certificates database is empty.

@param [String] kid Key ID @return [OpenSSL::X509::Certificate] @example

FirebaseIdToken::Certificates.request
cert = FirebaseIdToken::Certificates.find! "1d6d01f4w7d54c7[...]"
#=> <OpenSSL::X509::Certificate: subject=#<OpenSSL [...]
# File lib/firebase_id_token/certificates.rb, line 140
def self.find!(kid)
  find(kid, raise_error: true)
end
new() click to toggle source

Sets two instance attributes: `:redis` and `:local_certs`. Those are respectively a Redis instance from {FirebaseIdToken::Configuration} and the certificates in it.

# File lib/firebase_id_token/certificates.rb, line 156
def initialize
  @redis = Redis::Namespace.new('firebase_id_token',
    redis: FirebaseIdToken.configuration.redis)
  @local_certs = read_certificates
end
present?() click to toggle source

Returns `true` if there's certificates data on Redis, `false` otherwise. @example

FirebaseIdToken::Certificates.present? #=> false
FirebaseIdToken::Certificates.request
FirebaseIdToken::Certificates.present? #=> true
# File lib/firebase_id_token/certificates.rb, line 84
def self.present?
  ! new.local_certs.empty?
end
request() click to toggle source

Calls {.request!} only if there are no certificates on Redis. It will return `nil` otherwise.

It will raise {Exceptions::CertificatesRequestError} if the request fails or {Exceptions::CertificatesTtlError} when Google responds with a low TTL, check out {.request!} for more info.

@return [nil, Hash] @see Certificates.request!

# File lib/firebase_id_token/certificates.rb, line 51
def self.request
  new.request
end
request!() click to toggle source

Triggers a HTTPS request to Google's x509 certificates API. If it responds with a status `200 OK`, saves the request body into Redis and returns it as a `Hash`.

Otherwise it will raise a {Exceptions::CertificatesRequestError}.

This is really rare to happen, but Google may respond with a low TTL certificate. This is a `SecurityError` and will raise a {Exceptions::CertificatesTtlError}. You are mostly like to never face it. @return [Hash]

# File lib/firebase_id_token/certificates.rb, line 65
def self.request!
  new.request!
end
request_anyway() click to toggle source

@deprecated Use only `request!` in favor of Ruby conventions. It will raise a warning. Kept for compatibility. @see Certificates.request!

# File lib/firebase_id_token/certificates.rb, line 72
def self.request_anyway
  warn 'WARNING: FirebaseIdToken::Certificates.request_anyway is '\
    'deprecated. Use FirebaseIdToken::Certificates.request! instead.'

  new.request!
end
ttl() click to toggle source

Returns the current certificates TTL (Time-To-Live) in seconds. *Zero meaning no certificates.* It's the same as the certificates expiration time, use it to know when to request again. @return [Fixnum]

# File lib/firebase_id_token/certificates.rb, line 148
def self.ttl
  ttl = new.redis.ttl('certificates')
  ttl < 0 ? 0 : ttl
end

Public Instance Methods

request() click to toggle source

@see Certificates.request

# File lib/firebase_id_token/certificates.rb, line 163
def request
  request! if @local_certs.empty?
end
request!() click to toggle source

@see Certificates.request!

# File lib/firebase_id_token/certificates.rb, line 168
def request!
  @request = HTTParty.get URL
  code = @request.code
  if code == 200
    save_certificates
  else
    raise Exceptions::CertificatesRequestError.new(code)
  end
end

Private Instance Methods

read_certificates() click to toggle source
# File lib/firebase_id_token/certificates.rb, line 180
def read_certificates
  certs = @redis.get 'certificates'
  certs ? JSON.parse(certs) : {}
end
save_certificates() click to toggle source
# File lib/firebase_id_token/certificates.rb, line 185
def save_certificates
  @redis.setex 'certificates', ttl, @request.body
  @local_certs = read_certificates
end
ttl() click to toggle source
# File lib/firebase_id_token/certificates.rb, line 190
def ttl
  cache_control = @request.headers['cache-control']
  ttl = cache_control.match(/max-age=([0-9]+)/).captures.first.to_i

  if ttl > 3600
    ttl
  else
    raise Exceptions::CertificatesTtlError
  end
end