Skip to content

Commit

Permalink
Flesh out Identity tests
Browse files Browse the repository at this point in the history
  • Loading branch information
georgeclaghorn committed Sep 2, 2018
1 parent 50124f2 commit df9f054
Show file tree
Hide file tree
Showing 8 changed files with 150 additions and 14 deletions.
3 changes: 2 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -150,8 +150,9 @@ DEPENDENCIES
bundler (~> 1.15)
byebug
google_sign_in!
jwt
rake
webmock

BUNDLED WITH
1.16.3
1.16.4
29 changes: 29 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,32 @@ Rake::TestTask.new do |test|
end

task default: :test

desc "Generates an X509 certificate for decoding test ID tokens"
task "test:certificate:generate" do
require "openssl"
require "active_support"
require "active_support/core_ext/integer/time"

key = OpenSSL::PKey::RSA.new(File.read(File.expand_path("test/key.pem", __dir__)))

certificate = OpenSSL::X509::Certificate.new
certificate.subject = certificate.issuer = OpenSSL::X509::Name.parse("/CN=google-sign-in-for-rails.example.com")
certificate.not_before = Time.now
certificate.not_after = 5.years.from_now
certificate.public_key = key.public_key
certificate.serial = 0
certificate.version = 1

extension_factory = OpenSSL::X509::ExtensionFactory.new
extension_factory.subject_certificate = certificate
extension_factory.issuer_certificate = certificate
certificate.extensions = [
extension_factory.create_extension("basicConstraints", "CA:FALSE", true),
extension_factory.create_extension("keyUsage", "digitalSignature", true),
extension_factory.create_extension("extendedKeyUsage", "clientAuth", true)
]

certificate.sign(key, OpenSSL::Digest::SHA1.new)
File.write(File.expand_path("test/certificate.pem", __dir__), certificate.to_pem)
end
1 change: 1 addition & 0 deletions google_sign_in.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ Gem::Specification.new do |s|
s.add_dependency 'oauth2', '>= 1.4.0'

s.add_development_dependency 'bundler', '~> 1.15'
s.add_development_dependency 'jwt'
s.add_development_dependency 'webmock'

s.files = `git ls-files`.split("\n")
Expand Down
16 changes: 5 additions & 11 deletions lib/google_sign_in/identity.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@

module GoogleSignIn
class Identity
class ValidationError < StandardError; end

class_attribute :validator, default: GoogleIDToken::Validator.new

def initialize(token)
ensure_client_id_present
set_extracted_payload(token)
ensure_proper_audience
end

def user_id
Expand Down Expand Up @@ -36,7 +37,7 @@ def locale
end

private
delegate :client_id, :logger, to: GoogleSignIn
delegate :client_id, to: GoogleSignIn

def ensure_client_id_present
if client_id.blank?
Expand All @@ -46,15 +47,8 @@ def ensure_client_id_present

def set_extracted_payload(token)
@payload = validator.check(token, client_id)
rescue GoogleIDToken::ValidationError => e
logger.error "Google token failed to validate (#{e.message})"
@payload = {}
end

def ensure_proper_audience
unless @payload["aud"].include?(client_id)
raise "Failed to locate the client_id #{client_id} in the authorized audience (#{@payload["aud"]})"
end
rescue GoogleIDToken::ValidationError => error
raise ValidationError, error.message
end
end
end
19 changes: 19 additions & 0 deletions test/certificate.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-----BEGIN CERTIFICATE-----
MIIDETCCAfmgAwIBAQIBADANBgkqhkiG9w0BAQUFADAvMS0wKwYDVQQDDCRnb29n
bGUtc2lnbi1pbi1mb3ItcmFpbHMuZXhhbXBsZS5jb20wHhcNMTgwOTAyMjI0MTQw
WhcNMjMwOTAyMjI0MTQwWjAvMS0wKwYDVQQDDCRnb29nbGUtc2lnbi1pbi1mb3It
cmFpbHMuZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
AQCtnO1OcLbdxj4f6I/aUMJkCJfrDNvp0v2ljUJoaq6hqWPmoZgcl92njBvt91np
JPfGaCy0ZYLfizUNBRKkfo6u3MXvubktYqk3SVCejy0TUE11PwRx1u/x0c2rCTa6
Y7ppAoO9Ur3yoccDmkceP8MpofHWetrdaxyhktlqy6gpM7V+kjj+anySQk4XqDJl
F4FXHt82HMNK3xbjXJyEoyMudGUISBDn/rG8b3LxEKawUiLVCI54g3+L/Oi4nZCE
XNCd/mvWpVFoPpFQGMoKW3S9KxFowvfkDSxWYwgYnWnsO0ueXS+WYML88KO1Qf7V
mEZ91u7w1/EiMVxiuczxycSfAgMBAAGjODA2MAwGA1UdEwEB/wQCMAAwDgYDVR0P
AQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMCMA0GCSqGSIb3DQEBBQUA
A4IBAQCVXynTCZhpbINC4j1prGaPfY6mRkCZzcRpQFim4C6hJtYSRn57qjpmYWG3
eVc3ElfNUAWgC3trACjN3hDqKv0/hH9TGTY9iFhc747L/VSaKWzH/uWewj1qTwsX
dUEFxZILAWvAMBNUT060t8bt+pSFc2h4fHsftOqFLfkFUcCr22QsWyueXzWZyDeZ
XWFGtD+WOR5SC4mIY359e75/vZsJymzZIfM+pfcaHnXtXez9SeLM81rvnRdR1b+H
/S0LT0dPRkXSvC2HRPwzHxVctNrDoaxON+OIMgd4lHAFs6doVoYmnprzO69+IBUK
s0LBENxbn2rd7IEl6EaC91cXCl3y
-----END CERTIFICATE-----
27 changes: 27 additions & 0 deletions test/key.pem
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEArZztTnC23cY+H+iP2lDCZAiX6wzb6dL9pY1CaGquoalj5qGY
HJfdp4wb7fdZ6ST3xmgstGWC34s1DQUSpH6OrtzF77m5LWKpN0lQno8tE1BNdT8E
cdbv8dHNqwk2umO6aQKDvVK98qHHA5pHHj/DKaHx1nra3WscoZLZasuoKTO1fpI4
/mp8kkJOF6gyZReBVx7fNhzDSt8W41ychKMjLnRlCEgQ5/6xvG9y8RCmsFIi1QiO
eIN/i/zouJ2QhFzQnf5r1qVRaD6RUBjKClt0vSsRaML35A0sVmMIGJ1p7DtLnl0v
lmDC/PCjtUH+1ZhGfdbu8NfxIjFcYrnM8cnEnwIDAQABAoIBAQCFsPlg1RVMlJNU
eP8Fq/j1lVR/UYird6mRacUAqV5O6SUf/cIoCp5Knm8Hgdl/2tLeu2vpgt4UDJvO
qeBgQYDYkPPvlcJOe9I428E0SKb6X3U2W0+t5kkhm2FYWyEEyTVMFf6itOvGwuOB
F7W6SnmcPrP/aN3PceM7XN0GC0w0ZPOgpsKO4zJiXyA+NQhOdV/xsUtAgM/zXPVb
UzJyixPDqJfBHBNCkP6FcJxkc+cLlxnZaG7ug2qfWzvj0l2EepcWQ3R/E56mixcN
nIoha07jBMJhX0KbRgcZsMmvR0/Z2sLkUgphTI/EdIsPQ/zvfqSQTQAKjDB4Orsi
quaIerupAoGBANVfWh+d4GnhcxtHYb1CCS6CU1dJDh6vGifeLvAAxuiAOBAepQUs
9+ewVaS7ARnZ7jwtqOOKRDvAZpEukh7lqFj5haqzuRrImTO6v/Lk8XAA2RC7hY6v
b65WszIYgGemSubJot7Tevk/lu8lhdkZKp+OZdPU9NS2cEPCcQ3zDchbAoGBANBM
IVJuQHutAzCTKfjhbZCn78e7AA/eQGMycZUM6i60muYC7L57P0W++ycK6A1cJ49X
XJ9VvKtjtfqITbIUGuvaFii7XAaGMV85LffJ5t1DjucLScIBDxYp5EJz5/Qj2RDw
hWJZ+IF0Z4+k1jmIM+eyoNzmjWyYvgpmhFfhx0gNAoGAHcRyt0x4PW1FeL3JpfSr
gUCPTfMUNDWriXpWxAbnuRICQEV2MjWm1DzmhdfM/IVJ1j1sfWoRwOBDrud3XTYe
+WK+QiVWoqTvsqbQFpvYDw8fOVVf+ZsCEqln0IpYh51Mf8wLm9iXJGS5st1iQfpf
1uivzhC8o1xcZyeeTBrnhlcCgYBHp7ja04SpRwZO4oFQ1bPMTIlHC0RlMQ6zUToT
jULOWGDk+WKZ0GoewylA8BaN6gLN477ALU1fJEkI63TW6uWr9vUig/mPYQCBAnmW
wUUDHud9AbwY2iZneHfGiHrl2KMmmac5AzxixDmQB6OOXRWGAkQmWcWS8ySFDWk/
ljLozQKBgE1gv+PXyxFOsuKNAylj2h8SlfzyOReS7XgPOX3x27yhp18Y8Wq9jeBr
INjRFL7CpydZZOB5B8i4CLqa3B0dA/lIYbc5FaLefBY+mt82YtpMsYP1I4bAe/ub
K7fp2z10/eKfa2e5mcTS2WBwJmVz0cR8Plqd2zPb3+yE+JMiU/k7
-----END RSA PRIVATE KEY-----
58 changes: 56 additions & 2 deletions test/models/identity_test.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
require 'test_helper'
require 'jwt'

class GoogleSignIn::IdentityTest < ActiveSupport::TestCase
test "client_id must be set" do
Expand All @@ -7,8 +8,57 @@ class GoogleSignIn::IdentityTest < ActiveSupport::TestCase
end
end

test "client_id must be part of the audience" do
# FIXME: Need to build a mock/simulate the google token to test this
test "client_id must be in the token audience" do
assert_raises GoogleSignIn::Identity::ValidationError do
GoogleSignIn::Identity.new(token_with(aud: "invalid"))
end
end

test "token must have a valid issuer" do
assert_raises GoogleSignIn::Identity::ValidationError do
GoogleSignIn::Identity.new(token_with(iss: "invalid"))
end
end

test "token must be signed with the correct key" do
assert_raises GoogleSignIn::Identity::ValidationError do
GoogleSignIn::Identity.new(token_with(key: OpenSSL::PKey::RSA.new(2048)))
end
end

test "token must not be expired" do
freeze_time do
assert_raises GoogleSignIn::Identity::ValidationError do
GoogleSignIn::Identity.new(token_with(iat: 10.minutes.ago.to_i, exp: 5.minutes.ago.to_i))
end
end
end

test "extracting user ID" do
assert_equal "573222559223877", GoogleSignIn::Identity.new(token_with(sub: "573222559223877")).user_id
end

test "extracting name" do
assert_equal "George Claghorn", GoogleSignIn::Identity.new(token_with(name: "George Claghorn")).name
end

test "extracting email address" do
assert_equal "george@basecamp.com", GoogleSignIn::Identity.new(token_with(email: "george@basecamp.com")).email_address
end

test "extracting email verification status" do
assert GoogleSignIn::Identity.new(token_with(email: "george@basecamp.com", email_verified: true)).email_verified?
assert_not GoogleSignIn::Identity.new(token_with(email: "george@basecamp.com", email_verified: false)).email_verified?
assert_not GoogleSignIn::Identity.new(token_with(email: "george@basecamp.com")).email_verified?
end

test "extracting avatar URL" do
assert_equal "https://example.com/avatar.png",
GoogleSignIn::Identity.new(token_with(picture: "https://example.com/avatar.png")).avatar_url
end

test "extracting locale" do
assert_equal "en-US", GoogleSignIn::Identity.new(token_with(locale: "en-US")).locale
end

private
Expand All @@ -19,4 +69,8 @@ def switch_client_id_to(value)
ensure
GoogleSignIn.client_id = previous_value
end

def token_with(aud: FAKE_GOOGLE_CLIENT_ID, iss: "https://accounts.google.com", key: GOOGLE_PRIVATE_KEY, **payload)
JWT.encode(payload.merge(aud: aud, iss: iss), key, "RS256")
end
end
11 changes: 11 additions & 0 deletions test/test_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@
require 'webmock/minitest'
require 'byebug'

require 'openssl'
GOOGLE_PRIVATE_KEY = OpenSSL::PKey::RSA.new(File.read(File.expand_path('key.pem', __dir__)))
GOOGLE_X509_CERTIFICATE = OpenSSL::X509::Certificate.new(File.read(File.expand_path('certificate.pem', __dir__)))

if GOOGLE_X509_CERTIFICATE.not_after <= Time.now
raise "Test certificate is expired. Generate a new one and run the tests again: `bundle exec rake test:certificate:generate`."
end

require 'google-id-token'
GoogleSignIn::Identity.validator = GoogleIDToken::Validator.new(x509_cert: GOOGLE_X509_CERTIFICATE)

class ActionView::TestCase
private
def assert_dom_equal(expected, actual, message = nil)
Expand Down

0 comments on commit df9f054

Please sign in to comment.