simple_google_auth 0.0.6 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: b07e9d444f3a69575224aae7742706c4e857fd6b
4
- data.tar.gz: 03baf12262054337668111cd951ea4b8e259faa1
3
+ metadata.gz: 2c7a9d7423d02f4853b934ee51ee465b3e5211dc
4
+ data.tar.gz: 4d0e7eab5f74f770e44fd2b33036010d9eef7c89
5
5
  SHA512:
6
- metadata.gz: 2fa8cf9287ed2bff1db2be4b8f347b65bc5ca6031f0b754758d886a8a9439c9ed0e6aa776d3c19f400982134a1b11d761ba97a2ec574386f0331b62125e6617f
7
- data.tar.gz: e8f0d7c17256ec5ac1b7dca2a75adb48e9a4a767702044cc2b283ea11a58b9295abbb7d222b2510965b7adfd7af3a0eec2cdbdf4c5b4239d4f926e3c8e68075c
6
+ metadata.gz: 00ff54f0f86e9c386e2d8324549b138b8e94abeffc94c9e4959c339c244ca21a0b6005ff8b95369ed34db98ce257d353f191b981c3200cdaef66565e569d4382
7
+ data.tar.gz: 3eaf3489ed9eaebc0a42e2600314a7d870ed38c354e25f325c2cc97ea8141f00a664d237c6153df927d56da67dabf6dc312c0d7eea4386aa0788d094f750f413
data/README.md CHANGED
@@ -13,42 +13,41 @@ such as OmniAuth's Google strategy.
13
13
 
14
14
  ## Installation
15
15
 
16
- Follow these five steps to integrate with your site.
16
+ Follow these four steps to integrate with your site.
17
17
 
18
- Step 1: Make yourself a project at https://cloud.google.com/console, if you haven't already.
18
+ Step 1: Make yourself a project at https://cloud.google.com/console, if you haven't already. In that project, go to the "APIs & auth" tab, then the "Credentials" tab. Create a new client ID of application type "Web application". Set the Authorized Redirect URI to
19
+ `https://yoursite.com/google-callback`. You might want to put in `http://localhost:3000/google-callback` so you can test locally too.
19
20
 
20
- Step 2: In that project, go to the "APIs & auth" tab, then the "Credentials" tab. Create a new client ID of application type "Web application". Set the Authorized Redirect URI to
21
- `http://yoursite.com/google-callback`. You might want to put in `http://localhost:3000/google-callback` so you can test locally too.
22
-
23
- Step 3: Add simple_google_auth to your Gemfile
21
+ Step 2: Add simple_google_auth to your `Gemfile` and run `bundle`
24
22
 
25
23
  gem 'simple_google_auth'
26
24
 
27
- Step 4: In your application.rb, put down some code inside the Application class:
25
+ Step 3: Add the following code to the bottom of your `config/application.rb` and tweak it with your site's values:
28
26
 
29
27
  SimpleGoogleAuth.configure do |config|
30
28
  config.client_id = "the client ID as supplied by Google in step 2"
31
29
  config.client_secret = "the client secret as supplied by Google in step 2"
32
30
  config.redirect_uri = "http://localhost:3000/google-callback"
33
31
  config.authenticate = lambda do |data|
34
- data["email"] == "your.email@example.com"
32
+ data.email == "your.email@example.com" || data.email.ends_with?("@example.net")
35
33
  end
36
34
  end
37
35
 
38
- Step 5: In your application_controller.rb, add a before filter:
36
+ Step 4: In your `application_controller.rb`, add a before action:
39
37
 
40
- before_filter :redirect_if_not_google_authenticated
38
+ before_action :redirect_if_not_google_authenticated
41
39
 
42
- Done! Any request to your site will now redirect off to Google for authentication.
43
- A route that captures requests coming in to /google-callback is automatically created and handled for you.
40
+ Done! Any request to your site will now redirect to Google for authentication.
41
+ A route that captures requests to `/google-callback` on your site is automatically created and handled for you.
44
42
 
45
- If you log in with `your.email@example.com`, it'll let you in to the site and take you to the page you were initially trying to go to.
43
+ If you log in with `your.email@example.com`, or any address in the `example.net` domain, it'll let you in to the site and take you to the page you were initially trying to go to.
46
44
  Otherwise it'll redirect to `/` (by default) with `params[:message]` set to the authentication error.
47
45
 
48
- ## Setting up multiple environments
46
+ ## Setting up a production environment
49
47
 
50
48
  You might want to put a different configure block in your development.rb and production.rb, each specifying
51
- a different redirect URI. Just pop them on the end of the file.
49
+ a different redirect URI. Just pop them on the end of the file. You can also have different client IDs and
50
+ secrets, or authentication criteria.
52
51
 
53
52
  # development.rb
54
53
  SimpleGoogleAuth.configure do |config|
@@ -62,23 +61,26 @@ a different redirect URI. Just pop them on the end of the file.
62
61
 
63
62
  ## How do I tell who is logged in?
64
63
 
65
- Call `#google_auth_data` from your controller or view and you'll get the identification hash that Google sends back.
64
+ Call `#google_auth_data` from your controller or view and you'll get the authentication data that Google sends back.
65
+
66
+ Welcome, <%= google_auth_data.email %>!
66
67
 
67
- Welcome, <%= google_auth_data["email"] %>!
68
+ SimpleGoogleAuth exposes the following data via methods: access_token, expires_in, token_type, refresh_token, id_token, iss, at_hash, email_verified, sub, azp, email, aud, iat, exp, hd. You can also use `google_auth_data` as a hash and get any additional fields not listed here.
68
69
 
69
- Take a look at https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo to find out more about the fields in the hash.
70
+ Take a look at [the Google OAuth documentation](https://developers.google.com/accounts/docs/OAuth2Login#obtainuserinfo)
71
+ to see more information about what these fields mean.
70
72
 
71
73
  ## Refreshing tokens and offline mode
72
74
 
73
- By default simple_google_auth doesn't check the expiry time
74
- on the credentials after they've been loaded from google the first time.
75
+ By default SimpleGoogleAuth doesn't check the expiry time
76
+ on the credentials after they've been loaded from Google the first time.
75
77
  This is less hassle if all you want is simple authentication for your site,
76
78
  but prevents you from using the credentials for other uses (eg. GCal integration)
77
- because the oauth tokens will expire and google won't accept them anymore.
79
+ because the oauth tokens will expire and Google won't accept them anymore.
78
80
 
79
81
  If you want the tokens to be refreshed when they expire then you need to
80
82
  add an extra line to your config. Doing so will ensure that your
81
- google auth tokens never get stale and allow you to use offline mode.
83
+ Google auth tokens never get stale and allow you to use offline mode.
82
84
 
83
85
  SimpleGoogleAuth.configure do |config|
84
86
  config.refresh_stale_tokens = true
@@ -89,17 +91,17 @@ be checked to make sure it's not stale. If it is stale the tokens will be
89
91
  refreshed before being returned.
90
92
 
91
93
  If your users have already allowed your site access to a certain set of scopes
92
- google won't re-issue you a refresh_token automatically. You'll need to set an
93
- extra param in the request_parameters configuration hash to force google to
94
+ Google won't re-issue you a refresh_token automatically. You'll need to set an
95
+ extra param in the request_parameters configuration hash to force Google to
94
96
  send you the refresh token every time your users authenticate.
95
97
 
96
98
  SimpleGoogleAuth.configure do |config|
97
99
  config.refresh_stale_tokens = true
98
- config.request_parameters.merge!({ approval_prompt: "force" })
100
+ config.request_parameters.merge!(approval_prompt: "force")
99
101
  end
100
102
 
101
- For more details on offline mode and approval_prompt refer to the google OAuth docs, as of
102
- writing you can find them #[here](https://developers.google.com/accounts/docs/OAuth2WebServer).
103
+ For more details on offline mode and approval_prompt refer to the
104
+ [Google OAuth documentation](https://developers.google.com/accounts/docs/OAuth2WebServer).
103
105
 
104
106
  ## Configuring
105
107
 
@@ -107,19 +109,25 @@ There are a few configuration options that can be set using `SimpleGoogleAuth.co
107
109
 
108
110
  Option | Default | Description
109
111
  --- | --- | ---
110
- client_id | (required) | Client ID as provided by Google.
111
- client_secret | (required) | Client secret as provided by Google.
112
+ client_id* | (required) | Client ID as provided by Google.
113
+ client_secret* | (required) | Client secret as provided by Google.
112
114
  redirect_uri | (required) | Where Google should redirect to after authentication.
113
115
  redirect_path | `nil` | A route is created at this path. If no path is specified, the path is taken from redirect_uri.
114
116
  authenticate | (required) | A lambda that's run to determine whether the user should be accepted as valid or not. Takes one argument, a hash of identification data as provided by Google. Should return true on success, or false if the login should not proceed.
115
117
  failed_login_path | `"/"` | Where to redirect to upon a failed login. `params[:message]` will be set with the error that occurred.
116
- ca_path | `"/etc/ssl/certs"` | A path or file of SSL certificates, used to check that we're really talking to the Google servers.
117
118
  google_auth_url | `"https://accounts.google.com/o/oauth2/auth"` | Google's authentication URL.
118
119
  google_token_url | `"https://accounts.google.com/o/oauth2/token"` | Google's token URL.
119
120
  state_session_key_name | `"simple-google-auth.state"` | The name of the session variable used to store a random string used to prevent CSRF attacks during authentication.
120
121
  data_session_key_name | `"simple-google-auth.data"` | The name of the session variable used to store identification data from Google.
121
- request_parameters | {scope: "openid email"} | Parameters to use when requesting a login from Google
122
+ request_parameters | `{scope: "openid email"}` | Parameters to use when requesting a login from Google
123
+
124
+ Items marked with * may be a lambda, which will be called when that config item is required.
122
125
 
123
126
  ## Licence
124
127
 
125
- MIT.
128
+ MIT. Copyright 2014-2015 Roger Nesbitt, Powershop New Zealand Limited.
129
+
130
+ ## Authors and contributors
131
+
132
+ - Roger Nesbitt
133
+ - Andy Newport
@@ -1,63 +1,37 @@
1
1
  require 'net/https'
2
+ require 'simple_google_auth/config'
2
3
 
3
4
  module SimpleGoogleAuth
4
- Config = Struct.new(
5
- :client_id,
6
- :client_secret,
7
- :redirect_uri,
8
- :redirect_path,
9
- :failed_login_path,
10
- :authenticate,
11
- :ca_path,
12
- :google_auth_url,
13
- :google_token_url,
14
- :state_session_key_name,
15
- :data_session_key_name,
16
- :request_parameters,
17
- :refresh_stale_tokens
18
- ) do
19
- def get_or_call(attribute)
20
- value = send(attribute)
21
- value.respond_to?(:call) ? value.call : value
22
- end
23
- end
24
-
25
5
  mattr_accessor :config
26
6
  self.config = Config.new
27
7
 
8
+ Error = Class.new(StandardError)
9
+ ProviderError = Class.new(Error)
10
+ NonJsonResponseError = Class.new(ProviderError)
11
+
28
12
  def self.configure
29
13
  yield config
30
14
 
31
15
  if config.refresh_stale_tokens
32
- config.request_parameters.merge!({ access_type: "offline" })
16
+ config.request_parameters.merge!(access_type: "offline")
33
17
  end
34
18
  end
35
-
36
- def self.uri(state)
37
- query = config.request_parameters.merge(
38
- response_type: "code",
39
- client_id: config.get_or_call(:client_id),
40
- redirect_uri: config.redirect_uri,
41
- state: state
42
- )
43
-
44
- "#{config.google_auth_url}?" + query.map {|k, v| "#{k}=#{CGI.escape v}"}.join("&")
45
- end
46
19
  end
47
20
 
21
+ require 'simple_google_auth/http_client'
22
+ require 'simple_google_auth/auth_data_presenter'
23
+ require 'simple_google_auth/oauth'
24
+ require 'simple_google_auth/authorization_uri_builder'
25
+ require 'simple_google_auth/engine'
26
+ require 'simple_google_auth/controller'
27
+ require 'simple_google_auth/receiver'
28
+
48
29
  SimpleGoogleAuth.configure do |config|
49
- config.ca_path = %w(/etc/ssl/certs).detect {|dir| Dir.exists?(dir)}
50
30
  config.google_auth_url = "https://accounts.google.com/o/oauth2/auth"
51
31
  config.google_token_url = "https://accounts.google.com/o/oauth2/token"
52
32
  config.state_session_key_name = "simple-google-auth.state"
53
33
  config.data_session_key_name = "simple-google-auth.data"
54
34
  config.failed_login_path = "/"
55
35
  config.request_parameters = {scope: "openid email"}
56
- config.authenticate = lambda { raise "You must define an authenticate lambda that sets the session" }
36
+ config.authenticate = lambda {|data| raise "You must define an authenticate lambda that determines whether a user should be allowed access or not"}
57
37
  end
58
-
59
- require 'simple_google_auth/http_client'
60
- require 'simple_google_auth/oauth'
61
- require 'simple_google_auth/engine'
62
- require 'simple_google_auth/controller'
63
- require 'simple_google_auth/receiver'
@@ -0,0 +1,50 @@
1
+ module SimpleGoogleAuth
2
+ class AuthDataPresenter
3
+ InvalidAuthDataError = Class.new(Error)
4
+
5
+ FIELDS = %w(
6
+ access_token
7
+ expires_in
8
+ token_type
9
+ refresh_token
10
+ id_token
11
+ iss
12
+ at_hash
13
+ email_verified
14
+ sub
15
+ azp
16
+ email
17
+ aud
18
+ iat
19
+ exp
20
+ hd
21
+ expires_at
22
+ )
23
+
24
+ def initialize(auth_data)
25
+ raise InvalidAuthDataError if auth_data["id_token"].nil?
26
+
27
+ token_data = unpack_json_web_token(auth_data["id_token"])
28
+ @data = auth_data.merge(token_data)
29
+ end
30
+
31
+ def [](field)
32
+ @data[field.to_s]
33
+ end
34
+
35
+ FIELDS.each do |field|
36
+ define_method(field) { @data[field.to_s] }
37
+ end
38
+
39
+ private
40
+
41
+ def unpack_json_web_token(id_token)
42
+ # We don't worry about validating the signature because we got this JWT directly
43
+ # from Google over HTTPS (see
44
+ # https://developers.google.com/identity/protocols/OpenIDConnect#obtainuserinfo)
45
+ signature, id_data_64 = id_token.split(".")
46
+ id_data_64 << "=" until id_data_64.length % 4 == 0
47
+ JSON.parse(Base64.decode64(id_data_64))
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,28 @@
1
+ module SimpleGoogleAuth
2
+ class AuthorizationUriBuilder
3
+ def initialize(state)
4
+ @state = state
5
+ end
6
+
7
+ def uri
8
+ params = config.request_parameters.merge(
9
+ response_type: "code",
10
+ client_id: config.client_id,
11
+ redirect_uri: config.redirect_uri,
12
+ state: @state
13
+ )
14
+
15
+ "#{config.google_auth_url}?#{params_to_query(params)}"
16
+ end
17
+
18
+ private
19
+
20
+ def config
21
+ SimpleGoogleAuth.config
22
+ end
23
+
24
+ def params_to_query(params)
25
+ params.map {|k, v| "#{CGI.escape k.to_s}=#{CGI.escape v.to_s}"}.join("&")
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,44 @@
1
+ module SimpleGoogleAuth
2
+ config_fields = [
3
+ :client_id,
4
+ :client_secret,
5
+ :redirect_uri,
6
+ :redirect_path,
7
+ :failed_login_path,
8
+ :authenticate,
9
+ :google_auth_url,
10
+ :google_token_url,
11
+ :state_session_key_name,
12
+ :data_session_key_name,
13
+ :request_parameters,
14
+ :refresh_stale_tokens
15
+ ]
16
+
17
+ class Config < Struct.new(*config_fields)
18
+ def ca_path=(value)
19
+ Rails.logger.warn "ca_path is no longer used by SimpleGoogleAuth as OpenSSL is clever enough to find its ca_path now"
20
+ end
21
+
22
+ def client_id
23
+ get_or_call super
24
+ end
25
+
26
+ def client_secret
27
+ get_or_call super
28
+ end
29
+
30
+ def authenticate=(value)
31
+ if !value.respond_to?(:call)
32
+ raise Error, "Your SimpleGoogleAuth authenticator must be an object that responds to :call, normally a lambda. See documentation for configuration details."
33
+ end
34
+
35
+ super
36
+ end
37
+
38
+ private
39
+
40
+ def get_or_call(value)
41
+ value.respond_to?(:call) ? value.call : value
42
+ end
43
+ end
44
+ end
@@ -1,36 +1,44 @@
1
1
  module SimpleGoogleAuth
2
2
  module Controller
3
3
  protected
4
+
4
5
  def redirect_if_not_google_authenticated
5
6
  redirect_to google_authentication_uri if google_auth_data.nil?
6
7
  end
7
8
 
8
9
  def google_authentication_uri
9
10
  state = session[SimpleGoogleAuth.config.state_session_key_name] = SecureRandom.hex + request.path
10
- SimpleGoogleAuth.uri(state)
11
+ SimpleGoogleAuth::AuthorizationUriBuilder.new(state).uri
11
12
  end
12
13
 
13
14
  def google_auth_data
14
- return unless google_auth_data_from_session
15
+ return unless cached_google_auth_data
15
16
 
16
17
  if should_refresh_google_auth_data?
17
18
  refresh_google_auth_data
18
19
  end
19
- google_auth_data_from_session
20
+ cached_google_auth_data
20
21
  end
21
22
 
22
23
  private
23
24
 
24
25
  def refresh_google_auth_data
25
26
  api = SimpleGoogleAuth::OAuth.new(SimpleGoogleAuth.config)
26
-
27
- auth_data = api.refresh_auth_token!(google_auth_data_from_session["refresh_token"])
27
+ auth_data = api.refresh_auth_token!(cached_google_auth_data["refresh_token"])
28
28
 
29
29
  session[SimpleGoogleAuth.config.data_session_key_name] = auth_data
30
+ @_google_auth_data_presenter = nil
31
+ end
32
+
33
+ def cached_google_auth_data
34
+ @_google_auth_data_presenter ||= google_auth_data_from_session
30
35
  end
31
36
 
32
37
  def google_auth_data_from_session
33
- session[SimpleGoogleAuth.config.data_session_key_name]
38
+ if auth_data = session[SimpleGoogleAuth.config.data_session_key_name]
39
+ AuthDataPresenter.new(auth_data)
40
+ end
41
+ rescue AuthDataPresenter::InvalidAuthDataError
34
42
  end
35
43
 
36
44
  def should_refresh_google_auth_data?
@@ -38,7 +46,7 @@ module SimpleGoogleAuth
38
46
  end
39
47
 
40
48
  def google_auth_data_stale?
41
- expiry_time = google_auth_data_from_session["expires_at"]
49
+ expiry_time = cached_google_auth_data["expires_at"]
42
50
 
43
51
  expiry_time.nil? || Time.parse(expiry_time).past?
44
52
  end
@@ -1,30 +1,35 @@
1
1
  module SimpleGoogleAuth
2
2
  class HttpClient
3
- def initialize(url, ca_path)
3
+ def initialize(url)
4
4
  @uri = URI(url)
5
5
  @http = Net::HTTP.new(@uri.host, @uri.port)
6
- setup_https(ca_path)
6
+
7
+ if @uri.scheme == "https"
8
+ @http.use_ssl = true
9
+ @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
10
+ end
7
11
  end
8
12
 
9
13
  def request(params)
10
14
  request = Net::HTTP::Post.new(@uri.request_uri)
11
15
  request.set_form_data(params)
12
16
  response = @http.request(request)
13
- response.body
14
- end
15
17
 
16
- private
17
- def setup_https(ca_path)
18
- if @uri.scheme == "https"
19
- @http.use_ssl = true
20
- if ca_path
21
- @http.verify_mode = OpenSSL::SSL::VERIFY_PEER
22
- @http.ca_path = ca_path
23
- else
24
- @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
25
- Rails.logger.warn "SimpleGoogleAuth does not have a ca_path configured; SSL with Google is not protected"
26
- end
18
+ if response.content_type != 'application/json'
19
+ raise NonJsonResponseError, "The server responded with non-JSON content"
20
+ end
21
+
22
+ data = begin
23
+ JSON.parse(response.body)
24
+ rescue JSON::ParserError
25
+ raise NonJsonResponseError, "The server responded with JSON content that was not parseable"
27
26
  end
27
+
28
+ if response.code !~ /\A2\d\d\z/
29
+ raise ProviderError, "The server responded with error #{response.code}: #{data.inspect}"
30
+ end
31
+
32
+ data
28
33
  end
29
34
  end
30
35
  end
@@ -2,7 +2,7 @@ module SimpleGoogleAuth
2
2
  class OAuth
3
3
  def initialize(config)
4
4
  @config = config
5
- @client = HttpClient.new(@config.google_token_url, @config.ca_path)
5
+ @client = HttpClient.new(@config.google_token_url)
6
6
  end
7
7
 
8
8
  def exchange_code_for_auth_token!(code)
@@ -25,27 +25,34 @@ module SimpleGoogleAuth
25
25
  client_secret: @config.client_secret,
26
26
  grant_type: "refresh_token")
27
27
 
28
- parse_auth_response(response).merge("refresh_token" => refresh_token)
28
+ response["refresh_token"] ||= refresh_token
29
+
30
+ parse_auth_response(response)
29
31
  end
30
32
 
31
33
  private
32
- def parse_auth_response(response)
33
- auth_data = JSON.parse(response)
34
+ def parse_auth_response(auth_data)
35
+ validate_data_present!(auth_data)
34
36
 
35
37
  auth_data["expires_at"] = calculate_expiry(auth_data).to_s
36
38
 
37
- id_data = decode_id_data(auth_data.delete("id_token"))
38
- auth_data.merge!(id_data)
39
+ auth_data
39
40
  end
40
41
 
41
- def calculate_expiry(auth_data)
42
- Time.now + auth_data["expires_in"] - 5.seconds
42
+ def validate_data_present!(auth_data)
43
+ %w(id_token expires_in).each do |field|
44
+ if auth_data[field].blank?
45
+ raise Error, "Expecting field '#{field}' to be set but it is blank"
46
+ end
47
+ end
48
+
49
+ if !auth_data['expires_in'].is_a?(Numeric) || auth_data['expires_in'] <= 0
50
+ raise Error, "Field 'expires_in' must be a number greater than 0"
51
+ end
43
52
  end
44
53
 
45
- def decode_id_data(id_data)
46
- id_data_64 = id_data.split(".")[1]
47
- id_data_64 << "=" until id_data_64.length % 4 == 0
48
- JSON.parse(Base64.decode64(id_data_64))
54
+ def calculate_expiry(auth_data)
55
+ Time.now + auth_data["expires_in"] - 5.seconds
49
56
  end
50
57
  end
51
58
  end
@@ -1,7 +1,5 @@
1
1
  module SimpleGoogleAuth
2
2
  class Receiver
3
- Error = Class.new(StandardError)
4
-
5
3
  def call(env)
6
4
  request = Rack::Request.new(env)
7
5
  config = SimpleGoogleAuth.config
@@ -10,7 +8,8 @@ module SimpleGoogleAuth
10
8
  api = SimpleGoogleAuth::OAuth.new(config)
11
9
  auth_data = api.exchange_code_for_auth_token!(request.params["code"])
12
10
 
13
- raise Error, "Authentication failed" unless config.authenticate.call(auth_data)
11
+ data = AuthDataPresenter.new(auth_data)
12
+ raise Error, "Authentication failed" unless config.authenticate.call(data)
14
13
 
15
14
  request.session[config.data_session_key_name] = auth_data
16
15
 
@@ -1,3 +1,3 @@
1
1
  module SimpleGoogleAuth
2
- VERSION = "0.0.6"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,41 @@
1
+ require 'spec_helper'
2
+
3
+ describe SimpleGoogleAuth::AuthDataPresenter do
4
+ let(:id_data) do
5
+ {
6
+ "iss" => "accounts.google.com",
7
+ "sub" => "10769150350006150715113082367",
8
+ "email" => "test@test.example",
9
+ "aud" => "1234987819200.apps.googleusercontent.com",
10
+ "iat" => 1353601026,
11
+ "exp" => 1353604926
12
+ }
13
+ end
14
+
15
+ let(:id_token) { "12345." + Base64.encode64(id_data.to_json).gsub('=', '') }
16
+ let(:auth_data) do
17
+ {
18
+ "id_token" => id_token,
19
+ "expires_in" => 1200,
20
+ "access_token" => "abcdef",
21
+ "token_type" => "Bearer"
22
+ }
23
+ end
24
+
25
+ subject { SimpleGoogleAuth::AuthDataPresenter.new(auth_data) }
26
+
27
+ it "provides indifferent hash access to data in the JWT" do
28
+ expect(subject['email']).to eq 'test@test.example'
29
+ expect(subject[:email]).to eq 'test@test.example'
30
+ end
31
+
32
+ it "provides method access to data in the JWT" do
33
+ expect(subject.email).to eq 'test@test.example'
34
+ end
35
+
36
+ it "raises if id_token not provided" do
37
+ expect {
38
+ SimpleGoogleAuth::AuthDataPresenter.new({})
39
+ }.to raise_error(SimpleGoogleAuth::AuthDataPresenter::InvalidAuthDataError)
40
+ end
41
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe SimpleGoogleAuth::AuthorizationUriBuilder do
4
+ subject do
5
+ SimpleGoogleAuth::AuthorizationUriBuilder.new("somestate")
6
+ end
7
+
8
+ describe "#uri" do
9
+ it "constructs an authorization URI" do
10
+ expect(subject.uri).to eq 'https://accounts.google.com/o/oauth2/auth?scope=openid+email&response_type=code&client_id=123&redirect_uri=%2Fabc&state=somestate'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,39 @@
1
+ require 'spec_helper'
2
+
3
+ describe SimpleGoogleAuth::Config do
4
+ subject { SimpleGoogleAuth::Config.new }
5
+
6
+ describe "#client_id" do
7
+ it "gets the value if it doesn't respond to call" do
8
+ subject.client_id = '12345'
9
+ expect(subject.client_id).to eq '12345'
10
+ end
11
+
12
+ it "calls to get the value if it responds to call" do
13
+ subject.client_id = lambda { '12345' }
14
+ expect(subject.client_id).to eq '12345'
15
+ end
16
+ end
17
+
18
+ describe "#authenticate=" do
19
+ it "saves the value if it is callable" do
20
+ fn = lambda {|data| true}
21
+ subject.authenticate = fn
22
+ expect(subject.authenticate).to eql fn
23
+ end
24
+
25
+ it "raises if the value isn't callable" do
26
+ expect {
27
+ subject.authenticate = "not a lambda"
28
+ }.to raise_error(SimpleGoogleAuth::Error, /responds to :call/)
29
+ end
30
+ end
31
+
32
+ describe "#ca_path=" do
33
+ it "logs a warning" do
34
+ Rails.logger ||= double
35
+ expect(Rails.logger).to receive(:warn)
36
+ subject.ca_path = "/etc/certs"
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,48 @@
1
+ require 'spec_helper'
2
+
3
+ describe SimpleGoogleAuth::Controller do
4
+ class TestController
5
+ include SimpleGoogleAuth::Controller
6
+
7
+ attr_reader :request, :session
8
+
9
+ def redirect_to(x)
10
+ end
11
+ end
12
+
13
+ subject { TestController.new }
14
+
15
+ let(:id_data) { Base64.encode64({email:"hi@hi"}.to_json).gsub('=', '') }
16
+ let(:auth_data) { {"id_token" => "123." + id_data} }
17
+ let(:request) { double(path: "/somepath") }
18
+ let(:session) { {} }
19
+
20
+ before do
21
+ allow(subject).to receive(:request).and_return(request)
22
+ allow(subject).to receive(:session).and_return(session)
23
+ end
24
+
25
+ describe "#redirect_if_not_google_authenticated" do
26
+ it "redirects if not authenticated" do
27
+ expect(SecureRandom).to receive(:hex).and_return("abcd")
28
+ expect(subject).to receive(:redirect_to).with("https://accounts.google.com/o/oauth2/auth?scope=openid+email&response_type=code&client_id=123&redirect_uri=%2Fabc&state=abcd%2Fsomepath")
29
+ subject.send(:redirect_if_not_google_authenticated)
30
+ end
31
+
32
+ it "does nothing if authenticated" do
33
+ session[SimpleGoogleAuth.config.data_session_key_name] = auth_data
34
+ expect(subject).to_not receive(:redirect_to)
35
+ subject.send(:redirect_if_not_google_authenticated)
36
+ end
37
+ end
38
+
39
+ describe "#google_auth_data" do
40
+ it "returns data from the session" do
41
+ session[SimpleGoogleAuth.config.data_session_key_name] = auth_data
42
+ data = subject.send(:google_auth_data)
43
+ expect(data.email).to eq 'hi@hi'
44
+ end
45
+
46
+ it "refreshes the token data if it's expired and refresh_stale_tokens is true"
47
+ end
48
+ end
@@ -0,0 +1,80 @@
1
+ require 'spec_helper'
2
+
3
+ describe SimpleGoogleAuth::HttpClient do
4
+ describe "#request" do
5
+ let(:http) { instance_double(Net::HTTP) }
6
+ let(:request) { instance_double(Net::HTTP::Post) }
7
+
8
+ before do
9
+ expect(Net::HTTP).to receive(:new).with("some.host", 443).and_return(http)
10
+ expect(http).to receive(:use_ssl=).with(true)
11
+ expect(http).to receive(:verify_mode=).with(OpenSSL::SSL::VERIFY_PEER)
12
+ expect(http).to receive(:request).with(request).and_return(response)
13
+
14
+ expect(Net::HTTP::Post).to receive(:new).with("/somepath").and_return(request)
15
+ expect(request).to receive(:set_form_data).with('some' => 'data')
16
+ end
17
+
18
+ subject { SimpleGoogleAuth::HttpClient.new("https://some.host/somepath") }
19
+
20
+ context "when the call is successful" do
21
+ let(:response) do
22
+ instance_double(
23
+ Net::HTTPSuccess,
24
+ code: '200',
25
+ body: {"data" => "very"}.to_json,
26
+ content_type: 'application/json'
27
+ )
28
+ end
29
+
30
+ it "returns the server's response" do
31
+ expect(subject.request('some' => 'data')).to eq("data" => "very")
32
+ end
33
+ end
34
+
35
+ context "when non-json data is returned" do
36
+ let(:response) do
37
+ instance_double(
38
+ Net::HTTPSuccess,
39
+ code: '200',
40
+ body: "some html",
41
+ content_type: 'text/html'
42
+ )
43
+ end
44
+
45
+ it "raises an error" do
46
+ expect { subject.request('some' => 'data') }.to raise_error(SimpleGoogleAuth::NonJsonResponseError, /non-JSON/)
47
+ end
48
+ end
49
+
50
+ context "when non-json-parseable data is returned" do
51
+ let(:response) do
52
+ instance_double(
53
+ Net::HTTPSuccess,
54
+ code: '200',
55
+ body: "some html",
56
+ content_type: 'application/json'
57
+ )
58
+ end
59
+
60
+ it "raises an error" do
61
+ expect { subject.request('some' => 'data') }.to raise_error(SimpleGoogleAuth::NonJsonResponseError, /parseable/)
62
+ end
63
+ end
64
+
65
+ context "when non-successful json data is returned" do
66
+ let(:response) do
67
+ instance_double(
68
+ Net::HTTPSuccess,
69
+ code: '400',
70
+ body: {"data" => "very"}.to_json,
71
+ content_type: 'application/json'
72
+ )
73
+ end
74
+
75
+ it "raises an error" do
76
+ expect { subject.request('some' => 'data') }.to raise_error(SimpleGoogleAuth::ProviderError, /400.+very/)
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,65 @@
1
+ require 'spec_helper'
2
+
3
+ describe SimpleGoogleAuth::OAuth do
4
+ let(:config) do
5
+ instance_double(
6
+ SimpleGoogleAuth::Config,
7
+ google_token_url: "/token/url",
8
+ client_id: '12345',
9
+ client_secret: 'abcde',
10
+ redirect_uri: '/ok'
11
+ )
12
+ end
13
+
14
+ let(:client) { instance_double(SimpleGoogleAuth::HttpClient) }
15
+ let(:response) { {"id_token" => "sometoken", "expires_in" => 1200, "other" => "data"} }
16
+ let(:expires_at) { Time.now + 1200 - 5 }
17
+
18
+ before do
19
+ now = Time.now
20
+ allow(Time).to receive(:now).and_return(now)
21
+
22
+ expect(SimpleGoogleAuth::HttpClient).to receive(:new).with(config.google_token_url).and_return(client)
23
+ end
24
+
25
+ subject { SimpleGoogleAuth::OAuth.new(config) }
26
+
27
+ describe "#exchange_code_for_auth_token!" do
28
+ before do
29
+ expect(client).to receive(:request).with(
30
+ code: "magic",
31
+ grant_type: "authorization_code",
32
+ client_id: "12345",
33
+ client_secret: "abcde",
34
+ redirect_uri: "/ok"
35
+ ).and_return(response)
36
+ end
37
+
38
+ it "returns a hash of auth token data" do
39
+ expect(subject.exchange_code_for_auth_token!('magic')).to eq('expires_in' => 1200, 'other' => 'data', 'id_token' => 'sometoken', 'expires_at' => expires_at.to_s)
40
+ end
41
+ end
42
+
43
+ describe "#refresh_auth_token!" do
44
+ context "when a refresh token is provided" do
45
+ before do
46
+ expect(client).to receive(:request).with(
47
+ refresh_token: "magic",
48
+ grant_type: "refresh_token",
49
+ client_id: "12345",
50
+ client_secret: "abcde",
51
+ ).and_return(response)
52
+ end
53
+
54
+ it "returns a hash of auth token data" do
55
+ expect(subject.refresh_auth_token!('magic')).to eq('expires_in' => 1200, 'other' => 'data', 'id_token' => 'sometoken', 'expires_at' => expires_at.to_s, 'refresh_token' => 'magic')
56
+ end
57
+ end
58
+
59
+ context "when no refresh token is provided" do
60
+ it "does nothing and returns nil" do
61
+ expect(subject.refresh_auth_token!(nil)).to be nil
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,77 @@
1
+ require 'spec_helper'
2
+
3
+ describe SimpleGoogleAuth::Receiver do
4
+ let(:authenticator) { double(call: true) }
5
+ let(:authentication_result) { true }
6
+ let(:session) { double }
7
+ let(:state) { "abcd" * 8 + "/place" }
8
+ let(:code) { "sekrit" }
9
+ let(:params) { {"state" => state, "code" => code} }
10
+ let(:request) { instance_double(Rack::Request, session: session, params: params) }
11
+ let(:api) { instance_double(SimpleGoogleAuth::OAuth) }
12
+ let(:auth_data) { double }
13
+ let(:env) { double }
14
+ let(:auth_data_presenter) { instance_double(SimpleGoogleAuth::AuthDataPresenter) }
15
+
16
+ before do
17
+ expect(Rack::Request).to receive(:new).with(env).and_return(request)
18
+ expect(session).to receive(:[]).at_least(:once).with('simple-google-auth.state').and_return(state)
19
+
20
+ SimpleGoogleAuth.config.authenticate = authenticator
21
+ SimpleGoogleAuth.config.failed_login_path = '/error'
22
+ end
23
+
24
+ subject { SimpleGoogleAuth::Receiver.new.call(env) }
25
+
26
+ context "when a valid code is provided to the receiver" do
27
+ before do
28
+ expect(SimpleGoogleAuth::OAuth).to receive(:new).with(SimpleGoogleAuth.config).and_return(api)
29
+ expect(api).to receive(:exchange_code_for_auth_token!).with(code).and_return(auth_data)
30
+
31
+ expect(SimpleGoogleAuth::AuthDataPresenter).to receive(:new).with(auth_data).and_return(auth_data_presenter)
32
+ expect(authenticator).to receive(:call).with(auth_data_presenter).and_return(authentication_result)
33
+ end
34
+
35
+ context "and the authenticator accepts the login" do
36
+ before do
37
+ expect(session).to receive(:[]=).with('simple-google-auth.data', auth_data)
38
+ end
39
+
40
+ it "redirects to the URL specified in the session" do
41
+ expect(subject).to eq [302, {"Location" => "/place"}, [" "]]
42
+ end
43
+ end
44
+
45
+ context "and the authenticator rejects the login" do
46
+ let(:authentication_result) { false }
47
+
48
+ it "redirects to the failed login path with a message" do
49
+ expect(subject).to eq [302, {"Location" => "/error?message=Authentication+failed"}, [" "]]
50
+ end
51
+ end
52
+ end
53
+
54
+ context "when the state doesn't match" do
55
+ let(:params) { {"state" => "doesnotmatch", "code" => code} }
56
+
57
+ it "redirects to the failed login path with a message" do
58
+ expect(subject).to eq [302, {"Location" => "/error?message=Invalid+state+returned+from+Google"}, [" "]]
59
+ end
60
+ end
61
+
62
+ context "when the google authentication fails" do
63
+ let(:params) { {"state" => state, "error" => "bad stuff"} }
64
+
65
+ it "redirects to the failed login path with a message" do
66
+ expect(subject).to eq [302, {"Location" => "/error?message=Authentication+failed%3A+bad+stuff"}, [" "]]
67
+ end
68
+ end
69
+
70
+ context "when no code is returned (unexpected)" do
71
+ let(:params) { {"state" => state} }
72
+
73
+ it "redirects to the failed login path with a message" do
74
+ expect(subject).to eq [302, {"Location" => "/error?message=No+authentication+code+returned"}, [" "]]
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,13 @@
1
+ require 'spec_helper'
2
+
3
+ describe SimpleGoogleAuth do
4
+ describe "::configure" do
5
+ it "yields the config object" do
6
+ SimpleGoogleAuth.configure do |config|
7
+ expect(config).to be_a(SimpleGoogleAuth::Config)
8
+ end
9
+ end
10
+
11
+ it "sets access_type to offline if refresh_stale_tokens set"
12
+ end
13
+ end
@@ -0,0 +1,102 @@
1
+ ENV['RAILS_ENV'] ||= 'test'
2
+
3
+ require 'rails/all'
4
+ #require 'rspec/rails'
5
+ require 'simple_google_auth'
6
+
7
+ SimpleGoogleAuth.configure do |c|
8
+ c.client_id = '123'
9
+ c.redirect_uri = '/abc'
10
+ end
11
+
12
+ # This file was generated by the `rspec --init` command. Conventionally, all
13
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
14
+ # The generated `.rspec` file contains `--require spec_helper` which will cause
15
+ # this file to always be loaded, without a need to explicitly require it in any
16
+ # files.
17
+ #
18
+ # Given that it is always loaded, you are encouraged to keep this file as
19
+ # light-weight as possible. Requiring heavyweight dependencies from this file
20
+ # will add to the boot time of your test suite on EVERY test run, even for an
21
+ # individual file that may not need all of that loaded. Instead, consider making
22
+ # a separate helper file that requires the additional dependencies and performs
23
+ # the additional setup, and require it from the spec files that actually need
24
+ # it.
25
+ #
26
+ # The `.rspec` file also contains a few flags that are not defaults but that
27
+ # users commonly want.
28
+ #
29
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
30
+ RSpec.configure do |config|
31
+ # rspec-expectations config goes here. You can use an alternate
32
+ # assertion/expectation library such as wrong or the stdlib/minitest
33
+ # assertions if you prefer.
34
+ config.expect_with :rspec do |expectations|
35
+ # This option will default to `true` in RSpec 4. It makes the `description`
36
+ # and `failure_message` of custom matchers include text for helper methods
37
+ # defined using `chain`, e.g.:
38
+ # be_bigger_than(2).and_smaller_than(4).description
39
+ # # => "be bigger than 2 and smaller than 4"
40
+ # ...rather than:
41
+ # # => "be bigger than 2"
42
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
43
+ end
44
+
45
+ # rspec-mocks config goes here. You can use an alternate test double
46
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
47
+ config.mock_with :rspec do |mocks|
48
+ # Prevents you from mocking or stubbing a method that does not exist on
49
+ # a real object. This is generally recommended, and will default to
50
+ # `true` in RSpec 4.
51
+ mocks.verify_partial_doubles = true
52
+ end
53
+
54
+ # The settings below are suggested to provide a good initial experience
55
+ # with RSpec, but feel free to customize to your heart's content.
56
+ =begin
57
+ # These two settings work together to allow you to limit a spec run
58
+ # to individual examples or groups you care about by tagging them with
59
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
60
+ # get run.
61
+ config.filter_run :focus
62
+ config.run_all_when_everything_filtered = true
63
+
64
+ # Limits the available syntax to the non-monkey patched syntax that is
65
+ # recommended. For more details, see:
66
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
67
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
68
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
69
+ config.disable_monkey_patching!
70
+
71
+ # This setting enables warnings. It's recommended, but in some cases may
72
+ # be too noisy due to issues in dependencies.
73
+ config.warnings = true
74
+
75
+ # Many RSpec users commonly either run the entire suite or an individual
76
+ # file, and it's useful to allow more verbose output when running an
77
+ # individual spec file.
78
+ if config.files_to_run.one?
79
+ # Use the documentation formatter for detailed output,
80
+ # unless a formatter has already been configured
81
+ # (e.g. via a command-line flag).
82
+ config.default_formatter = 'doc'
83
+ end
84
+
85
+ # Print the 10 slowest examples and example groups at the
86
+ # end of the spec run, to help surface which specs are running
87
+ # particularly slow.
88
+ config.profile_examples = 10
89
+
90
+ # Run specs in random order to surface order dependencies. If you find an
91
+ # order dependency and want to debug it, you can fix the order by providing
92
+ # the seed, which is printed after each run.
93
+ # --seed 1234
94
+ config.order = :random
95
+
96
+ # Seed global randomization in this process using the `--seed` CLI option.
97
+ # Setting this allows you to use `--seed` to deterministically reproduce
98
+ # test failures related to randomization by passing the same `--seed` value
99
+ # as the one that triggered the failure.
100
+ Kernel.srand config.seed
101
+ =end
102
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simple_google_auth
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Roger Nesbitt
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-01-28 00:00:00.000000000 Z
11
+ date: 2015-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: 3.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec-rails
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.2'
27
41
  description: An extremely easy way to protect your site by requiring Google logins
28
42
  without having to set up a traditional authentication system
29
43
  email:
@@ -37,12 +51,24 @@ files:
37
51
  - Rakefile
38
52
  - config/routes.rb
39
53
  - lib/simple_google_auth.rb
54
+ - lib/simple_google_auth/auth_data_presenter.rb
55
+ - lib/simple_google_auth/authorization_uri_builder.rb
56
+ - lib/simple_google_auth/config.rb
40
57
  - lib/simple_google_auth/controller.rb
41
58
  - lib/simple_google_auth/engine.rb
42
59
  - lib/simple_google_auth/http_client.rb
43
60
  - lib/simple_google_auth/oauth.rb
44
61
  - lib/simple_google_auth/receiver.rb
45
62
  - lib/simple_google_auth/version.rb
63
+ - spec/simple_google_auth/auth_data_presenter_spec.rb
64
+ - spec/simple_google_auth/authorization_uri_builder_spec.rb
65
+ - spec/simple_google_auth/config_spec.rb
66
+ - spec/simple_google_auth/controller_spec.rb
67
+ - spec/simple_google_auth/http_client_spec.rb
68
+ - spec/simple_google_auth/oauth_spec.rb
69
+ - spec/simple_google_auth/receiver_spec.rb
70
+ - spec/simple_google_auth_spec.rb
71
+ - spec/spec_helper.rb
46
72
  homepage: https://github.com/mogest/simple_google_auth
47
73
  licenses:
48
74
  - MIT
@@ -67,4 +93,13 @@ rubygems_version: 2.2.2
67
93
  signing_key:
68
94
  specification_version: 4
69
95
  summary: Super simple Google authentication for your Rails site
70
- test_files: []
96
+ test_files:
97
+ - spec/simple_google_auth/auth_data_presenter_spec.rb
98
+ - spec/simple_google_auth/authorization_uri_builder_spec.rb
99
+ - spec/simple_google_auth/config_spec.rb
100
+ - spec/simple_google_auth/controller_spec.rb
101
+ - spec/simple_google_auth/http_client_spec.rb
102
+ - spec/simple_google_auth/oauth_spec.rb
103
+ - spec/simple_google_auth/receiver_spec.rb
104
+ - spec/simple_google_auth_spec.rb
105
+ - spec/spec_helper.rb