OAuth 2.0 Authentication
Warning
The password grant type is prohibited in the latest OAuth 2.0 Security Best Current Practice (opens in a new tab) and is not supported by Speakeasy.
Client Credentials Flow
OAuth 2.0 defines several methods for building a request to the tokenUrl endpoint.
Client Authentication Method | Description | Speakeasy Support |
---|---|---|
client_secret_post | The secret is provided in the request body as application/x-www-form-urlencoded form data | ✅ |
client_secret_basic | The secret is provided in the Authorization header using the Basic authentication scheme | ✅ with hooks |
others | - | ✅ 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: - drinkscomponents: 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.0generation: 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();
Custom Refresh Token Flow
To enable custom OAuth refresh token handling, implement security callbacks (opens in a new tab) along with additional configuration outside of 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: OKcomponents: securitySchemes: auth: type: oauth2 flows: clientCredentials: tokenUrl: https://speakeasy.bar/oauth2/token/ scopes: {}security: - auth: []
Step 2: Add Callback Function to the SDK
Add a file called oauth.ts
(or oauth.py
, oauth.go
) 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 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 accesssecurity: - 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 },});