Saltar al contenido

Uso de Google OAuth2 con Flask

Hola usuario de nuestro sitio web, descubrimos la respuesta a tu pregunta, deslízate y la encontrarás más abajo.

Solución:

Otra respuesta menciona Flask-Rauth, pero no entra en detalles sobre cómo usarlo. Hay algunas trampas específicas de Google, pero finalmente las he implementado y funciona bien. Lo integro con Flask-Login para poder decorar mis vistas con azúcar útil como @login_required.

Quería poder admitir múltiples proveedores de OAuth2, por lo que parte del código es genérico y se basa en la excelente publicación de Miguel Grinberg sobre el soporte de OAuth2 con Facebook y Twitter aquí.

Primero, agregue su información de autenticación de Google específica de Google en la configuración de su aplicación:

GOOGLE_LOGIN_CLIENT_ID = ".apps.googleusercontent.com"
GOOGLE_LOGIN_CLIENT_SECRET = ""

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

Y cuando crea su aplicación (en mi caso, el módulo __init__.py):

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

En el módulo de su aplicación, cree 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'])

Esto crea un genérico OAuthSignIn clase que puede ser subclasificada. La subclase de Google extrae su información de la lista de información publicada de Google (en formato JSON aquí). Esta es información que está sujeta a cambios, por lo que este enfoque asegurará que esté siempre actualizada. Una limitación de esto es que si una conexión a Internet no está disponible en su servidor en el momento en que se inicializa la aplicación Flask (el módulo importado), no se instanciará correctamente. Esto casi nunca debería ser un problema, pero almacenar los últimos valores conocidos en la base de datos de configuración para cubrir esta eventualidad es una buena idea.

Finalmente, la clase devuelve una tupla de name, email en el callback() función. De hecho, Google devuelve mucha más información, incluido el perfil de Google+, si está disponible. Inspeccione el diccionario devuelto por oauth_session.get('').json() para verlo todo. Si en el authorize() función amplía el alcance (para mi aplicación, email es suficiente), puede acceder a más información a través de la API de Google.

A continuación, escriba el puntos de vista para atarlo todo junto:

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

@app.route('/authorize/')
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/')
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'))

Finalmente, mi /login vista y plantilla para que todo suceda:

@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 %

    

Sign In

% endblock %

Asegúrese de que las direcciones de devolución de llamada correctas estén registradas en Google, y el usuario simplemente debe hacer clic en “Iniciar sesión con Google” en su página de inicio de sesión, y las registrará e iniciará sesión.

He buscado bastante sobre el uso de diferentes bibliotecas, pero todas me parecían exageradas en cierto sentido (puede usarlo en cualquier plataforma, pero para eso necesita toneladas de código) o la documentación no explicaba lo que quería. En pocas palabras: lo escribí desde cero, entendiendo así el proceso de autenticación. true API de Google. No es tan difícil como parece. Básicamente, debe seguir las https://developers.google.com/accounts/docs/OAuth2WebServer pautas y eso es todo. Para ello, también deberá registrarse en https://code.google.com/apis/console/ para generar credenciales y registrar sus enlaces. He usado un subdominio simple que apunta a la IP de mi oficina, ya que solo permite dominios.

Para el inicio de sesión / administración de usuarios y las sesiones, he usado este complemento para el matraz http://packages.python.org/Flask-Login/; habrá algún código basado en eso.

Así que lo primero es lo primero: vista de índice:

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')

por lo que esta vista no se abrirá hasta que tengamos un usuario autenticado. Hablando de usuarios – modelo de usuario:

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 "" 
                % (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'[email protected]',
            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

Probablemente haya algo mal con UserMixin, pero me ocuparé de eso último. Su modelo de usuario se verá diferente, solo hágalo compatible con flask-login.

Entonces, lo que queda: la autenticación en sí misma. Me puse para flask-login esa vista de inicio de sesión es 'login'. Login ver renderiza html con un botón de inicio de sesión que apunta a google – google redirecciona a Auth vista. Debería ser posible simplemente redirigir al usuario a Google en caso de que su sitio web sea solo para usuarios registrados.

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'))

Entonces, todo se divide en dos partes: una para obtener el token de Google _get_token. Otro para usarlo y recuperar datos básicos del usuario en _get_data.

Mi archivo de configuración contiene:

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/'

Tenga en cuenta que las vistas deben tener una ruta de URL adjunta a la aplicación, por lo que he usado esta urls.py para poder rastrear mis vistas más fácilmente e importar menos cosas al archivo de creación de la aplicación del matraz:

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)

Todo esto en conjunto hace que la autorización de Google funcione en Flask. Si lo copia y lo pega, podría necesitar algunos arreglos con la documentación de inicio de sesión del matraz y las asignaciones de SQLAlchemy, pero la idea está ahí.

Prueba Authomatic (soy el mantenedor de ese proyecto). Es muy simple de usar, funciona con cualquier marco de Python y apoya 16 OAuth 2.0, 10 OAuth 1.0a proveedores y OpenID.

A continuación, se muestra un ejemplo sencillo sobre cómo autenticar a un usuario con Google y obtener su lista de videos de YouTube:

# 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//', 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)

También hay un tutorial de Flask muy simple que muestra cómo autenticar a un usuario por Facebook y Twitter y hablar con sus API para leer las fuentes de noticias del usuario.

Ten en cuenta dar recomendación a esta noticia si te fue de ayuda.

¡Haz clic para puntuar esta entrada!
(Votos: 3 Promedio: 4.3)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *