SDKs
OAuth 2.0 authentication
Speakeasy supports the OAuth 2.0 security implementation, including type generation for OAuth schemas and in many cases the complete management of the token refresh flow. End users of Speakeasy SDKs 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 that aren’t part of the standard OpenAPI specification.
This document covers the following types of OAuth 2.0 flows:
Other custom flows can be implemented using a combination of hooks and custom security schemes.
Client credentials flow
Section titled “Client credentials flow”The client credentials flow is used to obtain an access token for API requests by prompting users for a client ID and client secret when instantiating the SDK. OAuth 2.0 defines several methods for building a request to the tokenUrl endpoint. Speakeasy supports the following authentication methods:
client_secret_post(default method)client_secret_basicauthorization-code(with hooks)- Custom flows (with hooks and custom security schemes)
To enable the client credentials flow in the SDK:
- define
type: oauth2andflows: clientCredentialsin your OpenAPI specification. - add the following to the
gen.yamlfile:
configVersion: 2.0.0generation: auth: OAuth2ClientCredentialsEnabled: trueclient_secret_post
Section titled “client_secret_post”The client_secret_post method sends the client credentials in the request body as application/x-www-form-urlencoded form data. This is the default authentication method used by Speakeasy when no specific method is specified.
components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: https://speakeasy.bar/oauth2/token/ # client_secret_post is the default method, so no additional configuration is needed scopes: {}When using this method, the client ID and client secret are sent in the request body as form parameters:
client_id=YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&grant_type=client_credentialsclient_secret_basic
Section titled “client_secret_basic”The client_secret_basic method sends the client credentials in the Authorization header using the Basic authentication scheme. The client ID and client secret are combined with a colon separator, Base64-encoded, and sent in the header.
To use this method, add the x-speakeasy-token-endpoint-authentication: client_secret_basic extension to your OAuth security scheme:
components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: https://speakeasy.bar/oauth2/token/ x-speakeasy-token-endpoint-authentication: client_secret_basic scopes: {}When using this method, the client ID and client secret are sent in the Authorization header:
Authorization: Basic base64(client_id:client_secret)This method is preferred by some OAuth providers for security reasons, as it keeps credentials out of request bodies and server logs.
tokenUrl
Section titled “tokenUrl”The tokenUrl property in OAuth 2.0 flows can be specified in two formats:
- A pathname (e.g.,
/auth/token) - An absolute URL (e.g.,
https://api.example.com/auth/token)
When a pathname is provided instead of an absolute URL, Speakeasy resolves it relative to the user’s configured server URL. This is particularly useful in environments where you have multiple server configurations and need the token endpoint to adapt accordingly.
For example, if your OpenAPI specification includes a pathname:
components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: /clientcredentials/token scopes: read: Read access write: Write accessAnd the user has configured their server URL as https://api.example.com, the effective token URL will be https://api.example.com/clientcredentials/token.
Alternatively, you can specify an absolute URL:
components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: https://auth.mycompany.com/clientcredentials/token scopes: read: Read access write: Write accessThis feature allows for more flexible authentication configurations across different environments without requiring changes to the OpenAPI specification.
Limited Support
Currently, absolute URLs are only supported in Go and Python.
additional properties
Section titled “additional properties”To provide additional properties to the tokenUrl, you need to use the x-speakeasy-token-endpoint-additional-properties extension.
For example, to include an audience parameter we need to modify our OpenAPI specification like this:
components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: /clientcredentials/token x-speakeasy-token-endpoint-additional-properties: audience: type: string example: "AUD" scopes: read: Read access write: Write accessLimited Support
Currently, x-speakeasy-token-endpoint-additional-properties are only
supported in Go, TypeScript, Python, and standalone MCP servers.
Example OpenAPI configuration
Section titled “Example OpenAPI configuration”Here’s a complete example showing how to configure OAuth client credentials flow in your OpenAPI specification:
paths: /drinks: get: operationId: listDrinks summary: Get a list of drinks. security: - clientCredentials: - read:drinks tags: - drinks /drink/{id}: get: operationId: viewDrink summary: View drink details. tags: - drinks put: operationId: updateDrink summary: Update drink details. security: - clientCredentials: - write:drinks tags: - drinkscomponents: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: /oauth2/token/ # Uncomment the following line to use client_secret_basic instead of the default client_secret_post # x-speakeasy-token-endpoint-authentication: client_secret_basic scopes: read:basic: Basic read access read:drinks: Allow listing available drinks write:drinks: Allow updating drinks inventorysecurity: - clientCredentials: - read:basicOAuth 2.0 scopes
When making a token Request to the tokenUrl, all operation-specific scopes
used in the specification will be requested alongside globally required
scopes. Please refer to the OAuth 2.0 scopes section for
more details.
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();Resource Owner Password Credentials flow
Section titled “Resource Owner Password Credentials flow”Also known informally as OAuth 2.0 Password flow.
Resource Owner Password Credentials (ROPC) flow is designed for obtaining access tokens directly in exchange for a username and password.
Below is an example of how ROPC Flow is configured in openapi.yaml. You’ll note that
oauth2 security scheme is linked to the listProducts operation and that the scope products:read is
required by the listProducts operation.
paths: /products: get: operationId: listProducts summary: List all products. responses: "200": description: Successful response. content: application/json: schema: $ref: "#/components/schemas/Products"
components: securitySchemes: oauth2: type: oauth2 flows: password: tokenUrl: http://localhost:35456/oauth2/token scopes: products:read: Permission to read/list products products:create: Permission to create products products:delete: Permission to delete products admin: Full permission including managing product inventoriessecurity: - oauth2: [products:read]To enable OAuth 2.0 ROPC flow in the SDK, add the following to the gen.yaml file:
configVersion: 2.0.0generation: auth: OAuth2PasswordEnabled: trueWhen making a call using this flow, the SDK security is configured with these parameters:
usernamepasswordclientIDclientSecretBelow are usage examples in supported languages:
const sdk = new SDK({ oauth2: { username: "testuser", password: "testpassword", clientID: "beezy", clientSecret: "secret", },});const result = await sdk.listProducts();It is also possbile to bypass token retrievals by passing an explicit token to the SDK object:
const sdk = new SDK({ oauth2: "THE_TOKEN",});const result = await sdk.listProducts();Authorization code flow
Section titled “Authorization code flow”Authorization code flows can vary in implementation, but there are 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 beingBasicorBearer- 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, documentation for that can be found here
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
Section titled “Custom refresh token flow”To enable custom OAuth refresh token handling, implement security callbacks along with additional configuration outside the OpenAPI spec.
Step 1: Define OAuth security in the OpenAPI spec
Section titled “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: OKcomponents: securitySchemes: auth: type: oauth2 flows: clientCredentials: tokenUrl: https://speakeasy.bar/oauth2/token/ scopes: {}security: - auth: []Step 2: Add a callback function to the SDK
Section titled “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
Section titled “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
Section titled “OAuth 2.0 scopes”Global security with OAuth 2.0 scopes
Section titled “Global security with OAuth 2.0 scopes”The available scopes for the OAuth 2.0 scheme can be listed in the scopes property when defining the security component.
components: securitySchemes: oauth2: type: oauth2 flows: clientCredentials: tokenUrl: /oauth2/token/ scopes: read: Grants read access write: Grants write accessThe following OpenAPI definition then applies global OAuth 2.0 scopes:
security: - oauth2: - read # Apply the read scope globally - write # Apply the write scope globallyIn this configuration the SDK automatically requests the read and write scopes for all operations. This is useful for APIs where most endpoints share the same level of access. 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.
Per-operation OAuth 2.0 scheme
Section titled “Per-operation OAuth 2.0 scheme”For more control over specific API operations, OAuth2 security schemes can be applied to specific operations only:
paths: /drinks: get: operationId: listDrinks # uses API key authentication summary: Get a list of drinks. /order/{id}: get: operationId: viewOrder # uses OAuth2 client credentials flow summary: Get order details. security: - clientCredentials: [read]components: securitySchemes: apiKey: type: apiKey name: Authorization in: header clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: /oauth2/token/ scopes: read: Grants read accesssecurity: - apiKey: []import { SDK } from "speakeasy";
const sdk = new SDK();
const result = await sdk.viewOrder({ security: { clientID: "<YOUR_CLIENT_ID_HERE>", clientSecret: "<YOUR_CLIENT_SECRET_HERE>", },});Operation level OAuth 2.0 scopes
Section titled “Operation level OAuth 2.0 scopes”Scopes defined in the root-level security section apply globally but can be overridden on a per-operation basis. In the following example:
- the
readscope is requested by default as it is defined in the root-levelsecuritysection. - the
writescope will be requested for theupdateOrderoperation only. - the
adminscope is available in thescopesproperty but not actually used by any operation.
paths: /order/{id}: get: operationId: viewOrder summary: View order details. put: operationId: updateOrder summary: Update order details. security: - clientCredentials: [write] requestBody: required: true content: application/json: schema: $ref: "#/components/schemas/Order"components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: /oauth2/token/ scopes: read: Grants read access write: Grants write access admin: Grants admin accesssecurity: - clientCredentials: [read]User-defined OAuth 2.0 scopes
Section titled “User-defined OAuth 2.0 scopes”By enabling the x-speakeasy-overridable-scopes extension, end users can override the list of scopes sent to the authorization server for each token request.
x-speakeasy-overridable-scopes
The x-speakeasy-overridable-scopes extension is currently only supported for
the clientCredentials flow.
First, add x-speakeasy-overridable-scopes: true to the clientCredentials flow definition:
components: securitySchemes: clientCredentials: type: oauth2 flows: clientCredentials: tokenUrl: /oauth2/token/ x-speakeasy-overridable-scopes: true scopes: read: Grants read access write: Grants write access admin: Grants admin accesssecurity: - clientCredentials: [read, write]Users can then specify a custom list of scopes at runtime by setting the scopes field when instantiating the security object. When provided, these scopes will take precedence over the ones defined in the OpenAPI specification.
async function run() { const sdk = new SDK({ security: { clientID: "<YOUR_CLIENT_ID_HERE>", clientSecret: "<YOUR_CLIENT_SECRET_HERE>", scopes: ["user:read", "user:write"], }, });}