class SessionsController < ApplicationController
def create
id_token = params.required(:id_token)
user = User.from_firebase(id_token)
end
end
class User < ApplicationRecord
CIRTIFICATE_URL = 'https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com'
EXP_LEEWAY = 30.seconds
def self.from_firebase(id_token)
firebase_project_id = Rails.application.credentials.dig(:firebase, :project_id)
valid_iss = "https://securetoken.google.com/#{firebase_project_id}"
a, decoded_token_header = JWT.decode(id_token, nil, false)
certificate = Rails.cache.fetch('firebase_securetoken_cirtificate', expires: 1.hour) do
uri = URI.parse(CIRTIFICATE_URL)
JSON.parse(Net::HTTP.get_response(uri).body).fetch(decoded_token_header["kid"])
end
public_key = OpenSSL::X509::Certificate.new(certificate).public_key
decoded_token_payload, _ = JWT.decode(
id_token,
public_key,
true,
exp_leeway: EXP_LEEWAY,
verify_iat: true,
aud: firebase_project_id,
verify_aud: true,
iss: valid_iss,
verify_iss: true,
verify_sub: true,
algorithm: decoded_token_header["alg"]
)
Rails.logger.debug("decoded_token_payload: #{decoded_token_payload.inspect}")
where(uid: decoded_token_payload.fetch("sub")).first_or_create
end
end
require 'rails_helper'
RSpec.describe "/session", type: :request do
describe 'create session' do
context "with valid id_token" do
it "returns Created" do
stub_id_token do |id_token|
post "/session", params: { id_token: id_token }
expect(response).to have_http_status(:created)
end
end
end
context "with invalid id_token" do
it "returns Unauthorized " do
stub_id_token("exp" => 0) do |id_token|
post "/session", params: { id_token: id_token }
expect(response).to have_http_status(:unauthorized)
end
end
end
end
module FirebaseIdTokenGenerator
def stub_id_token(override = {}, &block)
unless defined?(@pkey)
@pkey, @cert = generate_key_pair
@kid = 'thekeyid'
certificates = {
'dummy' => generate_key_pair[1].to_pem,
@kid => @cert.to_pem
}
WebMock.stub_request(:get, User::CIRTIFICATE_URL).to_return(status: 200, body: certificates.to_json)
end
block.call(get_id_token(generate_payload(override)))
end
def get_id_token(payload)
JWT.encode(payload, @pkey, 'RS256', { kid: @kid, typ: 'JWT' })
end
def generate_payload(override = {})
firebase_project_id = Rails.application.credentials.dig(:firebase, :project_id)
{
"name"=>"Takeuchi Yuichi",
"picture"=>"https://test.host/picture.jpeg",
"iss" => "https://securetoken.google.com/#{firebase_project_id}",
"aud" => firebase_project_id,
"auth_time"=> Time.current.to_i,
"user_id"=>"theuserid",
"sub"=>"theuserid",
"iat"=> 1.hour.ago.to_i,
"exp"=> 1.hour.from_now.to_i,
"email"=>"yuichi.takeuchi@takeyuweb.co.jp",
"email_verified"=>true,
"firebase"=>{
"identities"=>{
"google.com"=>["000000000000000000000"],
"email"=>["yuichi.takeuchi@takeyuweb.co.jp"]
},
"sign_in_provider"=>"google.com"
}
}.deep_merge(override)
end
def generate_key_pair
ca_passphrase = SecureRandom.alphanumeric
digest = OpenSSL::Digest::SHA1.new
issu = OpenSSL::X509::Name.new
issu.add_entry('C' , 'JP')
issu.add_entry('ST', 'Saitama')
issu.add_entry('DC', 'Omiya-ku')
issu.add_entry('O' , 'TakeyuWeb, Inc.')
issu.add_entry('CN', 'MexiCasita Test CA')
issu_rsa = OpenSSL::PKey::RSA.generate(2048)
issu_cer = OpenSSL::X509::Certificate.new
issu_cer.not_before = Time.current
issu_cer.not_after = 10.years.from_now
issu_cer.public_key = issu_rsa.public_key
issu_cer.serial = 1
issu_cer.issuer = issu
issu_cer.subject = issu
ex = OpenSSL::X509::Extension.new('basicConstraints', OpenSSL::ASN1.Sequence([OpenSSL::ASN1::Boolean(true)]))
issu_cer.add_extension(ex)
issu_cer.sign(issu_rsa, digest)
return [issu_rsa, issu_cer]
end
end
RSpec.configure do |config|
config.include FirebaseIdTokenGenerator
config.after(:all, type: :request) do
remove_instance_variable(:@pkey) if defined?(@pkey)
end
end