BlackSheep implements built-in support for OpenID Connect authentication, meaning that it can be easily integrated with identity provider services such as:
This page documents:
- How to use OpenID Connect integration to provide sign-in and sign-up features, and to identify users who use the application
- How to use OpenID Connect integration to obtain
access_tokens to use APIs (in addition, or instead of
- How tokens are protected and how to configure applications to support multiple instances and regions
Using JWT Bearer and OpenID integrations requires more dependencies: use
pip install blacksheep[full] to use these features
A basic example with any of the identity providers listed above, having
implicit flow enabled for
id_token, looks like the following:
from blacksheep import Application, html, pretty_json from blacksheep.server.authentication.oidc import OpenIDSettings, use_openid_connect from guardpost.authentication import Identity app = Application() use_openid_connect( app, OpenIDSettings( authority="<YOUR_AUTHORITY>", client_id="<CLIENT_ID>", callback_path="<CALLBACK_PATH>", ), ) @app.route("/") async def home(user: Identity): if user.is_authenticated(): response = pretty_json(user.claims) return response return html("<a href='/sign-in'>Sign in</a><br/>")
When the application is configured with
use_openid_connect, request handlers
are automatically configured to handle users' sign-in, the redirect after a
user signs-in, and signs-out. After a user signs-in successfully, a signed and
encrypted cookie containing the claims of the
id_token is set automatically
for the client, having an expiration time matching the expiration time of the
id_token itself. User's identity is automatically restored at each web
request by an authentication middleware, and can be read as in the provided
@app.route("/") async def home(user: Identity): if user.is_authenticated(): ...
|app||Application||Instance of BlackSheep application.|
|settings||OpenIDSettings||Instance of OpenIDSettings.|
|tokens_store||Optional[BaseTokensStore] = None||Optional instance of
|parameters_builder||Optional[ParametersBuilder] = None||Optional instance of
|is_default||bool = True||If default, clients are automatically redirected to the
OpenIDSettings class has the following properties:
|client_id||str||ID of the application in the identity server.|
|authority||Optional[str] = None||If specified, URL of the authorization server.|
|audience||Optional[str] = None||If specified, the
|client_secret||Optional[str] = None||For requests that use
|discovery_endpoint||Optional[str] = None||If specified, the exact URL to the discovery point (useful with Okta when using custom scopes for an authorization server).|
|entry_path||str = "/sign-in"||The local entry-path for sign-in (this redirects to the sign-in page of the identity server).|
|logout_path||str = "/sign-out"||The local path to the sign-out endpoint (this removes authentication cookie).|
|post_logout_redirect_path||str = "/"||The local path to which a user is redirected after signing-out.|
|callback_path||str = "/authorization-callback"||The local path to handle the redirect after a user signs-in (the reply_url in the identity server must be configured accordingly).|
|scope||str = "openid profile email"||The scope of the request, by default an
|redirect_uri||Optional[str] = None||If specified, the redirect URL that must match the one configured for the application. If not provided, a redirect_url is obtained automatically (see note 🗡️).|
|scheme_name||str = "OpenIDConnect"||The name of the authentication scheme, affecting the name of authentication cookies (see note 🍒).|
|error_redirect_path||Optional[str] = None||If specified, the local path to which a user is redirected in case of error.|
- 🗡️ obtaining a redirect_url automatically can require handling of forward headers, when an application is deployed behind a proxy. See remotes for more information.
- 🍒 this should be changed when configuring more than one OIDC identity provider.
access_tokens issued for APIs can be validated using
JWT Bearer authentication
Examples using custom scopes¶
An integration with a
Auth0 application that uses custom scopes, where the
application obtains both an
id_token and an
access_token for an API,
looks like the following:
import os from blacksheep import Application, html, pretty_json from blacksheep.server.authentication.oidc import ( OpenIDSettings, use_openid_connect, CookiesTokensStore, ) from guardpost.authentication import Identity app = Application() # basic Auth0 integration that handles only an id_token use_openid_connect( app, OpenIDSettings( authority="<YOUR_AUTHORITY>", audience="<API_ID>", client_id="<CLIENT_ID>", client_secret=os.environ["AUTH0_APP_SECRET"], callback_path="<CALLBACK_PATH>", scope="openid profile custom_scope", ), tokens_store=CookiesTokensStore(), ) @app.route("/") async def home(user: Identity): if user.is_authenticated(): response = pretty_json(user.claims) return response return html("<a href='/sign-in'>Sign in</a><br/>")
An integration with
Azure Active Directory:
use_openid_connect( app, OpenIDSettings( authority="https://login.microsoftonline.com/<YOUR_TENANT_ID>/v2.0/", client_id="<CLIENT_ID>", client_secret=os.environ["AAD_APP_SECRET"], scope="openid profile custom_scope", ), tokens_store=CookiesTokensStore(), )
An integration with
Okta, using the
default authorization server:
use_openid_connect( app, OpenIDSettings( discovery_endpoint="https://<YOUR_DOMAIN>.okta.com/oauth2/default/.well-known/oauth-authorization-server", client_id="<CLIENT_ID>", client_secret=os.environ["OKTA_APP_SECRET"], scope="openid email custom_scope", ), tokens_store=CookiesTokensStore(), )
The API exposes the following events:
from blacksheep.server.authentication.oidc import ( OpenIDSettings, TokenResponse, use_openid_connect ) oidc = use_openid_connect(...) @oidc.events.on_id_token_validated async def id_token_callback(context, id_token_claims): """ This callback is called after an id_token is received, successfully verified using public RSA keys from the identity provider, and parsed Using this event handler is possible to modify the claims obtained from the id_token before they are set in the authentication cookie. For example to remove certain claims, or add other information. """ @oidc.events.on_tokens_received async def on_tokens_received(context, token_response: TokenResponse): """ This callback is called after a successful web request to the token endpoint to exchange a code with an access_token, and eventually refresh_token, and id_token. """ @oidc.events.on_error async def on_error(context, data: Dict[str, Any]): """ This callback is called when an error is returned by the authorization server in the redirect handler. Note that this can happen for a common scenario, like the user refusing to grant consent on the application. """
refresh_token(s) are not stored. To store
use_openid_connect function supports a
that will handle storing and restoring tokens. A concrete implementation is
CookiesTokenStore, storing tokens in cookies. It is possible to
create custom implementations of the
BaseTokensStore, to use other mechanisms,
for example to store tokens in a Redis cache.
When a user is authenticated, and has an
access_token (and/or a
refresh_token), they are accessible through the
@app.route("/") async def home(user: Identity): if user.is_authenticated(): print(user.access_token) print(user.refresh_token) ...
To see how to use a
BaseTokensStore, refer to the examples above that use
How tokens are protected¶
Tokens that are stored in cookies are signed and encrypted using
with symmetric encryption. This means that BlackSheep applications need secrets
to protect sensitive data. When keys are not specified, they are generated
automatically in memory, for best user's experience.
This means that keys are not persisted when applications restart, and not consistent when multiple instances of the same application are deployed across regions, or within a same server. This is acceptable during local development, but should not be the case in production environments.
To use consistent keys, configure one or more environment variables like the following:
Keys can be configured in a host environment, or fetched from a dedicated
service such as
AWS Secrets Manager or
Azure Key Vault at application
start-up, and configured as environment settings for the application.
DO NOT store secrets that are meant to be used in production
under source control.
For more information, refer to data protection.