Skip to content

Commit

Permalink
fix: replace deprecated flask-oauthlib with authlib (#1411)
Browse files Browse the repository at this point in the history
* Replace flask-oauthlib with authlib

Flask-oauthlib has been deprecated by its maintainer; authlib is the
recommended successor.

* Update examples with changes required by authlib.

* Update documentation to reference authlib.
  • Loading branch information
kdwyer authored Jul 3, 2020
1 parent c5ca06b commit 85567d5
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 59 deletions.
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

0 comments on commit 85567d5

Please sign in to comment.