Skip to content

OpenID Connect

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 id_tokens)
  • How tokens are protected and how to configure applications to support multiple instances and regions

Warning

Using JWT Bearer and OpenID integrations requires more dependencies: use pip install blacksheep[full] to use these features

Basic example

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>",
    ),
)


@get("/")
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 examples:

@get("/")
async def home(user: Identity):
    if user.is_authenticated():
        ...

use_openid_connect

Parameter Type, default Description
app Application Instance of BlackSheep application.
settings OpenIDSettings Instance of OpenIDSettings.
auth_handler Optional[OpenIDTokensHandler] = None (CookiesOpenIDTokensHandler) Instance of OpenIDTokensHandler that can handle tokens for requests and responses for the OpenID Connect flow. This class is responsible for communicating tokens to clients, and restoring tokens context for following requests.
parameters_builder Optional[ParametersBuilder] = None Optional instance of ParametersBuilder, used to handle parameters configured in redirects and requests to the authorization server.
is_default bool = True If default, clients are automatically redirected to the sign-in page when a non-authenticated user tries to access in GET a web page that requires authentication.

OpenIDSettings

The OpenIDSettings class has the following properties:

Parameter Type, default Description
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 audience for requests using scopes to an API (ref.).
client_secret Optional[str] = None For requests that use Authorization Code Grant flow, the secret of the client application in the identity server.
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).
refresh_token_path str = "/refresh-token" The local path used to handle refresh tokens to obtain new tokens.
scope str = "openid profile email" The scope of the request, by default an id_token is obtained with email and profile.
response_type str = "code" Type of OAuth response.
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.
end_session_endpoint Optional[str] = None If specified, the local path to which the user can log out.

Notes:

  • 🗡️ 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.

Info

access_tokens issued for APIs can be validated using JWT Bearer authentication

Examples using custom scopes

An integration with an 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:

"""
This example shows how to configure an OpenID Connect integration with Auth0, obtaining
an id_token, an access_token, and a refresh_token. The id_token is exchanged with the
client using a response cookie (also used to authenticate users
for following requests), while the access token and the refresh token are not stored
and can only be accessed using optional events.
"""
import uvicorn
from blacksheep.server.application import Application
from blacksheep.server.authentication.oidc import OpenIDSettings, use_openid_connect
from dotenv import load_dotenv

from common.routes import register_routes
from common.secrets import Secrets

load_dotenv()
secrets = Secrets.from_env()
app = Application(show_error_details=True)


# Auth0 with a custom scope
use_openid_connect(
    app,
    OpenIDSettings(
        authority="https://neoteroi.eu.auth0.com",
        audience="http://localhost:5000/api/todos",
        client_id="OOGPl4dgG7qKsm2IOWq72QhXV4wsLhbQ",
        client_secret=secrets.auth0_client_secret,
        callback_path="/signin-oidc",
        scope="openid profile read:todos",
        error_redirect_path="/sign-in-error",
    ),
)

register_routes(app)


if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=5000, log_level="debug")

An integration with Entra ID:

handler = use_openid_connect(
    app,
    OpenIDSettings(
        authority="https://login.microsoftonline.com/b62b317a-19c2-40c0-8650-2d9672324ac4/v2.0/",
        client_id="499adb65-5e26-459e-bc35-b3e1b5f71a9d",
        client_secret=secrets.aad_client_secret,
        scope="openid profile offline_access email "
        "api://65d21481-4f1a-4731-9508-ad965cb4d59f/example",
    ),
)

An integration with Okta, using the default authorization server:

use_openid_connect(
    app,
    OpenIDSettings(
        discovery_endpoint="https://dev-34685660.okta.com/oauth2/default/.well-known/oauth-authorization-server",
        client_id="0oa2gy88qiVyuOClI5d7",
        client_secret=secrets.okta_client_secret,
        callback_path="/authorization-code/callback",
        scope="openid read:todos",
    ),
)

Events

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.
    """

Storing tokens

By default, access_token(s) and refresh_token(s) are not stored. To store them, the auth_handler.tokens_store property. The examples repository includes an example that shows how to use Redis to store tokens: Redis example

A concrete implementation is provided in CookiesTokenStore, storing tokens in cookies. It is possible to create custom implementations of the TokensStore, 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 Identity:

@get("/")
async def home(user: Identity):
    if user.is_authenticated():
        print(user.access_token)
        print(user.refresh_token)
        ...

To see how to use a TokensStore, refer to the examples above that use the built-in CookiesTokensStore.

Useful references

How tokens are protected

Tokens that are stored in cookies are signed and encrypted using itsdangerous, 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 the best user experience.

Danger

This means that keys are not persisted when applications restart, and are not consistent when multiple instances of the same application are deployed across regions, or within the 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:

  • APPSECRET_1="***"
  • APPSECRET_2="***"
  • APPSECRET_3="***"

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.

Last modified on: 2023-12-18 17:52:09

EW
RV