Skip to main content

Authorization

Overview

Reboot has native support for authorizing calls using the widely used Authorization: Bearer header, as well as via mechanisms like cookies.

Depending on your application's requirements, callers might use API keys or access tokens (such as JWTs), produced by your choice of provider, e.g., Auth0 or Ory.

Setting bearer tokens

To include a bearer token for all calls made to a particular state instance you can specify it in ref():

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

You can also pass it as an option when doing explicit construction:

bank, _ = await Bank.Create(
context,
SINGLETON_BANK_ID,
Options(bearer_token=VALID_JWT),
)

External to a Reboot application

To include a bearer token on all calls made from an ExternalContext, or from React generated code, do the following:

context = ExternalContext(
name='Example',
url='http://localhost:9991',
bearer_token=token,
)

Authorizing calls to your application

Authorization in Reboot is done by first performing token verification and then calling any authorizers.

Token verification

To verify that a provided token is valid you must provide a TokenVerifier. A TokenVerifier is set globally on your Reboot Application (or on Reboot.up in the case of unit tests):

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

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: Optional[str],
) -> Optional[Auth]:

In addition to being able to set any arbitrary properties on the returned Auth object that authorizers can consume, there is a specific user_id (Python), userId (TypeScript) property meant to indicate that this is valid user (and its internally represented ID).

tip

Token verification does not necessarily mean user authentication!

Depending on where/how tokens are generated they may or may not indicate that these tokens represent valid users for your application, just that this token is valid for a user from a particular provider. Read the provider's documentation carefully to ensure that you are validating the tokens in such a way that they are specific to your application.

Authorizers

After token verification Reboot will call into any provided authorizers. You provide an authorizer by implementing the authorizer() method on your Servicers:

from reboot.aio.auth.authorizers import allow, allow_if

class AccountServicer(Account.Servicer):

def authorizer(self):
return Account.Authorizer(
Balance=allow_if(any=[is_admin, is_account_owner]),
Deposit=allow(),
Withdraw=allow_if(all=[is_account_owner]),
...
)

...

When you create an authorizer for a specific state type you provide authorizer "rules" for each method, for example, deny(), allow(), or allow_if().

You can alternatively return a single authorizer rule directly from authorizer() which will apply to all methods.

from reboot.aio.auth.authorizers import allow_if

class AccountServicer(Account.Servicer):

def authorizer(self):
return allow_if(all=[is_admin])

...

Authorizer rules

deny()

An authorizer rule that will deny all requests.

allow()

An authorizer rule that will allow all requests.

allow_if()

An authorizer rule that takes a set of authorizer callables, i.e., functions that will perform the authorization. These functions return an Authorizer.Decision, which is either Ok, PermissionDenied, or Unauthenticated. For example, here is an authorizer callable for making sure that the token was verified for a valid user:

from rbt.v1alpha1 import errors_pb2

def is_valid_user(
*,
context: ReaderContext,
state: Optional[Message],
request: Optional[Message],
**kwargs,
):
if context.auth is None:
return errors_pb2.Unauthenticated()

if await validate_user(context.auth.user_id):
return errors_pb2.Ok()

return errors_pb2.PermissionDenied()
important

If you want to allow a call only when all of your authorizer callables decide Ok, pass them via all. If you want to allow a call if any (even just one) of the authorizer callables decide Ok, pass them via any.

note

Verifying a token and running authorizers should not have effects, so they always receive a ReaderContext regardless of the kind of method that they are authorizing for.

Default authorizer

By default, if you don't provide an authorizer by overriding authorizer(), only application internal calls are allowed.

important

Why am I seeing log messages about MISSING AUTHORIZATION?

To streamline development using rbt dev, calls missing authorization are allowed. When running with rbt serve or via rbt cloud those same calls will be denied so we emit a log warning to help you identify those calls.

Implement the authorizer() method on your Servicer to silence the warning.

Thirdparty auth providers

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

  1. Implementing a TokenVerifier that validates access tokens, and produces Auth objects containing any relevant properties.
  2. Providing authorizer rules for your servicers.