Skip to content

Commit baaf3b7

Browse files
authored
chore(doc): update JavaScript example in README (#472)
* chore(doc): update JavaScript example in README * chore(doc): update example for javascript
1 parent dcea9d2 commit baaf3b7

File tree

3 files changed

+155
-143
lines changed

3 files changed

+155
-143
lines changed

README.md

Lines changed: 90 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -239,53 +239,109 @@ For your views you can login using:
239239

240240
An overview is available at https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview
241241

242+
#### Note about multi-platform authentication (Web, Android, IOS, ...)
243+
244+
If you authenticate your user from multiple different platforms with a single API you will likely have different Google `client_id` depending on the platform.
245+
246+
This could raise errors in the callback step because the `client_id` used in the callback needs to be the same as the one used in the sign in request.
247+
248+
To handle multiple `client_id` you can register multiple omniauth middlewares in your devise initializer with different names and different client ids. You can then register each middleware in your omniauthable model and add a new action in your `OmniauthCallbacksController` for each additional middleware.
249+
250+
```ruby
251+
# config/initializers/devise.rb
252+
253+
config.omniauth :google_oauth2, 'GOOGLE_CLIENT_ID', 'GOOGLE_CLIENT_SECRET', { name: 'google_oauth2' }
254+
255+
# Native mobile applications don't require a `client_secret`
256+
config.omniauth :google_oauth2, 'GOOGLE_CLIENT_ID_ANDROID', { name: 'google_oauth2_android' }
257+
config.omniauth :google_oauth2, 'GOOGLE_CLIENT_ID_IOS', { name: 'google_oauth2_ios' }
258+
```
259+
260+
```ruby
261+
# app/models/user.rb
262+
263+
devise :omniauthable, omniauth_providers: %i[google_oauth2 google_oauth2_android google_oauth2_ios]
264+
```
265+
266+
```ruby
267+
# app/controllers/users/omniauth_callbacks_controller.rb:
268+
269+
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
270+
def google_oauth2
271+
# ...
272+
end
273+
274+
def google_oauth2_android
275+
# ...
276+
end
277+
278+
def google_oauth2_ios
279+
# ...
280+
end
281+
end
282+
```
283+
242284
### One-time Code Flow (Hybrid Authentication)
243285

244-
Google describes the One-time Code Flow [here](https://developers.google.com/identity/sign-in/web/server-side-flow). This hybrid authentication flow has significant functional and security advantages over a pure server-side or pure client-side flow. The following steps occur in this flow:
286+
Google describes the One-time Code Flow [here](https://developers.google.com/identity/protocols/oauth2). This hybrid authentication flow has significant functional and security advantages over a pure server-side or pure client-side flow. The following steps occur in this flow:
245287

246-
1. The client (web browser) authenticates the user directly via Google's JS API. During this process assorted modals may be rendered by Google.
288+
1. The client (web browser) authenticates the user directly via Google's OAuth 2 API. During this process assorted modals may be rendered by Google.
247289
2. On successful authentication, Google returns a one-time use code, which requires the Google client secret (which is only available server-side).
248290
3. Using a AJAX request, the code is POSTed to the Omniauth Google OAuth2 callback.
249-
4. The Omniauth Google OAuth2 gem will validate the code via a server-side request to Google. If the code is valid, then Google will return an access token and, if this is the first time this user is authenticating against this application, a refresh token. Both of these should be stored on the server. The response to the AJAX request indicates the success or failure of this process.
291+
4. The Omniauth Google OAuth2 gem will validate the code via a server-side request to Google. If the code is valid, then Google will return an access token and, if this is the first time this user is authenticating against this application, a refresh token. Both of these should be stored on the server. The response to the AJAX request indicates the success or failure of this process.
250292

251293
This flow is immune to replay attacks, and conveys no useful information to a man in the middle.
252294

253295
The omniauth-google-oauth2 gem supports this mode of operation when `provider_ignores_state` is set to `true`. Implementors simply need to add the appropriate JavaScript to their web page, and they can take advantage of this flow. An example JavaScript snippet follows.
254296

255297
```javascript
256298
// Basic hybrid auth example following the pattern at:
257-
// https://developers.google.com/identity/sign-in/web/reference
258-
259-
<script src="https://apis.google.com/js/platform.js?onload=init" async defer></script>
260-
261-
...
262-
263-
function init() {
264-
gapi.load('auth2', function() {
265-
// Ready.
266-
$('.google-login-button').click(function(e) {
267-
e.preventDefault();
268-
269-
gapi.auth2.authorize({
270-
client_id: 'YOUR_CLIENT_ID',
271-
cookie_policy: 'single_host_origin',
272-
scope: 'email profile',
273-
response_type: 'code'
274-
}, function(response) {
275-
if (response && !response.error) {
276-
// google authentication succeed, now post data to server.
277-
jQuery.ajax({type: 'POST', url: '/auth/google_oauth2/callback', data: response,
278-
success: function(data) {
279-
// response from server
280-
}
281-
});
282-
} else {
283-
// google authentication failed
284-
}
285-
});
286-
});
299+
// https://developers.google.com/identity/protocols/oauth2/javascript-implicit-flow
300+
301+
const handleGoogleOauthSignIn = () => {
302+
// Google's OAuth 2.0 endpoint for requesting an access token
303+
const oauth2Endpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
304+
305+
// Parameters to pass to OAuth 2.0 endpoint.
306+
const params = new URLSearchParams({
307+
client_id: YOUR_CLIENT_ID,
308+
prompt: 'select_account',
309+
redirect_uri: YOUR_REDIRECT_URI, // This redirect_uri needs to redirect to the same domain as the one where this request is made from.
310+
response_type: 'code',
311+
scope: 'email openid profile',
312+
state: 'google', // The state will be added in the redirect_uri's query params. Use can this if you use the same redirect_uri with different omniauth provider to know which one you're currently handling for example.
313+
});
314+
315+
const url = `${oauth2Endpoint}?${params.toString()}`;
316+
317+
// Create <a> element to redirect to OAuth 2.0 endpoint.
318+
const a = document.createElement('a');
319+
a.href = url;
320+
a.target = '_self';
321+
322+
// Add a to page and click it to open the OAuth 2.0 endpoint.
323+
document.body.appendChild(a);
324+
a.click();
325+
}
326+
327+
// Call this method when redirected to your `redirect_uri`
328+
const handleGoogleOauthCallback = async () => {
329+
// Get the query params Google included in your `redirect_uri`
330+
const params = new URL(document.location.toString()).searchParams;
331+
const code = params.get('code');
332+
const state = params.get('state') // the `state` you added in the sign in request is here if you need it.
333+
334+
const response = fetch('your.api.domain/auth/google_oauth2/callback', {
335+
body: JSON.stringify({
336+
code,
337+
redirect_uri: YOUR_REDIRECT_URI, // The `redirect_uri` used in the server needs to be the same as as initially used in the client.
338+
}),
339+
headers: {
340+
'Content-type': 'application/json',
341+
},
342+
method: 'POST',
287343
});
288-
};
344+
}
289345
```
290346

291347
#### Note about mobile clients (iOS, Android)
@@ -298,66 +354,6 @@ In that case, ensure to send an additional parameter `redirect_uri=` (empty stri
298354

299355
If you're making POST requests to `/auth/google_oauth2/callback` from another domain, then you need to make sure `'X-Requested-With': 'XMLHttpRequest'` header is included with your request, otherwise your server might respond with `OAuth2::Error, : Invalid Value` error.
300356

301-
#### Getting around the `redirect_uri_mismatch` error (See [Issue #365](https://github.com/zquestz/omniauth-google-oauth2/issues/365))
302-
303-
If you are struggling with a persistent `redirect_uri_mismatch`, you can instead pass the `access_token` from [`getAuthResponse`](https://developers.google.com/identity/sign-in/web/reference#googleusergetauthresponseincludeauthorizationdata) directly to the `auth/google_oauth2/callback` endpoint, like so:
304-
305-
```javascript
306-
// Initialize the GoogleAuth object
307-
let googleAuth;
308-
gapi.load('client:auth2', async () => {
309-
await gapi.client.init({ scope: '...', client_id: '...' });
310-
googleAuth = gapi.auth2.getAuthInstance();
311-
});
312-
313-
// Call this when the Google Sign In button is clicked
314-
async function signInGoogle() {
315-
const googleUser = await googleAuth.signIn(); // wait for the user to authorize through the modal
316-
const { access_token } = googleUser.getAuthResponse();
317-
318-
const data = new FormData();
319-
data.append('access_token', access_token);
320-
321-
const response = await api.post('/auth/google_oauth2/callback', data)
322-
console.log(response);
323-
}
324-
```
325-
326-
#### Using Axios
327-
If you're making a GET resquests from another domain using `access_token`.
328-
```
329-
axios
330-
.get(
331-
'url(path to your callback}',
332-
{ params: { access_token: 'token' } },
333-
headers....
334-
)
335-
```
336-
337-
If you're making a POST resquests from another domain using `access_token`.
338-
```
339-
axios
340-
.post(
341-
'url(path to your callback}',
342-
{ access_token: 'token' },
343-
headers....
344-
)
345-
346-
--OR--
347-
348-
axios
349-
.post(
350-
'url(path to your callback}',
351-
null,
352-
{
353-
params: {
354-
access_token: 'token'
355-
},
356-
headers....
357-
}
358-
)
359-
```
360-
361357
## Fixing Protocol Mismatch for `redirect_uri` in Rails
362358

363359
Just set the `full_host` in OmniAuth based on the Rails.env.

examples/Gemfile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
source 'https://rubygems.org'
44

55
gem 'omniauth-google-oauth2', path: '..'
6+
gem 'rackup'
67
gem 'rubocop'
78
gem 'sinatra'
89
gem 'webrick'

examples/config.ru

Lines changed: 64 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -38,62 +38,77 @@ class App < Sinatra::Base
3838
<html>
3939
<head>
4040
<title>Google OAuth2 Example</title>
41-
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
41+
</head>
42+
43+
<body>
44+
<ul>
45+
<li>
46+
<form method="post" action="/auth/google_oauth2">
47+
<input type="hidden" name="authenticity_token" value="#{request.env['rack.session']['csrf']}">
48+
<button type="submit">Login with Google</button>
49+
</form>
50+
</li>
51+
52+
<li>
53+
<a href="#" class="googleplus-login">Sign in with Google via AJAX</a>
54+
</li>
55+
</ul>
56+
4257
<script>
43-
jQuery(function() {
44-
return $.ajax({
45-
url: 'https://apis.google.com/js/client:plus.js?onload=gpAsyncInit',
46-
dataType: 'script',
47-
cache: true
48-
});
49-
});
58+
const a = document.querySelector('.googleplus-login');
5059
51-
window.gpAsyncInit = function() {
52-
gapi.auth.authorize({
53-
immediate: true,
54-
response_type: 'code',
55-
cookie_policy: 'single_host_origin',
60+
const handleGoogleOauthSignIn = () => {
61+
const oauth2Endpoint = 'https://accounts.google.com/o/oauth2/v2/auth';
62+
63+
const params = new URLSearchParams({
5664
client_id: '#{ENV['GOOGLE_KEY']}',
57-
scope: 'email profile'
58-
}, function(response) {
59-
return;
60-
});
61-
$('.googleplus-login').click(function(e) {
62-
e.preventDefault();
63-
gapi.auth.authorize({
64-
immediate: false,
65-
response_type: 'code',
66-
cookie_policy: 'single_host_origin',
67-
client_id: '#{ENV['GOOGLE_KEY']}',
68-
scope: 'email profile'
69-
}, function(response) {
70-
if (response && !response.error) {
71-
// google authentication succeed, now post data to server.
72-
jQuery.ajax({type: 'POST', url: "/auth/google_oauth2/callback", data: response,
73-
success: function(data) {
74-
// Log the data returning from google.
75-
console.log(data)
76-
}
77-
});
78-
} else {
79-
// google authentication failed.
80-
console.log("FAILED")
81-
}
82-
});
65+
prompt: 'select_account',
66+
redirect_uri: 'http://localhost:3000/callback',
67+
response_type: 'code',
68+
scope: 'email openid profile',
8369
});
84-
};
70+
71+
const url = `${oauth2Endpoint}?${params.toString()}`;
72+
window.location.href = url;
73+
}
74+
75+
a.addEventListener('click', event => {
76+
event.preventDefault();
77+
handleGoogleOauthSignIn();
78+
});
8579
</script>
80+
</body>
81+
</html>
82+
HTML
83+
end
84+
85+
get '/callback' do
86+
<<-HTML
87+
<!DOCTYPE html>
88+
<html>
89+
<head>
90+
<title>Google OAuth2 Example</title>
8691
</head>
92+
8793
<body>
88-
<ul>
89-
<li>
90-
<form method='post' action='/auth/google_oauth2'>
91-
<input type="hidden" name="authenticity_token" value="#{request.env['rack.session']['csrf']}">
92-
<button type='submit'>Login with Google</button>
93-
</form>
94-
</li>
95-
<li><a href='#' class="googleplus-login">Sign in with Google via AJAX</a></li>
96-
</ul>
94+
<p>Redirected</p>
95+
96+
<script>
97+
const handleGoogleOauthCallback = async () => {
98+
const params = new URL(document.location.toString()).searchParams;
99+
const code = params.get('code');
100+
101+
const response = fetch('http://localhost:3000/auth/google_oauth2/callback', {
102+
body: JSON.stringify({ code, redirect_uri: 'http://localhost:3000/callback' }),
103+
headers: {
104+
'Content-type': 'application/json',
105+
},
106+
method: 'POST',
107+
});
108+
}
109+
110+
handleGoogleOauthCallback();
111+
</script>
97112
</body>
98113
</html>
99114
HTML

0 commit comments

Comments
 (0)