Access Control
Chapter 7: The basics on how to control access to resources and methods.Resgate and the RES protocol allows full access control on all resources and their methods. Whenever a client wants to get a resource, or call a method, Resgate will first verify that the client is authorized to do so.
Earlier in Chapter 3 - Serving Resources we saw how we could grant full access to all resources for a service. Now, let’s take a closer look at how to use access control.
Access requests
Unlike REST APIs, access is granted in a separate access request1. This allows Resgate to use cached resources, and only validate access when a client requests a resource. To listen for access requests , the service subscribes to a subject with the following pattern:
access.<resource>
Where resource
is the resource ID being requested access for.
In code it could look like this:
nats.subscribe('access.example.foo', (msg, reply) => {
/* Authorize and send a response based on token */
});
Tip
By using a full wildcard (
>
), a service can have a single access handler for all of its resources.nats.subscribe('access.example.>', (msg, reply, subject) => { /* Authorize and send a response based on token and subject */ });
Request message
The access request’s message is a JSON object with the following properties:
cid
- connection ID string, generated by Resgate, and unique for every client connectiontoken
- access token. May be null.query
- optional query part of the resource ID explained in Advanced Topics - Query ResourcesisHttp
- boolean true if the request is sent during a HTTP request, explained in Advanced Topics - Setting HTTP headers.
The message of an access request may look like this:
{
"cid": "bhrq7ht8smgioj39ibr0",
"token": { "userId": 42, "role": "resgate-admin", "foo": "bar" }
}
The token
should contain the information required for validation. How to set the access token in explained in the section Setting an access token below.
Access response
The result sent on an access request is a JSON object with the following properties:
get
- boolean that flags if read/get access is grantedcall
- comma separated string of methods that the client can call. Eg."set,foo,bar"
.
Tip
By setting the
call
property to a single asterisk character ("*"
), the client is granted access to calling any method.
As with all successful responses, the result is embedded in a JSON object with a result
property, which could look like this:
{
"result": {
"get": true, // Grant access to get the resource
"call": "set,foo" // Grant access only to call those two methods
}
}
Error response
Any error response2 would deny the client access to the resource, and Resgate will respond to the client’s request with the error.
Most common errors are Not found and Internal error, and may look like this:
{
"error": {
"code": "system.notFound",
"message": "Not Found"
}
}
Look at the pre-defined errors3 in the specification for a full list of error codes.
Note
Responding with a
system.accessDenied
error will have the same effect as in sending an access response denying access.
Example
Here’s an example of an access request handler requiring a token with a role
property set to "admin"
:
nats.subscribe('access.example.foo', (msg, reply) => {
let { token } = JSON.parse(msg);
// Verify we have a token (it may be null) and that the role is "admin"
if (token && token.role == "admin") {
// Grant full access
nats.publish(reply, JSON.stringify({ result: { get: true, call: "*" }}));
} else {
// Deny all access
nats.publish(reply, JSON.stringify({ result: { get: false }}));
}
});
Auth requests
The access token is often set during an auth request4, which is similar to the call request described in Chapter 6 - Methods, but differs in these areas:
- request subject starts with
auth
instead ofcall
- auth request message contains additional information, such as headers, host, remote address, and uri.
- auth requests methods are accessible to all client, and cannot be put under access control
Subject
To listen for auth requests, the service must subscribe to a subject with the pattern:
auth.<resource>.<method>
Where resource
is the resource ID of the resource that has the auth method, and method
is the name you’ve given to the method.
Tip
Do not confuse this auth resource with any resource ID used in access requests described above.
Auth and call methods doesn’t have to be on a gettable resource. You may even use the service name only as the resource ID for the methods:
nats.subscribe('auth.example.login', (msg, reply) => { /* Handle the auth request */ });
Request message
The auth request’s message is a JSON object that, in addition to the properties found in a call request, also contains information which the client provided when establishing the HTTP/WebSocket connection with Resgate. The object contains the following properties:
params
- method parameters as sent by the clientcid
- connection ID string, generated by Resgate, and unique for every client connectionheader
- HTTP header object where the key is the canonical format of the MIME header, and the value is an array of strings associated with the key.host
- host string on which the URL is sought by the client. This is either the value of the Host header or the host name given in the URL itself.remoteAddr
- network address string of the client that sent the requesturi
- unmodified Request-URI of the Request-Line as sent by the client when connecting to Resgatetoken
- access token. Often null, unless the client is already logged inquery
- optional query part of the resource ID explained in Advanced Topics - Query Resources.isHttp
- boolean true if the request is sent during a HTTP request, explained in Advanced Topics - Setting HTTP headers.
The message payload could look like this (but with more headers):
{
"params": { "username": "foo", "password": "secret" },
"token": null,
"cid": "bhrq7ht8smgioj39ibr0",
"header": {
"Accept-Encoding": [ "gzip, deflate, br" ],
"Accept-Language": [ "sv-SE,sv;q=0.9,en-US;q=0.8,en;q=0.7" ],
"Origin": [ "http://localhost:8000" ],
"Cookie": [ "access-token=eyJhbGciO" ]
},
"host": "localhost:8080",
"remoteAddr": "[::1]:50894",
"uri": "/"
}
Response
The response of an auth request is exactly the same as that of a call request described in Chapter 6 - Methods.
The token is not set through the response, but is instead set using an event as described in the section Setting access token below. You might want to respond with some status info, a reference to a connection resource, or with some refresh token of your own, but this is up to you.
Warning
If an auth request results in the user getting a new token, the token must be set prior to sending the response.
Setting access token
The access token, also called connection token as it is tied to the client connection, is set using something called a connection token event5. It will pass a custom token to Resgate, which will store it and include it in any subsequent request made by the client.
Note
Access tokens are never sent to the clients.
If a client needs information contained in the token, this must be sent through other means, such as the response of the auth request.
When sending a token event, the subject of the message will have the following pattern:
conn.<cid>.token
Where cid
is the connection ID of the connection for which you wish to set the token. It is usually the cid
of the requesting client, but you may set tokens for other connections as well.
The event message is a JSON object containing the properties:
token
- access token for the connection. A null value clears any previously set token.tid
- optional token ID used to update/revoke a token, explained in Advanced Topics - Token Update.
In code it could look like this:
nats.publish("conn." + cid + ".token", JSON.stringify({
"token": {
"id": 42,
"foo": "bar",
"role": "admin",
"lang": "en"
}
}));
Note
A connection token event doesn’t have to be sent from within an auth request. A logout method could be implemented as a call method that sends a token event clearing the access token.
Header authentication
If HTTP headers are used for authentication, Resgate can be configured to automatically make an auth request each time a new connection is made.
For HTTP GET or POST requests, this can be configured with --headauth
resgate --headauth=authService.jwtHeaderAuth
For WebSocket connections, the configuration is --wsheadauth
:
resgate --wsheadauth=authService.jwtHeaderAuth
Note
Both
--headauth
and--wsheadauth
may use the same auth request method, but are configured separately.
Example
Let’s make a simple example to see how it all fits together.
Login auth request
nats.subscribe('auth.authexample.login', (msg, reply) => {
let { params, cid } = JSON.parse(msg);
// We use hardcoded login parameters for the example
if (params && params.user == "admin" && params.pass == "secret") {
// Set a token
nats.publish("conn." + cid + ".token", JSON.stringify({
"token": { "loggedIn": true }
}));
// Send successful response
nats.publish(reply, JSON.stringify({
"result": null
}));
} else {
// Send an custom error response
nats.publish(reply, JSON.stringify({
error: {
code: "example.loginFailed",
message: "Invalid username or password"
}
}));
}
});
Validating token
nats.subscribe('access.example.>', (msg, reply, subject) => {
let { token } = JSON.parse(msg);
// Validate we have logged in
if (token && token.loggedIn) {
// Grant full access
nats.publish(reply, JSON.stringify({
"result": { get: true, call: "*" }
}));
} else {
// Deny all access
nats.publish(reply, JSON.stringify({
"result": { get: false }
}));
}
});
Tip
Authentication through auth requests is preferably done in a separate microservice, for reusability.
Authorization through access requests may also be done in a centralized microservice seperate from the microservices that serves the actual resources. This can be useful for having all access control management in one place.