skills/hack23/riksdagsmonitor/Authentication and Credentials for Agentic Workflows

Authentication and Credentials for Agentic Workflows

SKILL.md

🔐 Authentication and Credentials for Agentic Workflows

📋 Overview

This skill provides comprehensive security patterns for managing authentication and credentials in GitHub Agentic Workflows. It covers GitHub token types, secure credential storage, token rotation strategies, least privilege access control, MCP server authentication, and API key management best practices for production-ready autonomous agent systems.

🎯 Core Concepts

Authentication Architecture

graph TB
    subgraph "Credential Sources"
        A[GitHub Secrets] --> B[Environment Variables]
        C[Vault] --> B
        D[AWS Secrets Manager] --> B
    end
    
    subgraph "Agent Runtime"
        B --> E[Credential Manager]
        E --> F{Token Type}
        F -->|GITHUB_TOKEN| G[GitHub API]
        F -->|PAT| H[Extended Permissions]
        F -->|GitHub App| I[Installation Token]
        F -->|API Keys| J[External Services]
    end
    
    subgraph "MCP Servers"
        E --> K[MCP Authentication]
        K --> L[GitHub MCP]
        K --> M[Custom MCP]
        K --> N[Third-Party MCP]
    end
    
    subgraph "Security Controls"
        E --> O[Least Privilege]
        E --> P[Token Rotation]
        E --> Q[Audit Logging]
    end
    
    style E fill:#00d9ff
    style O fill:#ff006e
    style P fill:#ffbe0b

Security Principles

  1. Least Privilege: Minimal permissions required for task
  2. Defense in Depth: Multiple layers of security
  3. Token Rotation: Regular credential updates
  4. Audit Trail: Complete authentication logging
  5. Secure Storage: Encrypted credential management
  6. Time-Limited Access: Short-lived tokens when possible

🔑 GitHub Token Types

1. GITHUB_TOKEN (Automatic)

Characteristics

# Automatic token provided by GitHub Actions
# - Automatically created per workflow run
# - Expires when workflow completes
# - Scoped to the repository
# - Configurable permissions

permissions:
  contents: read       # Read repository contents
  pull-requests: write # Create/update PRs
  issues: write       # Create/update issues
  statuses: read      # Read commit statuses

Usage Pattern

# .github/workflows/agent-pr-review.yml
name: Agent PR Review

on:
  pull_request:
    types: [opened, synchronize]

permissions:
  contents: read
  pull-requests: write

jobs:
  review:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.GITHUB_TOKEN }}  # Automatic token
          
      - name: Run Agent Review
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          node scripts/agents/pr-reviewer.js \
            --pr-number=${{ github.event.pull_request.number }}
            
      - name: Post Review Comment
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            await github.rest.pulls.createReview({
              owner: context.repo.owner,
              repo: context.repo.repo,
              pull_number: context.issue.number,
              body: 'Agent review completed ✅',
              event: 'COMMENT'
            });

Limitations

# GITHUB_TOKEN limitations:
# ❌ Cannot trigger other workflow runs
# ❌ Cannot access other repositories
# ❌ Cannot push to protected branches (unless configured)
# ❌ Expires at end of workflow run
# ❌ Limited to repository permissions

# Use PAT for these scenarios:
# ✅ Triggering workflows
# ✅ Cross-repository operations
# ✅ Protected branch pushes
# ✅ Long-running agent processes

2. Personal Access Token (PAT)

Classic PAT Configuration

# GitHub Settings → Developer settings → Personal access tokens → Tokens (classic)
# 
# Recommended Scopes for Agentic Workflows:
# ✅ repo (Full control of private repositories)
#    - repo:status (Access commit status)
#    - repo_deployment (Access deployment status)
#    - public_repo (Access public repositories)
#    - repo:invite (Access repository invitations)
# ✅ workflow (Update GitHub Action workflows)
# ✅ write:packages (Upload packages to GitHub Package Registry)
# ✅ read:packages (Download packages from GitHub Package Registry)
# ✅ admin:org (Full control of organizations)
#    - read:org (Read organization data)
# ✅ admin:repo_hook (Full control of repository hooks)

# Expiration: 90 days (recommended for security)

Fine-Grained PAT (Recommended)

# GitHub Settings → Developer settings → Personal access tokens → Fine-grained tokens
#
# Permissions for Agentic Workflows:
# 
# Repository permissions:
#   - Actions: Read and write
#   - Contents: Read and write
#   - Issues: Read and write
#   - Metadata: Read-only (mandatory)
#   - Pull requests: Read and write
#   - Workflows: Read and write
#
# Organization permissions (if needed):
#   - Members: Read-only
#
# Account permissions:
#   - None (unless required)
#
# Repository access:
#   - Only select repositories (principle of least privilege)
#
# Expiration: 90 days

Usage in Workflows

# Store PAT as repository secret:
# Repository Settings → Secrets and variables → Actions → New repository secret
# Name: COPILOT_MCP_GITHUB_PERSONAL_ACCESS_TOKEN

jobs:
  agent-task:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout with PAT
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.COPILOT_MCP_GITHUB_PERSONAL_ACCESS_TOKEN }}
          fetch-depth: 0
          
      - name: Run Agent with PAT
        env:
          GITHUB_TOKEN: ${{ secrets.COPILOT_MCP_GITHUB_PERSONAL_ACCESS_TOKEN }}
        run: |
          # Agent can now:
          # - Push to protected branches
          # - Trigger other workflows
          # - Access multiple repositories
          node scripts/agents/cross-repo-agent.js
          
      - name: Create PR (Triggers Workflows)
        uses: peter-evans/create-pull-request@v6
        with:
          token: ${{ secrets.COPILOT_MCP_GITHUB_PERSONAL_ACCESS_TOKEN }}
          title: 'Agent Update'
          body: 'Automated changes by agent'
          branch: 'agent/update'

3. GitHub App Token

App Creation and Configuration

# 1. Create GitHub App
# GitHub Settings → Developer settings → GitHub Apps → New GitHub App

# App Configuration:
# - Name: Agentic Workflow Bot
# - Homepage URL: https://github.com/your-org/your-repo
# - Webhook: Optional (for event-driven agents)
# - Permissions:
#   Repository permissions:
#     - Actions: Read & write
#     - Contents: Read & write
#     - Issues: Read & write
#     - Pull requests: Read & write
#     - Workflows: Read & write
#   Organization permissions:
#     - Members: Read-only (if needed)

# 2. Generate Private Key
# Download private key (PEM file)

# 3. Install App on Repository
# Install App → Select repositories

# 4. Store App Credentials
# - APP_ID: From app settings
# - PRIVATE_KEY: PEM file content (as secret)
# - INSTALLATION_ID: From installation URL

Generate Installation Token

// scripts/agents/lib/github-app-auth.js
import { createAppAuth } from '@octokit/auth-app';
import { Octokit } from '@octokit/rest';
import fs from 'fs';

/**
 * GitHub App authentication for agentic workflows
 */
class GitHubAppAuth {
  constructor(options = {}) {
    this.appId = options.appId || process.env.GITHUB_APP_ID;
    this.privateKey = options.privateKey || process.env.GITHUB_APP_PRIVATE_KEY;
    this.installationId = options.installationId || process.env.GITHUB_APP_INSTALLATION_ID;
    
    if (!this.appId || !this.privateKey) {
      throw new Error('GitHub App credentials not configured');
    }
  }
  
  /**
   * Create authenticated Octokit instance
   */
  async createOctokit() {
    const auth = createAppAuth({
      appId: this.appId,
      privateKey: this.privateKey,
      installationId: this.installationId
    });
    
    const { token } = await auth({ type: 'installation' });
    
    return new Octokit({
      auth: token,
      userAgent: 'agentic-workflow-bot/1.0.0'
    });
  }
  
  /**
   * Get installation token (for direct use)
   */
  async getInstallationToken() {
    const auth = createAppAuth({
      appId: this.appId,
      privateKey: this.privateKey,
      installationId: this.installationId
    });
    
    const { token, expiresAt, permissions } = await auth({ type: 'installation' });
    
    return {
      token,
      expiresAt: new Date(expiresAt),
      permissions
    };
  }
  
  /**
   * Revoke installation token (cleanup)
   */
  async revokeToken(token) {
    const octokit = new Octokit({ auth: token });
    await octokit.apps.deleteToken({
      client_id: this.appId,
      access_token: token
    });
  }
}

export default GitHubAppAuth;

// Usage in workflow
const appAuth = new GitHubAppAuth({
  appId: process.env.GITHUB_APP_ID,
  privateKey: process.env.GITHUB_APP_PRIVATE_KEY,
  installationId: process.env.GITHUB_APP_INSTALLATION_ID
});

const octokit = await appAuth.createOctokit();

// Use octokit for API calls
const { data: pr } = await octokit.pulls.get({
  owner: 'owner',
  repo: 'repo',
  pull_number: 123
});

Workflow Integration

# .github/workflows/agent-github-app.yml
name: Agent with GitHub App

on:
  issues:
    types: [opened]

jobs:
  process-issue:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
        
      - name: Generate App Token
        id: app-token
        uses: actions/create-github-app-token@v1
        with:
          app-id: ${{ secrets.GITHUB_APP_ID }}
          private-key: ${{ secrets.GITHUB_APP_PRIVATE_KEY }}
          
      - name: Run Agent with App Token
        env:
          GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
        run: |
          node scripts/agents/issue-processor.js \
            --issue-number=${{ github.event.issue.number }}
            
      - name: Cleanup (token automatically revoked)
        run: echo "Token expires in 1 hour"

🔒 Credential Storage

1. GitHub Secrets

Repository Secrets

# Add secret via GitHub UI:
# Repository → Settings → Secrets and variables → Actions → New repository secret

# Or via GitHub CLI:
gh secret set ANTHROPIC_API_KEY --body "sk-ant-..."
gh secret set OPENAI_API_KEY --body "sk-..."
gh secret set MCP_DATABASE_URL --body "postgresql://..."

# List secrets:
gh secret list

# Delete secret:
gh secret delete ANTHROPIC_API_KEY

Environment Secrets

# Use GitHub Environments for stage-specific secrets
# Repository → Settings → Environments → New environment

# Development environment
name: development
secrets:
  - API_KEY: dev-key-123
  - DATABASE_URL: postgresql://dev-db

# Production environment
name: production
secrets:
  - API_KEY: prod-key-456
  - DATABASE_URL: postgresql://prod-db
protection_rules:
  - required_reviewers: 2
  - wait_timer: 5  # minutes
# Use environment secrets in workflow
jobs:
  deploy-dev:
    runs-on: ubuntu-latest
    environment: development
    steps:
      - name: Deploy Agent
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: ./deploy.sh
        
  deploy-prod:
    runs-on: ubuntu-latest
    environment: production
    needs: deploy-dev
    steps:
      - name: Deploy Agent
        env:
          API_KEY: ${{ secrets.API_KEY }}
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
        run: ./deploy.sh

Organization Secrets

# For multi-repository access
# Organization → Settings → Secrets and variables → Actions

# Add organization secret:
gh secret set SHARED_API_KEY \
  --org your-org \
  --repos "repo1,repo2,repo3" \
  --body "shared-key-789"

2. External Secret Managers

AWS Secrets Manager

# .github/workflows/agent-aws-secrets.yml
name: Agent with AWS Secrets

jobs:
  agent-task:
    runs-on: ubuntu-latest
    permissions:
      id-token: write  # For OIDC authentication
      contents: read
      
    steps:
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/GitHubActionsRole
          aws-region: us-east-1
          
      - name: Retrieve Secrets
        id: secrets
        run: |
          # Get secret from AWS Secrets Manager
          SECRET_JSON=$(aws secretsmanager get-secret-value \
            --secret-id agentic-workflow/production \
            --query SecretString \
            --output text)
          
          # Parse and export secrets
          echo "::add-mask::$(echo $SECRET_JSON | jq -r '.api_key')"
          echo "API_KEY=$(echo $SECRET_JSON | jq -r '.api_key')" >> $GITHUB_ENV
          
      - name: Run Agent with Secret
        env:
          API_KEY: ${{ env.API_KEY }}
        run: node scripts/agents/secure-agent.js

HashiCorp Vault

# .github/workflows/agent-vault.yml
name: Agent with Vault

jobs:
  agent-task:
    runs-on: ubuntu-latest
    steps:
      - name: Retrieve Secrets from Vault
        id: secrets
        uses: hashicorp/vault-action@v2
        with:
          url: https://vault.example.com
          method: approle
          roleId: ${{ secrets.VAULT_ROLE_ID }}
          secretId: ${{ secrets.VAULT_SECRET_ID }}
          secrets: |
            secret/data/agentic-workflow api_key | API_KEY ;
            secret/data/agentic-workflow db_password | DB_PASSWORD
            
      - name: Run Agent
        env:
          API_KEY: ${{ steps.secrets.outputs.API_KEY }}
          DB_PASSWORD: ${{ steps.secrets.outputs.DB_PASSWORD }}
        run: node scripts/agents/vault-agent.js

3. Secure Secret Handling

// scripts/agents/lib/credential-manager.js
import { SecretsManager } from '@aws-sdk/client-secrets-manager';
import crypto from 'crypto';

/**
 * Secure credential manager for agentic workflows
 */
class CredentialManager {
  constructor(options = {}) {
    this.secretsClient = options.secretsClient || new SecretsManager({
      region: process.env.AWS_REGION || 'us-east-1'
    });
    this.cache = new Map();
    this.cacheTTL = options.cacheTTL || 300000; // 5 minutes
  }
  
  /**
   * Get secret with caching
   */
  async getSecret(secretName, options = {}) {
    // Check cache
    const cached = this.cache.get(secretName);
    if (cached && Date.now() - cached.timestamp < this.cacheTTL) {
      return cached.value;
    }
    
    // Retrieve from secrets manager
    let secret;
    if (secretName.startsWith('aws:')) {
      secret = await this.getAWSSecret(secretName.replace('aws:', ''));
    } else if (secretName.startsWith('env:')) {
      secret = process.env[secretName.replace('env:', '')];
    } else {
      throw new Error(`Unknown secret source for: ${secretName}`);
    }
    
    // Cache secret
    this.cache.set(secretName, {
      value: secret,
      timestamp: Date.now()
    });
    
    return secret;
  }
  
  /**
   * Get secret from AWS Secrets Manager
   */
  async getAWSSecret(secretId) {
    const response = await this.secretsClient.getSecretValue({
      SecretId: secretId
    });
    
    if (response.SecretString) {
      return JSON.parse(response.SecretString);
    } else {
      throw new Error(`Secret ${secretId} is not a string`);
    }
  }
  
  /**
   * Rotate secret
   */
  async rotateSecret(secretName, newValue) {
    if (secretName.startsWith('aws:')) {
      await this.rotateAWSSecret(secretName.replace('aws:', ''), newValue);
    }
    
    // Invalidate cache
    this.cache.delete(secretName);
  }
  
  /**
   * Rotate AWS secret
   */
  async rotateAWSSecret(secretId, newValue) {
    await this.secretsClient.updateSecret({
      SecretId: secretId,
      SecretString: JSON.stringify(newValue)
    });
  }
  
  /**
   * Mask sensitive values in logs
   */
  maskSecret(value) {
    if (!value || value.length < 8) return '***';
    
    // Show first and last 4 characters
    return `${value.slice(0, 4)}...${value.slice(-4)}`;
  }
  
  /**
   * Validate secret format
   */
  validateSecret(secret, type) {
    switch (type) {
      case 'github-token':
        // GitHub tokens start with specific prefixes
        return /^(ghp_|gho_|ghu_|ghs_|ghr_)/.test(secret);
        
      case 'anthropic-key':
        return /^sk-ant-api03-/.test(secret);
        
      case 'openai-key':
        return /^sk-/.test(secret);
        
      default:
        return true;
    }
  }
  
  /**
   * Clear cache (on shutdown)
   */
  clearCache() {
    this.cache.clear();
  }
}

export default CredentialManager;

// Usage
const credManager = new CredentialManager();

// Get secret
const apiKey = await credManager.getSecret('aws:agentic-workflow/api-key');

// Mask in logs
console.log(`Using API key: ${credManager.maskSecret(apiKey)}`);

// Validate
if (!credManager.validateSecret(apiKey, 'anthropic-key')) {
  throw new Error('Invalid API key format');
}

🔄 Token Rotation

1. Automated Rotation Strategy

# .github/workflows/rotate-secrets.yml
name: Rotate Secrets

on:
  schedule:
    - cron: '0 0 1 * *'  # Monthly rotation
  workflow_dispatch:

permissions:
  contents: read
  issues: write

jobs:
  check-expiration:
    name: Check Token Expiration
    runs-on: ubuntu-latest
    outputs:
      needs_rotation: ${{ steps.check.outputs.needs_rotation }}
      
    steps:
      - name: Check PAT Expiration
        id: check
        env:
          GITHUB_TOKEN: ${{ secrets.COPILOT_MCP_GITHUB_PERSONAL_ACCESS_TOKEN }}
        run: |
          # Get token expiration
          EXPIRATION=$(gh api /user \
            -H "Accept: application/vnd.github+json" \
            | jq -r '.token_expires_at // "never"')
          
          if [ "$EXPIRATION" = "never" ]; then
            echo "⚠️ Token has no expiration (not recommended)"
            echo "needs_rotation=false" >> $GITHUB_OUTPUT
            exit 0
          fi
          
          # Calculate days until expiration
          EXPIRE_DATE=$(date -d "$EXPIRATION" +%s)
          NOW=$(date +%s)
          DAYS_LEFT=$(( ($EXPIRE_DATE - $NOW) / 86400 ))
          
          echo "Days until expiration: $DAYS_LEFT"
          
          if [ $DAYS_LEFT -lt 14 ]; then
            echo "⚠️ Token expires in $DAYS_LEFT days - rotation needed"
            echo "needs_rotation=true" >> $GITHUB_OUTPUT
          else
            echo "✅ Token valid for $DAYS_LEFT days"
            echo "needs_rotation=false" >> $GITHUB_OUTPUT
          fi
          
  notify-rotation:
    name: Notify About Rotation
    needs: check-expiration
    if: needs.check-expiration.outputs.needs_rotation == 'true'
    runs-on: ubuntu-latest
    
    steps:
      - name: Create Rotation Issue
        uses: actions/github-script@v7
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            // Check if rotation issue already exists
            const { data: issues } = await github.rest.issues.listForRepo({
              owner: context.repo.owner,
              repo: context.repo.repo,
              state: 'open',
              labels: 'security,credential-rotation',
              per_page: 10
            });
            
            const existingIssue = issues.find(issue => 
              issue.title.includes('Token Rotation Required')
            );
            
            if (existingIssue) {
              console.log('Rotation issue already exists:', existingIssue.number);
              return;
            }
            
            // Create new rotation issue
            await github.rest.issues.create({
              owner: context.repo.owner,
              repo: context.repo.repo,
              title: '🔐 Token Rotation Required',
              body: `## Security Notice
              
The GitHub Personal Access Token used by agentic workflows is expiring soon.

### Action Required
1. Go to [GitHub Settings → Personal Access Tokens](https://github.com/settings/tokens)
2. Generate a new fine-grained token with same permissions
3. Update repository secret: \`COPILOT_MCP_GITHUB_PERSONAL_ACCESS_TOKEN\`
4. Test workflows after rotation
5. Close this issue

### Token Details
- **Current Status**: Expires in < 14 days
- **Used In**: Agentic workflow automations
- **Permissions**: Contents, PRs, Issues, Workflows

---
*Auto-generated rotation reminder*`,
              labels: ['security', 'credential-rotation', 'high-priority'],
              assignees: ['security-team']
            });

2. Zero-Downtime Rotation

// scripts/agents/lib/rotation-handler.js

/**
 * Handle token rotation without downtime
 */
class RotationHandler {
  constructor(options = {}) {
    this.primaryToken = options.primaryToken;
    this.fallbackToken = options.fallbackToken;
    this.rotationState = 'active'; // active, rotating, completed
  }
  
  /**
   * Get active token (with fallback)
   */
  async getToken() {
    try {
      // Try primary token
      await this.validateToken(this.primaryToken);
      return this.primaryToken;
    } catch (error) {
      if (this.fallbackToken) {
        console.warn('Primary token failed, using fallback');
        return this.fallbackToken;
      }
      throw error;
    }
  }
  
  /**
   * Validate token is still valid
   */
  async validateToken(token) {
    const response = await fetch('https://api.github.com/user', {
      headers: {
        'Authorization': `Bearer ${token}`,
        'Accept': 'application/vnd.github+json'
      }
    });
    
    if (!response.ok) {
      throw new Error('Token validation failed');
    }
    
    return true;
  }
  
  /**
   * Perform rotation
   */
  async rotate(newToken) {
    this.rotationState = 'rotating';
    
    try {
      // Validate new token
      await this.validateToken(newToken);
      
      // Set new token as fallback first
      this.fallbackToken = newToken;
      
      // Wait for in-flight operations to complete
      await this.waitForInFlightOperations();
      
      // Switch tokens
      this.primaryToken = newToken;
      this.fallbackToken = null;
      
      this.rotationState = 'completed';
      console.log('✅ Token rotation completed successfully');
    } catch (error) {
      this.rotationState = 'failed';
      console.error('❌ Token rotation failed:', error);
      throw error;
    }
  }
  
  async waitForInFlightOperations() {
    // Wait for pending operations
    await new Promise(resolve => setTimeout(resolve, 5000));
  }
}

export default RotationHandler;

🛡️ Least Privilege Access

1. Granular Permissions

# Minimal permissions for different agent types

# Read-only analysis agent
permissions:
  contents: read
  pull-requests: read

# PR review agent
permissions:
  contents: read
  pull-requests: write

# Issue triage agent
permissions:
  contents: read
  issues: write

# Deployment agent
permissions:
  contents: read
  deployments: write
  statuses: write

# Full automation agent (use sparingly)
permissions:
  contents: write
  pull-requests: write
  issues: write
  workflows: write

2. Permission Validation

// scripts/agents/lib/permission-checker.js

/**
 * Validate agent has required permissions
 */
class PermissionChecker {
  constructor(octokit) {
    this.octokit = octokit;
    this.permissions = null;
  }
  
  /**
   * Get current token permissions
   */
  async getPermissions() {
    if (this.permissions) return this.permissions;
    
    const { data } = await this.octokit.rest.users.getAuthenticated();
    this.permissions = data.permissions || {};
    
    return this.permissions;
  }
  
  /**
   * Check if permission is granted
   */
  async hasPermission(permission, level = 'read') {
    const perms = await this.getPermissions();
    const currentLevel = perms[permission];
    
    if (!currentLevel) return false;
    
    const levels = ['read', 'write', 'admin'];
    const currentIndex = levels.indexOf(currentLevel);
    const requiredIndex = levels.indexOf(level);
    
    return currentIndex >= requiredIndex;
  }
  
  /**
   * Require specific permission (throw if not granted)
   */
  async requirePermission(permission, level = 'read') {
    const has = await this.hasPermission(permission, level);
    
    if (!has) {
      throw new Error(
        `Missing required permission: ${permission}:${level}. ` +
        `Please update workflow permissions.`
      );
    }
  }
  
  /**
   * Check multiple permissions
   */
  async checkPermissions(required) {
    const results = {};
    
    for (const [permission, level] of Object.entries(required)) {
      results[permission] = await this.hasPermission(permission, level);
    }
    
    return results;
  }
}

// Usage
const checker = new PermissionChecker(octokit);

// Check single permission
await checker.requirePermission('pull_requests', 'write');

// Check multiple permissions
const perms = await checker.checkPermissions({
  contents: 'read',
  pull_requests: 'write',
  issues: 'write'
});

if (!perms.pull_requests) {
  console.error('Missing required PR write permission');
}

🔌 MCP Server Authentication

1. GitHub MCP Server

// .github/copilot-mcp.json
{
  "mcpServers": {
    "github": {
      "type": "local",
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-github",
        "--toolsets", "all",
        "--tools", "*"
      ],
      "env": {
        "GITHUB_TOKEN": "${{ secrets.COPILOT_MCP_GITHUB_PERSONAL_ACCESS_TOKEN }}",
        "GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.COPILOT_MCP_GITHUB_PERSONAL_ACCESS_TOKEN }}",
        "GITHUB_OWNER": "Hack23",
        "GITHUB_API_URL": "https://api.githubcopilot.com/mcp/insiders"
      },
      "tools": ["*"]
    }
  }
}

2. Custom MCP Server with Auth

// scripts/mcp-servers/authenticated-mcp.js
import { McpServer } from '@modelcontextprotocol/sdk';
import { createHmac, timingSafeEqual } from 'crypto';

/**
 * Custom MCP server with authentication
 */
class AuthenticatedMCPServer extends McpServer {
  constructor(options = {}) {
    super(options);
    
    this.apiKey = options.apiKey || process.env.MCP_API_KEY;
    this.allowedOrigins = options.allowedOrigins || ['localhost'];
    
    if (!this.apiKey) {
      throw new Error('MCP_API_KEY not configured');
    }
    
    // Add authentication middleware
    this.use(this.authenticateRequest.bind(this));
  }
  
  /**
   * Authenticate incoming requests
   */
  async authenticateRequest(req, res, next) {
    const authHeader = req.headers['authorization'];
    
    if (!authHeader) {
      return res.status(401).json({
        error: 'Missing Authorization header'
      });
    }
    
    const [type, credentials] = authHeader.split(' ');
    
    if (type === 'Bearer') {
      // API key authentication
      if (!this.validateAPIKey(credentials)) {
        return res.status(401).json({
          error: 'Invalid API key'
        });
      }
    } else if (type === 'HMAC') {
      // HMAC signature authentication
      if (!this.validateHMACSignature(req, credentials)) {
        return res.status(401).json({
          error: 'Invalid HMAC signature'
        });
      }
    } else {
      return res.status(401).json({
        error: 'Unsupported authentication type'
      });
    }
    
    next();
  }
  
  /**
   * Validate API key
   */
  validateAPIKey(providedKey) {
    // Use timing-safe comparison
    const expected = Buffer.from(this.apiKey);
    const provided = Buffer.from(providedKey);
    
    if (expected.length !== provided.length) {
      return false;
    }
    
    return timingSafeEqual(expected, provided);
  }
  
  /**
   * Validate HMAC signature
   */
  validateHMACSignature(req, signature) {
    const payload = JSON.stringify(req.body);
    const hmac = createHmac('sha256', this.apiKey);
    hmac.update(payload);
    const expected = hmac.digest('hex');
    
    return timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signature)
    );
  }
  
  /**
   * Generate HMAC signature for client
   */
  static generateSignature(payload, apiKey) {
    const hmac = createHmac('sha256', apiKey);
    hmac.update(JSON.stringify(payload));
    return hmac.digest('hex');
  }
}

export default AuthenticatedMCPServer;

3. MCP Client Authentication

// scripts/agents/lib/mcp-client-auth.js

/**
 * MCP client with authentication
 */
class AuthenticatedMCPClient {
  constructor(options = {}) {
    this.gatewayUrl = options.gatewayUrl || 'http://localhost:3000';
    this.apiKey = options.apiKey || process.env.MCP_API_KEY;
    this.authType = options.authType || 'bearer'; // bearer or hmac
  }
  
  /**
   * Call MCP tool with authentication
   */
  async callTool(toolName, params) {
    const headers = this.getAuthHeaders(params);
    
    const response = await fetch(`${this.gatewayUrl}/tools/${toolName}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        ...headers
      },
      body: JSON.stringify(params)
    });
    
    if (!response.ok) {
      throw new Error(`MCP call failed: ${response.statusText}`);
    }
    
    return response.json();
  }
  
  /**
   * Get authentication headers
   */
  getAuthHeaders(params) {
    if (this.authType === 'bearer') {
      return {
        'Authorization': `Bearer ${this.apiKey}`
      };
    } else if (this.authType === 'hmac') {
      const signature = AuthenticatedMCPServer.generateSignature(params, this.apiKey);
      return {
        'Authorization': `HMAC ${signature}`
      };
    }
    
    throw new Error(`Unknown auth type: ${this.authType}`);
  }
}

export default AuthenticatedMCPClient;

🔑 API Key Management

1. Multi-Provider Key Management

// scripts/agents/lib/api-key-manager.js

/**
 * Manage API keys for multiple providers
 */
class APIKeyManager {
  constructor() {
    this.keys = {
      anthropic: process.env.ANTHROPIC_API_KEY,
      openai: process.env.OPENAI_API_KEY,
      github: process.env.GITHUB_TOKEN,
      riksdag: process.env.RIKSDAG_API_TOKEN
    };
    
    this.usage = new Map();
    this.rateLimits = {
      anthropic: { requests: 50, period: 60000 },  // 50/min
      openai: { requests: 60, period: 60000 },     // 60/min
      github: { requests: 5000, period: 3600000 }  // 5000/hour
    };
  }
  
  /**
   * Get API key for provider
   */
  getKey(provider) {
    const key = this.keys[provider];
    
    if (!key) {
      throw new Error(`API key not configured for provider: ${provider}`);
    }
    
    return key;
  }
  
  /**
   * Check rate limit before API call
   */
  async checkRateLimit(provider) {
    const limit = this.rateLimits[provider];
    if (!limit) return true;
    
    const usage = this.usage.get(provider) || { count: 0, resetAt: Date.now() + limit.period };
    
    // Reset if period expired
    if (Date.now() >= usage.resetAt) {
      usage.count = 0;
      usage.resetAt = Date.now() + limit.period;
    }
    
    // Check limit
    if (usage.count >= limit.requests) {
      const waitTime = usage.resetAt - Date.now();
      throw new Error(
        `Rate limit exceeded for ${provider}. ` +
        `Wait ${Math.ceil(waitTime / 1000)}s before next request.`
      );
    }
    
    // Increment usage
    usage.count++;
    this.usage.set(provider, usage);
    
    return true;
  }
  
  /**
   * Make authenticated API call
   */
  async callAPI(provider, url, options = {}) {
    await this.checkRateLimit(provider);
    
    const key = this.getKey(provider);
    const headers = this.getAuthHeaders(provider, key);
    
    const response = await fetch(url, {
      ...options,
      headers: {
        ...headers,
        ...options.headers
      }
    });
    
    return response;
  }
  
  /**
   * Get auth headers for provider
   */
  getAuthHeaders(provider, key) {
    switch (provider) {
      case 'anthropic':
        return {
          'x-api-key': key,
          'anthropic-version': '2023-06-01'
        };
        
      case 'openai':
        return {
          'Authorization': `Bearer ${key}`
        };
        
      case 'github':
        return {
          'Authorization': `Bearer ${key}`,
          'Accept': 'application/vnd.github+json',
          'X-GitHub-Api-Version': '2022-11-28'
        };
        
      default:
        return {
          'Authorization': `Bearer ${key}`
        };
    }
  }
}

export default APIKeyManager;

🔒 Security Best Practices

1. Secret Scanning

# .github/workflows/secret-scan.yml
name: Secret Scanning

on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  scan:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          
      - name: TruffleHog Secret Scan
        uses: trufflesecurity/trufflehog@main
        with:
          path: ./
          base: ${{ github.event.repository.default_branch }}
          head: HEAD
          extra_args: --debug --only-verified

2. Credential Auditing

// scripts/security/audit-credentials.js

/**
 * Audit credential usage
 */
class CredentialAuditor {
  constructor() {
    this.auditLog = [];
  }
  
  /**
   * Log credential access
   */
  logAccess(credentialName, operation, metadata = {}) {
    const entry = {
      timestamp: new Date().toISOString(),
      credential: credentialName,
      operation,
      agent_id: process.env.AGENT_ID,
      session_id: process.env.GITHUB_RUN_ID,
      ...metadata
    };
    
    this.auditLog.push(entry);
    
    // Log to stdout (captured by workflow)
    console.log(JSON.stringify({
      event_type: 'credential_access',
      ...entry
    }));
  }
  
  /**
   * Export audit log
   */
  exportLog() {
    return {
      audit_period: {
        start: this.auditLog[0]?.timestamp,
        end: this.auditLog[this.auditLog.length - 1]?.timestamp
      },
      total_accesses: this.auditLog.length,
      unique_credentials: new Set(this.auditLog.map(e => e.credential)).size,
      entries: this.auditLog
    };
  }
}

export default CredentialAuditor;

📚 Related Skills

🔗 References

GitHub Documentation

Security Best Practices

✅ Remember Checklist

When managing authentication and credentials:

  • Use fine-grained PATs over classic PATs
  • Set token expiration (90 days maximum)
  • Apply least privilege permissions
  • Store secrets in GitHub Secrets or external vault
  • Never commit credentials to repository
  • Implement token rotation strategy
  • Audit credential access
  • Use environment-specific secrets
  • Validate token permissions before use
  • Mask secrets in logs
  • Enable secret scanning
  • Use short-lived tokens when possible
  • Implement fallback authentication
  • Monitor token expiration
  • Document credential requirements

License: Apache-2.0
Version: 1.0.0
Last Updated: 2026-02-17
Maintained by: Hack23 Organization

Weekly Installs
0
GitHub Stars
2
First Seen
Jan 1, 1970