-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Juan Pablo Gil
committed
Oct 26, 2022
1 parent
0e47fe7
commit bfbd7f8
Showing
4 changed files
with
223 additions
and
0 deletions.
There are no files selected for viewing
56 changes: 56 additions & 0 deletions
56
lib/generators/auth/templates/models/identity_plataform/cert_store.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
6 changes: 6 additions & 0 deletions
6
lib/generators/auth/templates/models/identity_plataform/error.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
116
lib/generators/auth/templates/models/identity_plataform/token.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
45 changes: 45 additions & 0 deletions
45
lib/generators/auth/templates/models/identity_plataform/warden_strategy.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |