Authentication history (Basic, Digest, Cookie, Session, Token, JWT, API key)
Let’s say you are building an app. Nothing overcomplicated. Simple data storage like Google drive. Users can upload and view their files. The question is how do we make sure users can only view their files and not someone else?
The naive approach would be to send UserId
in every request to a server. It is not only naive but also creates a huge security hole! As soon as your users have basic knowledge of how the Internet works they will try sending requests to a server with a different UserId
.
The only valid option would be to let the server distinguish which user sent a request. There is a variety of authentication options, like Basic authentication, Digest, Cookie, Session, Token and JWT. If you are interested in getting deeper knowledge in what are those, which problems they solve, and their benefits and drawbacks, read this article to the very end.
In case you are new here, there are other articles on this topic.
Authentication theory:
- Authentication history (Basic, Digest, Cookie, Session, Token, JWT, API key)
- Token gang (Bearer token, Reference token, Opaque token, Self-contained token, JWT, Access token, ID token, Refresh token)
- OAuth 2.0 and OpenID in simple terms
How to implement authentication in C# environment:
- Cookie-based Authentication in ASP
- Authorization in ASP
- Identity Server integration with ASP MVC
- Identity Server for microservices
- Identity Server integration with SPA
- SSO with Identity server
You may also check out some others stories:
- What is authentication schema in ASP about?
- Authorization policy under the hood
- Encapsulate authentication with DelegatingHandler
- How to parse token on client side
Meanwhile we are starting.
Basic Authentication
The simplest authentication mechanism is HTTP Basic Authentication. Here’s an overview of how it works:
- A user enters his login and password in the login form
- If the server successfully validates those, the client now can store the user’s login and password
- User Authentication. The client includes an
Authorization
header in all subsequent requests with the word "Basic" followed by a space and a base64-encoded string of the form "login:password".
HTTP: https://my-app.com/get-photos
Authorization: Basic base64encode(login:password)
4. Server Verification. The server, upon receiving the request, decodes the base64 string, retrieves the login and password, and verifies them against DB. If the credentials are valid, the server allows access to the requested resource; otherwise, it returns a 401 Unauthorized
status code.
Despite its simplicity, Basic Authorization has multiple drawbacks:
- security. credentials are sent in an easily decodable Base64-encoded format therefore should only be used in conjunction with HTTPS
- privacy. user’s credentials are exposed and can not be used by other systems
- revocation. the server has no way to end the user’s session
- storage. user’s credential is stored in a browser and could be exposed
- performance. additional db calls have to be made to validate credentials (consider adding cache)
Digest Authentication
To overcome the shortcoming of the Basic Authentication a more secure way to transmit credentials was found.
It involves hashing the username, password, and other information in the request, making it harder to intercept and decode:
- A user enters his login and password in the login form
- If the server succesfully validates those, the client create a hash based on login and password
- User Authentication. The client includes an
Authorization
header in all subsequent requests with the word "Digest" followed user’s login and generated hash:
HTTP: https://my-app.com/get-photos
Authorization: Digest username="john", hash="5ccc069c403eb171e9517f40e41"
4. Server Verification. The server, upon receiving the client’s request, performs its own hash calculations using the stored password. If the calculated hash matches the one sent by the client, access is granted.
In practice it is a bit more complicated than that, but it is enough for you to understand Digest Authentication.
This time we don’t send user’s password but a hash value, which is irreversible comparing to encoding. This approach is a bit more secure, but we still have issues:
- performance. validation of credential cost time and resources
- revocation. the server still has no way to end the user’s session
Cookie
The traditional authentication approach on the web involves cookies.
A cookie could be anything, a number, string, serialized object, and so on. However, for security reasons, the cookie responsible for authentication is encrypted.
The usual flow of using cookies is as follows:
- A user enters his login and password in the login form
- If the server successfully validates those, it issues an encrypted cookie. That cookie can also contain any additional information about the user like age, permissions, preferences etc
- User Authentication. The user’s browser stores that cookie and includes it in all subsequent requests as a Cookie header:
HTTP: https://my-app.com/get-photos
Cookie: uLfpKTEAAxgVbjjJs6S5RcXnQlLnK4knnz2zBa5wN3wO/zqQM4ZhAv604
4. Server Verification. The server, upon receiving the client’s request, validates a cryptographic signature attached to the cookie, its expiration time, domain path etc. If the cookie sent by the client is valid, access is granted.
There are a few things worth noting:
- self-contained. cookies are self-contained. It means the server can just verify its signature without the need to do a DB request. This is how validation cost issues were solved
- stateless: the server does not need to store additional data. This simplifies server-side implementations and allows for easier horizontal scaling
- revocation. server has multiple ways to terminate a user’s session (cookie expiration time, timeout session for inactive users, send invalidate cookie header)
- in-build support. but the most lovely thing about cookies is that they are so common you don’t have to implement any code on the client side. Your browser will attach cookies on every request automatically. That is just ideal for MPA
Regardless of how good it sounds, there are always some caveats:
- performance. cookies are sent out for every single request (even for requests that don’t require authentication)
- interoperability. cookies work out of the box only in the browser. For all the other clients (desktop, mobile etc) you still would have to write code
- scale poorly. cookies are bound to a server’s domain and, therefore are not applicable in SPA or microservices
You can find an example of cookie-based authentication here.
Session
You can make cookies work in a distributed environment with sessions:
- A user enters his login and password in the login form
- If the server successfully validates those, it gets or creates a user’s session in the database and issues a cookie with the session id
- User Authentication. The user’s browser stores that cookie and includes it in all subsequent requests as a
Cookie
header:
HTTP: https://my-app.com/get-photos
Cookie: SessionId=270299
4. Server Verification. The server, upon receiving the client’s request, validates the cookie and retrieves session from the DB
A session is just a record in the database about a user. It contains all the data you would usually have in the cookie, but this time it is completely on the server side.
Despite weak advantages:
- server-side storage. sensitive data is not exposed to the client
- centralized control on the server side. it is easier to implement features like session timeouts, account locking, and other security measures
- cross-domain communication. a session can be utilized across a distributed environment
You have almost the same issues as before:
- latency. session validation requires DB call
- performance. cookies are sent out for every single request
- interoperability. cookies do not work on their own outside the browser
- scale poorly. a database can be distributed too, which leads even to more issues
Token
Long story short, token authentication is the same as the session one. We just no longer have cookies, and instead of session id, a random string is used, so it is harder to compromise it:
- A user enters his login and password in the login form
- If the server successfully validates those, it gets or creates a user’s session in the database and issues a random string related to this session called a token
- User Authentication. The client stores that token and includes it in all subsequent requests:
HTTP: https://my-app.com/get-photos
Authorization: pS4zFXYx0URtDEkMWOcGunMgxA7cGyXYx0URtDEkMWOcGunMgxA7
4. Server Verification. The server, upon receiving the client’s request, validates there is a session in the DB with such token. If so the access is granted
I know 😔. Cookies seemed to be such a good idea. So why would we get rid of it and use a token instead?
- interoperability. token can be used on any device (browser, mobile, desktop)
- cross-domain communication. support distributed architecture
- privacy. no credential is exposed, so the token can be used by third-party
- server-side storage. sensitive data is not exposed to the client
- centralized control on the server side. it is easier to implement features like session timeouts, account locking, and other security measures
However, we just get back to square one:
- no in-build support. we have to implement everything by ourselves
- storage. token stored on the client side can be exposed
- performance. token validation costs a DB request. For microservices, it is even worse. You need an http call to an identity server that will do a DB request
JWT
Finally, we grow up to good old JWT tokens. 😌
So what is the deal with it? JWT tries to keep the benefits of token authentication but decreases performance issues. Do you remember how exactly the validation cost was reduced with cookies? 🤔 Cookies are self-contained. So let’s make our token self-contained.
JWT (JSON Web Token) is a cryptographically secure token that contains all needed data inside itself in JSON format:
{
"alg": "HS256",
"typ": "JWT"
}
.
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
.
signature
Additionally it is encrypted to make it more size concise. So the token above will be encoded to:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
The flow is pretty much the same as what we are used to:
- A user enters his login and password in the login form
- If the server succesfully validates those, it issues JWT token
- User Authentication. The client stores that token and includes an
Authorization
header in all subsequent requests with the word “Bearer” followed by a space and a JWT token:
HTTP: https://my-app.com/get-photos
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
4. Server Verification. The server, upon receiving the client’s request, validates the signature and grants access
The benefits are:
- performance. no database lookup since the token is self-contained
- interoperability. the token can be used on any device (browser, mobile, desktop etc)
- stateless: the server does not need to store additional data. This simplifies server-side implementations and allows for easier horizontal scaling
- cross-domain communication. support distributed architecture
- privacy. no credential is exposed, so the oken can be used by third-party
But remember about those issues:
- storage. token still can be hijacked by an attacker (can be partially fixed with a refresh token)
- revocation. the server has no way to end the user’s session (JWT token is valid even after a user logs out)
- no in-build support. we need to write code (one can argue it is a benefit since it is more agile and flexible but I will let you decide)
- sensitive data exposure. care must be taken not to include sensitive information in the token payload
As you can see, even though JWT is so popular, it is not a panacea either.
API Key
Since we are already on this topic, a few final words for API Key authentication.
The API Key flow is avoided for user authentication but is commonly used for machine-to-machine (M2M) communication or third-party integrations, where one software application or system interacts with another API (Application Programming Interfaces).
Here is a basic overview of the API Key flow:
- Developers or administrators generate API keys beforehand
- Server Authentication. The client includes an
Authorization
header in all subsequent requests with the word "ApiKey" followed by the key:
HTTP: https://my-app.com/get-photos
Authorization: ApiKey 9c403eb171e9517f40e415ccc069c403eb171e9517f40e41
You can also find an implementation where the API key is included as a query parameter:
HTTP: https://my-app.com/get-photos?
api-key=9c403eb171e9517f40e415ccc069c403eb171e9517f40e41
3. Server Verification. The server, upon receiving the client’s request, validates the API key to check if it is legitimate and associated with a valid application
Even though it is almost a standard it still has some minute flaws:
- storage. can be stored insecurely, such as hardcoding them in source code, storing them in configuration files, or sharing them inappropriately
- no expiration. they don’t expire unless explicitly revoked allowing hijackers to have infinite access to the API
Final Thoughts
And that is all for today 😌. In this article I was aiming to guide you through the authentication landscape, offering insights into the diverse tools and techniques that have shaped the authentication over time.
It is essential not only to understand and be able to use any of those approaches in parallel but also to combine them. Regardless of how clean and experience-proved those ideas look, in practice, you would have to deal with a complete horror of legacy code 😁.
It happens so, that authentication is so widely known and a completely misunderstood topic at the same time, that most of the time it results in a complete mess. I have seen jwt is stored in cookies, cookies used for API-only communication, or jwt for m2m. I have been through that kind of sh*t you don’t even want to hear. Most of the time those implementations were done by me 😅. Don’t do as I have done. Be smarter. Learn ahead and find a solution that suits you best 🙃.
Let me know in the comment what was your worst experience with authentication?💬
Give this article a clap or a few if you like it 👏
You can support me with a link below ☕️
And don’t forget to follow if you are interested to read about all those authentication methods in details ✅