Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: replace deprecated flask-oauthlib with authlib #1411

Merged
merged 3 commits into from
Jul 3, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions docs/security.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Supported Authentication Types
:REMOTE_USER: Reads the *REMOTE_USER* web server environ var, and verifies if it's authorized with the framework users table.
It's the web server responsibility to authenticate the user, useful for intranet sites, when the server (Apache, Nginx)
is configured to use kerberos, no need for the user to login with username and password on F.A.B.
:OAUTH: Authentication using OAUTH (v1 or v2). You need to install flask-oauthlib.
:OAUTH: Authentication using OAUTH (v1 or v2). You need to install authlib.

Configure the authentication type on config.py, take a look at :doc:`config`

Expand Down Expand Up @@ -454,33 +454,33 @@ permission to your app to access or manage the user's account on the provider.

So you can send tweets, post on the users facebook, retrieve the user's linkedin profile etc.

To use OAuth you need to install `Flask-OAuthLib <https://flask-oauthlib.readthedocs.org/en/latest/>`_. It's useful
To use OAuth you need to install `AuthLib <https://docs.authlib.org/en/latest/index.html>`_. It's useful
to get to know this library since F.A.B. will expose the remote application object for you to play with.

Take a look at the `example <https://github.com/dpgaspar/Flask-AppBuilder/tree/master/examples/oauth>`_
to get an idea of a simple use for this.

Use **config.py** configure OAUTH_PROVIDERS with a list of oauth providers, notice that the remote_app
key is just the configuration for flask-oauthlib::
key is just the configuration for authlib::

AUTH_TYPE = AUTH_OAUTH

OAUTH_PROVIDERS = [
{'name':'twitter', 'icon':'fa-twitter',
'remote_app': {
'consumer_key':'TWITTER KEY',
'consumer_secret':'TWITTER SECRET',
'base_url':'https://api.twitter.com/1.1/',
'client_id':'TWITTER KEY',
'client_secret':'TWITTER SECRET',
'api_base_url':'https://api.twitter.com/1.1/',
'request_token_url':'https://api.twitter.com/oauth/request_token',
'access_token_url':'https://api.twitter.com/oauth/access_token',
'authorize_url':'https://api.twitter.com/oauth/authenticate'}
},
{'name':'google', 'icon':'fa-google', 'token_key':'access_token',
'remote_app': {
'consumer_key':'GOOGLE KEY',
'consumer_secret':'GOOGLE SECRET',
'base_url':'https://www.googleapis.com/oauth2/v2/',
'request_token_params':{
'client_id':'GOOGLE KEY',
'client_secret':'GOOGLE SECRET',
'api_base_url':'https://www.googleapis.com/oauth2/v2/',
'client_kwargs':{
'scope': 'email profile'
},
'request_token_url':None,
Expand Down Expand Up @@ -510,7 +510,7 @@ To override/customize the user information retrieval from oauth, you can create
def my_user_info_getter(sm, provider, response=None):
if provider == 'github':
me = sm.oauth_remotes[provider].get('user')
return {'username': me.data.get('login')}
return {'username': me.json().get('login')}
else:
return {}

Expand Down
28 changes: 28 additions & 0 deletions examples/oauth/app/security.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
from flask import redirect, session
from flask_appbuilder import expose
from flask_appbuilder.security.views import AuthOAuthView
from flask_appbuilder.security.sqla.manager import SecurityManager


class MyAuthOAuthView(AuthOAuthView):

@expose("/logout/")
def logout(self):
"""Delete access token before logging out."""
session.pop('oauth_token', None)
return super().logout()


class MySecurityManager(SecurityManager):
authoauthview = MyAuthOAuthView

def set_oauth_session(self, provider, oauth_response):
"""Store the ouath token in the session for later retrieval.

In this example, the token is only required to send a tweet.
"""
res = super().set_oauth_session(provider, oauth_response)
# DON'T DO THIS IN PRODUCTION, SAVE TO A DB IN PRODUCTION
if provider == "twitter":
session["oauth_token"] = oauth_response
return res
30 changes: 17 additions & 13 deletions examples/oauth/app/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@ def form_get(self, form):
form.message.data = "Flask-AppBuilder now supports OAuth!"

def form_post(self, form):
resp = self.appbuilder.sm.oauth_remotes["twitter"].post(
"statuses/update.json", data={"status": form.message.data}
remote_app = self.appbuilder.sm.oauth_remotes["twitter"]
resp = remote_app.post(
"statuses/update.json", data={"status": form.message.data},
token=remote_app.token
)
if resp.status != 200:
if resp.status_code != 200:
flash("An error occurred", "danger")
else:
flash(self.message, "info")
Expand All @@ -35,25 +37,27 @@ def get_oauth_user_info(sm, provider, response=None):
# for GITHUB
if provider == 'github' or provider == 'githublocal':
me = sm.oauth_remotes[provider].get('user')
return {'username': "github_" + me.data.get('login')}
return {'username': "github_" + me.json().get('login')}
# for twitter
if provider == 'twitter':
me = sm.oauth_remotes[provider].get('account/settings.json')
return {'username': "twitter_" + me.data.get('screen_name', '')}
return {'username': "twitter_" + me.json().get('screen_name', '')}
# for linkedin
if provider == 'linkedin':
me = sm.oauth_remotes[provider].get('people/~:(id,email-address,first-name,last-name)?format=json')
return {'username': "linkedin_" + me.data.get('id', ''),
'email': me.data.get('email-address', ''),
'first_name': me.data.get('firstName', ''),
'last_name': me.data.get('lastName', '')}
data = me.json()
return {'username': "linkedin_" + data.get('id', ''),
'email': data.get('email-address', ''),
'first_name': data.get('firstName', ''),
'last_name': data.get('lastName', '')}
# for Google
if provider == 'google':
me = sm.oauth_remotes[provider].get('userinfo')
return {'username': me.data.get('id', ''),
'first_name': me.data.get('given_name', ''),
'last_name': me.data.get('family_name', ''),
'email': me.data.get('email', '')}
data = me.json()
return {'username': data.get('id', ''),
'first_name': data.get('given_name', ''),
'last_name': data.get('family_name', ''),
'email': data.get('email', '')}
"""


Expand Down
28 changes: 16 additions & 12 deletions examples/oauth/config.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import os
from flask import session
from flask_appbuilder.security.manager import (
AUTH_OID,
AUTH_REMOTE_USER,
Expand Down Expand Up @@ -44,23 +45,24 @@
"name": "twitter",
"icon": "fa-twitter",
"remote_app": {
"consumer_key": os.environ.get("TWITTER_KEY"),
"consumer_secret": os.environ.get("TWITTER_SECRET"),
"base_url": "https://api.twitter.com/1.1/",
"client_id": os.environ.get("TWITTER_KEY"),
"client_secret": os.environ.get("TWITTER_SECRET"),
"api_base_url": "https://api.twitter.com/1.1/",
"request_token_url": "https://api.twitter.com/oauth/request_token",
"access_token_url": "https://api.twitter.com/oauth/access_token",
"authorize_url": "https://api.twitter.com/oauth/authenticate",
"fetch_token": lambda: session.get("oauth_token"), # DON'T DO THIS IN PRODUCTION
},
},
{
"name": "google",
"icon": "fa-google",
"token_key": "access_token",
"remote_app": {
"consumer_key": os.environ.get("GOOGLE_KEY"),
"consumer_secret": os.environ.get("GOOGLE_SECRET"),
"base_url": "https://www.googleapis.com/oauth2/v2/",
"request_token_params": {"scope": "email profile"},
"client_id": os.environ.get("GOOGLE_KEY"),
"client_secret": os.environ.get("GOOGLE_SECRET"),
"api_base_url": "https://www.googleapis.com/oauth2/v2/",
"client_kwargs": {"scope": "email profile"},
"request_token_url": None,
"access_token_url": "https://accounts.google.com/o/oauth2/token",
"authorize_url": "https://accounts.google.com/o/oauth2/auth",
Expand All @@ -71,11 +73,11 @@
"icon": "fa-windows",
"token_key": "access_token",
"remote_app": {
"consumer_key": os.environ.get("AZURE_APPLICATION_ID"),
"consumer_secret": os.environ.get("AZURE_SECRET"),
"base_url": "https://login.microsoftonline.com/{AZURE_TENANT_ID}/oauth2",
"request_token_params": {
"scope": "User.read name preferred_username email profile",
"client_id": os.environ.get("AZURE_APPLICATION_ID"),
"client_secret": os.environ.get("AZURE_SECRET"),
"api_base_url": "https://login.microsoftonline.com/{AZURE_TENANT_ID}/oauth2",
"client_kwargs": {
"scope": "User.read name preferred_username email profile upn",
"resource": os.environ.get("AZURE_APPLICATION_ID"),
},
"request_token_url": None,
Expand Down Expand Up @@ -158,3 +160,5 @@
# APP_THEME = "spacelab.css"
# APP_THEME = "united.css"
# APP_THEME = "yeti.css"

FAB_SECURITY_MANAGER_CLASS = "app.security.MySecurityManager"
38 changes: 21 additions & 17 deletions flask_appbuilder/security/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,14 +247,14 @@ def __init__(self, appbuilder):
if self.auth_type == AUTH_OID:
self.oid = OpenID(app)
if self.auth_type == AUTH_OAUTH:
from flask_oauthlib.client import OAuth
from authlib.integrations.flask_client import OAuth

self.oauth = OAuth()
self.oauth = OAuth(app)
self.oauth_remotes = dict()
for _provider in self.oauth_providers:
provider_name = _provider["name"]
log.debug("OAuth providers init {0}".format(provider_name))
obj_provider = self.oauth.remote_app(
obj_provider = self.oauth.register(
provider_name, **_provider["remote_app"]
)
obj_provider._tokengetter = self.oauth_tokengetter
Expand Down Expand Up @@ -510,34 +510,38 @@ def get_oauth_user_info(self, provider, resp):
# for GITHUB
if provider == "github" or provider == "githublocal":
me = self.appbuilder.sm.oauth_remotes[provider].get("user")
log.debug("User info from Github: {0}".format(me.data))
return {"username": "github_" + me.data.get("login")}
data = me.json()
log.debug("User info from Github: {0}".format(data))
return {"username": "github_" + data.get("login")}
# for twitter
if provider == "twitter":
me = self.appbuilder.sm.oauth_remotes[provider].get("account/settings.json")
log.debug("User info from Twitter: {0}".format(me.data))
return {"username": "twitter_" + me.data.get("screen_name", "")}
data = me.json()
log.debug("User info from Twitter: {0}".format(data))
return {"username": "twitter_" + data.get("screen_name", "")}
# for linkedin
if provider == "linkedin":
me = self.appbuilder.sm.oauth_remotes[provider].get(
"people/~:(id,email-address,first-name,last-name)?format=json"
)
log.debug("User info from Linkedin: {0}".format(me.data))
data = me.json()
log.debug("User info from Linkedin: {0}".format(data))
return {
"username": "linkedin_" + me.data.get("id", ""),
"email": me.data.get("email-address", ""),
"first_name": me.data.get("firstName", ""),
"last_name": me.data.get("lastName", ""),
"username": "linkedin_" + data.get("id", ""),
"email": data.get("email-address", ""),
"first_name": data.get("firstName", ""),
"last_name": data.get("lastName", ""),
}
# for Google
if provider == "google":
me = self.appbuilder.sm.oauth_remotes[provider].get("userinfo")
log.debug("User info from Google: {0}".format(me.data))
data = me.json()
log.debug("User info from Google: {0}".format(data))
return {
"username": "google_" + me.data.get("id", ""),
"first_name": me.data.get("given_name", ""),
"last_name": me.data.get("family_name", ""),
"email": me.data.get("email", ""),
"username": "google_" + data.get("id", ""),
"first_name": data.get("given_name", ""),
"last_name": data.get("family_name", ""),
"email": data.get("email", ""),
}
# for Azure AD Tenant. Azure OAuth response contains
# JWT token which has user info.
Expand Down
16 changes: 10 additions & 6 deletions flask_appbuilder/security/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,20 +651,24 @@ def login(self, provider=None, register=None):
log.debug("Login to Register")
session["register"] = True
if provider == "twitter":
return self.appbuilder.sm.oauth_remotes[provider].authorize(
callback=url_for(
return self.appbuilder.sm.oauth_remotes[
provider
].authorize_redirect(
redirect_uri=url_for(
".oauth_authorized",
provider=provider,
_external=True,
state=state,
)
)
else:
return self.appbuilder.sm.oauth_remotes[provider].authorize(
callback=url_for(
return self.appbuilder.sm.oauth_remotes[
provider
].authorize_redirect(
redirect_uri=url_for(
".oauth_authorized", provider=provider, _external=True
),
state=state,
state=state.decode("ascii"),
)
except Exception as e:
log.error("Error on OAuth authorize: {0}".format(e))
Expand All @@ -674,7 +678,7 @@ def login(self, provider=None, register=None):
@expose("/oauth-authorized/<provider>")
def oauth_authorized(self, provider):
log.debug("Authorized init")
resp = self.appbuilder.sm.oauth_remotes[provider].authorized_response()
resp = self.appbuilder.sm.oauth_remotes[provider].authorize_access_token()
if resp is None:
flash(u"You denied the request to sign in.", "warning")
return redirect(self.appbuilder.get_url_for_login)
Expand Down