Skip to content

Instantly share code, notes, and snippets.

@leehambley
Last active December 31, 2015 02:59
Show Gist options
  • Save leehambley/7924521 to your computer and use it in GitHub Desktop.
Save leehambley/7924521 to your computer and use it in GitHub Desktop.
Authorisation as a service?

Problem

Web applications need understand what permissions are granted to a current user in two key areas.

  1. When enforcing the permission server side (e.g returning 403 when trying to access a resource outside of one's graph)
  2. When rendering the user interface, so as not to render misleading controls (e.g "Edit this Widget", if the user lacks the appropriate permissions.

Further, in many applications in the wild (for better, or worse, perhaps I need new friends and colleagues) I've seen ways implementd to nerf or flat-out disable authorisation controls. In addition to the regular graph-based authorisation flow, the concept of super users is prevelant, and dangerous.

Background

In a REST API we can be assured that the request will look something like this:

GET /users/3da676f8-cdf4-43f0-9a5e-52efe3d9f029/projects
Host: www.example.com
Accept: */*
Pragma: no-cache
X-Our-Authentication: f50d6986-b9e2-44bd-baa6-96b1c0e6392e
Referer: http://example.com/

Or perhaps:

DELETE /users/3da676f8-cdf4-43f0-9a5e-52efe3d9f029/projects
Host: www.example.com
Accept: */*
Pragma: no-cache
X-Our-Authentication: f50d6986-b9e2-44bd-baa6-96b1c0e6392e
Referer: http://example.com/

From these example queries, we can infer a couple of important things:

  1. The action (Hurray, REST!)
  2. The what
  3. The who (via their authentication token, which in this case replaces the use of cookies, and in the example app around which this idea has formed is in local storage, and sent as a header)
  4. (any context that might be in play, such as the object's relationship to the user, denoted by the URL in the case of nested resources)

Proposal to solve Problem #1

The application layer needs to enforce that authorisation rules are followed, it can either take responsibility for that itself (a gross mangling of concerns in this author's opinion), or defer that responsibility to another system.

I propose implementing authentication as a service. That is an HTTP service, which recieves a copy, or a subset of the original HTTP request, and determines, using rules, witchcraft of divination if the action should be allowed, and responds appropriately.

# First HTTP Request (to API)
GET /users/3da676f8-cdf4-43f0-9a5e-52efe3d9f029/projects
Host: www.example.com
Accept: */*
Pragma: no-cache
X-Our-Authentication: f50d6986-b9e2-44bd-baa6-96b1c0e6392e
Referer: http://example.com/

# Authorisation Service HTTP Request
GET /users/3da676f8-cdf4-43f0-9a5e-52efe3d9f029/projects
X-Our-Authentication: f50d6986-b9e2-44bd-baa6-96b1c0e6392e

# Authrisation Service HTTP Response
HTTP/1.1 200 OK
Status: 200 OK
Content-Type: application/json; charset=utf-8

{"allowed":"true", reason:"permissions.project.owner"}

Notice the query to the authorisation service is smaller (probably too small, there would be value in making this cacheable for a short time, as well as to allow other things such as referrer or user agent to possibly play a role, not for security's sake, but that's not important in the scope of this discussion) request, with fewer headers, and responds with a small, sane response, not mis-using HTTP status codes.

Pros Cons
Good separation of concerns. Another service to maintain.
Possibility to keep authorization logic out of the main application. Additional failure modes when the service is offline HTTP 502?.
A log of all requests made to authorise actions on a resource.

Proposal to solve Problem #2

To solve problem two, we only have to infer the HTTP verb, and know the URI and send a kind of pre-flight to the server to check if an action would work, given something like:

<form accept-charset="UTF-8" action="/users/fe6303a8-52b2-43e0-966d-ac292760bb31" method="post">
    <input name="_method" type="hidden" value="DELETE" />
    <button type="Submit">Delete Account</button>
</form>

Although, in my test case the application uses JSON HAL and Angular.js the form example above stands. From the form we can infer that this form is designed to delete a user, the _method input field will be used to rewrite the request in flight to the sever to support browsers that do not allow DELETE as a form action, and our API server will eventually receive a DELETE /users/fe6303a8-52b2-43e0-966d-ac292760bb31 request, with the authentication token of someone who may or may not have permission to delete that resource.

In the browser, when deciding whether or not to allow this action, whether to disable the button, or skip rendering this component at all, we could perform a preflight request to the same authorisation service, which might even return a message or message key (which we could in turn run through an I18n map) explaining the reason why, we might even be able to add that to the button as a title="" attribute, so there is at least a hint why a given action is disabled.

Of course, in a page of 10 items, with the standard CRUD verbs, we'd expect this to cause an additional 20 HTTP requests ((U + D) * 10 items). (We assume that the R is taken care of before we get the list and C … well, one just has to try that one out), this won't scale well under HTTP/1.1, however under HTTP/2.0 or SPDY it might just work.

In the meantime, we can provide a manifest along with the GET that created the list already of what actions may, or may not be availale by doing this work server-side, and within the authorisation service's local network.

This might look like:


# POST /organisations/
# {"Name":"Test Organisation 1"}
{
  "subject": {
    "uuid": "db88ff66-e588-4c6d-7857-007306481c36",
    "name": "Test Organisation 1",
    "createdAt": "2013-12-12T09:33:43.002663+01:00"
  },
  "_links": {
    "memberships": "http://localhost:3251/organisations/db88ff66-e588-4c6d-7857-007306481c36/memberships",
    "projects":    "http://localhost:3251/organisations/db88ff66-e588-4c6d-7857-007306481c36/projects",
    "self":        "http://localhost:3251/organisations/db88ff66-e588-4c6d-7857-007306481c36"
  },
  "_authorisations": {
    "http://localhost:3251/organisations/db88ff66-e588-4c6d-7857-007306481c36": ["GET", "PATCH", "DELETE"]
  }

Notes

  • In my testing, the Go service which handles the authorisation service, with it's naive implementation returns answers in 0.001s.
  • Some writing of the manifest could be done by middlewares outside the API application stack, it's not a concern of the API to optimise for the protocol being used.
  • The authorisation service could cache locally, for ~15s (speculation), to cover the case that calculating the permissions is expensive and actions will typically be taken immediately after being made available via the UI.

Roundup

Whilst there are some outstanding issues, such as:

@dhamidi
Copy link

dhamidi commented Dec 12, 2013

@kylewelsby
Copy link

As for CORs every request performs a preflight on the resource URI with method OPTIONS, given this response returns 20X the resource can successfully be called, modern browsers implement this for free.

OPTIONS /organisations/
Origin: http://www2.example.com
Host: www.example.com
Accept: application/json
Pragma: no-cache
X-Our-Authentication: f50d6986-b9e2-44bd-baa6-96b1c0e6392e
Status: 200

{
    "parameters": [
       {
          "name": "name",
          "type": "string",
          "required": true,
          "pattern": "[a-zA-Z0-9]{4,32}"
       }
    ],
    "methods": [
        "GET", 
        "PATCH", 
        "DELETE"
    ]
}

For your second problem having the client know something can not be performed prior to trying, a manifest does seem like the ideal solution. I would mirror somewhat the above, but as a instal get request which outlines abilities.

OPTIONS /auth_manifest
Origin: http://www2.example.com
Host: www.example.com
Accept: application/json
Pragma: no-cache
X-Our-Authentication: f50d6986-b9e2-44bd-baa6-96b1c0e6392e
Status: 204

Pre-flight would just return contentless success

GET /auth_manifest
Origin: http://www2.example.com
Host: www.example.com
Accept: application/json
Pragma: no-cache
X-Our-Authentication: f50d6986-b9e2-44bd-baa6-96b1c0e6392e
Status: 200

[
    {
        "url": "http://localhost:3251/organisations/db88ff66-e588-4c6d-7857-007306481c36",
        "headers": [
            "Origin",
            "X-Our-Authentication",
            "Pragma",
            "Accept"
        ],
        "exposedHeaders": [
            "Location"
        ],
        "methods": [
            "GET",
            "PATCH",
            "DELETE"
        ],
        "parameters": [
            {
                "name": "name",
                "type": "string",
                "required": true,
                "pattern": "[a-zA-Z0-9]{4,32}"
            }
        ]
    }
]

From the above it kind-of outlines what would be expected in a CORS response, the Access-Control-Allow-Headers, Access-Control-Expose-Headers, Access-Control-Allow-Methods, with addition of parameters that tells the client what parameters can be received and their format.

Having this singular resource outline the API and respond differently given the X-Our-Athentication could be a singular resource clients can check when booting, and after authentication

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment