Skip to content

8. Authentication and authorization

Authentication and authorization are important, yet quite difficult topics in web development. In their most basic form, these are not so difficult to implement, but the struggle arises from all the needs related to these: two-factor authentication, forgotten passwords, access control, etc. And security. Especially security.

The issue with web applications is that each request-response pair is handled as an individual transaction. This means you usually need to verify the identity of the user at each request. At the same time, some endpoints are often available to everyone, both authenticated users as well as guests, and some endpoints are available to guests only (e.g., login, register).

In addition to checking whether a request comes from a logged-in user or a guest (checking for identity), you might also want to check whether the user has the right to perform the operation they have requested to perform on your system (checking for authorization).

Authentication vs. authorization

Authentication is establishing the identity of the user.

Authorization is about establishing the rights of the (identified) user.

For these, the HTTP status codes provide the corresponding error codes:

  • 401 Unauthorized - You did not authenticate correctly.
  • 403 Forbidden - You are authenticated but not authorized, i.e., you do not have the right to carry on.

Remember the status codes from chapter 2?

Response code Response family
1xx Informational resposes
2xx Success responses
3xx Redirect responses
4xx Client errors
5xx Server errors

4xx client errors, aside from 404, often originate from authentication- or authorization-related errors. That is, accessing certain actions that are not permitted will cause an error, such as 403 Forbidden.

HTTP authentication headers

HTTP protocol defines security access control methods. First, a client asks for a page with HTTP GET nethod. Then, a server sends back a response HTTP/1.0 401 Unauthorized. In addition to this status code, the servers response - also known as challenge in this context - comprises authentication instructions. These are the type, as well as other needed information.

The more simple one of these is called the Basic access authentication:

Type Response header with status code 401 Request header with required info
Basic WWW-Authenticate: realm=Basic, charset="UTF-8" Authorization: Basic B64_auth

With basic auth, the autorization info is transmitted as Base64 encoded string, which is super trivial to reverse. Because of this, basic auth is non-secure unless used in conjunction with TLS (HTTPS, coming up next).

The other authentication method is Digest access authentication. With digest, the response header with status code 401 is as follows:

WWW-Authenticate: Digest realm="test@host.com",
qop="auth,auth-int",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
opaque="5ccc069c403ebaf9f0171e9517f40e41"

To which the client can reply with a request header with the required info like this:

Authorization: Digest username="Mufasa",
realm="test@host.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
uri="/dir/index.html",
...
opaque="5ccc069c403ebaf9f0171e9517f40e41"

| Extra: Wikipedia - Digest access authentication

In addition to these, we have also Token-based authorization such as OAuth and JSON Web Token (JWT). For example with JWT the response header with status code 401 is:

WWW-Authenticate: Bearer realm="example"

To which the client can reply with request header with required info:

Authorization: Bearer <JWT_TOKEN>

And finally, we have Session-based authorization with cookies. The 401 response can have:

Set-Cookie: <cookie-name>=<cookie-value>;

... to which the client can reply with request header:

Cookie: <cookie-name>=<cookie-value>

HTTPS for transport layer security (TLS)

HTTPS makes HTTP more secure with encryption using TLS. TLS encryption is based on the same RSA cryptography as SSH. By default, browsers assume the protocol is HTTP and try to communicate with the server on the port 80, unless you specify otherwise. If you prefix the URL with https:// instead of http://, the browser will attempt to use the HTTPS protocol, which the server - by convention - should be listening for on port 433. However, the server may also accept HTTP on port 80 and perform a redirect to HTTPS:433 without the client needing to do anything.

Protocol scheme Port
http:// 80
https:// 433

HTTPS comes in two flavours: simple (one-way) where a server is verified or mutual (two-way) where both server and client are verified.

| Extra: TLS/SSL Explained

TLS handshake

TLS handshake with RSA
  1. Client hello: With you I want privately to talk - here SSL versions I support (+ protocol version and client random)
  2. Server hello: .. to be able to verify my id implies that some CA my public key certifies (+ cipher suite and server random)
    • Digital signature = selected data encrypted with the server's private key. Decryption successful only with the server's public key.
  3. Client: Which kind of certificate authority (CA) is it, just a sec, the trustworthiness of it I must check. OK, the root CA seems authentic, your public key got, soon we click. (This is one-way, since the client checks the server and not vice verse.)
  4. Both calculate the premaster secret: Instead of the client generating the premaster secret and sending it to the server, as in an RSA handshake, the client and server use the DH parameters they exchanged to calculate a matching premaster secret separately.
  5. Session keys created: Now, the client and server calculate session keys from the premaster secret, client random, and server random, just like in an RSA handshake.
  6. Client is ready and server is ready. Encrypted communication starts.

Certificates

Certificates relate to HTTPS, but are not actually required for HTTPS: certificates are used to verify identity, not to secure data transmission. You can achieve secure connection by serving your site using HTTPS. But doing so without a certificate will result in most browsers with a warning, that the site is possibly dangerous. Certificates have to do with trust. For encryption only, you can create your own certificate easily and configure the server to use it by so-called self-signing. The self-signed certificate does not provide identity verification.

| Extra: SSL certificate self-signing instrcutions in Heroku

The trust aspect is satisfied with a top-down process: a top-level certificate holder issues certificates to others. Traditionally, these have been commercial ones (see prices), but services like Let's Encrypt offer them also for free. The current TUNI recommendation for servers required for research purposes for a short time is to use Let's Encrypt.

To check the certificate, press 🔒 on the left to the URL, then the certificate

But how do you go about setting up SSL/TSL with Node.js servers? We'll get to that, but we need to first discuss some topics leading to that.

Reverse proxy

Chances are you are already familiar with forward proxies, especially if you've frequeneced LAN parties in the past. Reverse proxy functions in a bit similar way.

The difference between a forward- and reverse proxy (courtesy of Sandeep Negi).
Forward proxy Reverse proxy
client's facade for the outer world server's facade for the outer world
hides addresses of the organization's subnet can implement TLS
before NAT, popular solution to multiply the address space in use can add security by defining stricter rules, such as which TLS version is accepted
caches popular sites, reduces network traffic often in combination with node servers
unlike NGINX or Apache, node server focuses more on app logic than network handling
can be used as a load-balancer

Reverse proxy for TLS termination

One way to implement TLS for websites is to use a reverse proxy. The proxy is then called a TLS termination proxy.

TLS termination proxies can:

  • work-around insecure TLS implementations of clients
  • enable better TLS configuration and security policies
  • implement certificate-based authentication, a server should provide its certificate for the purpose
  • tunnel plain text in TLS
  • inspect and analyze encrypted traffic

NGINX as reverse proxy

NGINX, pronounced "engine x", is a popular server software. Personally, I started with Apache, then moved to NGINX, and now reach for NGINX by default. NGINX is a versatile and configurable solution for building servers. It can be used to serve HTML directly as well as to direct requests to other processes. As reverse proxy, NGINX proxies a request, decrypts it and sends it to a Node.js server. After node sends it back, the proxy fetches the response, encrypts it and sends it back to the client.

NGINX Unit 1.5 (released in 2018) enables dynamic configuration of SSL/TLS certificates:

"NGINX Unit is configurable on the fly, without relying on static configuration files and requiring process reloads. With NGINX Unit, certificates can be uploaded and applied dynamically, with zero downtime and no changes to the application processes. Besides quick and easy configuration, use of TLS with NGINX Unit relieves Node.js engineers from the burden of implementing encryption in the application code itself. Now Node.js apps can be deployed into a larger production environment faster, more safely, and with no downtime." - NGINX Unit

| Extra: NGINX - 5 performance tips for Node.js applications

Authentication types - Basic, Digest, and Token-based systems

You might be most familiar with login systems build around HTML forms. But those are not the only option. Your browser is happy to ask for your login credentials and transfer those to the server. Let's go over some authentication strategies.

So, like discussed before, HTTP Basic is the most simple authentication header type and perhaps the most simple way to authenticate users. The flow of the authentication is as follows:

Authentication process with HTTP Basic auth.
  1. Client requests a protected resource
  2. The server responses with a 401 response with a header WWW-Authenticate, for example, WWW-Authenticate: Basic realm="my secret stuff"
  3. Client constructs a string "username:password", which is encoded with Base64: const B64_auth=window.btoa(username + ':' + password);. Encoding primarily ensures valid ASCII chars of this string, and secondarily obfuscates it. Encoding is by no means the same as encryption.
  4. The authorization method and a space () is put in the header before the encoded string. For example, Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==. The authorization will be valid until the browser is closed.

| Extra: Mozilla Developer Network - Basic Access Authentication

In Basic auth, username and password are sent in every request and in an encoded, not encrypted, form. Thus, for more security, HTTPS must be used. Actually, arguably all traffic should now be encrypted, as there is quite small performance penalty compared with the added security.

For security reasons, also the embedded credentials style below is obsoleted:

https://lassi:secret_pw@www.some-site.com

In Chrome, the support was dropped first without wider discussion in standardization bodies, see https://www.chromestatus.com/feature/5669008342777856 and LMakarov: Goodbye to embedded credentials!

Restricting access with Apache and Basic authentication

Practical implementations require server configurations and go outside the scope of this course, but without some concrete example, it might be difficult to understand this authentication scheme at all. In practice, the server would have two files, .htaccess and .htpasswd. The .htaccess file would typically look like this:

AuthType Basic
AuthName "Access to my site"
AuthUserFile /path/to/.htpasswd
Require valid-user

... while the .htpasswd could look like this:

aladdin:$apr1$ZjTqBB3f$IF9gdYAGlMrs2fuINjHsz.
user2:$apr1$O04r.y2H$/vEkesPhVInBByJUkXitA/

So, the .htpasswd contains a username, followed by a colon (:), and a hashed password with one user per line. The .htaccess file tells the web server that the files in this folder should not be served without first prompting the client to provide their credentials. In most browsers this would trigger a prompt windows, much like JavaScripts prompt(), but with username and password fields.

HTTP Digest adds encryption to HTTP Basic authentication. The server sends an extra singe use value: nonce. Nonce and other values are joindes together and an MD5 sum is calculated and sent back. The server then does the same and compares hashed values. If values match, access is granted.

Wikipedia has a detailed example of Digest authentication.

Cons of HTTP Basic and Digest

  1. Basic and Digest authentication remain "open" for as long as the browser window is open - there is no logout.
  2. A password is neede with both HTTP Basic and Dogest. This makes them poor choises for services, that is, you wouldn't want to make API calls with username and password.
  3. These authentication types provide no control to the look and feel of the authentication form, since the browser takes care of styling.

This might be the most familiar to most web users.

HTML forms can be used for authentication when HTTPS is used, and hence the traffic is encrypted. The login information should be sent over POST.

GET or POST

Use GET in cases where it would make sense to bookmark or share the URL with the parameters, for example https://www.google.com/?q=xkcd. If sharing the parameters does not make sense, perhaps POST makes more sense.

The server will proceed to check the login information and send, as a part of the response, an authentication/session cookie on successful login. On subsequent requests, the cookie is used for authentication. Logout is achieved by deleting the cookie.

Authentication is easy to get wrong which is why using, for example, modules such as Node.js Express middleware for authentication and session control is a good idea.

Token-based authorization

Once authentication is dealt with, what should happen on the subsequent requests? Cookie-based authentication keeps sending the authentication/session cookie with every request, thus the session can be kept "alive". But for API use especially, a cookie-based solution is not optimal. Instead, we can use token-based authorization.

So,

  • a token is sent to a client after a successful authentication.
  • The token is stored by the client.
  • No session information is stored on the server, not even the token.
  • The token is sent and verified with each request.
  • Use HTTPS for security (as always).

The token authentication may be enhanced with mobile phones serving as additional code generators: they generate security passcodes for accessing the network. For instance, a user receives a one-time passcode (a soft token) limited in time to, for example, 60 seconds.

JSON web token (JWT)

JSON web tokens:

  • are an open standard (RFC 7519 updated by RFC 8725, Feb 2020).
  • are URL-safe JSON-based security tokens that contain a set of claims that can be signed and/or encrypted.
  • transmits verified and signed information between parties as JSON objects.
  • Signing may use e.g. a secret or public/private key pairs (RSA).
  • are still vulnerable to substitution attact (reuse of token for unintented sites) also known as cross-JWT confusion, injection attacks, and server-side request forgery.
  • Mitigations include:
    • sanitation of key ID
    • checking received claims
JWT authorization

JWT generation follows form-based authentication. A page contains a login form with username and password that are POSTed to a server.

The server authenticates the user and encrypts user data into a JWT with a secret. JWT is sent back to the client in response. The client stores the token, for example, in localStorage, and sends a header in every subsequent request:

headers: {
    "Authorization": "Bearer ${JWT_TOKEN}"
}

The server receives and validates the JWT before proceeding: since JWT is generated with a secret key, the authenticity of a request can be checked with the key.

Shared and combined authentication

"Let someone else worry about authentication."

The basic idea is that user has credentials to some other system and those credentials can be shared with the system user wants to access. "Log in with Facebook" or "Log in with Google" exemplify this.

Popular schemes are:

  • OAuth, for example, Facebook and Google authentication
  • Shibboleth, used in TUNI authentication

Different authentication systems can also be combined: multiple authentication schemes may serve different purposes, for example, form-based login is provided for humans and token-based authentication (API key) for RESTful services.

HTTP Cookies and Session cookies

"When receiving an HTTP request, a server can send a Set-Cookie header with the response. Afterward, the cookie value is sent along with every request made to the this server as Cookie HTTP header. Additionally, an expiration date can be specified. Restrictions for a specific domain and path can be specified as well." Mozilla Developer Network - Cookies

HTTP Cookies are often used for session/login management. Remember chapter 2, where we discussed how cookies are a common way to handle state?

HTTP Cookie, or web cookie, browser cookie, ...

  • is a small piece of data, that a server sends to the client in response headers.
  • is stored by a browser and sent back with the next request to the same server.
  • typically, tells if subsequent requests are coming from the same browser - and keeping a user logged-in, for example.
  • provides a "state" for stateless HTTP protocol.

Set-cookie HTTP response header sends cookies from a server to a client. A simple cookie is set like this:

Set-Cookie: 'cookie-name'='cookie-value'

Here, the server tells the client to store the following cookies:

HTTP/1.0 200 OK 
Content-type: text/html
Set-Cookie: yummy_cookie=choco
Set-Cookie: tasty_cookie=strawberry 

[page content]

With every new request to the server, the browser will send back all previously stored cookies using the Cookie header:

GET /sample_page.html HTTP/1.1

Host: www.example.org
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
Cookies for storing data?

Yes, in "ye olden days" of web, cookies were used to store application data. There is, however, the downside, that all cookies are sent back and fort on every request creating extra network traffic. Because of this, you should now use, for example, local storage instead of cookies for application data. Let's leave cookies for session use.

In Node.js, a cookie is set with the following command:

response.setHeader('Set-Cookie', ['foo=bar', 'bar=baz']);

... or with details:

response.setHeader('Set-Cookie', ['foo=bar; Expires=Wed, 21 Oct 2020 07:28:00 GMT', 'bar=baz']);

Cookies of the request can be printed with the following command:

'Cookies: ' +  request.headers.cookie;

Long-living session cookies

Normal cookies are deleted when a clien is closed. Making a cookie "permanent" requires Expires or Max-Age directive, that:

  • expires at a specific date (Expires) or after certain time (Max-Age). For example Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2020 07:28:00 GMT; Secure; HttpOnly; SameSite;.
  • is renewed per session that makes it look like a browser was never closed.
  • Secure; HttpOnly; SameSite; are recommended attributes for more security.

In addition, web browsers may use session restoring and revive sessions.

Having gone over the basics of both cookie and JWT based authorization, which is better? Well, that depends. Many HTML form based login systems still use cookie-based authorization. But once you move to SPAs (single-page applications) and APIs, a JWT may server you better. Here's sequence diagrams of both approaches. First, the traditional cookie-based auth:

Traditional Cookie-Based Auth

When sessions are used in server-side authorization, server sends an encrypted session id to a user. Next time the request comes from this user, the user can be authorized based on the session id. First, it is decrypted and then searched from the session store.

Then the modern token-based auth:

Modern Token-Based Auth

Notice also the change in the endpoints - the token-based approach communicates with the back-end through an API, while the traditional cookie-based approach has the front-end and the back-end more tightly knitted (in this example).

Moving to the front + API approach allows easier swapping of the front - for example from HTML to an Android app.

Both token-based and session-based authorization have their benefits:

Token-based authorization Session-base authorization
stored in the client-side stored in the server-side
single-sign-on support between services of the same provider (shared secret key) no additional overhead of "claims"
if combined with "claims" provides fine-grained permission options simple - thus more maintainable
no issues with cookies easier to setup/integrate as well as less costly
scales better in case of a huge number of users, which is often the case with micro-services capable of providing slightly more robust user experience
more modern; popular in RESTful APIs and micro-services legacy of older web apps + ofter better support in scripting langs (e.g., PHP) and frameworks (e.g., Spring)

They also have security concerns (remember, that authorization must be combined with HTTPS, otherwise security is totally lost):

Token-based authorization Session-base authorization
claims and their creative combinations exploited in attacks bad practices, such as exposing Session IDs in the URL, compromise the security
not vulnerable to CSRF but XSS attack (to steal token) protected further with such cookie attributes as:
in case of successful XSS, the JWT token cannot be invalidated, but it required switching the secret key in a server, thus logout of all the users Secure;
things to be extra cautious with are password and permission changes, if something bad happens, recovery is not simple HttpOnly; no javascript access
SameSite; no cross-site sending, protects agains CSRF-attacks

Disclaimer: for Node.js apps, express and its security middleware, in particular helmet are highly recommended, if app is in production.

Obsolete: cookies as a client-side storage

Cookies were once used for general client-side storage. Lack of other viable options legitimated the practice. Now-a-days, modern storage APIs are recommended.

Modern APIs for client storage are the Web storage API (localStorage and sessionStorage) and IndexedDB, introduced in the next chapter.

Cookies are sent with every request, so they can worsen performance (especially for mobile data connections).

Role-based access control

User roles and access rights

This far we have been discussing a binary authorization scenario - a user is either approved or not to do something. In most applications this is not enough. We do not want all users to be superusers with unlimited rights. Instead, we might want to separate users as customers, maintainers, and administrators, for example, and set different rights for each role. This middle layer of user role is required so that we do not need to micro-manage each individual users rights.

For defining roles and permissions, role-based access control (RBAC) is a popular method.

Each user has a role and id specified. It is also possible to have one or more roles per user. RBAC grants rights to operations instead of data objects. For example, all users with the role of maintainer have the right of deleting comments.

Conditions can be combined. For example, access is OK, if resource.owner == 29 or resource.role == 3.

RBAC and CRUD

REST API defines URLs for resources, for example, for all devices: /api/devices and for one specific device: /api/devices/{id}.

REST targets a uniform data handling with HTTP methods; correspondence with create-read-update-delete (CRUD) in databases.

HTTP method Operation in DB REST semantics
GET SELECT read resource(s), both URLs possible
POST INSERT create resource, new item as JSON payload, use 'all URL'
PUT UPDATE update resource, use 'id URL'
DELETE DELETE delete resource, use 'id URL'

Complication: REST principles cannot be mandated. For example, GET is meant to be idempotent and safe without side-effects, yet this cannot be guaranteed.

Operations can be controller with RBAC. Auth type can vary.

Summary

Authentication and authorization are an important aspect of creating secure applications. With HTTP, each transaction is authorized individually. And you need to make checks on the frontend as well as on the backend. On the frontend, it wouldn't make sense to show the user controls for features they do not have the right to access. And on the backend, you need to be prepared that the user will try to "cheat" the frontend checks and access the restricted features anyways.

If you're exposing an API, instead of serving server-rendered views, JSON web tokens are a fitting solution for authentication. If you stick with server-rendered views, you're most likely better served with a session cookie.