OpenAPI Tips - How to Handle Auth
Tips and best practices for how to correctly configure auth in your OpenAPI spec.
The Problem
The OpenAPI spec is best known for descriptions of RESTful APIs, but it’s designed to be capable of describing any HTTP API, whether that be REST or something more akin to RPC based calls. That leads to the spec having a lot of flexibility baked-in; there's a lot of ways to achieve the exact same result that are equally valid in the eyes of the spec. Because of this, the OpenAPI documentation is very ambiguous when it comes to how you should define your API. That’s why we’d like to take the time to eliminate some of the most common ambiguities that you’ll encounter when you build your OpenAPI spec. In this case, we’ll be taking a look at how to correctly configure auth in your OpenAPI spec.
What Authentication mechanisms are available?
OpenAPI supports a number of different options for API authentication, which can be daunting when first starting out. Before we give our thoughts on the different methods, it’s worth highlighting that regardless of the method of authentication you choose, you should pair it with TLS. TLS encrypts the messages to and from your API, to protect you and your users from attack. Learn more about setting up TLS here. Some of the common types of authentication are listed below:
apiKey: This is the most common form of authentication for machine-to-machine (M2M) APIs and supports passing a pre-shared secret in a number of different ways i.e. either via the Authorization header (or another custom header), as a query parameter, or via a cookie. While this is probably the most commonly used mechanism, it is generally one of the least secure. This is especially true if the key is passed outside of headers or cookies (i.e. via query params as various logging mechanisms normally store query param information). The biggest security flaw is that most pre-shared secrets are long-lived and if intercepted, can be used until they are either revoked or expire (generally in a number of months or years). This risk is normally tolerated for M2M applications as the chance of interception (especially when using private VPCs/TLS and other mechanisms) is relatively low when compared to a key from a user’s device traveling on a public network.
basic: This is a simple authentication mechanism baked into the HTTP protocol. It supports sending an Authorization header containing an encoded username and password. While this can be a relatively simple mechanism to get started with, if used incorrectly can risk leaking easy-to-decode passwords. It also shares a lot of the downsides of apiKeys below.
bearer: This scheme allows the passing of a token (most commonly a JWT) in the Authorization header. This is generally used for short-lived tokens that are granted to the users of your API through an additional login mechanism. Using a JWT allows for the storage of additional metadata within the token, which can be helpful for some use cases, such as storing scopes for permissions models.
oauth2: A popular open authentication mechanism that supports an authentication flow that allows servers to authenticate on behalf of a user or organization. While more generally used for authenticating clients and end-users, it is quite often used in machine-to-machine applications as well, but is less popular due to the added complexity of the authentication flows. OAuth2 is considered more secure than other mechanisms due to its granted privileges through short lived tokens, that limit damage from intercepting the tokens.
openIdConnect: Is an authentication mechanism built on top of OAuth2 that allows obtaining identity information about the authenticating user via JWTs.
Global Authentication vs Endpoint Authentication
The OpenAPI specification allows you to describe all the above authentication mechanisms and more from the HTTP Authentication Scheme Registry.
Describing security in your OpenAPI document is then done through 1 of 2 different options:
Global security: The security you describe is available for all operations in your document.
Per operation security: when described, it overrides any global level security described.
Here is an example of describing security in the ways mentioned above:
openapi: 3.0.3
info:
title: Example Security Definitions
version: 1.0.0
servers:
- url: http://api.prod.speakeasyapi.dev
# Here we are describing the Global security schemes used by the operations in this document
# This is a list of security schemes names defined in the components section
security:
- APIKey: []
components:
# The definition of the used security schemes
securitySchemes:
APIKey:
type: apiKey
in: header
name: X-API-Key
Bearer:
type: http
scheme: bearer
bearerFormat: JWT
paths:
/test:
get:
# The security schemes defined here will override the global security schemes for this operation
security:
- Bearer: []
responses:
200:
description: OK
# This operation used the global security schemes defined as it doesn't provide its own
delete:
responses:
200:
description: OK
The important parts of the above example are the security and securitySchemes sections. We will go into some details about how they are defined and the options available.
How To Describe Security
The security section is a list (actually a list of key-value pairs, but we will talk a bit more about that later) of security schemes that can be used to authenticate all operations or a particular operation (depending on the scope of the security list).
Below is an example of a number of different ways you can use the security section of your document:
# The below example shows a single mandatory scheme needed for the API
security:
- APIKey: []
# The below example shows that one of the below schemes is required for the API
security:
- APIKey: []
- Bearer: []
# The below example shows there is no security required for the API
# this is equivalent to not having a security section at all at the Global scope
# or disabling security at the per operation level
security: []
# The below example shows that security is optional for the API
# this may be used if an API provides additional functionality when authenticated
security:
- APIKey: []
- {}
# The below example shows that certain scopes are required by the OAuth token used
# to authenticate the API
security:
- OAuth:
- read
- write
The items in the list are key-value pairs with a name or key of a security scheme defined in the components section. We recommend giving them a boring name that explains what they are.
The values are an array of scopes used only by the oauth2 and openIdConnect type schemes and define what scopes are needed for the API.
When used as shown above, it provides a list of available schemes that can be used, with the end-user of the API being able to choose one of the available schemes to use to authenticate.
If more than one scheme is required to authenticate an API, then that is where additional pairs in the key-value pairs come in. See the example below:
# The below example shows 2 options for an end user to choose, as long as they use one or the other
# they will be able to access the API
security:
- APIKey: []
- Bearer: []
# The example below differs as it is a single option with multiple schemes
# Both the APIKey and SigningKey need to be used together to access the API
security:
- APIKey: []
SigningKey: []
# The example below shows multiple options for an end user to chose
# with one of them requiring the use of multiple schemes
security:
- APIKey: []
SigningKey: []
- Bearer: []
Combining schemes like the above gives you the option to define AND/OR type functionality when it comes to the requirements of your API.
How To Describe Security Schemes
securitySchemes are the actual details of the options provided in the security sections of your document. The security schemes are components that are defined with the components section of your document. Below is an example of the 5 types of security schemes described above and how they are defined:
...
components:
schemas:
...
responses:
...
# The definition of the used security schemes
securitySchemes:
BasicAuth: # An arbitrary scheme name, we recommend something descriptive
type: http
scheme: basic
Bearer:
type: http
scheme: bearer
bearerFormat: JWT # Optional token format
APIKey:
type: apiKey
in: header # or query/cookie
name: X-API-Key
OAuth:
type: oauth2
flows: # Many different flows are available - https://spec.openapis.org/oas/v3.1.0#oauth-flows-object
implicit:
authorizationUrl: https://test.com/oauth/authorize
scopes:
read: Grants read access
write: Grants write access
OpenIdConnect:
type: openIdConnect
openIdConnectUrl: https://test.com/.well-known/openid-configuration
Best Practices
I generally recommend considering developer experience and weighing this up against the security requirements of your API. Consider its use cases such as will it be called from another server? A client? Or a combination of both. Based on your needs, try to describe your security requirements in your OpenAPI document as simply as possible, if you can avoid multiple options or too many per operation differences, then it will generally require less friction for your end-user to get up and running and start using your API. This is the main reason we still see pre-shared secrets (described by the apiKey type above) being the most ubiquitous option amongst APIs today, but if not managed correctly, it can be one of the least secure options available.