Securing Routes with Authorizer
Instances of Authorizer
are added to an application channel to verify HTTP request's authorization information before passing the request onwards. They protect channel access and typically come right after route
. Here's an example:
@override
Controller get entryPoint {
final router = Router();
router
.route("/protected")
.link(() => Authorizer.bearer(authServer))
.link(() => ProtectedController());
router
.route("/other")
.link(() => Authorizer.basic(authServer))
.link(() => OtherProtectedController());
return router;
}
An Authorizer
parses the Authorization header of an HTTP request. The named constructors of Authorizer
indicate the required format of Authorization header. The Authorization.bearer()
constructor expects an OAuth 2.0 bearer token in the header, which has the following format:
Authorization: Bearer 768iuzjkx82jkasjkd9z9
Authorizer.basic
expects HTTP Basic Authentication, where the username and password are joined with the colon character (:
) and Base 64-encoded:
// 'dXNlcjpwYXNzd29yZA==' is 'user:password'
Authorization: Basic dXNlcjpwYXNzd29yZA==
If the header can't be parsed, doesn't exist or is in the wrong format, an Authorizer
responds to the request with a 401 status code and prevents the next controller from receiving the request.
Once parsed, an Authorizer
sends the information - either the bearer token, or the username and password - to its AuthServer
for verification. If the AuthServer
rejects the authorization info, the Authorizer
responds to the request with a 401 status code and prevents the next controller from receiving the request. Otherwise, the request continues to the next controller.
For Authorizer.bearer
, the value in a request's header must be a valid, unexpired access token. These types of authorizers are used when an endpoint requires a logged in user.
For Authorizer.basic
authorizers, credentials are verified by finding an OAuth 2.0 client identifier and ensuring its client secret matches. Routes with this type of authorizer are known as client authenticated routes. These types of authorizers are used when an endpoint requires a valid client application, but not a logged in user.
Authorizer and OAuth 2.0 Scope
An Authorizer
may restrict access to controllers based on the scope of the request's bearer token. By default, an Authorizer.bearer
allows any valid bearer token to pass through it. If desired, an Authorizer
is initialized with a list of required scopes. A request may only pass the Authorizer
if it has access to all scopes listed in the Authorizer
. For example, the following requires at least user:posts
and location
scope:
router
.route("/checkin")
.link(() => Authorizer.bearer(authServer, scopes: ["user:posts", "location"]))
.link(() => CheckInController());
Note that you don't have to use an Authorizer
to restrict access based on scope. A controller has access to scope information after the request has passed through an Authorizer
, so it can use the scope to make more granular authorization decisions.
Authorization Objects
A bearer token represents a granted authorization - at some point in the past, a user provided their credentials and the token is the proof of that. When a bearer token is sent in the authorization header of an HTTP request, the application can look up which user the token is for and the client application it was issued for. This information is stored in an instance of Authorization
after the token has been verified and is assigned to Request.authorization
.
Controllers protected by an Authorizer
can access this information to further determine their behavior. For example, a social networking application might have a /news_feed
endpoint protected by an Authorizer
. When an authenticated user makes a request for /news_feed
, the controller will return that user's news feed. It can determine this by using the Authorization
:
class NewsFeedController extends ResourceController {
NewsFeedController(this.context);
ManagedContext context;
@Operation.get()
Future<Response> getNewsFeed() async {
var forUserID = request.authorization.ownerID;
var query = Query<Post>(context)
..where((p) => p.author).identifiedBy(forUserID);
return Response.ok(await query.fetch());
}
}
In the above controller, it's impossible for a user to access another user's posts.
Authorization
objects also retain the scope of an access token so that a controller can make more granular decisions about the information/action in the endpoint. Checking whether an Authorization
has access to a particular scope is accomplished by either looking at the list of its scopes
or using authorizedForScope
:
class NewsFeedController extends ResourceController {
NewsFeedController(this.context);
ManagedContext context;
@Operation.get()
Future<Response> getNewsFeed() async {
if (!request.authorization.authorizedForScope("user:feed")) {
return Response.unauthorized();
}
var forUserID = request.authorization.ownerID;
var query = Query<Post>(context)
..where((p) => p.author).identifiedBy(forUserID);
return Response.ok(await query.fetch());
}
}
Using Authorizers Without AuthServer
Throughout this guide, the argument to an instance of Authorizer
has been referred to as an AuthServer
. This is true - but only because AuthServer
implements AuthValidator
. AuthValidator
is an interface for verifying bearer tokens and username/password credentials.
You may use Authorizer
without using AuthServer
. For example, an application that doesn't use OAuth 2.0 could provide its own AuthValidator
interface to simply verify the username and password of every request:
class BasicValidator implements AuthValidator {
@override
FutureOr<Authorization> validate<T>(AuthorizationParser<T> parser, T authorizationData, {List<AuthScope> requiredScope}) {}
var user = await userForName(usernameAndPassword.username);
if (user.password == hash(usernameAndPassword.password, user.salt)) {
return Authorization(...);
}
// Will end up creating a 401 Not Authorized Response
return null;
}
}
The validate
method must return an Authorization
if the credentials are valid, or null if they are not. The parser
lets the validator know the format of the Authorization header (e.g., 'Basic' or 'Bearer') and authorizationData
is the meaningful information in that header. There are two concrete types of AuthorizationParser<T>
: AuthorizationBasicParser
and AuthorizationBearerParser
. The authorization data for a basic parser is an instance of AuthBasicCredentials
that contain the username and password, while the bearer parser's authorization data is the bearer token string.