standard_id-google 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +94 -0
- data/README.md +65 -5
- data/lib/generators/standard_id/google/install/install_generator.rb +3 -2
- data/lib/generators/standard_id/google/install/templates/initializer.rb.erb +13 -12
- data/lib/standard_id/google/providers/google.rb +128 -105
- data/lib/standard_id/google/version.rb +1 -1
- data/lib/standard_id/google.rb +6 -1
- metadata +5 -12
- data/lib/standard_id/google/railtie.rb +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f22ca920dcb306788b3588bf99ac704b9ce5e40cbb9c423a551e064ab6b236a9
|
|
4
|
+
data.tar.gz: f8492f9d9af4dbf74af4062524e0c217e20e50ec3420605a76d363154cacc2f5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9b8d7e1bb7bdfa6302108521bf603a8727a61cd2a6893dcfd8ed230f9999697aa627f912b870528833f9b4781cbe1dbd868f761bdec013ae6cb7ff3a2f8af004
|
|
7
|
+
data.tar.gz: 53b44b47d85d30da76de8dbdf1f55953c3b3f4a31fb3808c769b8a72175210d8ac0fdbbbbbe0d52eb7e45817666d17edd5ce2947b1dfc16462d3f12d92226544
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,100 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-09-24
|
|
11
|
+
|
|
12
|
+
Adopts the provider-plugin API of standard_id 0.42.
|
|
13
|
+
|
|
14
|
+
### Upgrade
|
|
15
|
+
|
|
16
|
+
- **Requires `standard_id` 0.42** (`~> 0.42`, was `>= 0.29, < 1.0`). Bump both
|
|
17
|
+
together.
|
|
18
|
+
- **Rename `GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET` to
|
|
19
|
+
`GOOGLE_CLIENT_ID` / `GOOGLE_CLIENT_SECRET`** when convenient. Initializers
|
|
20
|
+
from the 0.4.0 generator assign the old names explicitly and keep working
|
|
21
|
+
unchanged; the old names are also read as a deprecated ENV fallback (see
|
|
22
|
+
Deprecated). sidekick-web already uses the canonical names.
|
|
23
|
+
- **Expect a boot warning if the secret is missing.** With `google_client_id`
|
|
24
|
+
set and `google_client_secret` blank, standard_id now logs a warning at boot
|
|
25
|
+
(raises in production under `c.social.provider_misconfiguration = :raise`).
|
|
26
|
+
An app that deliberately accepts only native ID tokens and has no secret
|
|
27
|
+
should keep that setting at `:warn`.
|
|
28
|
+
- **Specs stubbing the tokeninfo call:** access-token verification now posts to
|
|
29
|
+
`TOKEN_INFO_ENDPOINT` (`https://oauth2.googleapis.com/tokeninfo`), not
|
|
30
|
+
`https://www.googleapis.com/oauth2/v3/tokeninfo`. ID-token stubs against
|
|
31
|
+
`TOKEN_INFO_ENDPOINT` (sidekick-web's `social_callback_spec.rb`) are
|
|
32
|
+
unaffected.
|
|
33
|
+
- **Anyone rescuing on message text:** several messages changed (see Changed).
|
|
34
|
+
`StandardId::Google::Railtie` no longer exists.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
|
|
38
|
+
- **Required config field.** `google_client_secret` is declared
|
|
39
|
+
`required: true`, so `StandardId::Providers::Google.configuration_errors` and
|
|
40
|
+
standard_id's boot check report it whenever `google_client_id` (the enabling
|
|
41
|
+
field) is set.
|
|
42
|
+
- **ENV fallback** through standard_id 0.42: `GOOGLE_CLIENT_ID`,
|
|
43
|
+
`GOOGLE_CLIENT_SECRET`.
|
|
44
|
+
- Specs for `resolve_params`, `skip_csrf?`, `supports_mobile_callback?` /
|
|
45
|
+
`flow_for`, nonce handling (web code exchange included) and configuration,
|
|
46
|
+
plus standard_id's `"a registered StandardId provider"` shared example.
|
|
47
|
+
|
|
48
|
+
### Changed
|
|
49
|
+
|
|
50
|
+
- **One tokeninfo endpoint.** ID tokens and access tokens are both verified
|
|
51
|
+
against `TOKEN_INFO_ENDPOINT`, `https://oauth2.googleapis.com/tokeninfo` —
|
|
52
|
+
the endpoint Google documents for both. Access tokens previously went to a
|
|
53
|
+
hard-coded `https://www.googleapis.com/oauth2/v3/tokeninfo`.
|
|
54
|
+
- **The id_token and access_token flows no longer need `google_client_secret`.**
|
|
55
|
+
Both verify through tokeninfo and compare the audience to the client ID; only
|
|
56
|
+
the web code exchange sends the secret. Previously every flow raised
|
|
57
|
+
`Google provider is not configured` without it.
|
|
58
|
+
- **Nonce mismatches no longer leak the nonce.** The message was
|
|
59
|
+
`ID token nonce mismatch. Expected: <nonce>, got: <nonce>`; it is now
|
|
60
|
+
standard_id's `ID token nonce mismatch`, and the comparison is constant-time.
|
|
61
|
+
The nonce is now checked after the audience and issuer.
|
|
62
|
+
- **Audience and issuer errors no longer echo values** — the old messages
|
|
63
|
+
included this app's client ID and the presented `aud`/`iss`. A token with no
|
|
64
|
+
`aud` at all is rejected explicitly.
|
|
65
|
+
- **The duplicated helpers are gone** in favour of standard_id's
|
|
66
|
+
`Providers::Base`: `rescue_to_oauth_error`, `verify_nonce!`,
|
|
67
|
+
`build_authorization_url`, `extract_tokens` (the private
|
|
68
|
+
`extract_token_payload` is removed). `lib/standard_id/google/railtie.rb` is
|
|
69
|
+
replaced by `StandardId::Providers.plugin_railtie(:google, ...)`.
|
|
70
|
+
- **Error messages**, now consistently Google-prefixed:
|
|
71
|
+
- `Either code, id_token, or access_token must be provided` → `Google sign-in requires a code, an id_token or an access_token`
|
|
72
|
+
- `Google provider is not configured` → `Google OAuth is not configured` (client ID missing) or `Google OAuth credentials are incomplete: google_client_secret not set` (code exchange only)
|
|
73
|
+
- `Missing authorization code` → `Google authorization code is missing`
|
|
74
|
+
- `Failed to exchange Google authorization code` → `...: <error>` (Google's `error`, else `HTTP <status>`)
|
|
75
|
+
- `Google response missing access token` → `Google token response is missing access_token`
|
|
76
|
+
- `Missing id_token` → `Google id_token is missing`
|
|
77
|
+
- `Invalid or expired id_token` → `Invalid Google ID token: invalid or expired`
|
|
78
|
+
- `ID token audience mismatch. Expected: ..., got: ...` → `Invalid Google ID token audience`
|
|
79
|
+
- `ID token issuer invalid. Expected Google, got: ...` → `Invalid Google ID token issuer`
|
|
80
|
+
- `Missing access token` → `Google access token is missing`
|
|
81
|
+
- `Invalid or expired access token` → `Invalid Google access token: invalid or expired`
|
|
82
|
+
- `Access token audience mismatch. Expected: ..., got: ...` → `Invalid Google access token audience`
|
|
83
|
+
- `Failed to fetch Google user info` → `...: HTTP <status>`
|
|
84
|
+
- **Spec layout mirrors `lib/`** and standard_id-apple:
|
|
85
|
+
`spec/standard_id/providers/google_spec.rb` →
|
|
86
|
+
`spec/standard_id/google/providers/google_spec.rb`, plus
|
|
87
|
+
`spec/standard_id/google/registration_spec.rb`.
|
|
88
|
+
- Install generator and README use the canonical `GOOGLE_CLIENT_ID` /
|
|
89
|
+
`GOOGLE_CLIENT_SECRET`, and document the ENV fallback, required field and
|
|
90
|
+
flows.
|
|
91
|
+
|
|
92
|
+
### Deprecated
|
|
93
|
+
|
|
94
|
+
- **`GOOGLE_OAUTH_CLIENT_ID` / `GOOGLE_OAUTH_CLIENT_SECRET`** as ENV sources.
|
|
95
|
+
Read only when the field is never assigned and the canonical variable is
|
|
96
|
+
unset, with one warning per variable per process through
|
|
97
|
+
`StandardId.deprecator`.
|
|
98
|
+
|
|
99
|
+
### Removed
|
|
100
|
+
|
|
101
|
+
- `StandardId::Google::Railtie` (replaced by the Railtie `plugin_railtie`
|
|
102
|
+
defines, `StandardId::Providers::Railties::Google`).
|
|
103
|
+
|
|
10
104
|
## [0.4.0] - 2026-07-31
|
|
11
105
|
|
|
12
106
|
### Added
|
data/README.md
CHANGED
|
@@ -4,6 +4,8 @@ This gem extracts the Google OAuth provider from the core [`standard_id`](https:
|
|
|
4
4
|
|
|
5
5
|
## Installation
|
|
6
6
|
|
|
7
|
+
Requires `standard_id` 0.42 or later.
|
|
8
|
+
|
|
7
9
|
Add the gem next to `standard_id`:
|
|
8
10
|
|
|
9
11
|
```ruby
|
|
@@ -41,15 +43,62 @@ configuring by hand. Configure your Google credentials inside the StandardId
|
|
|
41
43
|
configuration block:
|
|
42
44
|
|
|
43
45
|
```ruby
|
|
44
|
-
# config/initializers/
|
|
46
|
+
# config/initializers/standard_id_google.rb
|
|
45
47
|
StandardId.configure do |config|
|
|
46
|
-
config.social.google_client_id = ENV.fetch("
|
|
47
|
-
config.social.google_client_secret = ENV.fetch("
|
|
48
|
+
config.social.google_client_id = ENV.fetch("GOOGLE_CLIENT_ID", nil)
|
|
49
|
+
config.social.google_client_secret = ENV.fetch("GOOGLE_CLIENT_SECRET", nil)
|
|
48
50
|
end
|
|
49
51
|
```
|
|
50
52
|
|
|
51
53
|
With those values in place, StandardId routes such as `/auth/callback/google` continue to function using this provider gem.
|
|
52
54
|
|
|
55
|
+
### ENV fallback
|
|
56
|
+
|
|
57
|
+
On `standard_id` 0.42+, a field you never assign falls back to the ENV
|
|
58
|
+
variable named after it, upper-cased:
|
|
59
|
+
|
|
60
|
+
| Field | ENV variable | Deprecated fallback |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| `google_client_id` | `GOOGLE_CLIENT_ID` | `GOOGLE_OAUTH_CLIENT_ID` |
|
|
63
|
+
| `google_client_secret` | `GOOGLE_CLIENT_SECRET` | `GOOGLE_OAUTH_CLIENT_SECRET` |
|
|
64
|
+
|
|
65
|
+
So with those variables set the block above is optional. Explicit
|
|
66
|
+
configuration, even `nil`, always wins. The `GOOGLE_OAUTH_*` names — what
|
|
67
|
+
install generators before 0.5.0 wrote — are still read when the canonical
|
|
68
|
+
variable is unset and the field is never assigned, with a deprecation warning;
|
|
69
|
+
rename them. (An initializer that assigns `ENV.fetch("GOOGLE_OAUTH_CLIENT_ID")`
|
|
70
|
+
explicitly keeps working as-is.)
|
|
71
|
+
|
|
72
|
+
### Required fields and the boot check
|
|
73
|
+
|
|
74
|
+
`google_client_id` switches the provider on — it is what
|
|
75
|
+
`StandardId.social_provider_enabled?(:google)` and the `google_enabled` Inertia
|
|
76
|
+
prop report. While it is set, `google_client_secret` is required: the web
|
|
77
|
+
sign-in's code exchange needs it, so without it the user authenticates with
|
|
78
|
+
Google and only then does the callback fail. StandardId checks this once every
|
|
79
|
+
plugin has registered:
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
StandardId::Providers::Google.configuration_errors
|
|
83
|
+
# => ["google_client_secret is required when google_client_id is set"]
|
|
84
|
+
|
|
85
|
+
config.social.provider_misconfiguration = :raise # fail a production boot instead of warning
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Flows
|
|
89
|
+
|
|
90
|
+
| Flow | Needs `google_client_secret` |
|
|
91
|
+
|---|---|
|
|
92
|
+
| Web (`/auth/callback/google`, authorization code) | yes |
|
|
93
|
+
| Native `id_token` (`/api/oauth/callback/google`) | no |
|
|
94
|
+
| Native `access_token` | no |
|
|
95
|
+
|
|
96
|
+
ID tokens and access tokens are both checked against Google's tokeninfo
|
|
97
|
+
endpoint (`https://oauth2.googleapis.com/tokeninfo`), then against this app's
|
|
98
|
+
client ID. An app that only accepts native ID tokens can run without a client
|
|
99
|
+
secret; it will get the boot warning above, because an enabled provider shows
|
|
100
|
+
the web sign-in button — leave `provider_misconfiguration` at `:warn` there.
|
|
101
|
+
|
|
53
102
|
### Boot ordering (standard_id <= 0.32.0)
|
|
54
103
|
|
|
55
104
|
These fields are declared by *this gem*, not by `standard_id`, and until
|
|
@@ -67,8 +116,8 @@ The workaround was to wrap the writes:
|
|
|
67
116
|
# Only needed on standard_id <= 0.32.0
|
|
68
117
|
Rails.application.config.after_initialize do
|
|
69
118
|
StandardId.configure do |config|
|
|
70
|
-
config.social.google_client_id = ENV.fetch("
|
|
71
|
-
config.social.google_client_secret = ENV.fetch("
|
|
119
|
+
config.social.google_client_id = ENV.fetch("GOOGLE_CLIENT_ID", nil)
|
|
120
|
+
config.social.google_client_secret = ENV.fetch("GOOGLE_CLIENT_SECRET", nil)
|
|
72
121
|
end
|
|
73
122
|
end
|
|
74
123
|
```
|
|
@@ -83,6 +132,17 @@ without this gem in your Gemfile**, on any `standard_id` version. Configuring
|
|
|
83
132
|
|
|
84
133
|
## Testing
|
|
85
134
|
|
|
135
|
+
In a host app, pin the plugin's registration with standard_id's shared
|
|
136
|
+
example:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
require "standard_id/testing"
|
|
140
|
+
|
|
141
|
+
RSpec.describe "StandardId social providers" do
|
|
142
|
+
it_behaves_like "a registered StandardId provider", :google
|
|
143
|
+
end
|
|
144
|
+
```
|
|
145
|
+
|
|
86
146
|
Run the provider test suite with:
|
|
87
147
|
|
|
88
148
|
```bash
|
|
@@ -61,8 +61,9 @@ module StandardId
|
|
|
61
61
|
say ""
|
|
62
62
|
say "Set these in the host's environment (1Password / DO app spec / .env):"
|
|
63
63
|
say ""
|
|
64
|
-
say "
|
|
65
|
-
say "
|
|
64
|
+
say " GOOGLE_CLIENT_ID the OAuth 2.0 Web application client ID"
|
|
65
|
+
say " GOOGLE_CLIENT_SECRET its client secret — required whenever GOOGLE_CLIENT_ID"
|
|
66
|
+
say " is set; StandardId checks for it at boot"
|
|
66
67
|
say ""
|
|
67
68
|
say "Register /auth/callback/google as an authorized redirect URI in the"
|
|
68
69
|
say "Google Cloud console for EVERY origin you serve — the console matches"
|
|
@@ -13,18 +13,19 @@
|
|
|
13
13
|
# routes a unique unqualified name to its owning scope, but it stops working
|
|
14
14
|
# the day another scope declares a colliding name. Use `config.social.`.
|
|
15
15
|
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
16
|
+
# ENV: on standard_id >= 0.42 a field this file does not assign falls back to
|
|
17
|
+
# the ENV variable named after it, upper-cased — GOOGLE_CLIENT_ID and
|
|
18
|
+
# GOOGLE_CLIENT_SECRET — which are the names used below. The assignments are
|
|
19
|
+
# kept so the wiring is visible; delete them to leave it to the fallback, or
|
|
20
|
+
# point them at another source (e.g. Rails credentials). Installs before
|
|
21
|
+
# standard_id-google 0.5.0 used GOOGLE_OAUTH_CLIENT_ID / _SECRET.
|
|
19
22
|
#
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
# `:load_config_initializers`, so the plain form is correct. Existing wrappers
|
|
26
|
-
# keep working and need no change.
|
|
23
|
+
# google_client_id switches the provider on. While it is set, StandardId's boot
|
|
24
|
+
# check requires google_client_secret, which the web sign-in's code exchange
|
|
25
|
+
# needs (warns by default; `config.social.provider_misconfiguration = :raise`
|
|
26
|
+
# fails a production boot instead). Native id_token / access_token sign-in
|
|
27
|
+
# needs only the client ID.
|
|
27
28
|
StandardId.configure do |config|
|
|
28
|
-
config.social.google_client_id = ENV.fetch("
|
|
29
|
-
config.social.google_client_secret = ENV.fetch("
|
|
29
|
+
config.social.google_client_id = ENV.fetch("GOOGLE_CLIENT_ID", nil)
|
|
30
|
+
config.social.google_client_secret = ENV.fetch("GOOGLE_CLIENT_SECRET", nil)
|
|
30
31
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
require "json"
|
|
2
|
-
require "
|
|
2
|
+
require "net/http"
|
|
3
3
|
|
|
4
4
|
module StandardId
|
|
5
5
|
module Providers
|
|
@@ -7,12 +7,26 @@ module StandardId
|
|
|
7
7
|
AUTH_ENDPOINT = "https://accounts.google.com/o/oauth2/v2/auth".freeze
|
|
8
8
|
TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token".freeze
|
|
9
9
|
USERINFO_ENDPOINT = "https://www.googleapis.com/oauth2/v2/userinfo".freeze
|
|
10
|
+
# Google's documented tokeninfo endpoint, for both ID tokens and access
|
|
11
|
+
# tokens (developers.google.com/identity/sign-in/web/backend-auth,
|
|
12
|
+
# developers.google.com/identity/protocols/oauth2). The legacy
|
|
13
|
+
# www.googleapis.com/oauth2/v3/tokeninfo host is no longer used.
|
|
10
14
|
TOKEN_INFO_ENDPOINT = "https://oauth2.googleapis.com/tokeninfo".freeze
|
|
15
|
+
VALID_ISSUERS = ["accounts.google.com", "https://accounts.google.com"].freeze
|
|
11
16
|
DEFAULT_SCOPE = "openid email profile".freeze
|
|
12
17
|
AUTHORIZATION_PARAM_DEFAULTS = {
|
|
13
18
|
scope: DEFAULT_SCOPE
|
|
14
19
|
}.freeze
|
|
15
20
|
|
|
21
|
+
# Pre-0.5.0 install generators wired the fields to these variables.
|
|
22
|
+
# Still read, with a deprecation warning, when the canonical variable
|
|
23
|
+
# (GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET) is unset and the host never
|
|
24
|
+
# assigns the field.
|
|
25
|
+
LEGACY_ENV = {
|
|
26
|
+
google_client_id: "GOOGLE_OAUTH_CLIENT_ID",
|
|
27
|
+
google_client_secret: "GOOGLE_OAUTH_CLIENT_SECRET"
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
16
30
|
class << self
|
|
17
31
|
def provider_name
|
|
18
32
|
"google"
|
|
@@ -23,18 +37,12 @@ module StandardId
|
|
|
23
37
|
end
|
|
24
38
|
|
|
25
39
|
def authorization_url(state:, redirect_uri:, **options)
|
|
26
|
-
|
|
40
|
+
build_authorization_url(
|
|
41
|
+
endpoint: AUTH_ENDPOINT,
|
|
27
42
|
client_id: credentials[:client_id],
|
|
28
|
-
redirect_uri
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
supported_authorization_params.each do |param|
|
|
34
|
-
query[param] = options[param] || AUTHORIZATION_PARAM_DEFAULTS[param]
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
"#{AUTH_ENDPOINT}?#{URI.encode_www_form(query.compact)}"
|
|
43
|
+
redirect_uri:, state:, options:,
|
|
44
|
+
defaults: AUTHORIZATION_PARAM_DEFAULTS
|
|
45
|
+
)
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
def get_user_info(code: nil, id_token: nil, access_token: nil, redirect_uri: nil, nonce: nil, **_options)
|
|
@@ -51,14 +59,25 @@ module StandardId
|
|
|
51
59
|
elsif code.present?
|
|
52
60
|
exchange_code_for_user_info(code: code, redirect_uri: redirect_uri, nonce: nonce)
|
|
53
61
|
else
|
|
54
|
-
raise StandardId::InvalidRequestError, "
|
|
62
|
+
raise StandardId::InvalidRequestError, "Google sign-in requires a code, an id_token or an access_token"
|
|
55
63
|
end
|
|
56
64
|
end
|
|
57
65
|
|
|
66
|
+
# `google_client_id` switches the provider on (it is the enabling
|
|
67
|
+
# field). `google_client_secret` is required while it is set: an
|
|
68
|
+
# enabled provider shows the web sign-in button, and the web flow's
|
|
69
|
+
# code exchange needs the secret — without it the user authenticates
|
|
70
|
+
# with Google and only then does the callback fail. The native
|
|
71
|
+
# id_token and access_token flows verify through Google's tokeninfo
|
|
72
|
+
# endpoint and need only the client ID.
|
|
73
|
+
#
|
|
74
|
+
# ENV fallbacks (standard_id >= 0.42): GOOGLE_CLIENT_ID and
|
|
75
|
+
# GOOGLE_CLIENT_SECRET; the pre-0.5.0 GOOGLE_OAUTH_* names are read as
|
|
76
|
+
# a deprecated fallback.
|
|
58
77
|
def config_schema
|
|
59
78
|
{
|
|
60
|
-
google_client_id: { type: :string, default:
|
|
61
|
-
google_client_secret: { type: :string, default:
|
|
79
|
+
google_client_id: { type: :string, default: -> { legacy_env(:google_client_id) } },
|
|
80
|
+
google_client_secret: { type: :string, default: -> { legacy_env(:google_client_secret) }, required: true }
|
|
62
81
|
}
|
|
63
82
|
end
|
|
64
83
|
|
|
@@ -67,139 +86,143 @@ module StandardId
|
|
|
67
86
|
end
|
|
68
87
|
|
|
69
88
|
def exchange_code_for_user_info(code:, redirect_uri:, nonce: nil)
|
|
70
|
-
|
|
89
|
+
rescue_to_oauth_error do
|
|
90
|
+
raise StandardId::InvalidRequestError, "Google authorization code is missing" if code.blank?
|
|
71
91
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
grant_type: "authorization_code",
|
|
77
|
-
redirect_uri: redirect_uri
|
|
78
|
-
}.compact)
|
|
79
|
-
|
|
80
|
-
unless token_response.is_a?(Net::HTTPSuccess)
|
|
81
|
-
raise StandardId::InvalidRequestError, "Failed to exchange Google authorization code"
|
|
82
|
-
end
|
|
92
|
+
creds = credentials
|
|
93
|
+
if creds[:client_secret].blank?
|
|
94
|
+
raise StandardId::InvalidRequestError, "Google OAuth credentials are incomplete: google_client_secret not set"
|
|
95
|
+
end
|
|
83
96
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
97
|
+
token_response = HttpClient.post_form(TOKEN_ENDPOINT, {
|
|
98
|
+
client_id: creds[:client_id],
|
|
99
|
+
client_secret: creds[:client_secret],
|
|
100
|
+
code: code,
|
|
101
|
+
grant_type: "authorization_code",
|
|
102
|
+
redirect_uri: redirect_uri
|
|
103
|
+
}.compact)
|
|
87
104
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
unless token_response.is_a?(Net::HTTPSuccess)
|
|
106
|
+
raise StandardId::InvalidRequestError,
|
|
107
|
+
"Failed to exchange Google authorization code: #{error_reason(token_response)}"
|
|
108
|
+
end
|
|
92
109
|
|
|
93
|
-
|
|
94
|
-
|
|
110
|
+
parsed_token = JSON.parse(token_response.body)
|
|
111
|
+
access_token = parsed_token["access_token"]
|
|
112
|
+
raise StandardId::InvalidRequestError, "Google token response is missing access_token" if access_token.blank?
|
|
95
113
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
114
|
+
# Web flow with a server-generated nonce: check it on the ID token.
|
|
115
|
+
if parsed_token["id_token"].present? && nonce.present?
|
|
116
|
+
verify_id_token(id_token: parsed_token["id_token"], nonce: nonce)
|
|
117
|
+
end
|
|
99
118
|
|
|
100
|
-
|
|
119
|
+
build_response(fetch_user_info(access_token: access_token), tokens: extract_tokens(parsed_token))
|
|
120
|
+
end
|
|
101
121
|
end
|
|
102
122
|
|
|
123
|
+
# Verifies through Google's tokeninfo endpoint, which checks the
|
|
124
|
+
# signature and expiry; the audience, issuer and nonce are checked
|
|
125
|
+
# here. Needs only the client ID.
|
|
103
126
|
def verify_id_token(id_token:, nonce: nil)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
response = HttpClient.post_form(TOKEN_INFO_ENDPOINT, id_token: id_token)
|
|
127
|
+
rescue_to_oauth_error do
|
|
128
|
+
raise StandardId::InvalidRequestError, "Google id_token is missing" if id_token.blank?
|
|
107
129
|
|
|
108
|
-
|
|
130
|
+
response = HttpClient.post_form(TOKEN_INFO_ENDPOINT, id_token: id_token)
|
|
131
|
+
raise StandardId::InvalidRequestError, "Invalid Google ID token: invalid or expired" unless response.is_a?(Net::HTTPSuccess)
|
|
109
132
|
|
|
110
|
-
|
|
133
|
+
token_info = JSON.parse(response.body)
|
|
111
134
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
token_nonce = token_info["nonce"]
|
|
115
|
-
if token_nonce != nonce
|
|
116
|
-
raise StandardId::InvalidRequestError,
|
|
117
|
-
"ID token nonce mismatch. Expected: #{nonce}, got: #{token_nonce}"
|
|
135
|
+
unless token_info["aud"].present? && token_info["aud"] == credentials[:client_id]
|
|
136
|
+
raise StandardId::InvalidRequestError, "Invalid Google ID token audience"
|
|
118
137
|
end
|
|
119
|
-
end
|
|
120
138
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
end
|
|
139
|
+
unless VALID_ISSUERS.include?(token_info["iss"])
|
|
140
|
+
raise StandardId::InvalidRequestError, "Invalid Google ID token issuer"
|
|
141
|
+
end
|
|
125
142
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
143
|
+
# Constant-time, and the error never echoes either value.
|
|
144
|
+
verify_nonce!(expected: nonce, actual: token_info["nonce"])
|
|
145
|
+
|
|
146
|
+
{
|
|
147
|
+
"sub" => token_info["sub"],
|
|
148
|
+
"email" => token_info["email"],
|
|
149
|
+
"email_verified" => token_info["email_verified"],
|
|
150
|
+
"name" => token_info["name"],
|
|
151
|
+
"given_name" => token_info["given_name"],
|
|
152
|
+
"family_name" => token_info["family_name"],
|
|
153
|
+
"picture" => token_info["picture"],
|
|
154
|
+
"locale" => token_info["locale"]
|
|
155
|
+
}.compact
|
|
129
156
|
end
|
|
130
|
-
|
|
131
|
-
{
|
|
132
|
-
"sub" => token_info["sub"],
|
|
133
|
-
"email" => token_info["email"],
|
|
134
|
-
"email_verified" => token_info["email_verified"],
|
|
135
|
-
"name" => token_info["name"],
|
|
136
|
-
"given_name" => token_info["given_name"],
|
|
137
|
-
"family_name" => token_info["family_name"],
|
|
138
|
-
"picture" => token_info["picture"],
|
|
139
|
-
"locale" => token_info["locale"]
|
|
140
|
-
}.compact
|
|
141
|
-
rescue StandardError => e
|
|
142
|
-
raise e if e.is_a?(StandardId::OAuthError)
|
|
143
|
-
|
|
144
|
-
raise StandardId::OAuthError, e.message, cause: e
|
|
145
157
|
end
|
|
146
158
|
|
|
147
159
|
def fetch_user_info(access_token:)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
verify_token(access_token)
|
|
151
|
-
user_response = HttpClient.get_with_bearer(USERINFO_ENDPOINT, access_token)
|
|
160
|
+
rescue_to_oauth_error do
|
|
161
|
+
raise StandardId::InvalidRequestError, "Google access token is missing" if access_token.blank?
|
|
152
162
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
end
|
|
163
|
+
verify_token(access_token)
|
|
164
|
+
user_response = HttpClient.get_with_bearer(USERINFO_ENDPOINT, access_token)
|
|
156
165
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
166
|
+
unless user_response.is_a?(Net::HTTPSuccess)
|
|
167
|
+
raise StandardId::InvalidRequestError, "Failed to fetch Google user info: HTTP #{user_response.code}"
|
|
168
|
+
end
|
|
160
169
|
|
|
161
|
-
|
|
170
|
+
JSON.parse(user_response.body)
|
|
171
|
+
end
|
|
162
172
|
end
|
|
163
173
|
|
|
164
174
|
private
|
|
165
175
|
|
|
176
|
+
# The client ID every flow needs, and the secret only the code
|
|
177
|
+
# exchange needs (checked there). Raises when the provider is off.
|
|
166
178
|
def credentials
|
|
167
179
|
client_id = StandardId.config.google_client_id
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
if client_id.blank? || client_secret.blank?
|
|
171
|
-
raise StandardId::InvalidRequestError, "Google provider is not configured"
|
|
172
|
-
end
|
|
180
|
+
raise StandardId::InvalidRequestError, "Google OAuth is not configured" if client_id.blank?
|
|
173
181
|
|
|
174
182
|
{
|
|
175
183
|
client_id: client_id,
|
|
176
|
-
client_secret:
|
|
184
|
+
client_secret: StandardId.config.google_client_secret
|
|
177
185
|
}
|
|
178
186
|
end
|
|
179
187
|
|
|
188
|
+
# Confirms an access token was issued to this app's client before its
|
|
189
|
+
# userinfo is trusted.
|
|
180
190
|
def verify_token(access_token)
|
|
181
|
-
response = HttpClient.post_form(
|
|
182
|
-
|
|
183
|
-
unless response.is_a?(Net::HTTPSuccess)
|
|
184
|
-
raise StandardId::InvalidRequestError, "Invalid or expired access token"
|
|
185
|
-
end
|
|
191
|
+
response = HttpClient.post_form(TOKEN_INFO_ENDPOINT, access_token: access_token)
|
|
192
|
+
raise StandardId::InvalidRequestError, "Invalid Google access token: invalid or expired" unless response.is_a?(Net::HTTPSuccess)
|
|
186
193
|
|
|
187
194
|
token_info = JSON.parse(response.body)
|
|
188
195
|
|
|
189
|
-
unless token_info["aud"] == credentials[:client_id]
|
|
190
|
-
raise StandardId::InvalidRequestError,
|
|
191
|
-
"Access token audience mismatch. Expected: #{credentials[:client_id]}, got: #{token_info["aud"]}"
|
|
196
|
+
unless token_info["aud"].present? && token_info["aud"] == credentials[:client_id]
|
|
197
|
+
raise StandardId::InvalidRequestError, "Invalid Google access token audience"
|
|
192
198
|
end
|
|
193
199
|
|
|
194
200
|
token_info
|
|
195
201
|
end
|
|
196
202
|
|
|
197
|
-
def
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
+
def error_reason(response)
|
|
204
|
+
body = JSON.parse(response.body.to_s)
|
|
205
|
+
reason = body["error"] if body.is_a?(Hash)
|
|
206
|
+
reason.presence || "HTTP #{response.code}"
|
|
207
|
+
rescue JSON::ParserError
|
|
208
|
+
"HTTP #{response.code}"
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def legacy_env(field)
|
|
212
|
+
name = LEGACY_ENV.fetch(field)
|
|
213
|
+
value = ENV[name]
|
|
214
|
+
return nil if value.blank?
|
|
215
|
+
|
|
216
|
+
# An unassigned field's default is re-evaluated on read; warn once.
|
|
217
|
+
@legacy_env_warned ||= {}
|
|
218
|
+
return value if @legacy_env_warned[name]
|
|
219
|
+
|
|
220
|
+
@legacy_env_warned[name] = true
|
|
221
|
+
StandardId.deprecator.warn(
|
|
222
|
+
"standard_id-google: reading #{field} from #{name} is deprecated. " \
|
|
223
|
+
"Rename the variable to #{field.to_s.upcase} (or assign config.social.#{field} explicitly)."
|
|
224
|
+
)
|
|
225
|
+
value
|
|
203
226
|
end
|
|
204
227
|
end
|
|
205
228
|
end
|
data/lib/standard_id/google.rb
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
require "active_support/core_ext/numeric/time"
|
|
2
2
|
require "active_support/core_ext/hash/indifferent_access"
|
|
3
3
|
require "standard_id"
|
|
4
|
+
require "standard_id/google/version"
|
|
4
5
|
require "standard_id/google/providers/google"
|
|
5
|
-
|
|
6
|
+
|
|
7
|
+
# Registers the provider from a Railtie's after_initialize (a no-op outside
|
|
8
|
+
# Rails). Its config fields are declared earlier, before config/initializers,
|
|
9
|
+
# by standard_id's own engine initializer.
|
|
10
|
+
StandardId::Providers.plugin_railtie(:google, "StandardId::Providers::Google")
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: standard_id-google
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jaryl Sim
|
|
@@ -27,22 +27,16 @@ dependencies:
|
|
|
27
27
|
name: standard_id
|
|
28
28
|
requirement: !ruby/object:Gem::Requirement
|
|
29
29
|
requirements:
|
|
30
|
-
- - "
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0.29'
|
|
33
|
-
- - "<"
|
|
30
|
+
- - "~>"
|
|
34
31
|
- !ruby/object:Gem::Version
|
|
35
|
-
version: '
|
|
32
|
+
version: '0.42'
|
|
36
33
|
type: :runtime
|
|
37
34
|
prerelease: false
|
|
38
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
39
36
|
requirements:
|
|
40
|
-
- - "
|
|
41
|
-
- !ruby/object:Gem::Version
|
|
42
|
-
version: '0.29'
|
|
43
|
-
- - "<"
|
|
37
|
+
- - "~>"
|
|
44
38
|
- !ruby/object:Gem::Version
|
|
45
|
-
version: '
|
|
39
|
+
version: '0.42'
|
|
46
40
|
description: Extracted StandardId::Providers::Google implementation packaged as a
|
|
47
41
|
standalone gem so StandardId installations can opt into Sign in with Google independently.
|
|
48
42
|
email:
|
|
@@ -59,7 +53,6 @@ files:
|
|
|
59
53
|
- lib/generators/standard_id/google/install/templates/initializer.rb.erb
|
|
60
54
|
- lib/standard_id/google.rb
|
|
61
55
|
- lib/standard_id/google/providers/google.rb
|
|
62
|
-
- lib/standard_id/google/railtie.rb
|
|
63
56
|
- lib/standard_id/google/version.rb
|
|
64
57
|
homepage: https://github.com/rarebit-one/standard_id_google
|
|
65
58
|
licenses:
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module StandardId
|
|
4
|
-
module Google
|
|
5
|
-
class Railtie < ::Rails::Railtie
|
|
6
|
-
config.after_initialize do
|
|
7
|
-
StandardId::ProviderRegistry.register(:google, StandardId::Providers::Google)
|
|
8
|
-
|
|
9
|
-
Rails.logger.debug("[StandardId::Google] registered provider") if Rails.logger
|
|
10
|
-
end
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|