home / skills / jeremylongshore / claude-code-plugins-plus-skills / linear-security-basics

This skill helps securely authenticate Linear integrations and manage keys and OAuth flows, reducing exposure and simplifying secure setup.

npx playbooks add skill jeremylongshore/claude-code-plugins-plus-skills --skill linear-security-basics

Review the files below or copy the command above to add this skill to your agents.

Files (1)
SKILL.md
7.1 KB
---
name: linear-security-basics
description: |
  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".
allowed-tools: Read, Write, Edit, Grep
version: 1.0.0
license: MIT
author: Jeremy Longshore <[email protected]>
---

# 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:**
```typescript
// 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:**
```bash
# .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:**
```typescript
// 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

```typescript
// 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
```typescript
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
```typescript
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
```typescript
// 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 |
|-------|-------|----------|
| `Invalid signature` | Webhook secret mismatch | Verify secret matches Linear settings |
| `Token expired` | Refresh token expired | Re-authorize user |
| `Invalid scope` | Missing permission | Request additional scopes |

## Resources
- [Linear OAuth Documentation](https://developers.linear.app/docs/oauth)
- [Webhook Security](https://developers.linear.app/docs/graphql/webhooks)
- [API Authentication](https://developers.linear.app/docs/graphql/authentication)

## Next Steps
Prepare for production with `linear-prod-checklist`.

Overview

This skill teaches secure API key management and OAuth best practices for Linear integrations. It focuses on safe secret storage, implementing OAuth 2.0 flows, token lifecycle handling, webhook verification, and secret rotation. Use it to harden integrations and reduce exposure risk in production systems.

How this skill works

The skill inspects common integration touchpoints and provides concrete code patterns: environment variable validation, OAuth initiation and callback handling with CSRF state checks, token refresh logic, webhook signature verification, and multi-key rotation strategies. It emphasizes encryption at rest, timing-safe comparisons, and startup validation to fail fast on misconfiguration.

When to use it

  • When building a user-facing app that authenticates with Linear via OAuth.
  • When storing or using Linear API keys in servers or automation scripts.
  • When receiving Linear webhooks and needing to validate payload authenticity.
  • When implementing token refresh and secure storage for long-running services.
  • When preparing an integration for production and enforcing secret rotation.

Best practices

  • Never hardcode API keys; load them from environment variables and provide a .env.example for docs.
  • Validate required env vars at startup and fail fast if formats or presence are incorrect.
  • Encrypt access and refresh tokens at rest and use a 5-minute buffer when refreshing.
  • Use a cryptographically random state value for OAuth and verify it on callback to prevent CSRF.
  • Verify webhook signatures with HMAC sha256 and use timing-safe comparisons.
  • Support multiple API keys during rotation and periodically rotate keys and secrets.

Example use cases

  • Implementing a Linear OAuth flow for a multi-tenant dashboard with per-user tokens.
  • Securing a backend job that uses a Linear API key to create issues automatically.
  • Validating incoming Linear webhooks in a ticketing integration before processing events.
  • Adding token refresh and encrypted storage for a long-lived connector service.
  • Rolling API keys without downtime by trying new and old keys during rotation.

FAQ

What scope should I request for OAuth?

Request the minimal scopes your integration needs (read, write, issues:create) and add more only when required.

How should I store environment files in source control?

Never commit .env files. Commit a .env.example with placeholder values and add .env and .env.* to .gitignore.

How do I handle an expired refresh token?

Prompt the user to re-authorize the app. Log and surface clear errors so re-auth flows are straightforward.