Skip to content

Authentication in BlackSheep

The words "authentication strategy" in the context of a web application refer to the ability to identify the user who is using the application. BlackSheep implements a built-in authentication strategy for request handlers. This page describes:

  • How to use the built-in authentication strategy.
  • How to configure a custom authentication handler.
  • How to use the built-in support for JWT Bearer authentication.
  • How to read the user's context in request handlers.

Warning

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

Underlying library

The authentication and authorization logic implemented for BlackSheep was packed and published into a dedicated library: guardpost (in pypi).

How to use built-in authentication

Examples of common strategies to identify users in web applications include:

  • reading an Authorization: Bearer xxx request header containing a JWT with claims that identify the user
  • reading a signed token from a cookie

The next paragraphs explain first how to use the built-in support for JWT Bearer tokens, and how to write a custom authentication handler.

Info

The word "user" is usually used only to refer to human users, while the word "service" is used to describe non-human clients. In Java and .NET, a common word to describe a generic identity is "principal".

OIDC

BlackSheep implements built-in support for OpenID Connect authentication, meaning that it can be easily integrated with identity provider services such as:

Examples in GitHub

The Neoteroi/BlackSheep-Examples/ repository in GitHub contains examples of JWT Bearer authentication and OpenID Connect integrations.

A basic example integration with any of the identity providers above, having implicit flow enabled for id_token (meaning that the code doesn't need to handle any secret), looks like the following:

from blacksheep import Application, get, html, pretty_json
from blacksheep.server.authentication.oidc import OpenIDSettings, use_openid_connect
from guardpost.authentication import Identity

app = Application()


# basic Auth0 integration that handles only an id_token
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/>")

Where:

Parameter Description
YOUR_AUTHORITY The URL to your account, like https://neoteroi.eu.auth0.com
CLIENT_ID Your app registration ID
CALLBACK_PATH The path that is enabled for reply_uri in your app settings, for example if you enabled for localhost: http://localhost:5000/authorization-callback, the value should be /authorization-callback

For more information and examples, refer to the dedicated page about OpenID Connect authentication.

JWT Bearer

BlackSheep implements built-in support for JWT Bearer authentication, and validation of JWTs:

  • issued by identity providers implementing OpenID Connect (OIDC) discovery (such as Auth0, Azure Active Directory)
  • and more in general, JWTs signed using asymmetric encryption and verified using public RSA keys

The following example shows how to configure JWT Bearer authentication for an application registered in Azure Active Directory, and also how to configure authorization to restrict access to certain methods, only for users who are successfully authenticated:

from guardpost import Policy, User
from guardpost.common import AuthenticatedRequirement

from blacksheep import Application, get, json
from blacksheep.server.authentication.jwt import JWTBearerAuthentication
from blacksheep.server.authorization import auth

app = Application()

app.use_authentication().add(
    JWTBearerAuthentication(
        authority="https://login.microsoftonline.com/<YOUR_TENANT_NAME>.onmicrosoft.com",
        valid_audiences=["<YOUR_APP_CLIENT_ID>"],
        valid_issuers=["https://login.microsoftonline.com/<YOUR_TENANT_ID>/v2.0"],
    )
)

# configure authorization, to restrict access to methods using @auth decorator
authorization = app.use_authorization()

authorization += Policy("example_name", AuthenticatedRequirement())


@get("/")
def home():
    return "Hello, World"


@auth("example_name")
@get("/api/message")
def example():
    return "This is only for authenticated users"


@get("/open/")
async def open(user: User | None):
    if user is None:
        return json({"anonymous": True})
    else:
        return json(user.claims)

The built-in handler for JWT Bearer authentication does not support JWTs signed with symmetric keys. Support for symmetric keys might be added in the future, inside guardpost library.

Info

💡 It is possible to configure several JWTBearerAuthentication handlers, for applications that need to support more than one identity provider. For example, for applications that need to support sign-in through Auth0, Azure Active Directory, Azure Active Directory B2C.

Writing a custom authentication handler

The example below shows how to configure a custom authentication handler that obtains the user's identity for each web request.

from blacksheep import Application, Request, auth, get, json
from guardpost import AuthenticationHandler, Identity, User


app = Application(show_error_details=True)


class ExampleAuthHandler(AuthenticationHandler):
    def __init__(self):
        pass

    async def authenticate(self, context: Request) -> Identity | None:
        # TODO: apply the desired logic to obtain a user's identity from
        # information in the web request, for example reading a piece of
        # information from a header (or cookie).
        header_value = context.get_first_header(b"Authorization")

        if header_value:
            # implement your logic to obtain the user
            # in this example, an identity is hard-coded just to illustrate
            # testing in the next paragraph
            context.identity = Identity({"name": "Jan Kowalski"}, "MOCK")
        else:
            # if the request cannot be authenticated, set the context.identity
            # to None - do not throw exception because the app might support
            # different ways to authenticate users
            context.identity = None
        return context.identity


app.use_authentication().add(ExampleAuthHandler())


@get("/")
def home():
    return "Hello, World"


@auth("example_name")
@get("/api/message")
def example():
    return "This is only for authenticated users"


@get("/open/")
async def open(user: User | None):
    if user is None:
        return json({"anonymous": True})
    else:
        return json(user.claims)

It is possible to configure several authentication handlers to implement different ways to identify users. To differentiate the way the user has been authenticated, use the second parameter of Identity's constructor:

identity = Identity({"name": "Jan Kowalski"}, "AUTHENTICATION_MODE")

The authentication context is the instance of Request created to handle the incoming web request. Authentication handlers must set the identity property on the request, to enable automatic injection of user by dependency injection.

Testing the example

To test the example above, start a web server as explained in the getting started guide, then navigate to its root. A web request to the root of the application without an Authorization header will produce a response with the following body:

{"anonymous":true}

While a web request with an Authorization header will produce a response with the following body:

{"name":"Jan Kowalski"}

For example, to generate web requests using curl:

curl  http://127.0.0.1:44555/open

Gets the output: {"anonymous":true}.

curl -H "Authorization: foo" http://127.0.0.1:44555/open

Gets the output: {"name":"Jan Kowalski"}.

The application has been started on port 44555 (e.g. uvicorn server:app --port=44555).

Reading a user's context

The example below shows how a user's identity can be read from the web request:

from guardpost.authentication import Identity


@get("/")
async def for_anybody(user: Identity | None):
    ...
@get("/")
async def for_anybody(request: Request):
    user = request.identity
    # user can be None or an instance of Identity (set in the authentication
    # handler)

Next

While authentication deals with identifying users, authorization deals with determining whether the user is authorized to do the action of the web request. The next page describes the built-in authorization strategy in BlackSheep.

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

EW
RV