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 connection
  • token - access token. May be null.
  • query - optional query part of the resource ID explained in Advanced Topics - Query Resources

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 granted
  • call - 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 of call
  • 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 client
  • cid - connection ID string, generated by Resgate, and unique for every client connection
  • header - 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 request
  • uri- unmodified Request-URI of the Request-Line as sent by the client when connecting to Resgate
  • token - access token. Often null, unless the client is already logged in
  • query - optional query part of the resource ID explained in Advanced Topics - Query Resources.

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 property:

  • token - access token for the connection. A null value clears any previously set token.

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.

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.