OAuth 2.0 authentication

Speakeasy supports the Oauth2.0 security implementation as a first class citizen. This includes type generation for OAuth schemas and in many cases the complete management of the token refresh flow. End users of Speakeasy SDKs users don’t need to retrieve and manage access tokens manually.

API builders also have the option to leverage Speakeasy’s custom security schemes to implement custom OAuth flows not part of the standard OpenAPI specification.

This document covers the following types of OAuth 2.0 flows

Oauth 2.0 typeDescription
client credentialsDocs
authorization flowDocs
username and passwordComing soon! Currently in early access please reach out to support@speakeasy.com
custom token refresh flowDocs

Other custom flows can be implemented using a combination of hooks and custom security schemes.

Client credentials flow

OAuth 2.0 defines several methods for building a request to the tokenUrl endpoint.

Client authentication methodDescriptionSpeakeasy support
client_secret_postThe secret is provided in the request body as application/x-www-form-urlencoded form data
client_secret_basicThe secret is provided in the Authorization header using the Basic authentication scheme✅ with hooks

Define type: oauth2 and flows: clientCredentials to prompt users for a client ID and client secret when instantiating the SDK. The client credentials flow is used to obtain an access token for API requests.

paths:
/drinks:
get:
operationId: listDrinks
summary: Get a list of drinks.
description: Get a list of drinks, if authenticated this will include stock levels and product codes otherwise it will only include public information.
security:
- clientCredentials:
- read:drinks
tags:
- drinks
components:
securitySchemes:
clientCredentials:
type: oauth2
flows:
clientCredentials:
tokenUrl: https://speakeasy.bar/oauth2/token/
scopes: {}
security:
- clientCredentials:
- read:basic

Global scopes defined for the OAuth 2.0 scheme are requested alongside any operation-specific scopes when making API requests.

To enable client credentials flow in the SDK, add the following to the gen.yaml file:

configVersion: 2.0.0
generation:
auth:
OAuth2ClientCredentialsEnabled: true
import { SDK } from "speakeasy";
async function run() {
const sdk = new SDK({
security: {
clientID: "<YOUR_CLIENT_ID_HERE>",
clientSecret: "<YOUR_CLIENT_SECRET_HERE>",
},
});
const result = await sdk.drinks.listDrinks();
// Handle the result
console.log(result);
}
run();

Authorization Code Flow

Authorization code flows can vary in implementation, but there is typically some secret values that need to be passed during the code token exchange.

The format for the secret values can also vary but a very common format is:

<Term>
<Base64><Client ID />:<Client Secret /></Base64
></Term>
  • <Term> often being Basic or Bearer
  • The following string being some format of Client ID and Client Secret, combined with a : and then base64 encoded.

To allow for any possible formatting Speakeasy offers support for Hooks, these hooks allow you to alter a request before it is sent to the server.

For this example we will be using the names id and secret, but you can use any names you like.

First we will define a custom security schema, documention for that can be found here (opens in a new tab)

tokenRequest:
type: http
scheme: custom
x-speakeasy-custom-security-scheme:
schema:
properties:
id:
type: string
example: app-speakeasy-123
secret:
type: string
example: MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTI
required:
- id
- secret
description: The string `Basic` with your ID and Secret separated with colon (:), Base64-encoded. For example, Client_ID:Client_Secret Base64-encoded is Q2xpZW50X0lEOkNsaWVudF9TZWNyZXQ=.

This security schema will then be applied to our OAuth token exchange endpoint.

paths:
/oauth/token:
post:
tags:
- OAuth2
summary: OAuth2 Token
description: Get an OAuth2 token for the API.
operationId: getToken
security:
- tokenRequest: []

This custom security schema allows us to supply the Id and Secret needed for the token exchange directly to that method, and generate the unique header value needed with a hook.

Next we add the hook to generate that header.

import type {BeforeRequestContext, BeforeRequestHook, Hooks} from "./types.js";
import {GetTokenSecurity} from "../models/operations/gettoken.js";
import {stringToBase64} from "../lib/base64.js";
class OAuthTokenRequestHook implements BeforeRequestHook {
beforeRequest(hookCtx: BeforeRequestContext, request: Request): Request {
switch (hookCtx.operationID) {
case "getToken": {
let sec = hookCtx.securitySource;
if (typeof sec === "function") {
sec = sec();
}
if (!sec) {
throw new Error("security source is not defined");
}
const customSec = sec as GetTokenSecurity;
const encoded = stringToBase64(
[customSec.Id || "", customSec.Secret || ""].join(":"),
);
request.headers.set("Authorization", `Basic ${encoded}`);
break;
}
}
return request;
}
}

Now that the hook is added, when you are using the SDK to acquire an OAuth token, you can pass in the values and the hook will generate the special header for you.

import { SDK } from "SDK";
const sdk = new SDK();
async function run() {
const result = await sdk.oAuth2.getToken({
Id: process.env["SDK_ID"] ?? "",
Secret: process.env["SDK_SECRET"] ?? "",
}, {
grantType: "authorization_code",
code: "1234567890",
redirectUri: "https://example.com/oauth/callback",
});
// Handle the result
console.log(result);
}
run();

Custom refresh token flow

To enable custom OAuth refresh token handling, implement security callbacks (opens in a new tab) along with additional configuration outside the OpenAPI spec.

Step 1: Define OAuth security in the OpenAPI spec

/oauth2/token:
get:
operationId: auth
security:
- []
responses:
200:
description: OK
content:
application/json:
schema:
type: object
properties:
access_token: string
required:
- access_token
/example:
get:
operationId: example
responses:
200:
description: OK
components:
securitySchemes:
auth:
type: oauth2
flows:
clientCredentials:
tokenUrl: https://speakeasy.bar/oauth2/token/
scopes: {}
security:
- auth: []

Step 2: Add a callback function to the SDK

Add a file called oauth with the appropriate file extension for the programming language (for example, oauth.ts for TypeScript, oauth.py for Python, oauth.go for Go, and so on) to implement OAuth token exchange logic.

import * as z from "zod";
import { SDK_METADATA } from "./lib/config";
// TypeScript SDKs use Zod for runtime data validation. We can use Zod
// to describe the shape of the response from the OAuth token endpoint. If the
// response is valid, Speakeasy can safely access the token and its expiration time.
const tokenResponseSchema = z.object({
access_token: z.string(),
expires_in: z.number().positive(),
});
// This is a rough value that adjusts when we consider an access token to be
// expired. It accounts for clock drift between the client and server
// and slow or unreliable networks.
const tolerance = 5 * 60 * 1000;
/**
* A callback function that can be used to obtain an OAuth access token for use
* with SDKs that require OAuth security. A new token is requested from the
* OAuth provider when the current token has expired.
*/
export function withAuthorization(
clientID: string,
clientSecret: string,
options: { tokenStore?: TokenStore; url?: string } = {},
) {
const {
tokenStore = new InMemoryTokenStore(),
// Replace this with your default OAuth provider's access token endpoint.
url = "https://oauth.example.com/token",
} = options;
return async (): Promise<string> => {
const session = await tokenStore.get();
// Return the current token if it has not expired yet.
if (session && session.expires > Date.now()) {
return session.token;
}
try {
const response = await fetch(url, {
method: "POST",
headers: {
"content-type": "application/x-www-form-urlencoded",
// Include the SDK's user agent in the request so requests can be
// tracked using observability infrastructure.
"user-agent": SDK_METADATA.userAgent,
},
body: new URLSearchParams({
client_id: clientID,
client_secret: clientSecret,
grant_type: "client_credentials",
}),
});
if (!response.ok) {
throw new Error("Unexpected status code: " + response.status);
}
const json = await response.json();
const data = tokenResponseSchema.parse(json);
await tokenStore.set(
data.access_token,
Date.now() + data.expires_in * 1000 - tolerance,
);
return data.access_token;
} catch (error) {
throw new Error("Failed to obtain OAuth token: " + error);
}
};
}
/**
* A TokenStore is used to save and retrieve OAuth tokens for use across SDK
* method calls. This interface can be implemented to store tokens in memory,
* a shared cache like Redis or a database table.
*/
export interface TokenStore {
get(): Promise<{ token: string; expires: number } | undefined>;
set(token: string, expires: number): Promise<void>;
}
/**
* InMemoryTokenStore holds OAuth access tokens in memory for use by SDKs and
* methods that require OAuth security.
*/
export class InMemoryTokenStore implements TokenStore {
private token = "";
private expires = Date.now();
constructor() {}
async get() {
return { token: this.token, expires: this.expires };
}
async set(token: string, expires: number) {
this.token = token;
this.expires = expires;
}
}

Step 3: Pass the callback function in SDK instantiation

Update the README to show how to pass the callback function when instantiating the SDK:

import { SDK } from "speakeasy";
const sdk = new SDK({
security: withAuthorization("client_id", "client_secret"),
});
await s.drinks.listDrinks();

OAuth 2.0 scopes

Global security with OAuth 2.0 scopes

When defining global security settings for OAuth 2.0, the SDK automatically requests the necessary scopes for all operations. This setup is useful for APIs where most endpoints share the same level of access. Global scopes are defined in the OpenAPI specification and applied to all requests unless specifically overridden.

The following OpenAPI definition applies global OAuth 2.0 scopes:

components:
securitySchemes:
oauth2:
type: oauth2
flows:
clientCredentials:
tokenUrl: https://speakeasy.bar/oauth2/token/
scopes:
read: Grants read access
write: Grants write access
security:
- oauth2:
- read # Apply the read scope globally
- write # Apply the write scope globally

The SDK automatically generates tokens with both read and `write“ scopes. When making a request, the SDK checks whether the token contains the required scopes for the operation. If the token lacks the necessary scopes or has expired, a new token is requested with the correct scopes.

In the SDK, global OAuth 2.0 scopes can be defined when the SDK is instantiated:

import { SDK } from "speakeasy";
const sdk = new SDK({
security: {
clientID: "<YOUR_CLIENT_ID_HERE>",
clientSecret: "<YOUR_CLIENT_SECRET_HERE>",
oAuth2Scopes: ["read"], // Global scope applied to all operations by default
},
});

Per-operation security with OAuth 2.0 scopes

For more control over specific API operations, per-operation security settings can be used. This allows different scopes to be applied to individual operations, overriding the global settings.

The following OpenAPI definition applies an operation-specific OAuth scope for the listDrinks operation:

paths:
/drinks:
get:
operationId: listDrinks
summary: Get a list of drinks.
description: Retrieves a list of drinks, requiring the `read` scope.
security:
- oauth2:
- read # Apply the read scope for this operation

In this case, the SDK requests a token with the read scope only when calling the listDrinks operation. If the token does not meet the required scope for the operation or has expired, the SDK regenerates the token with the correct scope.

Here’s how the SDK can be used with per-operation security:

import { SDK } from "speakeasy";
const sdk = new SDK({
security: {
clientID: "<YOUR_CLIENT_ID_HERE>",
clientSecret: "<YOUR_CLIENT_SECRET_HERE>",
},
});
const result = await sdk.drinks.listDrinks({
security: {
oAuth2Scopes: ["read"], // Specify the scope for this operation
},
});