tl;dr: nginx-sso is a lightweight, offline Single-Sign-On (SSO) system which works with cookies and ECDSA. It can easily be used in together with vanilla nginx and any backend application. The reference implementation is written in golang and has some cool additional features such as authorization.
This posts describes the technical background of the system, especially the motivation for using such a system as opposed to other established SSO solutions. If you want a technical description of the protocol and the authentication flow, consult the TECHNICAL.md in the GitHub repository.
When I was studying at RWTH Aachen University, I had a student-job at the university NOC (Network Operation Center). What might sound like a boring sys-admin thing was really much more interesting as I got to develop applications and systems to work for the roughly 50.000 people at the university. At some point we were told to make all of our applications work with the newly introduced SSO-system called Shibboleth which we used in conjunction with Grouper. I’m not going to talk about Shibboleth today as it is a huge system with a different focus, but one thing that struck me was how easy it was to integrate applications with Shibboleth, once it was set up.
Remote-User
and Remote-Groups
Our Shibboleth setup worked by installing an Apache-module for each service
which would perform all of the SSO magic for the backend application. All the
backend application had to do was to consume the HTTP / environment variables
Remote-User
and Remote-Groups
and do something with them.
All of the sudden, the headaches of user authentication and management were
gone. No longer did your application have to implement user and credential
storage and authentication, and for authorization if often sufficed to hardcode
a specific Grouper-group into the application. Even better, a lot of available
web applications already had some support for working with Remote-User
.
My time at the NOC ended in 2010. Fast-forward to 2015 and look at the authentication landscape for modern web applications. Unless you are running inside some corporate context, chances are you have flirted with using existing Identity Providers (IdPs) for your project. You can choose between Google, Facebook, LinkedIn, GitHub and more sites, neatly covering your user-base. Outsourcing authentication to these guys is a better idea than (mis)handling user credentials yourself!
While these options are definitely the way to go for most applications, there are scenarios where they fall short:
Another reality nowadays is that your app is quite likely to run behind another HTTP process. Many people (myself included) today use nginx for terminating TLS, load-balancing requests, etc. That’s why I decided to come up with my own lightweight SSO which works with nginx and arbitrary applications.
When designing nginx-sso, I came up with a list of necessary and nice-to-have features:
The Apache mod_auth_tkt
comes pretty close in terms of functionality. The big difference is that it
works as a native module only on Apache and uses shared secrets.
Pubcookie is another similar project. It also uses
shared keys and is available as an Apache module. Plus its more complicated.
The project that is closest to nginx-sso is probably mod_auth_pubtkt, which uses RSA/DSA. It includes a lot of similar features, but sadly is also limited to Apache. On the other hand, it is still actively developed, so if all you need is Apache, it might be your best choice.
When thinking about disconnected / offline SSO it is obvious that the user will provide his own credentials to the application server which has to decide whether it is legit. That means verifying the integrity and authenticity of the users claim, both of which are usually accomplished by using either MACs or signatures.
For me, MACs were not an option since an attacker would be able to issue his own tickets by compromising a single application server. That leaves public-key signatures, based on DSA or ECC. ECC signatures are the better choice since they are more efficient and take up less space in a cookie.
nginx-sso uses a plain cookie with an additional ECSDA signature. The signature is made over the payload of the cookie (username, groups) as well as the expiry timestamp and the IP of the user.
Authentication using vanilla nginx is possible mostly thanks to an awesome nginx-plugin called auth_request. From the website:
The ngx_http_auth_request_module module (1.5.4+) implements client authorization based on the result of a subrequest. If the subrequest returns a 2xx response code, the access is allowed. If it returns 401 or 403, the access is denied with the corresponding error code. Any other response code returned by the subrequest is considered an error.
nginx auth_request documentation
With this module, every access to configured resources on your nginx server will trigger an HTTP request to an authentication backend. This request will contain the headers of the original request which your authentication backend uses to grant or deny access. The auth_request module is not compiled by default or in every distribution, but it is part of the mainline nginx codebase and major distributions have packages compiled with this module.
Here we can see the nginx configuration snippet which protects the resource
/secret
with a subrequest to the internal resource /auth
which proxies to
the ssoauth server running on localhost.
While I was able to do authentication and authorization in the auth backend, it still did not help my backend application in identifying the user. Fortunately, I discovered how to pass variables that are returned by the auth endpoint to the backend applications, see the example nginx.conf. Now the backend service can simply assume the presense and correctness of these HTTP headers and does not have to deal with the sso cookie at all.
As a nice side-effect, since we’re already making a subrequest for each HTTP request, we can also use the auth endpoint to do authorization. To do that, I’ve implemented a very rudimentary ACL in ssoauth. It has a list of vhosts and prefixes and for each of those contains a list of allowed users and groups. This way, even static resources can easily be protected.
If you are writing a custom application and want to use this (or a similar)
system, it really could not be easier. You can simply use the Remote-User
and
Remote-Groups
headers to do authorization, for example by saying Everyone in
group xyz is an admin. Alternatively, you can have your own user-database and
only use the Remote-User
header to create and later look up the correct user.
This way you can have additional attributes (and permissions) for each user.
If you are using stock software you might be able to use this scheme as well. A
lot of software comes with support for logging in via Remote-User
, even if
the software then implements its own user-database on top of this. For
closed-source software you can sometimes find plugins which enable this
functionality.
Development of nginx-sso is at the very beginning, both in terms of code quality and features. I have a lot of things still written down in my TODO file. I’d appreciate any help in making the codebase more readable and examining any potential weaknesses of the current system.
Comments