6
votes

Developing a Flask app (Python3/Heroku) for internal company use and successfully implemented Google Login (Oauth2) based on brijieshb42's article which uses requests_oauthlib.

Research has indicated that if I pass parameter "hd" (hosted domain) in my authorization url it should do the trick. E.g.

https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=OUR_CLIENT_ID&redirect_uri=https%3A%2F%2FOUR_APP.herokuapp.com%2Fconnect&scope=profile+email&state=STATE&hd=our_google_apps_domain.com&access_type=offline

My understanding based is that this parameter should provide client-side restriction and only allow logins from emails from our google apps domain (server-side I'll handle after this!) based on Google Documentation, this mailing list post and these stackoverflow posts: post1, post2.

However, though my code generates the authorization URL I pasted above -- I can still login with my personal gmail account (@gmail.com vs @our apps domain.com).

Can anyone shed some light as to why this isn't working? Or provide a different approach? Basically would prefer preventing non-employees from logging in.

I can share code as needed, but pretty much pasted from the brijeshb42 article and essentially looks like this:

OAuth2Session(
  OUR_CLIENT_ID,
  redirect_uri=https://OUR_APP.herokuapp.com/connect,
  scope=['profile', 'email']).authorization_url(
      https://accounts.google.com/o/oauth2/auth,
      hd='our_google_apps_domain.com',
      access_type='offline')

Which returns the auth url I pasted above!

2
If I understood correctly: before generating the authorization URL, your app should check if the e-mail is on the list of employees e-mails. If it isn't, then you don't want them to log in.Cătălin Matei
Correct me if I'm wrong, but my understanding of the oauth2 flow is that you don't actually receive the email until they've logged in/been authenticated because they enter the email into the Google Sign-In page and you receive a response based on the auth flow with the email e.g. (sorry for shitty formatting from brijesh article/same as how I did) google = get_google_auth(token=token) resp = google.get(Auth.USER_INFO) if resp.status_code == 200: user_data = resp.json() email = user_data['email']danchow
You are right, you don't know the e-mail beforehand. My advice is to check on your app too the user's e-mail anyway, regardless of what google says. You have to match the USER_INFO with an user in your app anyway, so if you don't have an active user to match it then authentication should fail.Cătălin Matei
The hd param is still useful because Google's auth system can then automatically pick the right user, and skip a step in the auth process.Chris
Hrm, I would need to retest -- I used the hd parameter before (1.5 years ago and it did nothing) -- maybe it works now?danchow

2 Answers

2
votes

After successful authentication, you have to check the provided email yourself. I have added the code snippet from the my article that you have referenced. I have added the extra check required in after comment.

@app.route('/gCallback')
def callback():
    # Redirect user to home page if already logged in.
    if current_user is not None and current_user.is_authenticated():
        return redirect(url_for('index'))
    if 'error' in request.args:
        if request.args.get('error') == 'access_denied':
            return 'You denied access.'
        return 'Error encountered.'
    if 'code' not in request.args and 'state' not in request.args:
        return redirect(url_for('login'))
    else:
        # Execution reaches here when user has
        # successfully authenticated our app.
        google = get_google_auth(state=session['oauth_state'])
        try:
            token = google.fetch_token(
                Auth.TOKEN_URI,
                client_secret=Auth.CLIENT_SECRET,
                authorization_response=request.url)
        except HTTPError:
            return 'HTTPError occurred.'
        google = get_google_auth(token=token)
        resp = google.get(Auth.USER_INFO)
        if resp.status_code == 200:
            user_data = resp.json()
            email = user_data['email']
            """
            Your Domain specific check will come here.
            """
            if email.split('@')[1] != 'domain.com':
                flash('You cannot login using this email', 'error')
                return redirect(url_for('login'))
            user = User.query.filter_by(email=email).first()
            if user is None:
                user = User()
                user.email = email
            user.name = user_data['name']
            print(token)
            user.tokens = json.dumps(token)
            user.avatar = user_data['picture']
            db.session.add(user)
            db.session.commit()
            login_user(user)
            return redirect(url_for('index'))
        return 'Could not fetch your information.'
1
votes

When you create the authorization URL, you can append optional parameters; appending hd= ... will do the trick:

auth_url, state = google.authorization_url(AUTH_URI, access_type='offline', hd='savv.ch')

This has many benefits. For example Google will then automatically pick the right account (if it matches the domain), which potentially saves a step in the Auth process, if the user is logged into multiple accounts.

http://requests-oauthlib.readthedocs.io/en/latest/api.html#requests_oauthlib.OAuth2Session.authorization_url