Skip to content

Commit

Permalink
Add idenity plataform model
Browse files Browse the repository at this point in the history
  • Loading branch information
Juan Pablo Gil committed Oct 26, 2022
1 parent 0e47fe7 commit bfbd7f8
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# frozen_string_literal: true

require 'net/http'

module IdentityPlatform
#= DecodeIdentityToken::CertStore
#
# This class is used by the DecodeIdentityToken service to retrieve and store
# the certificates used to properly decode tokens issued by Google Cloud
# Identity Platform
class CertStore
extend MonitorMixin

CERTS_PATH = '/robot/v1/metadata/x509/[email protected]'
CERTS_EXPIRY = 3600

mattr_reader :certs_last_refresh

def self.client
@client ||= Faraday.new('https://www.googleapis.com') do |f|
f.response :json # decode response bodies as JSON
f.adapter :net_http
end
end

def self.certs_cache_expired?
return true unless certs_last_refresh

Time.current > certs_last_refresh + CERTS_EXPIRY
end

def self.certs
refresh_certs if certs_cache_expired?
@@certs
end

def self.fetch_certs
client.get(CERTS_PATH).tap do |response|
raise Error, 'Failed to fetch certs' unless response.success?
end
end

def self.refresh_certs
synchronize do
return unless (res = fetch_certs)

new_certs = res.body.transform_values do |cert_string|
OpenSSL::X509::Certificate.new(cert_string)
end

(@@certs ||= {}).merge! new_certs
@@certs_last_refresh = Time.current
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# frozen_string_literal: true

module IdentityPlatform
#= IdentityPlatform::Error
class Error < StandardError; end
end
116 changes: 116 additions & 0 deletions lib/generators/auth/templates/models/identity_plataform/token.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# frozen_string_literal: true

module IdentityPlatform
#= IdentityPlatform::Token
#
# The tokens we obtain when authenticating users through Google Cloud Identity
# Platform
class Token
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Validations::Callbacks

ISSUER_PREFIX = 'https://securetoken.google.com/'

PAYLOAD_KEY_MAP = {
'iss' => 'issuer',
'sub' => 'subject',
'aud' => 'audience',
'iat' => 'issued_at',
'exp' => 'expires_at',
'auth_time' => 'authenticated_at'
}.freeze

PAYLOAD_MAPPER = proc { |key| PAYLOAD_KEY_MAP.fetch key, key }

# Transient attributes:
attr_accessor :token, :payload, :header

attribute :issuer, type: :string
attribute :subject, type: :string
attribute :audience, type: :string
attribute :issued_at, type: :datetime
attribute :expires_at, type: :datetime
attribute :authenticated_at, type: :datetime
attribute :created_at, type: :datetime

before_validation :extract_token_payload

def self.load(given_token)
new(token: given_token)
end

def self.decode_token_with_cert(token, key, cert)
public_key = cert.public_key

JWT.decode(
token,
public_key,
!public_key.nil?,
decoding_options.merge(kid: key)
)
end

def self.expected_audience
ENV.fetch 'GOOGLE_CLOUD_PROJECT', 'fir-rails-f5432'
end

def self.expected_issuer
"#{ISSUER_PREFIX}#{expected_audience}"
end

def self.decoding_options
{
algorithm: 'RS256',
iss: expected_issuer,
aud: expected_audience,
verify_aud: true,
verify_iss: true
}
end

delegate :certs, to: CertStore
delegate :decode_token_with_cert, to: :class

private

def extract_token_payload
decode_token_with_certs
return errors.add(:token, 'invalid token') if payload.blank?

assign_attributes string_attributes_from_payload
assign_attributes timestamp_attributes_from_payload
end

def string_attributes_from_payload
payload.slice(*%w[iss sub aud]).transform_keys(&PAYLOAD_MAPPER)
end

def timestamp_attributes_from_payload
payload
.slice(*%w[iat exp auth_time])
.transform_keys(&PAYLOAD_MAPPER)
.transform_values { |value| Time.at(value) }
end

def decode_token_with_certs
certs.detect do |key, cert|
assign_payload_and_header_with_key_and_cert(key, cert)
break if payload.present? || errors.any?
end
end

def assign_payload_and_header_with_key_and_cert(key, cert)
return if payload.present?

@payload, @header = decode_token_with_cert(token, key, cert)
@payload = @payload&.with_indifferent_access
rescue JWT::ExpiredSignature
errors.add :token, 'signature expired'
rescue JWT::InvalidIssuerError
errors.add :token, 'invalid issuer'
rescue JWT::DecodeError
nil
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

module IdentityPlatform
#= IdentityPlatform::WardenStrategy
#
# A warden strategy to authenticate users with a token from Identity Platform
class WardenStrategy < Warden::Strategies::Base
def valid?
!token_string.nil?
end

def authenticate!
fail! 'invalid_token' and return unless token&.valid?

success! User.from_identity_token(token)
end

def store?
false
end

private

def token
@token ||= IdentityPlatform::Token.load(token_string) if valid?
end

def token_string
token_string_from_header || token_string_from_request_params
end

def token_string_from_header
Rack::Auth::AbstractRequest::AUTHORIZATION_KEYS.each do |key|
if env.key?(key) && (token_string = env[key][/^Bearer (.*)/, 1])
return token_string
end
end
nil
end

def token_string_from_request_params
params['access_token']
end
end
end

0 comments on commit bfbd7f8

Please sign in to comment.