Flask¶
Flask is probably the most popular Python web framework.
The Fief Python client provides tools to help you integrate Fief authentication in your Flask project. Let's see how to use them!
Install the client¶
Install the Fief client with the optional Flask dependencies:
API example¶
This is for you if...
- Your Flask backend will work as a pure REST API.
- You have a separate frontend, like a JavaScript or mobile app, that'll take care of the OAuth2 flow.
In this first example, we won't implement routes to perform the OAuth2 authentication. The goal here is just to show you how to protect your API route with a Fief access token.
from fief_client import Fief
from fief_client.integrations.flask import (
FiefAuth,
FiefAuthForbidden,
FiefAuthUnauthorized,
get_authorization_scheme_token,
)
from flask import Flask, g
fief = Fief( # (1)!
"https://fief.mydomain.com",
"YOUR_CLIENT_ID",
"YOUR_CLIENT_SECRET",
)
auth = FiefAuth(fief, get_authorization_scheme_token()) # (2)!
app = Flask(__name__)
@app.errorhandler(FiefAuthUnauthorized) # (3)!
def fief_unauthorized_error(e):
return "", 401
@app.errorhandler(FiefAuthForbidden) # (4)!
def fief_forbidden_error(e):
return "", 403
@app.get("/authenticated")
@auth.authenticated() # (5)!
def get_authenticated():
return g.access_token_info # (6)!
@app.get("/authenticated-scope")
@auth.authenticated(scope=["openid", "required_scope"]) # (7)!
def get_authenticated_scope():
return g.access_token_info
@app.get("/authenticated-permissions")
@auth.authenticated(permissions=["castles:read"]) # (8)!
def get_authenticated_permissions():
return g.access_token_info
-
Fief client instantiation
As we showed in the standard Python section, we instantiate here a Fief client here with the base tenant URL and client credentials.
-
Fief helper for Flask
This is the helper doing the tedious work for you with Flask. It first needs an instance of the Fief client we created above and a function retrieving the access token from the Flask request.
It's a simple function which can use the global
request
object to retrieve the access token.For convenience, we provide two of them:
get_authorization_scheme_token
andget_cookie
. -
Error handler for
FiefAuthUnauthorized
When a protected route is called without a valid access token, the Fief helper will raise the
FiefAuthUnauthorized
.By registering a Flask error handler, we can catch this error and customize the response returned to the user. Here, we just return an empty response with the 401 status code.
-
Error handler for
FiefAuthForbidden
When a request is made with a valid access token but without the required scope, the Fief helper will raise the
FiefAuthForbidden
.By registering a Flask error handler, we can catch this error and customize the response returned to the user. Here, we just return an empty response with the 403 status code.
-
authenticated
decoratorThis is where the magic happens:
FiefAuth
gives you aauthenticated
decorator to check for the access token and optionally for required scopes.If everything goes well, the route logic will be executed.
-
access_token_info
dictionary is available ing
When a valid access token is found in the request, the
access_token_info
decorator will automatically add theaccess_token_info
property to the globalg
application context of Flask.This
access_token_info
property is aFiefAccessTokenInfo
dictionary containing the ID of the user, the list of allowed scopes and permissions and the raw access token. -
Check for scopes
The
access_token_info
decorator accepts an optionalscope
argument where you can list the scope required to access this route.If the access token doesn't have the required scope,
FiefAuthForbidden
error is raised. -
Check for permissions
The
access_token_info
decorator accepts an optionalpermissions
argument where you can list the permissions required to access this route.If the user doesn't have the required permissions,
FiefAuthForbidden
is raised.
And that's about it!
Optional user¶
Sometimes, you need to have a route retrieving the user if there is one authenticated, but still working if there none. To do this, you can leverage the optional
parameter of the authenticated
decorator.
from fief_client import Fief
from fief_client.integrations.flask import (
FiefAuth,
FiefAuthForbidden,
FiefAuthUnauthorized,
get_authorization_scheme_token,
)
from flask import Flask, g
fief = Fief(
"https://fief.mydomain.com",
"YOUR_CLIENT_ID",
"YOUR_CLIENT_SECRET",
)
auth = FiefAuth(fief, get_authorization_scheme_token())
app = Flask(__name__)
@app.errorhandler(FiefAuthUnauthorized)
def fief_unauthorized_error(e):
return "", 401
@app.errorhandler(FiefAuthForbidden)
def fief_forbidden_error(e):
return "", 403
@app.get("/authenticated")
@auth.authenticated(optional=True)
def get_authenticated():
if g.access_token_info is None:
return {"message": "Anonymous user"}
return g.access_token_info
Web application example¶
This is for you if...
- Your Flask backend will render HTML pages.
- Your application is intended to be used in a browser.
Prerequisites
- Allow the following Redirect URI on your Fief Client:
http://localhost:8000/auth-callback
The examples we showed previously are working well in a pure REST API context: a frontend, like interactive documentation, a JavaScript application or a mobile app will take care of the OAuth2 authentication flow to retrieve an access token before making request to your API.
Another common context is traditional web application, where the server takes care of generating HTML pages before returning it to the browser. In this case, we'll need some routes to redirect the user to the Fief login page if they're not authenticated and take care of storing the access token somewhere. This is what'll show in this example.
Besides, we'll usually need the basic information about the authenticated user, like its email or the values of the custom user fields. We'll see how we can use it.
Basically, here's what we'll do:
- This time, we'll expect the access token to be passed through a traditional cookie instead of an HTTP header. Cookies are very convenient when designing web apps because they are handled automatically by the browser.
- If the cookie is not present, we'll redirect the user to the Fief login page. Once again, the browser will help us a lot here since it'll automatically follow the redirection.
- Upon successful login, Fief will automatically redirect the user to the callback route. This callback route will take care of setting a new cookie containing the access token. It means that the access token will be safely stored in the browser memory.
- Finally, the user is redirected back to the protected route. The browser will automatically send the cookie containing the access token: our request is now authenticated!
import uuid
from typing import Optional
from fief_client import Fief, FiefUserInfo
from fief_client.integrations.flask import (
FiefAuth,
FiefAuthForbidden,
FiefAuthUnauthorized,
get_cookie,
)
from flask import Flask, g, redirect, request, session, url_for
SESSION_COOKIE_NAME = "user_session"
SECRET_KEY = "SECRET" # (1)!
fief = Fief( # (2)!
"https://fief.mydomain.com",
"YOUR_CLIENT_ID",
"YOUR_CLIENT_SECRET",
)
def get_userinfo_cache(id: uuid.UUID) -> Optional[FiefUserInfo]: # (3)!
return session.get(f"userinfo-{str(id)}")
def set_userinfo_cache(id: uuid.UUID, userinfo: FiefUserInfo) -> None: # (4)!
session[f"userinfo-{str(id)}"] = userinfo
auth = FiefAuth(
fief,
get_cookie(SESSION_COOKIE_NAME), # (5)
get_userinfo_cache=get_userinfo_cache, # (6)!
set_userinfo_cache=set_userinfo_cache,
)
app = Flask(__name__)
app.secret_key = SECRET_KEY
@app.errorhandler(FiefAuthUnauthorized) # (7)!
def fief_unauthorized_error(e):
redirect_uri = url_for("auth_callback", _external=True) # (8)!
auth_url = fief.auth_url(redirect_uri, scope=["openid"]) # (9)!
return redirect(auth_url) # (10)!
@app.errorhandler(FiefAuthForbidden)
def fief_forbidden_error(e):
return "", 403
@app.get("/auth-callback") # (11)!
def auth_callback():
redirect_uri = url_for("auth_callback", _external=True)
code = request.args["code"]
tokens, userinfo = fief.auth_callback(code, redirect_uri) # (12)!
response = redirect(url_for("protected")) # (13)!
response.set_cookie( # (14)!
SESSION_COOKIE_NAME,
tokens["access_token"],
max_age=tokens["expires_in"],
httponly=True, # (15)!
secure=False, # ❌ Set this to `True` in production (16)!
)
set_userinfo_cache(uuid.UUID(userinfo["sub"]), userinfo) # (17)!
return response
@app.get("/protected")
@auth.current_user() # (18)!
def protected():
user = g.user # (19)!
return f"<h1>You are authenticated. Your user email is {user['email']}</h1>"
-
Define a secret key for Flask
We'll use the Sessions mechanism from Flask to keep user information in cache. To enable it, we need to set a secret key for Flask.
Generate a strong passphrase and don't share it.
Avoid to hardcode your secrets in your code
It's usually not recommended to hardcode secrets like Client ID and Secret in your code like this. If your code gets published on the web, for example on GitHub, the security of your instance would be compromised.
Besides, it'll be harder if you need to deploy on several environments, like a staging or testing one, in addition to your production environment.
A standard and widely-used approach is to use environment variables.
-
This doesn't change from the previous example
The
Fief
client is always at the heart of the integration 😉 -
We define a function to retrieve user information from cache
To make sure we don't call the Fief API every time we want the user data, we'll cache it in our application. It'll be way more performant!
To do this, we implement a simple function allowing us to retrieve the user information given a user ID.
In this example, we simply use the Sessions mechanism from Flask, but it can be something more complex, like reading from a Redis store.
-
We define a function to set user information in cache
As you probably have guessed, we need the other side of the operation: saving user information in cache.
To do this, we implement a simple function accepting a user ID and a
FiefUserInfo
dictionary as arguments. There, you'll need to store this data in cache.In this example, we simply use the Sessions mechanism from Flask, but it can be something more complex, like writing to a Redis store.
-
We use a cookie getter
Contrary to the previous examples, we expect the access token to be passed in a cookie. Thus, we use the
get_cookie
getter.Notice that we set the name of this cookie through the
SESSION_COOKIE_NAME
constant. -
We pass both
get_userinfo_cache
andset_userinfo_cache
as argumentsBasically, we tell
FiefAuth
to use the caching functions we implemented when we want to get the user information.That's why it's important to strictly follow the functions signature presented above:
FiefAuth
will call them inside its logic. -
We change the error handler for
FiefAuthUnauthorized
This time, we'll generate a redirect response so the user can login on Fief.
-
We build the redirect URL
This points to our
/auth-callback
route that we define below. -
We generate an authorization URL on the Fief server
Thanks to the
auth_url
method on the Fief client, we can automatically generate the authorization URL on the Fief server. -
We build a redirect response
We redirect the user to the Fief authorization URL.
-
We implement an
/auth-callback
routeThis is the route that'll take care of exchanging the authorization code with a fresh access token and save it in a cookie.
-
We generate an access token
We finish the OAuth2 flow by exchanging the authorization code with a fresh access token.
-
We build a redirection to the
/protected
routeThe user will now be correctly authenticated to our web application. Thus, we can redirect them to a protected page.
-
We build a new cookie containing the access token
The response will contain a
Set-Cookie
header instructing the browser to save the access token in its memory. This method allows us to configure each properties of the cookie.You can read more about HTTP cookies on the MDN documentation.
-
Set the cookie as
HTTPOnly
For such sensitive values, it's strongly recommended to set the cookie as
HTTPOnly
. It means that it won't be possible to read its value from JavaScript, reducing potential attacks. -
Set the cookie as secure in production
For such sensitive values, it's strongly recommended to set the cookie as
Secure
. It tells the browser to send the cookie only on HTTPS (SSL) connection, reducing the risk of the access token to be stolen by a attacker between the client and the server.However, in a local environment, you usually don't serve your application with SSL. That's why we set it to
False
in this example. A common approach to handle this is to have an environment variable to control this parameter, so you can disable it in local and enable it in production. -
We cache the user information
When a user has successfully authenticated, we do not only get the access token: we also get an ID token which already contains the user information.
Hence, we'll take this opportunity to store it in our cache! The ID token is automatically decoded by
fief.auth_callback
method.Thus, we just have to use our cache function to store it!
-
Use the
current_user
decoratorThis time, we use the
current_user
decorator instead ofauthenticated
. Under the hood, it'll stil callauthenticated
and check if the cookie is available in the request and proceed if everything goes well. However, it'll return you aFiefUserInfo
dictionary containing the data of the user.If the request is not authenticated, an
FiefAuthUnauthorized
error will be raised and the user will be redirected to the Fief login page. -
user
dictionary is available ing
If the request is properly authenticated, the
current_user
decorator will automatically add theuser
property to the globalg
application context of Flask.This
user
property is aFiefUserInfo
dictionary containing the user data. If it's not available in cache, it's automatically retrieved from the Fief API.
That's it! If you run this application and go to http://localhost:8000/protected, you'll be redirected to the Fief login page and experience the authentication flow before getting back to this route with a proper authentication cookie.
current_user
can also check for scope and permissions
In a similar way as we shown in the API example, you can also require the access token to be granted a list of scopes or the user to be granted a list of permissions.
You can also optionally require the user
In a similar way as we shown in the API example, you can leverage the optional
parameter to make the route works even if no user is authenticated.