Skip to main content

Authentication

Overview

Reboot has native support for authenticating clients using the widely used Authorization: Bearer header.

Depending on your application's requirements, clients might use access tokens (such as JWTs) or long-lived API keys -- produced by your choice of authentication backend.

Client

To include a token on all requests made from a particular client context, a token can be set at the context level:

from reboot.aio.external import ExternalContext

context = ExternalContext(
name='example-context',
gateway=..,
bearer_token=token,
)

A token may also be specified for all calls made to a particular state instance by specifying it in lookup:

from_account = Account.lookup(
request.from_account_id,
bearer_token=bearer_token,
)

Backend

To authenticate and authorize using a token on the backend, Reboot applications use two interfaces: TokenVerifier (which handles parsing and validation of tokens) and Authorizer (which decides whether a caller has access to a particular state or state method).

info

Python backends support authentication. TypeScript support is coming soon.

TokenVerifier

The TokenVerifier interface has a single method, which receives the token from the Authorization: Bearer header, and which should return an Auth object if the token was valid:

@abstractmethod
async def verify_token(
self,
context: ReaderContext,
token: str,
) -> Optional[Auth]:

A TokenVerifier is set globally on your Reboot Application:

async def main():
application = Application(
servicers=[...],
token_verifier=MyTokenVerifier(...),
)
await application.run()

When a TokenVerifier is installed, Authorizers must also be installed in order to consume the Auth objects that are produced.

Authorizer

Authorizers are associated with state Servicers, and the authorizer method returns the Authorizer that will be used:

class AuthAccountServicer(AccountServicer):
"""Servicer that shares implementation with `AccountServicer` but uses
a custom Authorizer.
"""

def authorizer(self) -> Optional[Account.Authorizer]:
return AccountAuthorizer()

There are two ways to implement the Authorizer interface:

Per-state authorization

If most of the methods of your servicer have the same authorization strategy and don't need access to the method arguments, then you can directly implement the Authorizer interface.

The built-in AllowAllIfAuthenticated Authorizer can be used unmodified, but also serves as a good example of implementing the Authorizer interface:

class AllowAllIfAuthenticated(Authorizer[Message, Message]):
"""An authorizer that allows all requests if the caller is authenticated."""

def can_authorize(self, method_name: str) -> bool:
return True

async def authorize(
self,
*,
method_name: str,
context: ReaderContext,
state: Optional[Message] = None,
request: Optional[Message] = None,
) -> Authorizer.Decision:
if context.auth is None:
return rbt.v1alpha1.errors_pb2.Unauthenticated()

return rbt.v1alpha1.errors_pb2.Ok()

Per-method authorization

Alternatively, if the methods of your state have a variety of different authorization strategies or need access to the method arguments, then you can use an instance of the Authorizer interface which is generated for each Servicer.

For example, a writer method in the Bank state like:

rpc SignUp(SignUpRequest) returns (SignUpResponse) {
option (rbt.v1alpha1.method).transaction = {
};
}

... results in a generated abstract Bank.Authorizer subclass with a authorization method that can be implemented like so:

async def SignUp(
self,
context: ReaderContext,
state: Bank.State,
request: bank_rbt.SignUpRequest,
) -> Bank.Authorizer.Decision:
if context.auth is None:
return Unauthenticated()

if context.auth.user_id != TEST_USER:
return PermissionDenied()

return Ok()
note

Authorizer methods should not have effects, so they always receive a ReaderContext regardless of the kind of method that they are authorizing for.

Thirdparty auth providers

Integrating a thirdparty auth provider like Auth0 or Ory into your backend usually involves:

  1. implementing a TokenVerifier that validates access tokens, and produces Auth objects containing any relevant permissions.
  2. implementing an Authorizer that authorizes calls at either the method or state level with those permissions.