Identity and security overview
When it comes to security. There is a lot of guidance and also advice and commentary on why you should or should not use each approach. It's hard to understand all the different options and security issues they have.
You also have different clients (mobile/web/app) and each has different security requirements for managing, storing and processing data in the form of tokens, cookies or sessions.
At the end, your app may scale and not all options can be deployed as multiple instance nodes without additional changes to the app and infrastructure.
This means that you really need to make decisions at the beginning and ask questions like: What are your customers? and What are your requirements? and What can be added to the system in the future? and How is it possible to deal with the chosen approach? etc. etc..
This article is part of opensource training.
Authentication vs Authorization
-
Authentication
- process of verifying who a user is (identity). -
Authorization
- process of checking what a user has access to (checking permissions to access a particular resource).
Tokens vs Cookie and Sessions
Tokens, cookies and sessions can be interpreted as resources or tools used by various protocols/standards/patterns (OAuth, OpenID, BFF) to perform identity-related tasks.
It is important to understand the basics as they end up being used in combination.
In the end, you can wrap the JWT token in a cookie and use it with session data for 'authorization' and 'authentication'.
Cookies
Cookies can be understood as small blocks of data created by the server that are written once in the response and automatically resent with each subsequent request from the web browser until their lifetime expires.
They can be used for authentication/authorization or tracking and marketing purposes. And much more...
IndentityServer cookies example:
There are 3 main properties of cookies:
-
HttpOnly
- An http-only cookie cannot be accessed by client-side APIs, such as JavaScript. The browser will not allow you to access this cookie from the front-end code. -
Secure
A secure cookie can only be transferred over an encrypted HTTPS connection. -
SameSite
3 option valuesStrict
,Lax
orNone
- this tells the browser which domain and address the cookie can be sent to.
What is origin?
The origin is usually ip and port or domain name and subdomain name.
// This are different origins since subdomain are different
https://developer.mozilla.org
https://mozilla.org
// This are also different origins since port number is different
https://localhost:5001
https://localhost:7001
Another cookie definitions:
-
Session cookies
- Created only for the browser session (in memory) and deleted/lost after closing. -
Third-party cookies
- Usually the domain attribute of a cookies matches the domain displayed in the address bar of the web browser. asfirst-party cookies
. Thethird-party cookies
does not match the current domain and is used astracking cookies
to track user activity.
Sessions
Session is used to temporarily store information on the server for use across multiple pages of the site. It is usually associated with a cookie that is used to identify the session stored on the server, but does not contain any data.
Tokens
Tokens are data elements that allow application systems to perform the authorization and authentication process. They are usually encoded as base64 strings.
There are several types of tokens:
-
access token
- Includes user claims and signs them with a secret. It uses JWT tokens. -
refresh token
- Used to "refresh " and get a new 'access token' after its lifetime expires. -
id token
- JSON encoded data about user profile information - etc, etc...
JWT Tokens
JSON Web Token is an open standard that defines how information can be securely transferred between parties as a JSON object.
They are used for authorization
and information exchange
as they provide a security proof that the information wrapped in them is valid and written by a trusted source.
You can easily write arbitrary data to tokens, sign that data, and then have clients use it to access server resources. The server can verify that the token was signed and is still valid.
Basic JWT token flow example:
JWT content:
JWT consist of 3 parts:
-
Header
- Contains information such as the type of token (JWT) and the signing algorithm used, e.g. HMAC SHA256 or RSA.{ "alg": "HS256", "typ": "JWT" }
-
Payload
- Securly signed data (claims)
{ "sub": "1234567890", "name": "John Doe", "admin": true }
-
Signature
- Encrypted header, the encrypted payload, a secret and signed by an algorithm specified in the header.
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret
More additional info about JWT tokens can be found at: official documentation.
The most commonly used token for authorizing access to APIs is the
Bearer
token.
Identity protocols
There are several protocols/specifications available to manage your identity or authorization process.
This was necessary to standardize authentication and authorization between services and clients. So we can use different global identity/authentication providers like Facebook, Google (external/internal) and also standardize the way the process is implemented.
This demo focuses on the most commonly used protocols OAuth
and OpenID Connect
.
Both protocols use a JWT token by default to encrypt and sign sensitive data, or to verify that the request was sent from a trusted source. It is also possible to use cookies on the front end and let the back end do the session and token authorization for you.
You can also watch and learn from various talks:
- OAuth 2.0 and OpenID Connect - [Nate Barbettini]
- Introduction to OAuth 2.0 and OpenID Connect - [Philippe De Ryck]
OAuth
Primarily used to authorize an app's access to a specific resource. This is done without having to share your password with external sites.
If you have ever signed in to a new app and agreed to access your contacts, calendar, etc., you have used OAuth 2.0. This protocol does not provide any information about the user's endpoint, just a token to access certain resources. You can read more about OAuth at this document.
OAuth generally provides clients with "secure delegated access " to certain resources. Imagine you are a Google user and an app wants to access your calendar data. This can be an example of a flow:
OAuth
flow example:
In the above example, an application like Slack, Jira, etc. only gets permission to access a specific resource (e.g. the calendar), but not the user itself, so profile data like username and email are not transferred and remain protected.
If you want to learn more about OAuth, you can watch the following presentations:
Token exchange flows
There are several ways in which grant
can be substituted. The choice depends on what kind of client is requesting access and how much that client is trusted.
- Authorization code flow
- Flow of authorization code with PKCE
- Implicit Flow
- Client credentials flow
Image from Okta
OpenID Connect
OpenID is a protocol for decentralized authentication
A login used by multiple internal/external applications. If you used your Google or Facebook etc. to log in to an external web or app, then you used OpenID Connect
.
OpenID Connect is based on OAuth 2.0. (OAuth is the underlying protocol and OpenId is the identity layer built on top of it) and also uses a JWT token called id_token
which encapsulates identity claims in JSON format. For more information about OpenId, see under this specification.
id_token
example:
{
"iss": "http://server.example.com",
"sub": "248289761001",
"aud": "s6BhdRkqt3",
"nonce": "n-0S6_WzA2Mj",
"exp": 1311281970,
"iat": 1311280970,
"name": "Dalibor Kundrat",
"given_name": "Dalibor",
"family_name": "Kundrat",
"gender": "male",
"birthdate": "0000-10-31",
"email": "d.kundrat@example.com",
"picture": "http://example.com/somepicture_of_dalibor.jpg"
}
OpenId
flow example:
There are several flows that can be used with OpenId
. You can read more about them under this article.
Each OpenId
server by specification provides multiple endpoints to interact with.
The URLs of all endpoints can be explored using the global discovery endpoint. Often referred to as disco. It is available under the path: /.well-known/openid-configuration
and returns JSON OpenID Connect metadata related to the specified authorization server.
Example disco response:
{
"issuer":"https://localhost:5001",
"jwks_uri":"https://localhost:5001/.well-known/openid-configuration/jwks",
"authorization_endpoint":"https://localhost:5001/connect/authorize",
"token_endpoint":"https://localhost:5001/connect/token",
"userinfo_endpoint":"https://localhost:5001/connect/userinfo",
"end_session_endpoint":"https://localhost:5001/connect/endsession",
"check_session_iframe":"https://localhost:5001/connect/checksession",
"revocation_endpoint":"https://localhost:5001/connect/revocation",
"introspection_endpoint":"https://localhost:5001/connect/introspect",
"device_authorization_endpoint":"https://localhost:5001/connect/deviceauthorization",
"frontchannel_logout_supported":true,
"frontchannel_logout_session_supported":true,
"backchannel_logout_supported":true,
"backchannel_logout_session_supported":true,
"scopes_supported":["profile","openid","email","role", "offline_access" //etc..],
"claims_supported":["name","family_name","profile","email", etc..],
"grant_types_supported":["authorization_code","client_credentials", "refresh_token", //etc..],
"response_types_supported":["code","token", "id_token","id_token token" //etc..],
"response_modes_supported":["form_post","query","fragment"],
"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post" ],
"id_token_signing_alg_values_supported":["RS256"],
"subject_types_supported":["public" ],
"code_challenge_methods_supported":["plain","S256"],
"request_parameter_supported":true,
"request_object_signing_alg_values_supported":["RS256","RS384" //etc..],
"authorization_response_iss_parameter_supported":true
}
Main OpenId endpoints:
-
/authorization_endpoint
- interacting with the resource owner and obtaining an authorization permission. -
/token_endpoint
- Obtain an access and/or ID token by presenting an authorization permission (code) or refresh token -
/revocation_endpoint
- Revoke an access or refresh token. -
/end_session_endpoint
- Ends the session associated with the specified ID token. -
/userinfo_endpoint
- Provides information about the authenticated end user.
⠀
NOTE: All of these values in the discovery endpoint refer to the current server configuration. You can adjust or enable/disable certain options in your code during idnetityserver configuration.
⠀
Backend for Frontend pattern (BFF)
BBF is a backend used by a particular front-end application.
Since endpoint APIs may have multiple clients with different requests, BFF can provide a client-specific backend mediator and act as a proxy that forwards and merges multiple requests to different service APIs.
⠀
Ok, we have cookies, tokens, and sessions. We use them for various authentication/authorization protocols (OpenId, OAuth, etc.) and what the hack BFF is good for.?Answer is:
- Security reasons
- Architecture reasons ⠀
Security reasons
In recent years, it was common to implement OpenID Connect for SPAs in Javascript (React, Angular, Vue...), and this is no longer recommended:
- Using access tokens in the browser has more security risks than using secure cookies.
- A SPA is a public client and cannot keep a secret, as such a secret would be part of the JavaScript and could be accessible to anyone inspecting the source code.
- Recent browser changes to prevent tracking may result in 'third-party cookies' being dropped.
- It is not possible to store something securely in the browser for a long period of time, as it can be stolen by various attacks.
Due to the above issues, the best security recommendation for SPA is to avoid storing tokens in the browser and create a lightweight backend to help with this process, called Backend for Frontend pattern (BFF).
This way, you can still use acces_tokens
to authorize access to all your APIs, but clients will use session cookies or tokens in case of mobile devices and BFF will proxy this sides.
BFF can be:
-
statefull
- stores tokens in memory and uses a session to manage them. -
stateless
- stores the tokens in encrypted HTTP-only, same-page cookies.
Architectural reasons
When you design your application, you have several options on how to access APIs from clients (web/mobile/external).
1) A single API gateway that provides a single API for all clients
2) A single API gateway that provides an API for each type of client
3) A per-client API gateway that provides an API to each client.
BFF vs API gateway
While an API Gateway
is a single entry point into to the system for all clients, a BFF
is only responsible for a single type of client.
BBF cookies termination and token isolation
As mentioned in the text, the most important thing:
- Avoid storing tokens in the browser. (No tokens in the browser policy).
- Store tokens on the server side and use encrypted/signed HTTP-only cookies.
Recommended BFF pattern to secure SPA frontends:
- Using this, all communication from the SPA frontend to the authorization server now passes through the BFF and tokens do not reach the SPA.
- The BFF now issues session cookies. These are part of the request to APIs and are exchanged for an access token at the proxy level.
- Client-side cookies are terminated by the BFF proxy.
What's next?
Next time I will show you concrete implementation of BFF.
Repository
You can find the full source code of the app, including identity, distributed logging, tracing and monitoring, in the open source Github repo:
Top comments (15)
KIller article! Thanks.
Awesome article! A little question if you don't mind. Should both the access token and refresh token be saved in the http only cookies? I have an express server that provides the tokens, and a nextjs bff. And I'm not sure if nextjs should store both of them in http only cookies, or encrypt them somehow :D
The think about BFF is that you do not store Access or Refres Token in client cookie at all... Client have only session cookie and make request to BFF =>The BFF adds data to request like accesstoken (that is stored on BFF server side) => and proxy request to API...
In case token is invalid BFF req. automaticaly new token From ID provider... Client have no idea what is happening behind the scenes.... Client have only session cookie...
In case your NextJS is BFF server it store access and refresh token in some DB/Table (SessionStore) and with all requests looks on client session cookie based on identifier search for token in this database/redis and adds this to request and query that from API.... basicaly BFF is some man-in-middle... But it is server side so thats main reason why it is more secure like storing this data on client...
Important:
Clinet <> BFF -> Session cookie (no tokens just BFF client identifier)
BFF <> API -> BFF proxy request to API and adds access_token to headers...
BFF <> IdentitiyServer -> Query refresh token automaticaly...
Refresh token is holded on BFF and only used for renew and deleted on logout... (invalidated). You dont send refresh token to Client or API... Only to ID provider to renew...
This is much clear now. Thank you!
In the case of my SPA application not having a login and password, how do I ensure the security of requests between my SPA and BFF without storing tokens on the SPA side? This has been my main concern, and I've been considering server-side rendering with Next.js.
Very nice article.
End user identity is contained in JWT stored in BFF(he is OIDC confidential client). I assume that API services are Resource servers.. My question is, what if we have chained API service calls one after another(API Service 1 i calling API Service 2), booth of them are Resource Server, how the edge cases are solved with JWT expiration, because RS are bearer only clients...?
Nice article!
I have a question for you. Do you put the identity server on a domain that is shared between the mobile app and the web?
Example:
login.example.com -> identity server
app.example.com -> BFF web
apimobile.example.com-> BFF mobile
Or put everything on the same domain:
example.com/ -> BFF website
example.com/login -> IS
example.com/apimobile -> BFF mobile
I usually use the first approach, for a user who logs in from the web. When you enter the mobile app and are redirected to login.example.com, you are already logged in.
I don't know if there is a better approach.
I personali had it under equal domain since Google cloud allow you to do this mapping domain>IP easy...
But most of companies use separate subdomain as identity.example.com... or (login.example.com)
If you use different domain you need to ensure CORS allow list is properly configured..
nice article!
nice article, waiting for the concrete implementation article too
Hi, Thanks for the article. I would like to ask the fate of the promised follow-up article. :))
Thanks a lot.
Nice article