Skip to content

Commit

Permalink
Added Facebook authentication
Browse files Browse the repository at this point in the history
  • Loading branch information
gijswobben committed Feb 3, 2021
1 parent 5adcac0 commit 72c0d07
Show file tree
Hide file tree
Showing 9 changed files with 207 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@ Customs comes out of the box with the following strategies:
- JWT
- Google
- Github
- Facebook

The list is growing, but if you still cannot find what you're looking for it is very easy to create a specific strategy for your purpose.
3 changes: 2 additions & 1 deletion customs/strategies/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from customs.strategies.jwt_strategy import JWTStrategy
from customs.strategies.google_strategy import GoogleStrategy
from customs.strategies.github_strategy import GithubStrategy
from customs.strategies.facebook_strategy import FacebookStrategy


__all__ = ["LocalStrategy", "BasicStrategy", "JWTStrategy", "GoogleStrategy", "GithubStrategy"]
__all__ = ["LocalStrategy", "BasicStrategy", "JWTStrategy", "GoogleStrategy", "GithubStrategy", "FacebookStrategy"]
61 changes: 61 additions & 0 deletions customs/strategies/facebook_strategy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from typing import Dict
from customs.exceptions import UnauthorizedException
from customs.strategies.oauth2_strategy import OAuth2Strategy

from requests_oauthlib import OAuth2Session # type: ignore
from requests_oauthlib.compliance_fixes import facebook_compliance_fix # type: ignore


class FacebookStrategy(OAuth2Strategy):
"""Authentication using Facebook as an OAuth2 provider."""

name = "facebook"
scopes = ["email", "public_profile"]
fields = ["id", "name", "first_name", "last_name", "picture", "email"]

authorization_base_url = "https://www.facebook.com/dialog/oauth"
token_url = "https://graph.facebook.com/oauth/access_token"
refresh_url = "https://graph.facebook.com/oauth/access_token"
user_profile_endpoint = "https://graph.facebook.com/me?"

def get_user_info(self) -> Dict:
"""Method to get user info for the logged in user.
Raises:
UnauthorizedException: When the user is not authenticated
Returns:
Dict: The user profile
"""

try:

# Get the token
token = self.token

# Helper method to update the token in the session
def token_updater(token):
self.token = token

# Get a session with auto-refresh of the token
client = OAuth2Session(
self.client_id,
token=token,
auto_refresh_kwargs={
"client_id": self.client_id,
"client_secret": self.client_secret,
},
auto_refresh_url=self.refresh_url,
token_updater=token_updater,
)

if self.name == "facebook":
facebook_compliance_fix(client)

# Return the user info
return client.get(
self.user_profile_endpoint + "fields=" + ",".join(self.fields)
).json()

except Exception:
raise UnauthorizedException()
2 changes: 1 addition & 1 deletion customs/strategies/jwt_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ def authenticate(self, request: Union[Request, FlaskRequest]) -> Any:
decoded = jwt.decode(token, self.key)
return self.deserialize_user(decoded)

except Exception as e:
except Exception:
raise UnauthorizedException()

def sign(self, user: Any) -> str:
Expand Down
12 changes: 12 additions & 0 deletions customs/strategies/oauth2_strategy.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
)
from werkzeug.wrappers import Request
from requests_oauthlib import OAuth2Session # type: ignore
from requests_oauthlib.compliance_fixes import facebook_compliance_fix # type: ignore

from customs.exceptions import UnauthorizedException
from customs.strategies.base_strategy import BaseStrategy
Expand Down Expand Up @@ -170,6 +171,9 @@ def token_updater(token):
token_updater=token_updater,
)

if self.name == "facebook":
facebook_compliance_fix(client)

# Return the user info
return client.get(self.user_profile_endpoint).json()

Expand Down Expand Up @@ -227,6 +231,10 @@ def login():
scope=self.scopes,
redirect_uri=url_for(".callback", _external=True),
)

if self.name == "facebook":
facebook_compliance_fix(client)

authorization_url, state = client.authorization_url(
self.authorization_base_url,
access_type="offline",
Expand All @@ -244,6 +252,10 @@ def callback():
state=session["oauth_state"],
redirect_uri=url_for(".callback", _external=True),
)

if self.name == "facebook":
facebook_compliance_fix(client)

self.token = client.fetch_token(
self.token_url,
client_secret=self.client_secret,
Expand Down
103 changes: 103 additions & 0 deletions examples/facebook/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import os

from typing import Any, Dict
from flask import Flask
from flask.templating import render_template

from customs import Customs
from customs.helpers import set_redirect_url
from customs.strategies import FacebookStrategy
from customs.exceptions import UnauthorizedException


# App credentials for Facebook
client_id = os.environ.get("FACEBOOK_CLIENT_ID")
client_secret = os.environ.get("FACEBOOK_CLIENT_SECRET")

# Create the Flask app
app = Flask(__name__)
app.secret_key = "d2cc6f0a-0d9e-414c-a3ca-6b75455d6332"

# Create customs to protect the app, no need for sessions as we're using JWT tokens
customs = Customs(app, unauthorized_redirect_url="/login")

# Mock in-memory database (NOTE: Will forget everything on restart!)
DATABASE: Dict[str, Dict] = {}


# Create an implementation of the strategy (tell the strategy how to interact with our database for example)
class FacebookAuthentication(FacebookStrategy):

scopes = ["email", "public_profile"]
fields = ["id", "name", "first_name", "last_name", "picture", "email"] # Which fields to fetch from the users profile

def get_or_create_user(self, user: Dict) -> Dict:

user_id = str(user.get("id"))

# Create the user if not in the database (so registration is open to everyone)
if user_id not in DATABASE:
DATABASE[user_id] = user

# Return the user from the database
return DATABASE[user_id]

def serialize_user(self, user: Any) -> Dict:

# Keep only the user ID on the session for speed and safety
user_id = str(user.get("id"))
return {"id": user_id}

def deserialize_user(self, data: Dict) -> Any:

# Get the user ID from the session
user_id = str(data.get("id"))

# Conver the ID back to the user data from the database
if user_id is not None and user_id in DATABASE:
return {"id": str(data.get("id")), **DATABASE[str(data.get("id"))]}
else:
raise UnauthorizedException()


# Create an instance of the strategy
facebook = FacebookAuthentication(
client_id=client_id,
client_secret=client_secret,
enable_insecure=True, # For testing purposes only, don't use in production!
)

# ----------------------- #
# Define some open routes #
# ----------------------- #


# Open to everyone
@app.route("/")
def index():
return render_template("index.html")


@app.route("/login")
def login():

# Store the URL of the page that redirected here and store
# it so we can redirect to it after authentication
set_redirect_url()
return render_template("login.html")


# ------------------------------ #
# Define some (protected) routes #
# ------------------------------ #


@app.route("/profile")
@customs.protect(strategies=[facebook])
def profile(user: Dict):
print(user)
return render_template("profile.html", user=user)


if __name__ == "__main__":
app.run()
7 changes: 7 additions & 0 deletions examples/facebook/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<html>
<body>
<h1>Home</h1>
<p><a href="/profile">Profile</a></p>
<p><a href="/auth/facebook/login">Facebook login</a></p>
</body>
</html>
6 changes: 6 additions & 0 deletions examples/facebook/templates/login.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<html>
<body>
<h1>Login</h1>
<a href="/auth/facebook/login">Facebook authentication</a>
</body>
</html>
14 changes: 14 additions & 0 deletions examples/facebook/templates/profile.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<html>
<body>
<h1>Profile</h1>
<table>
<tbody>
<tr>
<td><img src="{{ user.picture.data.url }}"></td>
<td>{{ user.name }}</td>
<td>{{ user.email }}</td>
</tr>
</tbody>
</table>
</body>
</html>

0 comments on commit 72c0d07

Please sign in to comment.