Skip to content

Commit bfbd7f8

Browse files
author
Juan Pablo Gil
committedOct 26, 2022
Add idenity plataform model
1 parent 0e47fe7 commit bfbd7f8

File tree

4 files changed

+223
-0
lines changed

4 files changed

+223
-0
lines changed
 
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# frozen_string_literal: true
2+
3+
require 'net/http'
4+
5+
module IdentityPlatform
6+
#= DecodeIdentityToken::CertStore
7+
#
8+
# This class is used by the DecodeIdentityToken service to retrieve and store
9+
# the certificates used to properly decode tokens issued by Google Cloud
10+
# Identity Platform
11+
class CertStore
12+
extend MonitorMixin
13+
14+
CERTS_PATH = '/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'
15+
CERTS_EXPIRY = 3600
16+
17+
mattr_reader :certs_last_refresh
18+
19+
def self.client
20+
@client ||= Faraday.new('https://www.googleapis.com') do |f|
21+
f.response :json # decode response bodies as JSON
22+
f.adapter :net_http
23+
end
24+
end
25+
26+
def self.certs_cache_expired?
27+
return true unless certs_last_refresh
28+
29+
Time.current > certs_last_refresh + CERTS_EXPIRY
30+
end
31+
32+
def self.certs
33+
refresh_certs if certs_cache_expired?
34+
@@certs
35+
end
36+
37+
def self.fetch_certs
38+
client.get(CERTS_PATH).tap do |response|
39+
raise Error, 'Failed to fetch certs' unless response.success?
40+
end
41+
end
42+
43+
def self.refresh_certs
44+
synchronize do
45+
return unless (res = fetch_certs)
46+
47+
new_certs = res.body.transform_values do |cert_string|
48+
OpenSSL::X509::Certificate.new(cert_string)
49+
end
50+
51+
(@@certs ||= {}).merge! new_certs
52+
@@certs_last_refresh = Time.current
53+
end
54+
end
55+
end
56+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# frozen_string_literal: true
2+
3+
module IdentityPlatform
4+
#= IdentityPlatform::Error
5+
class Error < StandardError; end
6+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
# frozen_string_literal: true
2+
3+
module IdentityPlatform
4+
#= IdentityPlatform::Token
5+
#
6+
# The tokens we obtain when authenticating users through Google Cloud Identity
7+
# Platform
8+
class Token
9+
include ActiveModel::Model
10+
include ActiveModel::Attributes
11+
include ActiveModel::Validations::Callbacks
12+
13+
ISSUER_PREFIX = 'https://securetoken.google.com/'
14+
15+
PAYLOAD_KEY_MAP = {
16+
'iss' => 'issuer',
17+
'sub' => 'subject',
18+
'aud' => 'audience',
19+
'iat' => 'issued_at',
20+
'exp' => 'expires_at',
21+
'auth_time' => 'authenticated_at'
22+
}.freeze
23+
24+
PAYLOAD_MAPPER = proc { |key| PAYLOAD_KEY_MAP.fetch key, key }
25+
26+
# Transient attributes:
27+
attr_accessor :token, :payload, :header
28+
29+
attribute :issuer, type: :string
30+
attribute :subject, type: :string
31+
attribute :audience, type: :string
32+
attribute :issued_at, type: :datetime
33+
attribute :expires_at, type: :datetime
34+
attribute :authenticated_at, type: :datetime
35+
attribute :created_at, type: :datetime
36+
37+
before_validation :extract_token_payload
38+
39+
def self.load(given_token)
40+
new(token: given_token)
41+
end
42+
43+
def self.decode_token_with_cert(token, key, cert)
44+
public_key = cert.public_key
45+
46+
JWT.decode(
47+
token,
48+
public_key,
49+
!public_key.nil?,
50+
decoding_options.merge(kid: key)
51+
)
52+
end
53+
54+
def self.expected_audience
55+
ENV.fetch 'GOOGLE_CLOUD_PROJECT', 'fir-rails-f5432'
56+
end
57+
58+
def self.expected_issuer
59+
"#{ISSUER_PREFIX}#{expected_audience}"
60+
end
61+
62+
def self.decoding_options
63+
{
64+
algorithm: 'RS256',
65+
iss: expected_issuer,
66+
aud: expected_audience,
67+
verify_aud: true,
68+
verify_iss: true
69+
}
70+
end
71+
72+
delegate :certs, to: CertStore
73+
delegate :decode_token_with_cert, to: :class
74+
75+
private
76+
77+
def extract_token_payload
78+
decode_token_with_certs
79+
return errors.add(:token, 'invalid token') if payload.blank?
80+
81+
assign_attributes string_attributes_from_payload
82+
assign_attributes timestamp_attributes_from_payload
83+
end
84+
85+
def string_attributes_from_payload
86+
payload.slice(*%w[iss sub aud]).transform_keys(&PAYLOAD_MAPPER)
87+
end
88+
89+
def timestamp_attributes_from_payload
90+
payload
91+
.slice(*%w[iat exp auth_time])
92+
.transform_keys(&PAYLOAD_MAPPER)
93+
.transform_values { |value| Time.at(value) }
94+
end
95+
96+
def decode_token_with_certs
97+
certs.detect do |key, cert|
98+
assign_payload_and_header_with_key_and_cert(key, cert)
99+
break if payload.present? || errors.any?
100+
end
101+
end
102+
103+
def assign_payload_and_header_with_key_and_cert(key, cert)
104+
return if payload.present?
105+
106+
@payload, @header = decode_token_with_cert(token, key, cert)
107+
@payload = @payload&.with_indifferent_access
108+
rescue JWT::ExpiredSignature
109+
errors.add :token, 'signature expired'
110+
rescue JWT::InvalidIssuerError
111+
errors.add :token, 'invalid issuer'
112+
rescue JWT::DecodeError
113+
nil
114+
end
115+
end
116+
end
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# frozen_string_literal: true
2+
3+
module IdentityPlatform
4+
#= IdentityPlatform::WardenStrategy
5+
#
6+
# A warden strategy to authenticate users with a token from Identity Platform
7+
class WardenStrategy < Warden::Strategies::Base
8+
def valid?
9+
!token_string.nil?
10+
end
11+
12+
def authenticate!
13+
fail! 'invalid_token' and return unless token&.valid?
14+
15+
success! User.from_identity_token(token)
16+
end
17+
18+
def store?
19+
false
20+
end
21+
22+
private
23+
24+
def token
25+
@token ||= IdentityPlatform::Token.load(token_string) if valid?
26+
end
27+
28+
def token_string
29+
token_string_from_header || token_string_from_request_params
30+
end
31+
32+
def token_string_from_header
33+
Rack::Auth::AbstractRequest::AUTHORIZATION_KEYS.each do |key|
34+
if env.key?(key) && (token_string = env[key][/^Bearer (.*)/, 1])
35+
return token_string
36+
end
37+
end
38+
nil
39+
end
40+
41+
def token_string_from_request_params
42+
params['access_token']
43+
end
44+
end
45+
end

0 commit comments

Comments
 (0)
Failed to load comments.