Why we love OAuth 2.0 at Kinde and why you should too
By Evgeny Komarevtsev —
Maybe you know all about it, or maybe you’re just getting to know it. Either way, this article aims to help you understand how and why we use OAuth 2.0 at Kinde, and what this means for connecting your apps to Kinde for authentication and authorization.
First, I’ll describe a short history of OAuth, establish some basic terms, then discuss two ways of authenticating the front-end and back-end applications, highlighting the major things to look for.
As the cherry on top, I’ll show how we support multi-API authentication and authorization.
At Kinde we value and support open standards. As much as we like to innovate on the wheel, authentication and authorization are not wheels that need reinventing.
Every language we support has its own authentication libraries for OAuth 2.0, and we never want to lock you in with a proprietary protocol. We also don’t believe we could build a more secure one ourselves, and the popularity of OAuth proves that.
We want founders to be able to use Kinde with niche tech or any tech easily, even if we haven’t provided an SDK for it (yet).
With reasonable limitations, our authentication is both OpenID and OAuth 2.0 compliant, so you can use your library of choice and run with it. We support OpenID Connect Discovery, so you won’t even need to know the details to connect.
Although outside of scope, our API is described in the Open API spec, which opens it up to generation by anyone.
It’s Fall (Autumn) of 2006, Twitter introduces OpenID initiative to share user identities between services without exposing passwords.
- 2007 — the OAuth (Open Authorization) discussion group is created, and the first draft of initial specification OAuth Core 1.0 is released.
- 2009 — A session fixation attack was discovered against OAuth Code 1.0
- 2009 — OAuth Code 1.0 revision A is published to address the session vulnerability
- 2010 — OAuth 1.0 RFC 5849 is published
- 2012 — OAuth 2.0 RFC 6749 is published (not backwards compatible with OAuth 1.0)
- 2013 — OAuth 2.0 thread model published as RFC 6819
- In-progress — OAuth 2.0 Security Best Current Practice
OAuth is a survivor through smart adaption, with a rich history and battle-tested code.
ID Token
identifies the user
Access token
authorizes the user or an application
Refresh token
can be used to generate a new ID and access token
Application
- referred to OAuth 2.0 client in the specification, in this document called application
Client
- a user agent authenticating against an application (web browser, mobile application or a server-side process)
OAuth 2.0 standard describes two types of clients (public & confidential), which can be characterized by their ability to securely hold secrets.
Here are examples of clients accessing Kinde applications:
- A web browser is classified as a public client
- A mobile application is classified as a public client
- A back-end web server is classified as a confidential client
Kinde application type | Secret provided is confidential? | Used from | Grants included | Scopes included | Authenticates id_token | Authorizes access_token |
---|---|---|---|---|---|---|
Single Page Web Application | No | Web browsers mobile applications | authorization_code refresh_token | openid profile email offline | Users | Users Organizations APIs |
Regular Web Application | Yes | Back-end systems | authorization_code refresh_token client_credentials implicit | openid profile email offline | Users | Users Organizations APIs |
Machine to machine Application | Yes | Back-end systems | client_credentials | - | n/a | Applications APIs |
Think about a
client secret
like a password that you need to store securely. To prevent accidentally exposing the secret, we don’t generate it for Single Page Web Applications.
As you can see, not all clients are created equal, which brings us to the saga of public client authentication. But first, a bit about JWTs.
They say that even if you develop the best medicine in the world, without an effective delivery system, it’s useless. Same goes for token delivery.
There are multiple approaches to handling latency and consistency in computer systems. Some things, like changing a user’s password need to be consistent, and some things are ok to catch-up over time.
OAuth 2.0 specification doesn’t enforce the format of tokens, but JWT (RFC 7519) is a de-facto pairing for a good reason. It provides a way to verify tokens on the receiving side, as well as being extensible.
Kinde has opted to support asymmetric encryption for JWT tokens only (SHA256). We are deliberately not supporting symmetric HMAC. Although this generates a larger payload, it provides greater security.
When integrating without an SDK, be sure to check the token signing method is
RS256
. This prevents JWT algorithm confusion attacks.
By using refresh tokens and adjusting token lifetimes you can tweak latency if you would like the data to be consistent.
On each token refresh you’ll receive an up-to-date snapshot of the state of the user.
In addition to standard OpenID claims, such as sub
, aud
, etc, we extend our user, application and ID tokens with the following claims. This list will grow as we add more features.
Token | Subject | Claim | Description |
---|---|---|---|
ID token | user | org_codes | A list of organizations user has access to |
Access token | user | org_code | The organization code user currently authorized to |
Access token | user | permissions | The list of user permissions for the authorized organization |
Access token | user | feature_flags | The list of feature flags available to the user in the current context |
Access token | application | azp | The application id requesting the token |
Access token | application | gty | The list of grant types, mostly used to identify client_credentials tokens |
Before authenticating the user, we need to know that the caller has access to the application in the first place. With the back-end applications it is usually achieved with the client secret
.
As you can guess, without the secret, it is impossible to trust the caller (hence the name public client), which brings us to the evolution of the approaches to making public client authentication more secure.
To reduce chances of accidentally exposing the client secret, Kinde does not provide one for the single-page applications.
This saga starts with the Resource Owner Password Credentials Grant. It was introduced as part of the initial OAuth 2.0 specification with a set of security recommendations and a list of possible vulnerabilities.
This is the initial take on authenticating of public clients. With this authentication flow, the client receives the ID token directly as a result of calling the authorization endpoint. This flow cannot be used to receive an access token and is used only for authentication.
The risk of this approach is well-recognized and we highly discourage to use it for any new builds. This is the reason Kinde doesn’t support implicit authentication flow for public clients.
Limitations of the implicit flow includes the inability to use refresh tokens and instead using the hidden iframe to maintain long-running sessions.
This flow relies on a user agent that can follow redirects, and introduces separation of the initial request and the token exchange, effectively removing the direct exposure of the tokens to the user agent (usually browser).
Additionally, it introduced the state
and redirect_uri
parameters to correlate multiple requests in the flow.
This auth flow provides the benefit of client authentication without the use of a secret.
The authorization code flow is susceptible to the following attacks (not exhaustive):
- Access token exposure (via browser history, in JS object state, browser local storage)
- Client-side CSRF
- Redirect URL poisoning
- Clickjacking (this type of attack is not specific to the authorization code flow)
In September 2015, the Proof Key for Code Exchange (PKCE) RFC 7636 was published.
The PKCE extension effectively prevents authorization code injection attacks, making clients more secure. It also enables public clients to support refresh tokens and long-running auth sessions without silent authentication.
For better security Kinde requires you to use the PKCE extension with public clients.
Before the PKCE extension, refresh tokens were mostly the concern of backend servers. However, with a certain consideration this has opened-up the ability for the public clients to operate more like the confidential clients.
When an application uses the authorization code flow with the PKCE extension, it opens it up to offline access. This enables Kinde to issue a refresh token which can be used to exchange for the fresh set of tokens, limiting the active token exposure time.
Kinde uses refresh token rotation technique making both access and refresh tokens invalid after the refresh. This dramatically decreases the exposure time to intercept and use the token.
For front-end apps, authorization code with PKCE plus refresh token with rotation is pretty much state of the art, but there is more to that picture.
The most secure system you can have is the system that never accessed or connected the network. But no connected system is 100% secure. So it is less about making it “bullet-proof”, but making it as difficult as possible to exploit. We live in a connected world and we need to look into ways of balancing those two paradigms.
There are two way of securing your back-end API or a web application. Both come with their own caveats, benefits, and drawbacks. Although Kinde supports both approaches, you will need to make your own determination on which one to choose.
Kinde’s admin interface uses back-end authentication with session management via cookies, completely hiding the tokens in the back-end. This prevents a possibility of learning the internals of the system to be later exploited, e.g. API audiences usually point to the URLs of the APIs.
This is where you use an authorization code flow with PKCE extension on a public client, then pass the acquired access token in the authorization header to the back-end APIs.
- Access and refresh token are exposed to the public clients, making it more challenging to secure the application. Incorrect storage of tokens can make them vulnerable to attacks.
- Rotating refresh tokens only reduces the risk, and does not fully eliminate it.
- Build simplicity. You will only need a user agent to implement this type of authentication.
This is where you use an authorization code flow (preferably with PKCE) on a confidential client, then maintain a session using other mechanisms, e.g. cookies.
You can still expose the access tokens to the front-end code. But this will reduce the overall security of your application.
- Extra engineering effort, as there are more moving parts involved. You need to securely store access and refresh tokens, then maintain a user session using other mechanisms.
- You will need to run a backend. It can be lightweight and serverless, but you still need it in this case.
- Strong application verification on token exchange using the client ID and client secret.
- The token exchange will happen in the server-to-server communication, making it impossible to intercept the tokens.
- You will never expose active access or refresh tokens to the public, making it the most secure approach available.
Let’s assume at some point you need to make an authorized call to your back-end API. Kinde provides support for registering one or more APIs, enabling them for your application.
To request a token to include the API authorization, you will need to include one or multiple API audiences to your initial authorization request.
Many authentication providers limit you to use a single API audience per request. Kinde doesn’t have this limitation, so you can authorize multiple APIs in a single token. OAuth 2.0 specification expects it to be an array of strings.
For example, if you want to authorize the following APIs https://evtest.com/api
, https://evtest.com/api2
, you will need to add the following to your request.
&audience=https%3A%2F%2Fevtest.com%2Fapi&audience=https%3A%2F%2Fevtest.com%2Fapi2
Assuming you enable APIs for your application, this will generate the access token with the aud
claim populated.
"aud": [
"https://evtest.com/api",
"https://evtest.com/api2"
]
Now, you could pass the access token to the downstream APIs, which need to validate their respective audiences.
We hope that this discussion of OAuth 2.0 and auth tokens has helped you understand what approaches are possible for authentication and authorization with Kinde. And that you now love OAuth 2.0 as much as we do.