Set default domain when using "Sign In With Google" - oauth-2.0

Using the instructions for Google sign in located here, We are implementing the "Sign in with Google" button.
This implementation has been successful, however, we want to set a default domain other than gmail.com as this will be used by our Gsuite for education users. This would make it so that users can click the sign in button and just type their username, instead of username#gSuiteDomain.com
When this button is clicked:
The user is redirected to:
How do we make it so the second image defaults to our gSuite domain instead of gmail.com?

I am pretty sure is you set the "hd" parameter to the G Suite domain name that it will work.
Please let us know if that works.

You can't use the simple directions at the location I linked in the question, as it is only for a basic minimum authentication.
For customization, you need to access the full client API here (server side validation can stay the same.)
This allows you to pass information to the init functions:
<script src="https://apis.google.com/js/platform.js?onload=onLoadCallback" async defer></script>
Then:
window.onLoadCallback = function () {
gapi.load('auth2', initSigninV2);
function initSigninV2() {
gapi.auth2.init({
client_id: 'Your_client_id',
hosted_domain: "YourHostedDomain.com"
}).then(function (authInstance) {
if (authInstance != null && authInstance != undefined) {
gapi.signin2.render("IDOfSignInButton", { onsuccess: onSignIn });
}
});
}
The rest is the same as in the original tutorial

Related

How to change the behavior of the "Sign In" button of the OAuthCard to just open the URL without redirect

I have the Webchat (from the Microsoft Bot Framework) running embedded in another application that uses a browser under the hood.
When trying to add Authentication to the bot, I realized that the OAuthCard's Sign-in button doesn't work because is trying to open a blank window (about:blank) that is used to redirect the user to the login page of the identity provider. In the embedded context, the OS doesn't know how to handle the about:blank call. See the image below.
I'm following this example, that it is actually working on the browser:
Add authentication to a bot
Bot authentication example
I want to know if there is a way to change the behavior of the "Sign In" button of the OAuthCard to just open the sign-in URI directly without using the about:blank and redirect technique.
I was able to make it work after finding out that was possible to change the button type of the OAuthCard from "signin" to "openUrl" before the Webchat does the rendering.
There seems to exist a similar issue with Microsoft Teams. Here where I found the clue:
https://github.com/microsoft/botframework-sdk/issues/4768
According to the Webchat reference, it is possible to intersect and change the activities:
https://github.com/microsoft/BotFramework-WebChat/blob/master/docs/API.md#web-chat-api-reference
Here is the solution, that is more like a hack. I hope this would be configurable in the future:
// Change the button type in the OAuthCard to the type of OpenUrl
const store = window.WebChat.createStore( {}, ( { dispatch } ) => next => action => {
if (action.type == "DIRECT_LINE/QUEUE_INCOMING_ACTIVITY" &&
action.payload.activity.hasOwnProperty("attachments") &&
action.payload.activity.attachments[0].contentType === "application/vnd.microsoft.card.oauth") {
action.payload.activity.attachments[0].content.buttons[0].type = "openUrl";
}
return next( action );
});
// Pass the store to the webchat
window.WebChat.renderWebChat({
...,
store
});

Auth pop up returning blank page

When I submit an auth call to google to I get the popup from the google window, then when I submit my credentials and press submit it forwards on to something like
https://auth.firebase.com/v2/FIREBASEREF/auth/google/callback?state= etc
And then all I get is a blank (blue background) screen.
$('#loginButton').click(function() {
myFirebaseRef.authWithOAuthPopup("google", function(error, authData) {
if (error) {
console.log("Login Failed!", error);
alert("Login Failed, please try again.")
} else {
console.log("Authenticated successfully with payload:", authData);
myUserID = authData.uid;
}
});
});
The same is also happening when trying to auth with Github, I can press "submit" or "login" etc. and then it just loads a blank page at auth.firebase.com
Any ideas?
Thanks
FWIW if anyone ends up on this issue: I was getting the exact same thing... the oauth popup was blank and not doing anything. I fixed it by removing the async/await from my form handler:
<form onSubmit={this.onSubmit}>
<button type="submit">Sign In with Google</button>
</form>
...
onSubmit = async event => {
await this.auth.signInWithPopup(this.googleProvider)
.then(....
Sometimes this should be an permissions issue related with the domain where you're trying to do the request like localhost or 127.0.0.1.
In that case, Go to Firebase Console -> Authentication -> Sign-In Method then scroll down to Authorized domains and add your current domain from you're trying to sign in e.g. 127.0.0.1.
Note: This is just for dev purposes, in case you want to deploy your app to Prod, you shouldn't have this configuration. For multiples environments you should check here
Another Note: If you're working with Javascript, you might add an event.preventDefault() before to call firebase.authWithOAuthPopup(...) to be able to obtain the result of the promise that return the authWithOAuthPopup function.
My solution was adding inAppBrowser from cordova and it no longer returned blank page.

Using a JSON user object for Authentication in AngularJS

So I've got an question about authentication and have been wondering how other people might handle this situation. I'm currently running an Angular app that is built on a Rails API.
So far for authentication I have a form that does a post to the Rails side which logs the user in and then sends them back to the Angular app on success. Once the cookie is set and the user is logged in, I'm able to access a user.json file which contains all the User information one might expect (Id, username, roles, rights, etc). Since verification all happens on Rails, if the user logs out then this information is removed. So the two states look like so...
Logged in
{
id: 99384,
name: "Username",
url: "//www.test.com/profiles/Username",
timezone: null,
rights: [ ],
roles: [
"admin"
],
}
Logged out
{
error: "You need to login or join before continuing."
}
So far I've seen all these millions of different ways to do auth for Angular, but it seems like nothing fits this type of method. So my question is, since the server is handling all of the verification, is there a way to just check if they user.json file is empty (displaying the error message) and if it is send the Angular app to the Rails login page? Is there really any point messing with Cookies, Tokens, etc when I can base it all on the JSON file?
You are already using cookies - the server is setting them. What you have done is a fairly standard way of doing things.
To check the json file, you can do something like this stub shows in your controller:
app.controller('AppControl', function($scope, $http, $location){
// Get the JSON file.
$http.get('/path/to/json/file')
.then(response){
if(response.data.error){
// redirect to login
$location.path('login');
}
else{
$scope.user = response.data;
// your app code here.
}
})
.catch(function (error){
// unable to reach the json file - handle this.
});
});
Of course, you should really move this out into a service so you can re-use it, and also cache the data, rather than getting the user every time you change route/page, but this gives you a vague idea.
EDIT Example factory:
.factory('User', function( $http ){
// Create a user object - this is ultimately what the factory will return.
// it's a singleton, so there will only ever by one instance of it.
var user = {};
// NOTE: I am assigning the "then" function of the login promise to
// "whenLoggedIn" - your controller code is then very easy to read.
user.whenLoggedIn = $http.get('user.json')
.then(function(response){
// Check to see if there is an error.
if (response.data.error !== undefined) {
// You could be more thorough with this check to determine the
// correct action (examine the error)
user.loggedIn = false;
}
else {
// the user is logged in
user.loggedIn = true;
user.details = response.data;
return user;
}
}).then; // <-- make sure you understand why that .then is there.
return user;
})
Usage in the controller
.controller('ExampleController', function($scope, User){
// It's handy to have the user on the scope - you can use it in your markup
// like I have with ng-show on index.html.
$scope.User = User;
// Do stuff only if the user is loggedin.
// See how neat this is because of the use of the .then function
User.whenLoggedIn( function (user){
console.log(user.details.name + " is logged in");
});
});
Because it's on the scope, we can do this in the html:
<body ng-controller="ExampleController">
<h1 ng-show="User.loggedIn == null">Logging in..</h1>
<h1 ng-show="User.loggedIn == true">Logged in as {{ User.details.name }}</h1>
<h1 ng-show="User.loggedIn == false">Not logged in</h1>
</body>
Here is an example on plunker where this is working.
Note the following:
If the user is/was already logged in, when you inject the service in the future, it won't check the file again. You could create other methods on the service that would re-check the file, and also log the user out, back in, etc. I will leave that up to you.
There are other ways to do this - this is just one possible option!
This might be obvious, but it's always worth saying. You need to primarily handle authentication and security on the server side. The client side is just user experience, and makes sure the user doesn't see confusing or conflicting screens.

Is it possible to limit the access given by a user so that it's read-only

I'm trying to use the YouTube Data API V2.0 to pull data insights for the videos/channels of our client. I have a developer key and a token that my client generates, and successfully figured out how to retrieve that information. My problem is, when my client uses the app for YouTube token generation, we are asking for an access that means EVERYTHING and to be able to "manage" their accounts.
This is a major concern for the client and they don't want us to have this kind of complete access. Is there a way to get a token generated with only read-only permission?
Thanks very much for any help!
I have successfully used https://googleapis.com/auth/youtube.readonly as a scope; if you ask for just that scope during the initial oAuth flow (and NOT for https://googleapis.com/auth/youtube at the same time, as that is the management scope which will override the readonly scope), then you will get a 403 error whenever attempting an action that requires management permissions (inserting, uploading, updating, deleting).
The google-api clients for v3 handle this quite smoothly, if you're using them. If you have written your own oAuth flow control, just make sure you have the sole readonly scope when requesting the initial token.
EDIT IN RESPONSE TO COMMENT: To view this in action (I'll use javascript to show), you can create a simple demo using the sample code provided by the API docs. Here's the general process:
1) In the Google API console, create a 'project' and authorize the YouTube API for that project (under the Services tab). Additionally,create a client ID for web applications (under the API access tab) and add in your domain as an authorized Javascript domain.
2) On your server, create and HTML file to serve as your interface (in this sample, it is designed to let you create a new playlist and add items to it). Here's the code, straight from the docs:
<!doctype html>
<html>
<head>
<title>Playlist Updates</title>
</head>
<body>
<div id="login-container" class="pre-auth">This application requires access to your YouTube account.
Please authorize to continue.
</div>
<div id="buttons">
<button id="playlist-button" disabled onclick="createPlaylist()">Create a new Private Playlist</button>
<br>
<label>Current Playlist Id: <input id="playlist-id" value='' type="text"/></label>
<br>
<label>Video Id: <input id="video-id" value='GZG9G5txtaE' type="text"/></label><button onclick="addVideoToPlaylist()">Add to current playlist</button>
</div>
<h3>Playlist: <span id="playlist-title"></span></h3>
<p id="playlist-description"></p>
<div id="playlist-container">
<span id="status">No Videos</span>
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.8.2/jquery.min.js"></script>
<script src="auth.js"></script>
<script src="playlist_updates.js"></script>
<script src="https://apis.google.com/js/client.js?onload=googleApiClientReady"></script>
</body>
</html>
3) In that same location, create the script "playlist_updates.js" with this code (again, straight from the docs):
// Some variables to remember state.
var playlistId, channelId;
// Once the api loads call a function to get the channel information.
function handleAPILoaded() {
enableForm();
}
// Enable a form to create a playlist.
function enableForm() {
$('#playlist-button').attr('disabled', false);
}
// Create a private playlist.
function createPlaylist() {
var request = gapi.client.youtube.playlists.insert({
part: 'snippet,status',
resource: {
snippet: {
title: 'Test Playlist',
description: 'A private playlist created with the YouTube API'
},
status: {
privacyStatus: 'private'
}
}
});
request.execute(function(response) {
var result = response.result;
if (result) {
playlistId = result.id;
$('#playlist-id').val(playlistId);
$('#playlist-title').html(result.snippet.title);
$('#playlist-description').html(result.snippet.description);
} else {
$('#status').html('Could not create playlist');
}
});
}
// Add a video id from a form to a playlist.
function addVideoToPlaylist() {
addToPlaylist($('#video-id').val());
}
// Add a video to a playlist.
function addToPlaylist(id, startPos, endPos) {
var details = {
videoId: id,
kind: 'youtube#video'
}
if (startPos != undefined) {
details['startAt'] = startPos;
}
if (endPos != undefined) {
details['endAt'] = endPos;
}
var request = gapi.client.youtube.playlistItems.insert({
part: 'snippet',
resource: {
snippet: {
playlistId: playlistId,
resourceId: details
}
}
});
request.execute(function(response) {
$('#status').html('<pre>' + JSON.stringify(response.result) + '</pre>');
});
}
Finally, create the file "auth.js" -- this is the code that actually does the oAuth2 flow:
// The client id is obtained from the Google APIs Console at https://code.google.com/apis/console
// If you run access this code from a server other than http://localhost, you need to register
// your own client id.
var OAUTH2_CLIENT_ID = '__YOUR_CLIENT_ID__';
var OAUTH2_SCOPES = [
'https://www.googleapis.com/auth/youtube'
];
// This callback is invoked by the Google APIs JS client automatically when it is loaded.
googleApiClientReady = function() {
gapi.auth.init(function() {
window.setTimeout(checkAuth, 1);
});
}
// Attempt the immediate OAuth 2 client flow as soon as the page is loaded.
// If the currently logged in Google Account has previously authorized OAUTH2_CLIENT_ID, then
// it will succeed with no user intervention. Otherwise, it will fail and the user interface
// to prompt for authorization needs to be displayed.
function checkAuth() {
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: true
}, handleAuthResult);
}
// Handles the result of a gapi.auth.authorize() call.
function handleAuthResult(authResult) {
if (authResult) {
// Auth was successful; hide the things related to prompting for auth and show the things
// that should be visible after auth succeeds.
$('.pre-auth').hide();
loadAPIClientInterfaces();
} else {
// Make the #login-link clickable, and attempt a non-immediate OAuth 2 client flow.
// The current function will be called when that flow is complete.
$('#login-link').click(function() {
gapi.auth.authorize({
client_id: OAUTH2_CLIENT_ID,
scope: OAUTH2_SCOPES,
immediate: false
}, handleAuthResult);
});
}
}
// Loads the client interface for the YouTube Analytics and Data APIs.
// This is required before using the Google APIs JS client; more info is available at
// http://code.google.com/p/google-api-javascript-client/wiki/GettingStarted#Loading_the_Client
function loadAPIClientInterfaces() {
gapi.client.load('youtube', 'v3', function() {
handleAPILoaded();
});
}
Note in there the OAUTH2_SCOPES constant. It's set to allow full management access, so if you then visit the html page in your browser and click on the 'authorize' link, you should see the window asking you to grant your domain access to manage your YouTube account. Do this, and then the code becomes functional ... you can add playlists and playlist items to your heart's content.
If you, however, then modify auth.js so that the OAUTH2_SCOPES looks like this:
var OAUTH2_SCOPES = [
'https://www.googleapis.com/auth/youtube.readonly'
];
and clear your cookies (to avoid inheriting the permissions you already granted ... just closing the browser and relaunching ought to be enough), then try again (visit the HTML, click the authorize link), you'll see that this time it's asking you to grant permission only to VIEW the account rather than manage it. If you grant that permission, then when you try to add a playlist through the interface you'll get an error message appearing that says you can't create the playlist.
If you're not using javascript, but instead a server-side language, as I mentioned the gapi clients are quite smooth. However, the handling of oAuth2 scope in these clients is not quite as transparent, and they're by design 'greedy' (in that, as it abstracts a service endpoint to an object, it will request the most thorough scope it needs to do any of the actions at that endpoint ... so even if you only intend to do list calls, if the service has an update action as well the client will request full management privileges). This can be modified, though, if you want to get into the client code -- or you could use it as a model for creating your own simplified client that you can granularly control in terms of scope.
That's about as thorough as I can be without knowing your underlying technologies. Hope the explanation helps!

Facebook Authorization on Rails app: why do we need to do Both server and client side authorization?

In Ryan's Railscast on Facebook authorization, he adds some Facebook SDK javascript at the end to "degrade facebook client side authorization with server side authorization." However, I do not see the use of it. If we already set up the authorization from the server side using omniauth, why do we have to add the client-side authorization again? What difference does it make?
The referenced javascript code is (From the linked Railscast):
jQuery ->
$('body').prepend('<div id="fb-root"></div>')
$.ajax
url: "#{window.location.protocol}//connect.facebook.net/en_US/all.js"
dataType: 'script'
cache: true
window.fbAsyncInit = ->
FB.init(appId: '<%= ENV["FACEBOOK_APP_ID"] %>', cookie: true)
$('#sign_in').click (e) ->
e.preventDefault()
FB.login (response) ->
window.location = '/auth/facebook/callback' if response.authResponse
$('#sign_out').click (e) ->
FB.getLoginStatus (response) ->
FB.logout() if response.authResponse
true
UPDATE:
One of the reasons we need to integrate FB.login authorization with the server-side authorization might be that the Omniauth server-side authorization does NOT work if it's accessed within the Facebook iFrame. If the user accesses the application for the first time, the application must ask for permissions; however, oAuth permission dialog cannot be loaded within the iFrame to prevent clickjacking. Calling FB.login can avoid such problem, because it will show the permission box as a popup(Omniauth popup option will not work).
So now I have a genuine reason to integrate client-side authorization, but the code from Railscasts does not work with my current settings. I've chosen to do it the following way.
Right now, I have the following script in my application.html.erb:
<script>
// Additional JS functions here
window.fbAsyncInit = function() {
FB.init({
appId : <%= ENV['FACEBOOK_KEY'] %>, // App ID
status : true, // check login status
cookie : true, // enable cookies to allow the server to access the session
xfbml : true // parse XFBML
});
};
// Load the SDK Asynchronously
(function(d){
var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0];
if (d.getElementById(id)) {return;}
js = d.createElement('script'); js.id = id; js.async = true;
js.src = "//connect.facebook.net/en_US/all.js";
ref.parentNode.insertBefore(js, ref);
}(document));
</script>
And in my view, I have the following link invoking the Facebook log in action:
<%= link_to 'log in with facebook', '/auth/facebook', id: 'fb_log_in_link' %>
I add the following script to the view page where I have the login link.
function login() {
FB.login(function(response) {
if (response.authResponse) {
window.location = '/auth/facebook/callback'
}
});
}
Also, I need to change the link to call the function instead of directing to /auth/facebook/
<%= link_to_function 'log in with facebook', 'login()' %>
Done! The server-side and client-side authorization are fully integrated. Since I was still confused after watching Ryan's Railscast, I want to add a little bit of explanation for those who might be also confused.
The way this works:
Facebook SDK is initailized when the while the page is loaded.
The user clicks the "log in with Facebook" link.
FB.login function is called by the link, and the user goes through all the permissions process (e.g. permission dialog showing up asking for the user's permissions).
Then, the user is directed to /auth/facebook/callback. From routes.rb we have the line match 'auth/:provider/callback', to: 'sessions#create'. Therefore, now the server will either create a new user or simply create a session if the user has already registered before.
Done! The user is logged in.
Merging server-side and client-side authorization has two major advantages:
1. If the user is logged into the application either inside Facebook(via appcenter) he will be logged into the application outside Facebook as well. Vice versa, if the user logs in outside Facebook, he will be logged in automatically if he accesses it within Facebook after.
2. Logging in with /auth/facebook does not work if the user logs in within Facebook iFrame. To prevent clickjacking Facebook prohibits prompting users to auth permissions dialog within Facebook iFrame. The only way to avoid this is to open the dialog in a separate popup, and logging in with FB.login solves the problem.
the short answer is - you don't.
you can choose between client side login (via javascript SDK) and server side login using omniauth.
the disadventage of server-side login is overloading the server for a call you can do from the client.
the advantage is that usually the token is longer (3 months token and not 1-2 hours like client side).
i suggest combine the two. use the client side for initial login, once you do that have an async call from the server side for extended token (only if you have to).
It just says,
Facebook provides a JavaScript SDK that we can use to authenticate a user on the client-side so that it doesn’t look to them like they’ve left our application then returned.
It means that this is for the client side understanding that when user returned from the application, it doesn't look like that they have indeed left it.

Resources