ChatGPT解决这个技术问题 Extra ChatGPT

Using Google OAuth2 with Flask

Can anyone point me to a complete example for authenticating with Google accounts using OAuth2 and Flask, and not on App Engine?

I am trying to have users give access to Google Calendar, and then use that access to retrieve information from the calendar and process it further. I also need to store and later refresh the OAuth2 tokens.

I have looked at Google's oauth2client library and can get the dance started to retrieve the authorization code, but I'm a little lost from there. Looking at Google's OAuth 2.0 Playground I understand that I need to request the refresh token and access token, but the provided examples in the library are for App Engine and Django only.

I have also tried using Flask's OAuth module that contains references to OAuth2, but I don't see any way to exchange the authorization code there either.

I could probably hand code the requests, but would much prefer to use or adapt an existing python module that makes requests easy, properly handles possible responses and maybe even assists in storage of tokens.

Is there such a thing?


e
erip

Another answer mentions Flask-Rauth, but doesn't go into detail about how to use it. There are a few Google-specific gotchas, but I have implemented it finally and it works well. I integrate it with Flask-Login so I can decorate my views with useful sugar like @login_required.

I wanted to be able to support multiple OAuth2 providers, so part of the code is generic and based on Miguel Grinberg's excellent post about supporting OAuth2 with Facebook and Twitter here.

First, add your specific Google authentication information from Google into your app's configuration:

GOOGLE_LOGIN_CLIENT_ID = "<your-id-ending-with>.apps.googleusercontent.com"
GOOGLE_LOGIN_CLIENT_SECRET = "<your-secret>"

OAUTH_CREDENTIALS={
        'google': {
            'id': GOOGLE_LOGIN_CLIENT_ID,
            'secret': GOOGLE_LOGIN_CLIENT_SECRET
        }
}

And when you create your app (in my case, the module's __init__.py):

app = Flask(__name__)
app.config.from_object('config')

In your app module, create auth.py:

from flask import url_for, current_app, redirect, request
from rauth import OAuth2Service

import json, urllib2

class OAuthSignIn(object):
    providers = None

    def __init__(self, provider_name):
        self.provider_name = provider_name
        credentials = current_app.config['OAUTH_CREDENTIALS'][provider_name]
        self.consumer_id = credentials['id']
        self.consumer_secret = credentials['secret']

    def authorize(self):
        pass

    def callback(self):
        pass

    def get_callback_url(self):
        return url_for('oauth_callback', provider=self.provider_name,
                        _external=True)

    @classmethod
    def get_provider(self, provider_name):
        if self.providers is None:
            self.providers={}
            for provider_class in self.__subclasses__():
                provider = provider_class()
                self.providers[provider.provider_name] = provider
        return self.providers[provider_name]

class GoogleSignIn(OAuthSignIn):
    def __init__(self):
        super(GoogleSignIn, self).__init__('google')
        googleinfo = urllib2.urlopen('https://accounts.google.com/.well-known/openid-configuration')
        google_params = json.load(googleinfo)
        self.service = OAuth2Service(
                name='google',
                client_id=self.consumer_id,
                client_secret=self.consumer_secret,
                authorize_url=google_params.get('authorization_endpoint'),
                base_url=google_params.get('userinfo_endpoint'),
                access_token_url=google_params.get('token_endpoint')
        )

    def authorize(self):
        return redirect(self.service.get_authorize_url(
            scope='email',
            response_type='code',
            redirect_uri=self.get_callback_url())
            )

    def callback(self):
        if 'code' not in request.args:
            return None, None, None
        oauth_session = self.service.get_auth_session(
                data={'code': request.args['code'],
                      'grant_type': 'authorization_code',
                      'redirect_uri': self.get_callback_url()
                     },
                decoder = json.loads
        )
        me = oauth_session.get('').json()
        return (me['name'],
                me['email'])

This creates a generic OAuthSignIn class that can be subclassed. The Google subclass pulls its information from Google's published list of information (in JSON format here). This is information that is subject to change, so this approach will make sure it is always up-to-date. One limitation of this is that if an Internet connection is not available on your server at the time the Flask application is initialized (the module imported), it will not be instantiated correctly. This should almost never be a problem, but storing last-known values in the configuration database to cover this eventuality is a good idea.

Finally, the class returns a tuple of name, email in the callback() function. Google actually returns a lot more information, including the Google+ profile if available. Inspect the dictionary returned by oauth_session.get('').json() to see it all. If in the authorize() function you expand the scope (for my app, email is sufficient), you can get access to even more information through the Google API.

Next, write the views to tie it all together:

from flask.ext.login import login_user, logout_user, current_user, login_required

@app.route('/authorize/<provider>')
def oauth_authorize(provider):
    # Flask-Login function
    if not current_user.is_anonymous():
        return redirect(url_for('index'))
    oauth = OAuthSignIn.get_provider(provider)
    return oauth.authorize()

@app.route('/callback/<provider>')
def oauth_callback(provider):
    if not current_user.is_anonymous():
        return redirect(url_for('index'))
    oauth = OAuthSignIn.get_provider(provider)
    username, email = oauth.callback()
    if email is None:
        # I need a valid email address for my user identification
        flash('Authentication failed.')
        return redirect(url_for('index'))
    # Look if the user already exists
    user=User.query.filter_by(email=email).first()
    if not user:
        # Create the user. Try and use their name returned by Google,
        # but if it is not set, split the email address at the @.
        nickname = username
        if nickname is None or nickname == "":
            nickname = email.split('@')[0]

        # We can do more work here to ensure a unique nickname, if you 
        # require that.
        user=User(nickname=nickname, email=email)
        db.session.add(user)
        db.session.commit()
    # Log in the user, by default remembering them for their next visit
    # unless they log out.
    login_user(user, remember=True)
    return redirect(url_for('index'))

Finally, my /login view and template to make it all happen:

@app.route('/login', methods=['GET', 'POST'])
def login():
    if g.user is not None and g.user.is_authenticated():
        return redirect(url_for('index'))
    return render_template('login.html',
                           title='Sign In')

login.html:

{% extends "base.html" %}

{% block content %}

    <div id="sign-in">
        <h1>Sign In</h1>
        <p>
        <a href={{ url_for('oauth_authorize', provider='google') }}><img src="{{ url_for('static', filename='img/sign-in-with-google.png') }}" /></a>
    </div>
{% endblock %}

Make sure the correct callback addresses are registered with Google, and the user should simply have to click on "Sign in with Google" on your login page, and it will register them and log them in.


I see no activity on Rauth github page for last 3 years. Seems like a dead module.
Now, no Rauth commits in 6 years. :(
Officially 7 years with no new commits for Rauth. Happy new year! github.com/joelverhagen/flask-rauth
8 years and no commits!
J
JackLeo

I've searched for quite a bit about using different libraries but all of them seemed ether overkill in some sense (you can use it on any platform but for that you need ton of code) or documentation did not explained what I wanted to. Long story short - I wrote it from scratch thus understanding process of authentication true Google API. It's not as hard as it sounds. Basically you need to follow https://developers.google.com/accounts/docs/OAuth2WebServer guidelines and that's it. For this you also will need to register at https://code.google.com/apis/console/ to generate credentials and register your links. I've used simple subdomain pointing to my office IP since it only allows domains.

For user login/management and sessions I've used this plugin for flask http://packages.python.org/Flask-Login/ - there will be some code based on that.

So first thing first - index view:

from flask import render_template
from flask.ext.login import current_user
from flask.views import MethodView

from myapp import app


class Index(MethodView):
    def get(self):
        # check if user is logged in
        if not current_user.is_authenticated():
            return app.login_manager.unauthorized()

        return render_template('index.html')

so this view will not open until we will have authenticated user. Talking about users - user model:

from sqlalchemy.orm.exc import NoResultFound
from sqlalchemy import Column, Integer, DateTime, Boolean, String

from flask.ext.login import UserMixin
from myapp.metadata import Session, Base


class User(Base):
    __tablename__ = 'myapp_users'

    id = Column(Integer, primary_key=True)
    email = Column(String(80), unique=True, nullable=False)
    username = Column(String(80), unique=True, nullable=False)

    def __init__(self, email, username):
        self.email = email
        self.username = username

    def __repr__(self):
        return "<User('%d', '%s', '%s')>" \
                % (self.id, self.username, self.email)

    @classmethod
    def get_or_create(cls, data):
        """
        data contains:
            {u'family_name': u'Surname',
            u'name': u'Name Surname',
            u'picture': u'https://link.to.photo',
            u'locale': u'en',
            u'gender': u'male',
            u'email': u'propper@email.com',
            u'birthday': u'0000-08-17',
            u'link': u'https://plus.google.com/id',
            u'given_name': u'Name',
            u'id': u'Google ID',
            u'verified_email': True}
        """
        try:
            #.one() ensures that there would be just one user with that email.
            # Although database should prevent that from happening -
            # lets make it buletproof
            user = Session.query(cls).filter_by(email=data['email']).one()
        except NoResultFound:
            user = cls(
                    email=data['email'],
                    username=data['given_name'],
                )
            Session.add(user)
            Session.commit()
        return user

    def is_active(self):
        return True

    def is_authenticated(self):
        """
        Returns `True`. User is always authenticated. Herp Derp.
        """
        return True

    def is_anonymous(self):
        """
        Returns `False`. There are no Anonymous here.
        """
        return False

    def get_id(self):
        """
        Assuming that the user object has an `id` attribute, this will take
        that and convert it to `unicode`.
        """
        try:
            return unicode(self.id)
        except AttributeError:
            raise NotImplementedError("No `id` attribute - override get_id")

    def __eq__(self, other):
        """
        Checks the equality of two `UserMixin` objects using `get_id`.
        """
        if isinstance(other, UserMixin):
            return self.get_id() == other.get_id()
        return NotImplemented

    def __ne__(self, other):
        """
        Checks the inequality of two `UserMixin` objects using `get_id`.
        """
        equal = self.__eq__(other)
        if equal is NotImplemented:
            return NotImplemented
        return not equal

There is probably something wrong with UserMixin, but I'll deal with that latter. Your user model will look differently, just make it compatible with flask-login.

So what is left - authentication it self. I set for flask-login that login view is 'login'. Login view renders html with login button that points to google - google redirects to Auth view. It should be possible just to redirect user to google in case it's website only for logged in users.

import logging
import urllib
import urllib2
import json

from flask import render_template, url_for, request, redirect
from flask.views import MethodView
from flask.ext.login import login_user

from myapp import settings
from myapp.models import User


logger = logging.getLogger(__name__)


class Login(BaseViewMixin):
    def get(self):
        logger.debug('GET: %s' % request.args)
        params = {
            'response_type': 'code',
            'client_id': settings.GOOGLE_API_CLIENT_ID,
            'redirect_uri': url_for('auth', _external=True),
            'scope': settings.GOOGLE_API_SCOPE,
            'state': request.args.get('next'),
        }
        logger.debug('Login Params: %s' % params)
        url = settings.GOOGLE_OAUTH2_URL + 'auth?' + urllib.urlencode(params)

        context = {'login_url': url}
        return render_template('login.html', **context)


class Auth(MethodView):
    def _get_token(self):
        params = {
            'code': request.args.get('code'),
            'client_id': settings.GOOGLE_API_CLIENT_ID,
            'client_secret': settings.GOOGLE_API_CLIENT_SECRET,
            'redirect_uri': url_for('auth', _external=True),
            'grant_type': 'authorization_code',
        }
        payload = urllib.urlencode(params)
        url = settings.GOOGLE_OAUTH2_URL + 'token'

        req = urllib2.Request(url, payload)  # must be POST

        return json.loads(urllib2.urlopen(req).read())

    def _get_data(self, response):
        params = {
            'access_token': response['access_token'],
        }
        payload = urllib.urlencode(params)
        url = settings.GOOGLE_API_URL + 'userinfo?' + payload

        req = urllib2.Request(url)  # must be GET

        return json.loads(urllib2.urlopen(req).read())

    def get(self):
        logger.debug('GET: %s' % request.args)

        response = self._get_token()
        logger.debug('Google Response: %s' % response)

        data = self._get_data(response)
        logger.debug('Google Data: %s' % data)

        user = User.get_or_create(data)
        login_user(user)
        logger.debug('User Login: %s' % user)
        return redirect(request.args.get('state') or url_for('index'))

So everything is splited to two parts - one for getting google token in _get_token. Other for using it and retrieving basic user data in _get_data.

My settings file contains:

GOOGLE_API_CLIENT_ID = 'myid.apps.googleusercontent.com'
GOOGLE_API_CLIENT_SECRET = 'my secret code'
GOOGLE_API_SCOPE = 'https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email'
GOOGLE_OAUTH2_URL = 'https://accounts.google.com/o/oauth2/'
GOOGLE_API_URL = 'https://www.googleapis.com/oauth2/v1/'

Keep in mind that views has to have url path attached to app so I've use this urls.py file so that I could track my views more easily and import less stuff to flask app creation file:

from myapp import app
from myapp.views.auth import Login, Auth
from myapp.views.index import Index


urls = {
    '/login/': Login.as_view('login'),
    '/auth/': Auth.as_view('auth'),
    '/': Index.as_view('index'),
}

for url, view in urls.iteritems():
    app.add_url_rule(url, view_func=view)

All of this together makes working Google authorization in Flask. If you copy paste it - it might take some mending with flask-login documentation and SQLAlchemy mappings, but the idea is there.


P
Peter Hudec

Give Authomatic a try (I'm the maintainer of that project). It is very simple to use, works with any Python framework and supports 16 OAuth 2.0, 10 OAuth 1.0a providers and OpenID.

Here is a simple example about how to authenticate a user with Google and get his/her list of YouTube videos:

# main.py

from flask import Flask, request, make_response, render_template
from authomatic.adapters import WerkzeugAdapter
from authomatic import Authomatic
from authomatic.providers import oauth2


CONFIG = {
    'google': {
        'class_': oauth2.Google,
        'consumer_key': '########################',
        'consumer_secret': '########################',
        'scope': oauth2.Google.user_info_scope + ['https://gdata.youtube.com'],
    },
}

app = Flask(__name__)
authomatic = Authomatic(CONFIG, 'random secret string for session signing')


@app.route('/login/<provider_name>/', methods=['GET', 'POST'])
def login(provider_name):
    response = make_response()

    # Authenticate the user
    result = authomatic.login(WerkzeugAdapter(request, response), provider_name)

    if result:
        videos = []
        if result.user:
            # Get user info
            result.user.update()

            # Talk to Google YouTube API
            if result.user.credentials:
                response = result.provider.access('https://gdata.youtube.com/'
                    'feeds/api/users/default/playlists?alt=json')
                if response.status == 200:
                    videos = response.data.get('feed', {}).get('entry', [])

        return render_template(user_name=result.user.name,
                               user_email=result.user.email,
                               user_id=result.user.id,
                               youtube_videos=videos)
    return response


if __name__ == '__main__':
    app.run(debug=True)

There is also a very simple Flask tutorial which shows how to authenticate a user by Facebook and Twitter and talk to their APIs to read the user's newsfeeds.


That looks like another excellent option, thank you very much. I will definitely evaluate this.
Thank you for this example. Would you mind extending this example to the following scenario? 1. When I try this with Twitter and reload the page (that is displayed after login), I get an error FailureError: Failed to obtain OAuth 1.0a oauth_token from https://api.twitter.com/oauth/access_token! HTTP status code: 401., clearly indicating that I'm doing something wrong. 2. How can I protect an API end point and redirect user to the login page if they did not login? Really appreciate your time.
Ideally, it would be of great help to beginners like me if you could cover a scenario like how the first answer covered: 1. Authenticate a user using FB/Twitter/Google, 2. Store necessary details (say, in sqlite) and current session, 3. Protect certain API endpoints. Thank you for your work!
@Legend, while other libraries use separate request handlers for different OAuth flow stages, Authomatic uses the same handler and distinguishes the stages by the request parameters. If there are no parameters it will redirect the user to the provider, if the request contains parameters oauth_verifier and oauth_token or denied (OAuth 1.0) Authomatic triggers the second stage of the authorization flow. When you refresh the page with all the parameters, Authomatic will trigger the second stage again, but with outdated values which results in an unauthorized error by the provider.
@Legend, There is a tutorial where user info is saved to cookies and the user is then redirected back to homepage and he/she can then post FB statuses and Tweets repeatedly. Unfortunately the tutorial is based on Google App Engine and Webapp2. I will port this example to Flask.
s
singingwolfboy

Flask-Dance is a new library that links together Flask, Requests, and OAuthlib. It has a beautiful API, and it has builtin support for Google auth, along with a quickstart for how to get started. Give it a try!


The quickstart link no longer exists. Has been updated to github.com/singingwolfboy/flask-dance-google from flask-dance.readthedocs.io/en/latest/quickstart.html
I used this library but i found many problems: First, the documentation is poor. For example this code (github.com/singingwolfboy/flask-dance-google) didn't work for me locally because i use http instead of https....
@ASSILITaher can you tell me what problems you had with the documentation? As for using http vs https, OAuth requires using https, and won't work using http.
I followed this example locally(github.com/singingwolfboy/flask-dance-google). This example is not working locally using http. I'am using also Google App Engine. Anyway, i'am trying to make my project works with https to solve the issue. Thank you.
b
bertdida

I was able to port the accepted answer to use Requests-OAuthlib instead of Rauth. As of this writing, the package's last commit was on June 2019 and was currently use by 30K+ repositories.

To install, run:

$ pip install requests_oauthlib

Note, OAUTHLIB_RELAX_TOKEN_SCOPE environment variable must be set to True to suppress Scope has changed warning. On windows, this can be done by running:

$ set OAUTHLIB_RELAX_TOKEN_SCOPE=1
...
from requests_oauthlib import OAuth2Session
from urllib.request import urlopen


class GoogleSignIn(OAuthSignIn):
    openid_url = "https://accounts.google.com/.well-known/openid-configuration"

    def __init__(self):
        super(GoogleLogin, self).__init__("google")
        self.openid_config = json.load(urlopen(self.openid_url))
        self.session = OAuth2Session(
            client_id=self.consumer_id,
            redirect_uri=self.get_callback_url(),
            scope=self.openid_config["scopes_supported"]
        )

    def authorize(self):
        auth_url, _ = self.session.authorization_url(
            self.openid_config["authorization_endpoint"])
        return redirect(auth_url)

    def callback(self):
        if "code" not in request.args:
            return None, None

        self.session.fetch_token(
            token_url=self.openid_config["token_endpoint"],
            code=request.args["code"],
            client_secret=self.consumer_secret,
        )

        me = self.session.get(self.openid_config["userinfo_endpoint"]).json()
        return me["name"], me["email"]

Requests-OAuthlib documentation can be found here https://requests-oauthlib.readthedocs.io/en/latest/index.html.


b
bluemoon

Flask-oauth is probably your best bet right now for a flask specific way to do it, as far as I know it doesn't support token refreshing but it will work with Facebook, we use it for that and it's oauth 2. If it doesn't need to be flask specific you might look at requests-oauth


e
emning

It looks like the new module Flask-Rauth is the answer to this question:

Flask-Rauth is a Flask extensions that allows you to easily interact with OAuth 2.0, OAuth 1.0a, and Ofly enabled applications. [...] This means that Flask-Rauth will allow users on your Flask website to sign in to external web services (i.e. the Twitter API, Facebook Graph API, GitHub, etc).

See: Flask-Rauth


u
user2901351

As oauth2client is now deprecated, I recommend what bluemoon suggests. Bruno Rocha's model of OAuth2 Google authentication in Flask is a nice starting point for using lepture's robust Flask-OAuthlib (pip-installable). I recommend mimicking, then expanding to suit your needs.


A
Andrei Sura

Not specifically for google -- https://github.com/lepture/flask-oauthlib and it has an example for how to implement the client and server at https://github.com/lepture/example-oauth2-server