| lang | en |
|---|---|
| title | Authorization |
| keywords | LoopBack 4.0, LoopBack 4 |
| sidebar | lb4_sidebar |
| permalink | /doc/en/lb4/Loopback-component-authorization.html |
Wikipedia: Authorization is the function of specifying access rights/privileges to resources
LoopBack's highly extensible authorization package @loopback/authorization provides various features and provisions to check access rights of a client on a API endpoint.
API clients login to get a credential (can be a token, api-key, claim or cert). When the client calls an API endpoint, they pass the credential in the request to identify themselves (Authentication) as well as claim their access rights (Authorization).
LoopBack's authorization component checks if the permissions associated with the credential provided by the client satisfies the accessibility criteria defined by the users.
A Principal could be a User, Application or Device. The Principal is
identified from the credential provided by a client, by the configured
Authentication strategy of the endpoint
(see, LoopBack Authentication).
Access rights of the client is either associated with or included in the
credential.
The Principal is then used by LoopBack's authorization mechanism to enforce
necessary privileges/access rights by using the permissions annotated by the
@authorize decorator on the controller methods.
The expectations from various stake holders (LoopBack, Architects, Developers) for implementation of the authorization features are given below in the Chain of Responsibility section.
LoopBack as a framework provides certain provisions and expects the developers to extend with their specific implementations. Architects or Security analysts generally provide security policies to clarify how the developers should approach authorization.
The framework provides,
- the
@authorizedecorator to provide authorization metadata and voters - various
typesandinterfacesto declare user providedartifacts - a mechanism to enforce authorization policies
- abstractions of
authorizersas user provided functions and voters - create a decision matrix to combine results of all user provided
authorizers - an interceptor which enforces policies by creating the decision matrix
- abstractions of
- a LoopBack authorization component which packs all the above
Architects should,
- separate global authorization concerns as
authorizers - identify specific responsibilities of an endpoint as
voters - provide security policies for conflicting decisions from
authorizersandvoters - provide authentication policies with necessary scopes and roles for every endpoint
Developers need to,
- mount the authorization component, see Registering the Authorization Component
- decorate endpoints with authorization metadata, see Configuring API Endpoints
- define
authorizerandvoterfunctions, see Programming Access Policies - design security policies as decision matrix, see Authorization by decision matrix
- plug in external enforcer libraries, see Enforcer Libraries
The @loopback/authorization package exports an
Authorization Component
class.
-
Developers will have to register this component to use access control features in their application.
const options: AuthorizationOptions = { precedence: AuthorizationDecision.DENY, defaultDecision: AuthorizationDecision.DENY, }; const binding = app.component(AuthorizationComponent); app.configure(binding.key).to(options);
-
The authorization
optionsare provided specifically for enforcing the decision matrix, which is used to combine voters from allauthorizefunctions. The options are described per the interface AuthorizationOptions.export interface AuthorizationOptions { /** * Default decision if all authorizers vote for ABSTAIN */ defaultDecision?: AuthorizationDecision.DENY | AuthorizationDecision.ALLOW; /** * Controls if Allow/Deny vote takes precedence and override other votes */ precedence?: AuthorizationDecision.DENY | AuthorizationDecision.ALLOW; }
The component also declares various types to use in defining necessary classes and inputs by developers.
-
Authorizer: A class implementing access policies. AcceptsAuthorizationContextandAuthorizationMetadataas input and returns anAuthorizationDecision. -
AuthorizationDecision: expected type to be returned by anAuthorizer -
AuthorizationMetadata: expected type of the authorization spec passed to the decorator used to annotate a controller method. Also provided as input parameter to theAuthorizer. -
AuthorizationContext: contains current principal invoking an endpoint, request context and expected roles and scopes. -
Enforcer: type of extension classes that provide authorization services for anAuthorizer. -
AuthorizationRequest: type of the input provided to anEnforcer. -
AuthorizationError: expected type of the error thrown by anAuthorizer.
The Authorization Component once registered binds an in-built interceptor to
all API calls.
The Authorization interceptor enforces authorization with user-provided
authorizers/voters
- The interceptor checks to see if an endpoint is annotated with an authorization specification.
- It collects all functions tagged as
Authorizer. The interceptor also collectsvotersprovided in the@authorizedecorator of the endpoint. - It executes each of the above collected functions provided by the user.
- Based on the result of all functions it enforces access/privilege control using a decision matrix.
Users can annotate the controller methods with access specifications using an
authorize decorator. The access specifications are defined as per type
AuthorizationMetadata
which consists of the following:
- type of the protected resource (such as customer or order)
- allowed roles and denied roles (to provide ACL based rules)
- scopes (oauth scopes such as
delete public imagesorregister user) - voters (supply a list of functions to vote on a decision about a subject's accessibility)
@post('/users/{userId}/orders', {
responses: {
'200': {
description: 'User.Order model instance',
content: {'application/json': {schema: {'x-ts-type': Order}}},
},
},
})
@authenticate('jwt')
@authorize({resource: 'order', scopes: ['create']})
async createOrder(
@param.path.string('userId') userId: string,
@requestBody() order: Order,
): Promise<Order> {
await this.userRepo.orders(userId).create(order);
}Please note that @authorize can also be applied at class level for all methods
within the class. In the code below remote method numOfViews() is protected
with ADMIN role, while authorization for remote method hello() is skipped by
@authorize.skip().
@authorize({allowedRoles: ['ADMIN']})
export class MyController {
@get('/number-of-views')
numOfViews(): number {
return 100;
}
@authorize.skip()
@get('/hello')
hello(): string {
return 'Hello';
}
}Users are expected to program policies that enforce access control in two of the following options:
Authorizerfunctions- The authorizer functions are applied globally, i.e, they are enforced on all endpoints in the application
Voterfunctions- voters are specific for the endpoint that is decorated with it
- multiple voters can be configured for an endpoint
Usually the
authorizefunctions are bound through a provider as below
- The
AuthorizationContextparameter of theauthorizefunction contains the current principal (in the example given above,that would be the current user invokingcancelOrder) and details of the invoked endpoint.- The
AuthorizationMetadataparameter of theauthorizefunction contains all the details provided in the invoked method's decorator.
- The
class MyAuthorizationProvider implements Provider<Authorizer> {
/**
* @returns an authorizer function
*
*/
value(): Authorizer {
return this.authorize.bind(this);
}
async authorize(
context: AuthorizationContext,
metadata: AuthorizationMetadata,
) {
events.push(context.resource);
if (
context.resource === 'OrderController.prototype.cancelOrder' &&
context.principals[0].name === 'user-01'
) {
return AuthorizationDecision.DENY;
}
return AuthorizationDecision.ALLOW;
}
}the
authorizefunction is then tagged to an application asAuthorizationTags.AUTHORIZERas below.
import AuthorizationTags from '@loopback/authorization';
let app = new Application();
app
.bind('authorizationProviders.my-provider')
.toProvider(MyAuthorizationProvider)
.tag(AuthorizationTags.AUTHORIZER);- This creates a list of
authorize()functions.- The
authorize(AuthorizationContext, AuthorizationMetadata)function in the provider class is expected to be called by theAuthorization Interceptorwhich is called for every API endpoint decorated with@authorize(). - The authorize interceptor gets the list of functions tagged with
AuthorizationTags.AUTHORIZER(and also the voters listed in the@authorizedecorator per endpoint) and calls the functions one after another. - The
authorize()function is expected to return an object of typeAuthorizationDecision. If the type returned isAuthorizationDecision.ALLOWthe currentPrincipalhas passed the executedauthorize()function's criteria.
- The
Voter functions are directly provided in the decorator of the remote method
async function compareId(
authorizationCtx: AuthorizationContext,
metadata: MyAuthorizationMetadata,
) {
let currentUser: UserProfile;
if (authorizationCtx.principals.length > 0) {
const user = _.pick(authorizationCtx.principals[0], [
'id',
'name',
'email',
]);
return AuthorizationDecision.ALLOW;
} else {
return AuthorizationDecision.DENY;
}
}
@authenticate('jwt')
@authorize({resource: 'order', scopes: ['patch'], voters: [compareId]})
async patchOrders(
@param.path.string('userId') userId: string,
@requestBody() order: Partial<Order>,
@param.query.string('where') where?: Where<Order>,
): Promise<Count> {
return this.userRepo.orders(userId).patch(order, where);
}In the above example compareId() is an authorizing function which is provided
as a voter in the decorator for the patchOrders() method.
The final decision to allow access for a subject is done by the interceptor by
creating a decision matrix from the voting results (from all the authorizer
and voter functions of an endpoint).
The following table illustrates an example decision matrix with 3 votes and corresponding options.
| Authorizer | Voter # 1 | Voter #2 | Options | Final Decision |
|---|---|---|---|---|
| Deny | Deny | Deny | any | Deny |
| Allow | Allow | Allow | any | Allow |
| Abstain | Allow | Abstain | any | Allow |
| Abstain | Deny | Abstain | any | Deny |
| Deny | Allow | Abstain | {precedence: Deny} | Deny |
| Deny | Allow | Abstain | {precedence: Allow} | Allow |
| Allow | Abstain | Deny | {precedence: Deny} | Deny |
| Allow | Abstain | Deny | {precedence: Allow} | Allow |
| Abstain | Abstain | Abstain | {defaultDecision: Deny} | Deny |
| Abstain | Abstain | Abstain | {defaultDecision: Allow} | Allow |
- Here, if suppose there is an
authorizerfunction and 2 voters for an endpoint.- if the
authorizerfunction returnsALLOW, but voter 1 in authorize decorator returnsABSTAINand voter 2 in decorator returnsDENY. - In this case, if the options provided while
registering the authorization component,
provides precedence as
DENY, then the access for the subject is denied to the endpoint.
- if the
Enforcer libraries help developers in coding security policies as configurations and provides out-of-the-box matching rules, strategies and authorization patterns. These libraries also help with mundane tasks like mapping user profiles to scopes and roles, modifying configurations dynamically, etc. Please look at the loopback shopping example to see how CasBin library is injected as an enforcer into the authorization provider.
import * as casbin from 'casbin';
// Class level authorizer
export class CasbinAuthorizationProvider implements Provider<Authorizer> {
constructor(@inject('casbin.enforcer') private enforcer: casbin.Enforcer) {}
/**
* @returns authorizeFn
*/
value(): Authorizer {
return this.authorize.bind(this);
}
async authorize(
authorizationCtx: AuthorizationContext,
metadata: AuthorizationMetadata,
) {
/*
* call enforcer and determine action
*/
return AuthorizationDecision.ABSTAIN;
}