技能 编程开发 Linear 安全指南

Linear 安全指南

v20260311
linear-security-basics
指导开发者构建 Linear 集成时的安全机制,涵盖 API Key 管理、OAuth 授权、令牌刷新、Webhook 签名校验与密钥轮换等实用操作。
获取技能
467 次下载
概览

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" });  # HTTP 400 Bad Request
  }

  // 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),  # 1000: 1 second in ms
  });

  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) {  # 1000: 1 second in ms
    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),  # 1 second in ms
    });

    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" });  # HTTP 401 Unauthorized
  }

  const event = JSON.parse(payload);
  // Process verified webhook...
  res.status(200).json({ received: true });  # HTTP 200 OK
});

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
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

Next Steps

Prepare for production with linear-prod-checklist.

Output

  • Configuration files or code changes applied to the project
  • Validation report confirming correct implementation
  • Summary of changes made and their rationale

Examples

Basic usage: Apply linear security basics to a standard project setup with default configuration options.

Advanced scenario: Customize linear security basics for production environments with multiple constraints and team-specific requirements.

信息
Category 编程开发
Name linear-security-basics
版本 v20260311
大小 7.69KB
更新时间 2026-03-12
语言