Class: StandardId::Providers::Google
- Inherits:
-
Base
- Object
- Base
- StandardId::Providers::Google
- Defined in:
- lib/standard_id/google/providers/google.rb
Constant Summary collapse
- AUTH_ENDPOINT =
"https://accounts.google.com/o/oauth2/v2/auth".freeze
- TOKEN_ENDPOINT =
"https://oauth2.googleapis.com/token".freeze
- USERINFO_ENDPOINT =
"https://www.googleapis.com/oauth2/v2/userinfo".freeze
- TOKEN_INFO_ENDPOINT =
Google's documented tokeninfo endpoint, for both ID tokens and access tokens (developers.google.com/identity/sign-in/web/backend-auth, developers.google.com/identity/protocols/oauth2). The legacy www.googleapis.com/oauth2/v3/tokeninfo host is no longer used.
"https://oauth2.googleapis.com/tokeninfo".freeze
- VALID_ISSUERS =
["accounts.google.com", "https://accounts.google.com"].freeze
- DEFAULT_SCOPE =
"openid email profile".freeze
- AUTHORIZATION_PARAM_DEFAULTS =
{ scope: DEFAULT_SCOPE }.freeze
- LEGACY_ENV =
Pre-0.5.0 install generators wired the fields to these variables. Still read, with a deprecation warning, when the canonical variable (GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET) is unset and the host never assigns the field.
{ google_client_id: "GOOGLE_OAUTH_CLIENT_ID", google_client_secret: "GOOGLE_OAUTH_CLIENT_SECRET" }.freeze
Class Method Summary collapse
- .authorization_url(state:, redirect_uri:, **options) ⇒ Object
-
.config_schema ⇒ Object
google_client_idswitches the provider on (it is the enabling field). - .default_scope ⇒ Object
- .exchange_code_for_user_info(code:, redirect_uri:, nonce: nil) ⇒ Object
- .fetch_user_info(access_token:) ⇒ Object
- .get_user_info(code: nil, id_token: nil, access_token: nil, redirect_uri: nil, nonce: nil, **_options) ⇒ Object
- .provider_name ⇒ Object
- .supported_authorization_params ⇒ Object
-
.verify_id_token(id_token:, nonce: nil) ⇒ Object
Verifies through Google's tokeninfo endpoint, which checks the signature and expiry; the audience, issuer and nonce are checked here.
Class Method Details
.authorization_url(state:, redirect_uri:, **options) ⇒ Object
39 40 41 42 43 44 45 46 |
# File 'lib/standard_id/google/providers/google.rb', line 39 def (state:, redirect_uri:, **) ( endpoint: AUTH_ENDPOINT, client_id: credentials[:client_id], redirect_uri:, state:, options:, defaults: AUTHORIZATION_PARAM_DEFAULTS ) end |
.config_schema ⇒ Object
google_client_id switches the provider on (it is the enabling
field). google_client_secret is required while it is set: an
enabled provider shows the web sign-in button, and the web flow's
code exchange needs the secret — without it the user authenticates
with Google and only then does the callback fail. The native
id_token and access_token flows verify through Google's tokeninfo
endpoint and need only the client ID.
ENV fallbacks (standard_id >= 0.42): GOOGLE_CLIENT_ID and GOOGLE_CLIENT_SECRET; the pre-0.5.0 GOOGLE_OAUTH_* names are read as a deprecated fallback.
77 78 79 80 81 82 |
# File 'lib/standard_id/google/providers/google.rb', line 77 def config_schema { google_client_id: { type: :string, default: -> { legacy_env(:google_client_id) } }, google_client_secret: { type: :string, default: -> { legacy_env(:google_client_secret) }, required: true } } end |
.default_scope ⇒ Object
84 85 86 |
# File 'lib/standard_id/google/providers/google.rb', line 84 def default_scope DEFAULT_SCOPE end |
.exchange_code_for_user_info(code:, redirect_uri:, nonce: nil) ⇒ Object
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 |
# File 'lib/standard_id/google/providers/google.rb', line 88 def exchange_code_for_user_info(code:, redirect_uri:, nonce: nil) rescue_to_oauth_error do raise StandardId::InvalidRequestError, "Google authorization code is missing" if code.blank? creds = credentials if creds[:client_secret].blank? raise StandardId::InvalidRequestError, "Google OAuth credentials are incomplete: google_client_secret not set" end token_response = HttpClient.post_form(TOKEN_ENDPOINT, { client_id: creds[:client_id], client_secret: creds[:client_secret], code: code, grant_type: "authorization_code", redirect_uri: redirect_uri }.compact) unless token_response.is_a?(Net::HTTPSuccess) raise StandardId::InvalidRequestError, "Failed to exchange Google authorization code: #{error_reason(token_response)}" end parsed_token = JSON.parse(token_response.body) access_token = parsed_token["access_token"] raise StandardId::InvalidRequestError, "Google token response is missing access_token" if access_token.blank? # Web flow with a server-generated nonce: check it on the ID token. if parsed_token["id_token"].present? && nonce.present? verify_id_token(id_token: parsed_token["id_token"], nonce: nonce) end build_response(fetch_user_info(access_token: access_token), tokens: extract_tokens(parsed_token)) end end |
.fetch_user_info(access_token:) ⇒ Object
159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/standard_id/google/providers/google.rb', line 159 def fetch_user_info(access_token:) rescue_to_oauth_error do raise StandardId::InvalidRequestError, "Google access token is missing" if access_token.blank? verify_token(access_token) user_response = HttpClient.get_with_bearer(USERINFO_ENDPOINT, access_token) unless user_response.is_a?(Net::HTTPSuccess) raise StandardId::InvalidRequestError, "Failed to fetch Google user info: HTTP #{user_response.code}" end JSON.parse(user_response.body) end end |
.get_user_info(code: nil, id_token: nil, access_token: nil, redirect_uri: nil, nonce: nil, **_options) ⇒ Object
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 |
# File 'lib/standard_id/google/providers/google.rb', line 48 def get_user_info(code: nil, id_token: nil, access_token: nil, redirect_uri: nil, nonce: nil, **) if id_token.present? build_response( verify_id_token(id_token: id_token, nonce: nonce), tokens: { id_token: id_token } ) elsif access_token.present? build_response( fetch_user_info(access_token: access_token), tokens: { access_token: access_token } ) elsif code.present? exchange_code_for_user_info(code: code, redirect_uri: redirect_uri, nonce: nonce) else raise StandardId::InvalidRequestError, "Google sign-in requires a code, an id_token or an access_token" end end |
.provider_name ⇒ Object
31 32 33 |
# File 'lib/standard_id/google/providers/google.rb', line 31 def provider_name "google" end |
.supported_authorization_params ⇒ Object
35 36 37 |
# File 'lib/standard_id/google/providers/google.rb', line 35 def [:nonce, :login_hint, :prompt, :scope, :access_type, :hd, :response_mode, :include_granted_scopes] end |
.verify_id_token(id_token:, nonce: nil) ⇒ Object
Verifies through Google's tokeninfo endpoint, which checks the signature and expiry; the audience, issuer and nonce are checked here. Needs only the client ID.
126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 |
# File 'lib/standard_id/google/providers/google.rb', line 126 def verify_id_token(id_token:, nonce: nil) rescue_to_oauth_error do raise StandardId::InvalidRequestError, "Google id_token is missing" if id_token.blank? response = HttpClient.post_form(TOKEN_INFO_ENDPOINT, id_token: id_token) raise StandardId::InvalidRequestError, "Invalid Google ID token: invalid or expired" unless response.is_a?(Net::HTTPSuccess) token_info = JSON.parse(response.body) unless token_info["aud"].present? && token_info["aud"] == credentials[:client_id] raise StandardId::InvalidRequestError, "Invalid Google ID token audience" end unless VALID_ISSUERS.include?(token_info["iss"]) raise StandardId::InvalidRequestError, "Invalid Google ID token issuer" end # Constant-time, and the error never echoes either value. verify_nonce!(expected: nonce, actual: token_info["nonce"]) { "sub" => token_info["sub"], "email" => token_info["email"], "email_verified" => token_info["email_verified"], "name" => token_info["name"], "given_name" => token_info["given_name"], "family_name" => token_info["family_name"], "picture" => token_info["picture"], "locale" => token_info["locale"] }.compact end end |