Skip to content

certified.fast — FastAPI Integration

Requires the http extra (pip install 'certified[http]').

Provides FastAPI dependencies for extracting peer certificate info and authorizing requests via biscuit tokens.

Peer certificate dependencies

get_peercert

FastAPI dependency for returning client cert. information

Example return

{"subject":[[["commonName","Charles T. User"]]], "issuer":[[["commonName","Charles T. User"]], [["pseudonym","Signing Certificate"]]], "version":3, "serialNumber":"03A2", "notBefore":"Sep 12 05:48:44 2024 GMT", "notAfter":"Sep 12 05:48:44 2025 GMT", "subjectAltName":[["email","hello@localhost"], ["DNS","localhost"], ["IP Address","127.0.0.1"]] }

Source code in certified/fast.py
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
def get_peercert(request: Request) -> Dict[str,Any]:
    """FastAPI dependency for returning client cert. information

    Example return:
      {"subject":[[["commonName","Charles T. User"]]],
       "issuer":[[["commonName","Charles T. User"]],
                 [["pseudonym","Signing Certificate"]]],
       "version":3,
       "serialNumber":"03A2",
       "notBefore":"Sep 12 05:48:44 2024 GMT",
       "notAfter":"Sep 12 05:48:44 2025 GMT",
        "subjectAltName":[["email","hello@localhost"],
                         ["DNS","localhost"],
                         ["IP Address","127.0.0.1"]]
      }
    """

    if "transport" not in request.scope:
        return {}
    transport = request.scope["transport"]
    ans = transport.get_extra_info("peercert")
    if ans is None:
        return {}
    return ans

get_remote_addr

Source code in certified/fast.py
26
27
28
29
def get_remote_addr(request: Request) -> Optional[str]:
    if request.client is None:
        return None
    return request.client.host

get_clientname

A helper to return either (in priority order)

  1. uid:{userid of client}
  2. cn:{common name of client}
  3. addr:{ip addr. of client}
Source code in certified/fast.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
def get_clientname(request: Request) -> str:
    """ A helper to return either (in priority order)

    1. uid:{userid of client}
    2. cn:{common name of client}
    3. addr:{ip addr. of client}
    """
    cert = get_peercert(request)
    try:
        return name_from_peer(cert)
    except KeyError:
        addr = get_remote_addr(request)
    if addr is None:
        raise HTTPException(status_code=401,
                            detail='Client identity is required.')
    return f"addr:{addr}"

name_from_peer

Source code in certified/fast.py
58
59
60
61
62
63
64
65
66
67
68
69
def name_from_peer(peer : Dict[str,Any]) -> str:
    name = None
    for nlist in peer["subject"]:
        for n in nlist:
            if n[0] == 'commonName':
                if name is None or name.startswith("cn:"):
                    name = f"cn:{n[1]}"
            elif n[0] == 'userID' or n[0] == 'userId':
                name = f"uid:{n[1]}"
    if name is None:
        raise KeyError("No usable name in peer certificate.")
    return name

PeerCert and ClientName are Annotated type aliases suitable for use as FastAPI dependency parameters:

from certified.fast import PeerCert, ClientName

@app.get("/info")
async def info(peer: PeerCert, name: ClientName):
    return {"name": name, "cert": peer}

Biscuit token issuance

Baker

This class provides a "get_token" method to prepare (bake) a token for a user.

It should be used to create a "/token" endpoint as follows:

from certified.fast import Baker cert = Certified() baker = Baker(cert.signer()) app.post("/token")(baker.get_token)

Source code in certified/fast.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
class Baker:
    """This class provides a "get_token" method
    to prepare (bake) a token for a user.

    It should be used to create a "/token" endpoint as follows:

    >>> from certified.fast import Baker
    >>> cert = Certified()
    >>> baker = Baker(cert.signer())
    >>> app.post("/token")(baker.get_token)
    """
    def __init__(self, ca : CA):
        self.ca = ca

    def get_token(self,
                  client: ClientName,
                  hours: Optional[float] = 24.0):
        """ Returns a biscuit certifying the user identity
        and token lifetime.

        Applications should supply this as their "/token" endpoint
        """
        builder : BiscuitBuilder
        if hours is None:
            builder = BiscuitBuilder(
                "user({client});", {'client': client})
        else:
            builder = BiscuitBuilder(
                "user({client});"
                " check if time($time), $time < {expiration};",
                { 'client': client,
                  'expiration': datetime.now(tz=timezone.utc)
                                 + timedelta(hours=hours)
                })
        biscuit = self.ca.sign_biscuit(builder)
        token = biscuit.to_base64()
        return token

get_token(client, hours=24.0)

Returns a biscuit certifying the user identity and token lifetime.

Applications should supply this as their "/token" endpoint

Source code in certified/fast.py
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
def get_token(self,
              client: ClientName,
              hours: Optional[float] = 24.0):
    """ Returns a biscuit certifying the user identity
    and token lifetime.

    Applications should supply this as their "/token" endpoint
    """
    builder : BiscuitBuilder
    if hours is None:
        builder = BiscuitBuilder(
            "user({client});", {'client': client})
    else:
        builder = BiscuitBuilder(
            "user({client});"
            " check if time($time), $time < {expiration};",
            { 'client': client,
              'expiration': datetime.now(tz=timezone.utc)
                             + timedelta(hours=hours)
            })
    biscuit = self.ca.sign_biscuit(builder)
    token = biscuit.to_base64()
    return token

Biscuit token authorization

BiscuitAuthz

For additional help on using biscuit attributes and checks, see docs/authz.md

Instances of this class are callables which will check (critique) a token for a particular purpose.

It may be used directly as follows:

from biscuit_auth import PublicKey from certified.fast import BiscuitAuthz app_name = name pubkey = lambda i: PublicKey.from_bytes( "authorizer pubkey" ) DefaultAuthz = Annotated[bool, BiscuitAuthz(app_name, pubkey)] async def get_info(info: str, authz: DefaultAuthz): # authz is always True here return info

This will allow use of the get_info endpoint only if the caller provides a valid biscuit that passes all its own internal restrictions.

Note this is a parameterized dependency. See https://fastapi.tiangolo.com/advanced/advanced-dependencies

Source code in certified/fast.py
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
class BiscuitAuthz:
    """For additional help on using biscuit attributes
    and checks, see docs/authz.md

    Instances of this class are callables which will
    check (critique) a token for a particular purpose.

    It may be used directly as follows:
    >>> from biscuit_auth import PublicKey
    >>> from certified.fast import BiscuitAuthz
    >>> app_name = __name__
    >>> pubkey = lambda i: PublicKey.from_bytes( "authorizer pubkey" )
    >>> DefaultAuthz = Annotated[bool, BiscuitAuthz(app_name, pubkey)]
    >>> async def get_info(info: str, authz: DefaultAuthz):
    >>>    # authz is always True here
    >>>    return info

    This will allow use of the get_info endpoint only
    if the caller provides a valid biscuit that passes all
    its own internal restrictions.

    Note this is a parameterized dependency.
    See https://fastapi.tiangolo.com/advanced/advanced-dependencies
    """
    app: str
    pubkeys: Callable[[int], PublicKey]
    auth_method: AuthMethod

    def __init__(self,
                 app: str,
                 pubkeys: Union[List[PublicKey],
                                Callable[[int], PublicKey]],
                 auth_method: AuthMethod = run_authz
                ) -> None:
        self.app = app

        if isinstance(pubkeys, list):
            self.pubkeys = lambda i: pubkeys[i]
        else:
            self.pubkeys = pubkeys
        self.auth_method = auth_method

    def lookup_public_key(self, kid : Optional[int] = None
                         ) -> PublicKey:
        ans = self.pubkeys(kid or 0)
        return ans

    def biscuit(self, token : str) -> Biscuit:
        # may throw BiscuitValidationError
        return Biscuit.from_base64(token, self.lookup_public_key)

    def __call__(self,
                 request: Request,
                 biscuit: Annotated[Union[str,None],Header()] = None
                ) -> bool:
        if biscuit is None:
            raise HTTPException(status_code=401, detail='Required header "Biscuit: urlsafe-b64-encoded-value" not found.')

        try:
            bis = self.biscuit(biscuit)
        except BiscuitValidationError:
            raise HTTPException(status_code=401, detail='Required header "Biscuit: urlsafe-b64-encoded-value" invalid format.')

        cert = get_peercert(request)
        client = get_clientname(request)

        authorizer = AuthorizerBuilder(
                    "time({now});"
                    " client({client});"
                    " service({srv});"
                    " path({path});"
                    " operation({operation});",
                    {'now': datetime.now(tz = timezone.utc),
                     'client': client,
                     'srv': self.app,
                     'path': request.url.path,
                     'operation': request.method
                    })
        if 'serialNumber' in cert:
            authorizer.add_fact(Fact('client_serial({no})',
                                {'no': cert['serialNumber']}))
        if not self.auth_method(authorizer, bis):
            raise HTTPException(status_code=403, detail='Forbidden')
        return True

Critic

Returns an Authorizer, which can be called with a custom authorizer to add authentication to your FastAPI endpoint.

The following example creates a list of accepted public keys, then creates a custom authorizer which adds a check, and then calls the rest of the auth chain. When creating an API route, Authz is a handy dependency to call this new auth chain.

from biscuit_auth import PublicKey, AuthorizerBuilder, Biscuit from certified.fast import Critic, run_authz pubkeys = [PublicKey("authorizer pubkey1"), PublicKey("authorizer pubkey2")] def custom(authorizer: AuthorizerBuilder, token: Biscuit) -> bool: authorizer.add_code(...) return run_authz(authorizer, token) Authz = Critic("frontend app name", pubkeys) async def post_config(info: str, authz: Annotated[bool, Authz(custom)): # authz is a check that is always True (or else FastAPI would have raised 403 already) do_something_with(info)

Since Authz("admin:write") has created a dependency (of type BiscuitAuthz(app, pubkeys, "admin:write")), that dependency can gather data from: - the client certificate, providing client({id}) - the URL accessed, providing path({path}) and operation({method}) - the call header, where a "Biscuit: b64_encoded_biscuit" is required

It then throws an HTTP Unauthorized/401 if a biscuit is missing, Forbidden/403 if the biscuit auth fails, or else returns True otherwise.

Source code in certified/fast.py
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
def Critic(app: str,
           pubkeys: Union[List[PublicKey],
                          Callable[[int], PublicKey]]
          ) -> Callable[[AuthMethod], bool]:
    """Returns an Authorizer, which can be called
    with a custom authorizer to add authentication to your
    FastAPI endpoint.

    The following example creates a list of accepted public keys,
    then creates a custom authorizer which adds a check, and then
    calls the rest of the auth chain.  When creating an API
    route, Authz is a handy dependency to call this new auth chain.

    >>> from biscuit_auth import PublicKey, AuthorizerBuilder, Biscuit
    >>> from certified.fast import Critic, run_authz
    >>> pubkeys = [PublicKey("authorizer pubkey1"), PublicKey("authorizer pubkey2")]
    >>> def custom(authorizer: AuthorizerBuilder, token: Biscuit) -> bool:
    >>>    authorizer.add_code(...)
    >>>    return run_authz(authorizer, token)
    >>> Authz = Critic("frontend app name", pubkeys)
    >>> async def post_config(info: str, authz: Annotated[bool, Authz(custom)):
    >>>    # authz is a check that is always True (or else FastAPI would have raised 403 already)
    >>>    do_something_with(info)

    Since `Authz("admin:write")` has created a dependency
    (of type BiscuitAuthz(app, pubkeys, "admin:write")),
    that dependency can gather data from:
     - the client certificate, providing client({id})
     - the URL accessed, providing path({path}) and operation({method})
     - the call header, where a "Biscuit: b64_encoded_biscuit" is
       required

    It then throws an HTTP Unauthorized/401 if a biscuit is missing,
    Forbidden/403 if the biscuit auth fails, or else returns True otherwise.
    """
    return lambda azfn: Depends(BiscuitAuthz(app, pubkeys, azfn))

run_authz

Default is to call auth.build(token).authorize() and ignore revocation id-s.

This will ensure all the biscuit's (client-controlled) checks pass, but not enforce any specific security requirements from the server's side other than that the certificate names a user.

Source code in certified/fast.py
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
def run_authz(auth: AuthorizerBuilder, token: Biscuit) -> bool:
    """ Default is to call auth.build(token).authorize()
        and ignore revocation id-s.

        This will ensure all the biscuit's (client-controlled)
        checks pass, but not enforce any specific security
        requirements from the server's side other than
        that the certificate names a user.
    """
    # Note: you should also examine
    # token.revocation_ids

    #auth.add_policy(Policy('allow if true'))
    auth.add_policy(Policy('allow if user($u)'))
    try:
        auth.build(token).authorize()
    except AuthorizationError:
        return False
    return True

AuthMethod

Bases: Protocol

Source code in certified/fast.py
129
130
131
class AuthMethod(Protocol):
    def __call__(self, auth: AuthorizerBuilder, token: Biscuit) -> bool:
        return False