101

I can't seem to find any documentation on how to restrict the login to my web application (which uses OAuth2.0 and Google APIs) to only accept authentication requests from users with an email on a specific domain name or set of domain names. I would like to whitelist as opposed to blacklist.

Does anyone have suggestions on how to do this, documentation on the officially accepted method of doing so, or an easy, secure work around?

For the record, I do not know any info about the user until they attempt to log in through Google's OAuth authentication. All I receive back is the basic user info and email.

Miguel Andres
  • 1,410
  • 11
  • 11
paradox870
  • 2,152
  • 4
  • 20
  • 30
  • 3
    I'm researching this also. I have an app that I want to be only accessible by people who have an account on our google apps for business domain. The google OpenID implementation may be more appropriate for both of us... – Aaron Bruce Jun 09 '12 at 00:22
  • 1
    How can I implement domain user login using google sdk and c#? – user1021583 Jul 23 '15 at 09:38
  • 1
    Please can some one look at this question http://stackoverflow.com/questions/34220051/oauth2-restrict-to-emails-with-specfic-domain-name-using-hd-not-working –  Dec 15 '15 at 18:29
  • 1
    Please i have an oben bounty on that question so can some one help me –  Dec 15 '15 at 18:31

6 Answers6

45

So I've got an answer for you. In the OAuth request you can add hd=example.com and it will restrict authentication to users from that domain (I don't know if you can do multiple domains). You can find hd parameter documented here

I'm using the Google API libraries from here: http://code.google.com/p/google-api-php-client/wiki/OAuth2 so I had to manually edit the /auth/apiOAuth2.php file to this:

public function createAuthUrl($scope) {
    $params = array(
        'response_type=code',
        'redirect_uri=' . urlencode($this->redirectUri),
        'client_id=' . urlencode($this->clientId),
        'scope=' . urlencode($scope),
        'access_type=' . urlencode($this->accessType),
        'approval_prompt=' . urlencode($this->approvalPrompt),
        'hd=example.com'
    );

    if (isset($this->state)) {
        $params[] = 'state=' . urlencode($this->state);
    }
    $params = implode('&', $params);
    return self::OAUTH2_AUTH_URL . "?$params";
}

I'm still working on this app and found this, which may be the more correct answer to this question. https://developers.google.com/google-apps/profiles/

Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
Aaron Bruce
  • 1,130
  • 1
  • 12
  • 24
  • I wasn't aware of this parameter, can you link to where you found out about it? – Jason Hall Jun 12 '12 at 17:21
  • Unfortunately I had to get the info from a colleague of mine, I didn't find this anywhere in google's docs. My coworker thinks he found the reference in the OpenID spec and tried it out here in the OpenAuth spec and it seems to work. Use with caution I suppose since it seems to be undocumented functionality. – Aaron Bruce Jun 13 '12 at 21:44
  • 37
    Important Note: Even though you are specifying an `hd` parameter in the `createAuthUrl` function, you will still need to verify that the user is logging in with your domain email address. It's very easy to change the link parameter to allow all email addresses and subsequently gain access to your application. – VictorKilo Feb 07 '13 at 23:00
  • I did a similar hack to set the loginHint (user email), which also if contains a hosted domain will redirect to the SAML login. Unfortunately I hadnt found this thread first and didnt know about the hd parameter. http://stackoverflow.com/questions/19653680/google-api-allow-connections-only-with-google-apps-domain/19691368#19691368 – HdN8 May 12 '14 at 12:24
  • The hd param does absolutely nothing for me. – Tyguy7 Nov 26 '15 at 00:52
  • The hd was working for a while but it suddenly stopped,so is there another solution –  Dec 12 '15 at 07:58
  • Hm, not that I know about. I'm not working on any apps that use this feature anymore so I haven't had to dig into it. It might warrant a new question on SO since this answer seems to be outdated and wrong – Aaron Bruce Dec 14 '15 at 00:02
  • 1
    For the Google Documentation on the `hd` parameter usage see https://developers.google.com/identity/work/it-apps And the reference of the `hd` URI parameter can be found https://developers.google.com/identity/protocols/OpenIDConnect#authenticationuriparameters In synopsis, the `hd` param should be viewed as a domain based display filter for the Google Auth side, but should still be validated on your side. – Will B. Apr 06 '16 at 14:07
  • 2
    Great, Currently, in `hd` parameter, I can only restrict one domain, Now what if I want to restrict two or three domains? – Jay Patel Oct 18 '18 at 06:52
  • Answer by @JBithell also works well... if using the PHP library just use $client->setHostedDomain('yourdomain.com') – cwd May 08 '20 at 14:19
13

Client Side:

Using the auth2 init function, you can pass the hosted_domain parameter to restrict the accounts listed on the signin popup to those matching your hosted_domain. You can see this in the documentation here: https://developers.google.com/identity/sign-in/web/reference

Server Side:

Even with a restricted client-side list you will need to verify that the id_token matches the hosted domain you specified. For some implementations this means checking the hd attribute you receive from Google after verifying the token.

Full Stack Example:

Web Code:

gapi.load('auth2', function () {
    // init auth2 with your hosted_domain
    // only matching accounts will show up in the list or be accepted
    var auth2 = gapi.auth2.init({
        client_id: "your-client-id.apps.googleusercontent.com",
        hosted_domain: 'your-special-domain.example'
    });

    // setup your signin button
    auth2.attachClickHandler(yourButtonElement, {});

    // when the current user changes
    auth2.currentUser.listen(function (user) {
        // if the user is signed in
        if (user && user.isSignedIn()) {
            // validate the token on your server,
            // your server will need to double check that the
            // `hd` matches your specified `hosted_domain`;
            validateTokenOnYourServer(user.getAuthResponse().id_token)
                .then(function () {
                    console.log('yay');
                })
                .catch(function (err) {
                    auth2.then(function() { auth2.signOut(); });
                });
        }
    });
});

Server Code (using googles Node.js library):

If you're not using Node.js you can view other examples here: https://developers.google.com/identity/sign-in/web/backend-auth

const GoogleAuth = require('google-auth-library');
const Auth = new GoogleAuth();
const authData = JSON.parse(fs.readFileSync(your_auth_creds_json_file));
const oauth = new Auth.OAuth2(authData.web.client_id, authData.web.client_secret);

const acceptableISSs = new Set(
    ['accounts.google.com', 'https://accounts.google.com']
);

const validateToken = (token) => {
    return new Promise((resolve, reject) => {
        if (!token) {
            reject();
        }
        oauth.verifyIdToken(token, null, (err, ticket) => {
            if (err) {
                return reject(err);
            }
            const payload = ticket.getPayload();
            const tokenIsOK = payload &&
                  payload.aud === authData.web.client_id &&
                  new Date(payload.exp * 1000) > new Date() &&
                  acceptableISSs.has(payload.iss) &&
                  payload.hd === 'your-special-domain.example';
            return tokenIsOK ? resolve() : reject();
        });
    });
};
Stephen Ostermiller
  • 23,933
  • 14
  • 88
  • 109
Jordon Biondo
  • 3,974
  • 1
  • 27
  • 37
9

When defining your provider, pass in a hash at the end with the 'hd' parameter. You can read up on that here. https://developers.google.com/accounts/docs/OpenIDConnect#hd-param

E.g., for config/initializers/devise.rb

config.omniauth :google_oauth2, 'identifier', 'key', {hd: 'yourdomain.com'}
Kosmonaut
  • 2,154
  • 2
  • 18
  • 19
  • 1
    This can easily be circumvented giving access to login with other domains. It will only work for limiting the available accounts shown to the user. – homaxto Jan 14 '16 at 14:07
2

Here's what I did using passport in node.js. profile is the user attempting to log in.

//passed, stringified email login
var emailString = String(profile.emails[0].value);
//the domain you want to whitelist
var yourDomain = '@google.com';
//check the x amount of characters including and after @ symbol of passed user login.
//This means '@google.com' must be the final set of characters in the attempted login 
var domain = emailString.substr(emailString.length - yourDomain.length);

//I send the user back to the login screen if domain does not match 
if (domain != yourDomain)
   return done(err);

Then just create logic to look for multiple domains instead of just one. I believe this method is secure because 1. the '@' symbol is not a valid character in the first or second part of an email address. I could not trick the function by creating an email address like mike@fake@google.com 2. In a traditional login system I could, but this email address could never exist in Google. If it's not a valid Google account, you can't login.

mjoyce91
  • 316
  • 2
  • 19
2

Since 2015 there has been a function in the library to set this without needing to edit the source of the library as in the workaround by aaron-bruce

Before generating the url just call setHostedDomain against your Google Client

$client->setHostedDomain("HOSTED DOMAIN")
JBithell
  • 627
  • 2
  • 11
  • 27
0

For login with Google using Laravel Socialite https://laravel.com/docs/8.x/socialite#optional-parameters

use Laravel\Socialite\Facades\Socialite;

return Socialite::driver('google')
    ->with(['hd' => 'pontomais.com.br'])
    ->redirect();
DEV Tiago França
  • 1,271
  • 9
  • 9