linear-security-basics by jeremylongshore
Secure API key management and OAuth best practices for Linear.Use when setting up authentication securely, implementing OAuth flows,or hardening Linear integrations.Trigger with phrases like "linear security", "linear API key security","linear OAuth", "secure linear integration", "linear secrets management".
Content & Writing
1.9K Stars
265 Forks
Updated Apr 3, 2026, 03:47 AM
Why Use This
This skill provides specialized capabilities for jeremylongshore's codebase.
Use Cases
- Developing new features in the jeremylongshore repository
- Refactoring existing code to follow jeremylongshore standards
- Understanding and working with jeremylongshore's codebase structure
Install Guide
2 steps- 1
Skip this step if Ananke is already installed.
- 2
Skill Snapshot
Auto scan of skill assets. Informational only.
Valid SKILL.md
Checks against SKILL.md specification
Source & Community
Repository claude-code-plugins-plus-skills
Skill Version
main
Community
1.9K 265
Updated At Apr 3, 2026, 03:47 AM
Skill Stats
SKILL.md 298 Lines
Total Files 1
Total Size 9.1 KB
License MIT
--- name: linear-security-basics description: | Secure API key management, OAuth best practices, and webhook verification for Linear integrations. Trigger: "linear security", "linear API key security", "linear OAuth", "secure linear", "linear webhook verification", "linear secrets management", "linear token refresh". allowed-tools: Read, Write, Edit, Grep version: 1.0.0 license: MIT author: Jeremy Longshore <[email protected]> compatible-with: claude-code, codex, openclaw tags: [saas, linear, api, security, authentication] --- # Linear Security Basics ## Overview Secure authentication patterns for Linear integrations: API key management, OAuth 2.0 with PKCE, token refresh (mandatory for new apps after Oct 2025), webhook HMAC-SHA256 signature verification, and secret rotation. ## Prerequisites - Linear account with API access - Understanding of environment variables and secret management - Familiarity with OAuth 2.0 and HMAC concepts ## Instructions ### Step 1: Secure API Key Storage ```typescript // NEVER hardcode keys // BAD: // const client = new LinearClient({ apiKey: "lin_api_xxxx" }); // GOOD: environment variable import { LinearClient } from "@linear/sdk"; const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY!, }); ``` **Environment setup:** ```bash # .env (never commit) LINEAR_API_KEY=lin_api_xxxxxxxxxxxxxxxxxxxxxxxxxxxx LINEAR_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx # .gitignore .env .env.* !.env.example # .env.example (commit for documentation) LINEAR_API_KEY=lin_api_your_key_here LINEAR_WEBHOOK_SECRET=your_webhook_secret_here ``` **Startup validation:** ```typescript function validateConfig(): void { const key = process.env.LINEAR_API_KEY; if (!key) throw new Error("LINEAR_API_KEY is required"); if (!key.startsWith("lin_api_")) throw new Error("LINEAR_API_KEY has invalid format"); if (key.length < 30) throw new Error("LINEAR_API_KEY appears truncated"); } validateConfig(); ``` ### Step 2: OAuth 2.0 with PKCE ```typescript import express from "express"; import crypto from "crypto"; const app = express(); const OAUTH = { clientId: process.env.LINEAR_CLIENT_ID!, clientSecret: process.env.LINEAR_CLIENT_SECRET!, redirectUri: process.env.LINEAR_REDIRECT_URI!, scopes: ["read", "write", "issues:create"], }; // Generate PKCE verifier and challenge function generatePKCE() { const verifier = crypto.randomBytes(32).toString("base64url"); const challenge = crypto.createHash("sha256").update(verifier).digest("base64url"); return { verifier, challenge }; } // Step 1: Redirect to Linear authorization app.get("/auth/linear", (req, res) => { const state = crypto.randomBytes(16).toString("hex"); const { verifier, challenge } = generatePKCE(); // Store state + verifier in session req.session!.oauthState = state; req.session!.codeVerifier = verifier; const url = new URL("https://linear.app/oauth/authorize"); url.searchParams.set("client_id", OAUTH.clientId); url.searchParams.set("redirect_uri", OAUTH.redirectUri); url.searchParams.set("response_type", "code"); url.searchParams.set("scope", OAUTH.scopes.join(",")); url.searchParams.set("state", state); url.searchParams.set("code_challenge", challenge); url.searchParams.set("code_challenge_method", "S256"); res.redirect(url.toString()); }); // Step 2: Handle callback — exchange code for tokens app.get("/auth/linear/callback", async (req, res) => { const { code, state } = req.query; // CSRF check if (state !== req.session!.oauthState) { return res.status(400).json({ error: "Invalid state parameter" }); } const response = await fetch("https://api.linear.app/oauth/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", code: code as string, client_id: OAUTH.clientId, client_secret: OAUTH.clientSecret, redirect_uri: OAUTH.redirectUri, code_verifier: req.session!.codeVerifier, }), }); const tokens = await response.json(); // Store tokens securely (encrypt at rest) await storeTokens(req.user!.id, { accessToken: encrypt(tokens.access_token), refreshToken: encrypt(tokens.refresh_token), expiresAt: new Date(Date.now() + tokens.expires_in * 1000), }); res.redirect("/dashboard"); }); ``` ### Step 3: Token Refresh As of Oct 2025, all new Linear OAuth apps issue refresh tokens. Existing apps must migrate by April 2026. ```typescript async function getValidToken(userId: string): Promise<string> { const stored = await getStoredTokens(userId); // Refresh if expired or expiring within 5 minutes if (stored.expiresAt.getTime() - Date.now() < 5 * 60 * 1000) { const response = await fetch("https://api.linear.app/oauth/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: decrypt(stored.refreshToken), client_id: process.env.LINEAR_CLIENT_ID!, client_secret: process.env.LINEAR_CLIENT_SECRET!, }), }); if (!response.ok) throw new Error(`Token refresh failed: ${response.status}`); const tokens = await response.json(); // Linear rotates refresh tokens — always store the new one await storeTokens(userId, { accessToken: encrypt(tokens.access_token), refreshToken: encrypt(tokens.refresh_token), expiresAt: new Date(Date.now() + tokens.expires_in * 1000), }); return tokens.access_token; } return decrypt(stored.accessToken); } ``` ### Step 4: Webhook Signature Verification Linear signs every webhook with HMAC-SHA256 using the webhook's signing secret. The signature is in the `Linear-Signature` header. ```typescript import crypto from "crypto"; function verifyWebhookSignature( rawBody: string, signature: string, secret: string ): boolean { const expected = crypto .createHmac("sha256", secret) .update(rawBody) .digest("hex"); // Timing-safe comparison to prevent timing attacks try { return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expected) ); } catch { return false; // Length mismatch } } // Express middleware — must use raw body parser app.post("/webhooks/linear", express.raw({ type: "*/*" }), (req, res) => { const signature = req.headers["linear-signature"] as string; const rawBody = req.body.toString(); if (!verifyWebhookSignature(rawBody, signature, process.env.LINEAR_WEBHOOK_SECRET!)) { return res.status(401).json({ error: "Invalid signature" }); } // Verify webhook timestamp (guard against replay attacks) const event = JSON.parse(rawBody); const age = Date.now() - event.webhookTimestamp; if (age > 60000) { return res.status(400).json({ error: "Webhook too old" }); } processEvent(event).catch(console.error); res.json({ received: true }); }); ``` ### Step 5: Secret Rotation ```typescript // Support multiple keys during rotation period const apiKeys = [ process.env.LINEAR_API_KEY_NEW, process.env.LINEAR_API_KEY_OLD, ].filter(Boolean) as string[]; async function getWorkingClient(): Promise<LinearClient> { for (const apiKey of apiKeys) { try { const client = new LinearClient({ apiKey }); await client.viewer; // Verify key works return client; } catch { continue; } } throw new Error("No valid Linear API key found"); } ``` ## Security Checklist - [ ] API keys in environment variables only (never in code) - [ ] `.env` files in `.gitignore` - [ ] OAuth state parameter validated (CSRF protection) - [ ] PKCE used for public/mobile clients - [ ] Tokens encrypted at rest in database - [ ] Token refresh implemented (mandatory for new apps) - [ ] Webhook signatures verified with `crypto.timingSafeEqual` - [ ] Webhook timestamp checked (< 60s age) - [ ] HTTPS enforced on all endpoints - [ ] Minimal OAuth scopes requested - [ ] API keys rotated quarterly ## Error Handling | Error | Cause | Solution | |-------|-------|----------| | `Invalid signature` | Webhook secret mismatch | Verify `LINEAR_WEBHOOK_SECRET` in Linear Settings > API > Webhooks | | `invalid_grant` | Refresh token expired/revoked | Re-initiate full OAuth flow | | `Invalid scope` | App not authorized for scope | Request only scopes your app needs | | `Authentication required` | Token expired, refresh failed | Trigger re-authentication | ## Examples ### Test Webhook Signature Locally ```typescript import crypto from "crypto"; const secret = "test-signing-secret"; const payload = JSON.stringify({ action: "create", type: "Issue", data: { id: "test", title: "Test" }, webhookTimestamp: Date.now(), }); const sig = crypto.createHmac("sha256", secret).update(payload).digest("hex"); console.log(`Signature: ${sig}`); console.log(`Valid: ${verifyWebhookSignature(payload, sig, secret)}`); ``` ## Resources - [OAuth 2.0 Authentication](https://linear.app/developers/oauth-2-0-authentication) - [OAuth Actor Authorization](https://linear.app/developers/oauth-actor-authorization) - [Webhooks Documentation](https://linear.app/developers/webhooks) - [API Settings](https://linear.app/settings/api)
Name Size