OAuth 2.0 authentication
Speakeasy fully supports the OAuth 2.0 security implementation. This includes 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:
Oauth 2.0 type | Description |
---|---|
client credentials | Docs |
authorization flow | Docs |
resource owner password credentials flow | Docs |
custom token refresh flow | Docs |
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 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 |
authorization-code | The secret is passed during the code token (see [Authorization](## Authorization code flow)) | ✅ with hooks |
Other | ✅ with hooks and custom security schemes |
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: listDrinkssummary: 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:drinkstags:- drinkscomponents:securitySchemes:clientCredentials:type: oauth2flows: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 resultconsole.log(result);}run();
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: listProductssummary: List all products.responses:"200":description: Successful response.content:application/json:schema:$ref: "#/components/schemas/Products"security:- oauth2: [products:read]components:securitySchemes:oauth2:type: oauth2flows:password:tokenUrl: http://localhost:35456/oauth2/tokenscopes:products:read: Permission to read/list productsproducts:create: Permission to create productsproducts:delete: Permission to delete productsadmin: Full permission including managing product inventories
To enable OAuth 2.0 ROPC flow in the SDK, add the following to the gen.yaml
file:
configVersion: 2.0.0generation:auth:OAuth2PasswordEnabled: true
When making a call using this flow, the SDK security is configured with these parameters:
Parameter | Notes |
---|---|
username | mandatory |
password | mandatory |
clientID | optional |
clientSecret | optional |
Below are usage examples in different 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
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 beingBasic
orBearer
- 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
tokenRequest:type: httpscheme: customx-speakeasy-custom-security-scheme:schema:properties:id:type: stringexample: app-speakeasy-123secret:type: stringexample: MTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIrequired:- id- secretdescription: 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:- OAuth2summary: OAuth2 Tokendescription: Get an OAuth2 token for the API.operationId: getTokensecurity:- 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 resultconsole.log(result);}run();
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
/oauth2/token:get:operationId: authsecurity:- []responses:200:description: OKcontent:application/json:schema:type: objectproperties:access_token: stringrequired:- access_token/example:get:operationId: exampleresponses:200:description: OKcomponents:securitySchemes:auth:type: oauth2flows: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: oauth2flows:clientCredentials:tokenUrl: https://speakeasy.bar/oauth2/token/scopes:read: Grants read accesswrite: 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: listDrinkssummary: 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},});