Configuring OIDC with your own API
Use OpenID Connect within your tasks to authenticate with your own API.
OpenID Connect (OIDC) allows your tasks to access your own API without needing to store API
credentials. At a high level, your API will verify incoming ID tokens are issued by Airplane and
then extract the token's claims. The token's claims contain metadata about how the token was
generated, such as the task it's coming from and the email of the runner.
These claims can be used to implement permissions by rejecting requests that don't match your
criteria, for example, and you can include these claims in your application's audit logs.
This guide will walk you through an example of how to configure your API to use Airplane OIDC
tokens. See Airplane OpenID Connect provider for an overview of how to generate
tokens and the token format.
Passing ID tokens to your API
Tokens can be generated at runtime inside your tasks using the Airplane SDK and passed to your API
via (e.g.) an HTTP header.
typescriptCopied1import airplane from "airplane";23export default airplane.task(4{5slug: "generate_oidc_token",6},7async (params) => {8const token = await airplane.auth.idToken("https://your-app.example.com");910// Attach the token as a header when calling your API.11fetch("https://your-app.example.com/endpoint", {12method: "POST",13headers: { Authorization: `Bearer ${token}` },14});15}16);
javascriptCopied1import airplane from "airplane";23export default airplane.task(4{5slug: "generate_oidc_token",6},7async (params) => {8const token = await airplane.auth.idToken("https://your-app.example.com");910// Attach the token as a header when calling your API.11fetch("https://your-app.example.com/endpoint", {12method: "POST",13headers: { Authorization: `Bearer ${token}` },14});15}16);
pythonCopied1import airplane2import requests34@airplane.task()5def generate_oidc_token():6token = airplane.auth.id_token("https://your-app.example.com")78# Attach the token as a header when calling your API.9requests.post(10"https://your-app.example.com/endpoint",11headers={"Authorization": f"Bearer {token}"},12)
For more information on how to generate tokens, see
Generating tokens.
Verifying the ID token
Once your API receives the ID token, you must verify the token is correctly signed by Airplane. We
recommend doing so with a standard JWT library that matches your stack. You'll typically want to
verify the token's signature, issuer, and audience. We recommend doing this in e.g. API middleware
to more broadly protect the relevant API endpoints.
Once you've verified that the token is signed correctly, you can decode the token to get access to
the token's claims. You'll always want to check
team_id
claim matches the ID of your team (team ID
can be found in Team Settings).You will need to install
jsonwebtoken
and jwks-rsa
.typescriptCopied1import * as jwt from "jsonwebtoken";2import * as jwksRsa from "jwks-rsa";34const verifyToken = async (token: string): Promise<string | jwt.JwtPayload> => {5const jwksClient = new jwksRsa.JwksClient({6// Found via Airplane's OIDC provider settings.7jwksUri: "https://api.airplane.dev/.well-known/jwks",8});9const signingKey = await jwksClient.getSigningKey();1011const claims = jwt.verify(token, signingKey.getPublicKey(), {12// Audience specified when generating the token.13audience: "https://your-app.example.com",14// Check the issuer is Airplane.15issuer: "https://api.airplane.dev",16});1718// Verify the token is for your Airplane team.19if (claims.team_id != "$YOUR_TEAM_ID") {20throw new Error("Invalid team ID");21}2223// e.g. claims.task_slug contains the slug of the task that generated the token.24return claims;25};
You will need to install
jsonwebtoken
and jwks-rsa
.javascriptCopied1import * as jwt from "jsonwebtoken";2import * as jwksRsa from "jwks-rsa";34const verifyToken = async (token) => {5const jwksClient = new jwksRsa.JwksClient({6// Found via Airplane's OIDC provider settings.7jwksUri: "https://api.airplane.dev/.well-known/jwks",8});9const signingKey = await jwksClient.getSigningKey();1011const claims = jwt.verify(token, signingKey.getPublicKey(), {12// Audience specified when generating the token.13audience: "https://your-app.example.com",14// Check the issuer is Airplane.15issuer: "https://api.airplane.dev",16});1718// Verify the token is for your Airplane team.19if (claims.team_id != "$YOUR_TEAM_ID") {20throw new Error("Invalid team ID");21}2223// e.g. claims.task_slug contains the slug of the task that generated the token.24return claims;25};
You will need to install
cryptography
and pyjwt
.pythonCopied1import jwt2import requests34def verify_token(token: str):5kid = jwt.get_unverified_header(token)["kid"]67jwks = requests.get("https://api.airplane.dev/.well-known/jwks").json()89signing_key, = [key for key in jwks["keys"] if key["kid"] == kid]1011claims = jwt.decode(12token,13jwt.algorithms.RSAAlgorithm.from_jwk(signing_key),14algorithms=["RS256"],15options={"verify_signature": True},16# Audience specified when generating the token.17audience="https://your-app.example.com",18# Check the issuer is Airplane.19issuer="https://api.airplane.dev",20)2122# Verify the token is for your team.23if claims["team_id"] != "$YOUR_TEAM_ID":24raise ValueError("Invalid token")2526# e.g. claims["task_slug"] contains the slug of the task that generated the token.27return claims
Here's a non-exhaustive list of third-party libraries you can use to verify OIDC tokens:
- Node: jsonwebtoken and jwks-rsa
- Python: pyjwt and cryptography
- Ruby: jwt
- Go: jwt-go
- Java: java-jwt
Airplane OIDC provider's public keys are available at https://api.airplane.dev/.well-known/jwks.
We strongly recommend using a third-party library for token verification. Doing so greatly
reduces the risk of a security vulnerability in your code.
Your API may want to perform additional checks on the token. For example:
- The
env_slug
claim can be used to gate certain API operations depending on the environment. For example, you may want to only have write access to a production database from theprod
environment. - The
runner_email
claim can be used to ensure your API is only being called by a specific user.
typescriptCopied1// Check that only the user "test@company.com" is allowed to access your API.2if (claims["runner_email"] != "test@company.com") {3throw new Error("Invalid user");4}
javascriptCopied1// Check that only the user "test@company.com" is allowed to access your API.2if (claims["runner_email"] != "test@company.com") {3throw new Error("Invalid user");4}
pythonCopied1# Check that only the user "test@company.com" is allowed to access your API.2if claims["runner_email"] != "test@company.com":3raise ValueError("Invalid user")
For the full set of token claims, see
Token payload reference.
Example: Integrating Airplane OIDC as middleware in your API
Here, we show an example of how to integrate Airplane OIDC as middleware in your Express app. You
will need to install
express
, jsonwebtoken
, and jwks-rsa
.javascriptCopied1import * as jwt from "jsonwebtoken";2import * as jwksRsa from "jwks-rsa";34// You will need to add validateAirplaneToken to your Express.js middleware stack.5//6// const app = express();7//8// app.use(validateAirplaneToken);910function validateAirplaneToken(req, res, next) {11// Assumes the Airplane ID token is passed in the Authorization header.12const idToken = req.headers.authorization.split(" ")[1];1314if (!idToken) {15return res.status(401).json({ error: "Authorization header not found" });16}1718try {19const claims = verifyToken(idToken);20} catch (e) {21return res.status(401).json({ error: "Unable to validate token claims" });22}2324// At this point, the token is valid. You can add the claims to the request object25// for use in your API.26//27// e.g. you can reference `req.airplaneTokenClaims.runner_email` in your API28// handlers to check which users can perform certain operations.29req.airplaneTokenClaims = claims;3031next();32}3334const verifyToken = async (token): => {35const jwksClient = new jwksRsa.JwksClient({36jwksUri: "https://api.airplane.dev/.well-known/jwks",37});38const signingKey = await jwksClient.getSigningKey();3940const claims = jwt.verify(token, signingKey.getPublicKey(), {41// Audience specified when generating the token.42audience: "https://your-app.example.com",43// Check the issuer is Airplane.44issuer: "https://api.airplane.dev",45});4647// Verify the token is for your Airplane team.48if (claims.team_id != "$YOUR_TEAM_ID") {49throw new Error("Invalid team ID");50}5152// e.g. claims.task_slug contains the slug of the task that generated the token.53return claims;54};
Here, we show an example of how to integrate Airplane OIDC as middleware in your Django API. You
will need to install
django
, cryptography
, and pyjwt
.pythonCopied1import requests2import jwt3from django.http import JsonResponse45# You will need to add AirplaneOIDCMiddleware to your MIDDLEWARE list in your Django settings:6#7# MIDDLEWARE = [8# ...9# "path.to.AirplaneOIDCMiddleware",10# ...11# ]1213class AirplaneOIDCMiddleware:14def __init__(self, get_response):15self.get_response = get_response1617def __call__(self, request):18error_response = validate_token(request)19if error_response:20return error_response21return self.get_response(request)2223def validate_airplane_token(request):24# Assumes the Airplane ID token is passed in the Authorization header.25id_token = request.META.get("Authorization", "").split(" ")[1]2627if not id_token:28return JsonResponse({"error": "Authorization header not found"}, status=401)2930try:31claims = verify_token(id_token)32except Exception:33return JsonResponse({"error": "Unable to validate token claims"}, status=401)3435# At this point, the token is valid. You can add the claims to the request object36# for use in your API.37#38# e.g. you can reference `request.airplane_token_claims['runner_email']` in your API39# handlers to check which users can perform certain operations.40request.airplane_token_claims = claims4142return None4344def verify_token(token: str):45kid = jwt.get_unverified_header(token)["kid"]4647jwks = requests.get("https://api.airplane.dev/.well-known/jwks").json()4849signing_key, = [key for key in jwks["keys"] if key["kid"] == kid]5051claims = jwt.decode(52token,53jwt.algorithms.RSAAlgorithm.from_jwk(signing_key),54algorithms=["RS256"],55options={"verify_signature": True},56# Audience specified when generating the token.57audience="https://your-app.example.com",58# Check the issuer is Airplane.59issuer="https://api.airplane.dev",60)6162# Verify the token is for your team.63if claims["team_id"] != "$YOUR_TEAM_ID":64raise ValueError("Invalid token")6566# e.g. claims["task_slug"] contains the slug of the task that generated the token.67return claims