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()
:
- Python
- TypeScript
from_account = Account.ref(
request.from_account_id,
bearer_token=bearer_token,
)
const fromAccount = Account.ref(request.fromAccountId, { bearerToken });
You can also pass it as an option when doing explicit construction:
- Python
- TypeScript
bank, _ = await Bank.Create(
context,
SINGLETON_BANK_ID,
Options(bearer_token=VALID_JWT),
)
const [bank] = await Bank.create(
context,
SINGLETON_BANK_ID,
{},
{ bearerToken: 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:
- Python
- React
- TypeScript
context = ExternalContext(
name='Example',
url='http://localhost:9991',
bearer_token=token,
)
import { useRebootContext } from "@reboot-dev/reboot-react";
const { setAuthorizationBearer } = useRebootContext();
setAuthorizationBearer(token);
const context = new ExternalContext({
name: "Example",
url: "http://localhost:9991",
bearerToken: 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):
- Python
- TypeScript
async def main():
application = Application(
servicers=[...],
token_verifier=MyTokenVerifier(...),
)
await application.run()
new Application({
servicers: [...],
...,
tokenVerifier: new MyTokenVerifier(...),
}).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:
- Python
- TypeScript
@abstractmethod
async def verify_token(
self,
context: ReaderContext,
token: Optional[str],
) -> Optional[Auth]:
abstract verifyToken(
context: ReaderContext,
token?: string
): Promise<Auth | null>;
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).
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 Servicer
s:
- Python
- TypeScript
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]),
...
)
...
import { allow, allowIf } from "@reboot-dev/reboot";
class AccountServicer extends Account.Servicer {
authorizer() {
return new Account.Authorizer({
balance: allowIf({ any: [isAdmin, isAccountOwner] }),
deposit: allow(),
withdraw: allowIf({ all: [isAccountOwner] }),
...
});
}
...
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.
- Python
- TypeScript
from reboot.aio.auth.authorizers import allow_if
class AccountServicer(Account.Servicer):
def authorizer(self):
return allow_if(all=[is_admin])
...
import { allowIf } from "@reboot-dev/reboot";
class AccountServicer extends Account.Servicer {
authorizer() {
return allowIf({ all: [isAdmin] });
}
...
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:
- Python
- TypeScript
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()
import { errors_pb } from "@reboot-dev/reboot-api";
function isValidUser({ context }: {
context: ReaderContext;
state?: Message;
request?: Message;
}) {
if (!context.auth) {
return new errors_pb.Unauthenticated();
}
if (await validateUser(context.auth.userId)) {
return new errors_pb.Ok();
}
return new errors_pb.PermissionDenied();
}
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
.
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.
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:
- Implementing a
TokenVerifier
that validates access tokens, and producesAuth
objects containing any relevant properties. - Providing authorizer rules for your servicers.