Claude-skill-registry linear-security-basics
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/linear-security-basics" ~/.claude/skills/majiayu000-claude-skill-registry-linear-security-basics && rm -rf "$T"
manifest:
skills/data/linear-security-basics/SKILL.mdsource content
Linear Security Basics
Overview
Implement secure authentication and API key management for Linear integrations.
Prerequisites
- Linear account with API access
- Understanding of environment variables
- Familiarity with OAuth 2.0 concepts
Instructions
Step 1: Secure API Key Storage
Never hardcode API keys:
// BAD - Never do this! const client = new LinearClient({ apiKey: "lin_api_xxxxxxxxxxxx" // Exposed in source code }); // GOOD - Use environment variables const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY! });
Environment Setup:
# .env (never commit this file) LINEAR_API_KEY=lin_api_xxxxxxxxxxxx # .gitignore (commit this) .env .env.* !.env.example # .env.example (commit this for documentation) LINEAR_API_KEY=lin_api_your_key_here
Validate on Startup:
// config/linear.ts function validateConfig(): void { const apiKey = process.env.LINEAR_API_KEY; if (!apiKey) { throw new Error("LINEAR_API_KEY environment variable is required"); } if (!apiKey.startsWith("lin_api_")) { throw new Error("LINEAR_API_KEY has invalid format"); } if (apiKey.length < 30) { throw new Error("LINEAR_API_KEY appears too short"); } } validateConfig();
Step 2: Implement OAuth 2.0 Flow
// For user-facing applications import express from "express"; import crypto from "crypto"; const app = express(); // OAuth configuration const OAUTH_CONFIG = { clientId: process.env.LINEAR_CLIENT_ID!, clientSecret: process.env.LINEAR_CLIENT_SECRET!, redirectUri: process.env.LINEAR_REDIRECT_URI!, scope: ["read", "write", "issues:create"], }; // Step 1: Initiate OAuth app.get("/auth/linear", (req, res) => { const state = crypto.randomBytes(16).toString("hex"); req.session!.oauthState = state; const authUrl = new URL("https://linear.app/oauth/authorize"); authUrl.searchParams.set("client_id", OAUTH_CONFIG.clientId); authUrl.searchParams.set("redirect_uri", OAUTH_CONFIG.redirectUri); authUrl.searchParams.set("response_type", "code"); authUrl.searchParams.set("scope", OAUTH_CONFIG.scope.join(",")); authUrl.searchParams.set("state", state); res.redirect(authUrl.toString()); }); // Step 2: Handle callback app.get("/auth/linear/callback", async (req, res) => { const { code, state } = req.query; // Verify state to prevent CSRF if (state !== req.session!.oauthState) { return res.status(400).json({ error: "Invalid state parameter" }); } // Exchange code for tokens 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_CONFIG.clientId, client_secret: OAUTH_CONFIG.clientSecret, redirect_uri: OAUTH_CONFIG.redirectUri, }), }); const tokens = await response.json(); // Store tokens securely (encrypted in database) 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 Flow
async function getValidAccessToken(userId: string): Promise<string> { const stored = await getStoredTokens(userId); // Check if token is expired or expiring soon (5 min buffer) 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!, }), }); const tokens = await response.json(); 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
import crypto from "crypto"; function verifyWebhookSignature( payload: string, signature: string, secret: string ): boolean { const expectedSignature = crypto .createHmac("sha256", secret) .update(payload) .digest("hex"); // Use timing-safe comparison to prevent timing attacks return crypto.timingSafeEqual( Buffer.from(signature), Buffer.from(expectedSignature) ); } // Express middleware app.post("/webhooks/linear", express.raw({ type: "*/*" }), (req, res) => { const signature = req.headers["linear-signature"] as string; const payload = req.body.toString(); if (!verifyWebhookSignature(payload, signature, process.env.LINEAR_WEBHOOK_SECRET!)) { return res.status(401).json({ error: "Invalid signature" }); } const event = JSON.parse(payload); // Process verified webhook... res.status(200).json({ received: true }); });
Step 5: Secret Rotation
// Support multiple API keys during rotation const apiKeys = [ process.env.LINEAR_API_KEY_NEW, process.env.LINEAR_API_KEY_OLD, ].filter(Boolean); async function getWorkingClient(): Promise<LinearClient> { for (const apiKey of apiKeys) { try { const client = new LinearClient({ apiKey: apiKey! }); await client.viewer; // Test the key return client; } catch { continue; } } throw new Error("No valid Linear API key found"); }
Security Checklist
- API keys stored in environment variables only
- .env files in .gitignore
- OAuth state parameter validated
- Tokens encrypted at rest
- Token refresh implemented
- Webhook signatures verified
- HTTPS enforced for all endpoints
- API keys rotated periodically
- Minimal OAuth scopes requested
Error Handling
| Error | Cause | Solution |
|---|---|---|
| Webhook secret mismatch | Verify secret matches Linear settings |
| Refresh token expired | Re-authorize user |
| Missing permission | Request additional scopes |
Resources
Next Steps
Prepare for production with
linear-prod-checklist.