Social Media Credentials Guide

This document covers the credentials required for automated social media publishing and how to securely store them.

Overview

Platform Auth Method Required Secrets
LinkedIn OAuth 2.0 LINKEDIN_ACCESS_TOKEN, LINKEDIN_ORG_ID
X/Twitter OAuth 2.0 TWITTER_API_KEY, TWITTER_API_SECRET, TWITTER_ACCESS_TOKEN, TWITTER_ACCESS_SECRET
Bluesky App Password BLUESKY_HANDLE, BLUESKY_APP_PASSWORD
Mastodon OAuth 2.0 MASTODON_INSTANCE, MASTODON_ACCESS_TOKEN
Buffer API Key BUFFER_ACCESS_TOKEN

LinkedIn

Credentials Required

Secret Name Description Example
LINKEDIN_ACCESS_TOKEN OAuth 2.0 access token AQV...xyz
LINKEDIN_ORG_ID Organization/Company ID (for company posts) 12345678
LINKEDIN_PERSON_URN Personal URN (for personal posts) urn:li:person:abc123

How to Obtain

  1. Create LinkedIn App:
    • Go to LinkedIn Developers
    • Create a new app under your company page
    • Request access to: w_member_social, r_liteprofile, r_organization_social
  2. Get OAuth Token:
    # OAuth 2.0 Authorization URL
    https://www.linkedin.com/oauth/v2/authorization?
      response_type=code&
      client_id=YOUR_CLIENT_ID&
      redirect_uri=YOUR_REDIRECT_URI&
      scope=w_member_social%20r_liteprofile
    
    # Exchange code for token
    curl -X POST https://www.linkedin.com/oauth/v2/accessToken \
      -d "grant_type=authorization_code" \
      -d "code=YOUR_AUTH_CODE" \
      -d "redirect_uri=YOUR_REDIRECT_URI" \
      -d "client_id=YOUR_CLIENT_ID" \
      -d "client_secret=YOUR_CLIENT_SECRET"
    
  3. Find Organization ID:
    • Go to your Company Page on LinkedIn
    • URL format: linkedin.com/company/12345678
    • The number is your Organization ID

Token Refresh

LinkedIn tokens expire after 60 days. Set up a refresh workflow:

# .github/workflows/refresh-linkedin-token.yml
name: Refresh LinkedIn Token
on:
  schedule:
    - cron: '0 0 1,15 * *'  # 1st and 15th of each month
jobs:
  refresh:
    runs-on: ubuntu-latest
    steps:
      - name: Refresh Token
        run: |
          # Implement token refresh logic
          # Store new token in GitHub Secrets via API

API Usage

# Post to personal profile
curl -X POST https://api.linkedin.com/v2/ugcPosts \
  -H "Authorization: Bearer $LINKEDIN_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "author": "urn:li:person:YOUR_PERSON_URN",
    "lifecycleState": "PUBLISHED",
    "specificContent": {
      "com.linkedin.ugc.ShareContent": {
        "shareCommentary": { "text": "Your post content" },
        "shareMediaCategory": "NONE"
      }
    },
    "visibility": { "com.linkedin.ugc.MemberNetworkVisibility": "PUBLIC" }
  }'

X/Twitter

Credentials Required

Secret Name Description
TWITTER_API_KEY API Key (Consumer Key)
TWITTER_API_SECRET API Secret (Consumer Secret)
TWITTER_ACCESS_TOKEN User Access Token
TWITTER_ACCESS_SECRET User Access Token Secret
TWITTER_BEARER_TOKEN Bearer Token (for read-only)

How to Obtain

  1. Apply for Developer Account:
  2. Create Project & App:
    • Create a new Project
    • Create an App within the Project
    • Set App permissions to “Read and Write”
  3. Generate Tokens:
    • In App settings → Keys and Tokens
    • Generate API Key and Secret
    • Generate Access Token and Secret
    • Copy Bearer Token

Rate Limits

Endpoint Limit
POST tweets 200/15 min (user), 300/15 min (app)
Media upload 615 uploads/15 min

API Usage (v2)

# Post a tweet
curl -X POST "https://api.twitter.com/2/tweets" \
  -H "Authorization: Bearer $TWITTER_BEARER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"text": "Your tweet content"}'

Bluesky

Credentials Required

Secret Name Description Example
BLUESKY_HANDLE Your Bluesky handle yourname.bsky.social
BLUESKY_APP_PASSWORD App-specific password xxxx-xxxx-xxxx-xxxx

How to Obtain

  1. Log into Bluesky:
    • Go to Settings → App Passwords
    • Click “Add App Password”
    • Name it (e.g., “GitHub Actions”)
    • Copy the generated password
  2. Note: App passwords are safer than your main password and can be revoked individually.

API Usage

# Create session
SESSION=$(curl -s -X POST https://bsky.social/xrpc/com.atproto.server.createSession \
  -H "Content-Type: application/json" \
  -d "{\"identifier\": \"$BLUESKY_HANDLE\", \"password\": \"$BLUESKY_APP_PASSWORD\"}")

ACCESS_JWT=$(echo $SESSION | jq -r '.accessJwt')
DID=$(echo $SESSION | jq -r '.did')

# Create post
curl -X POST https://bsky.social/xrpc/com.atproto.repo.createRecord \
  -H "Authorization: Bearer $ACCESS_JWT" \
  -H "Content-Type: application/json" \
  -d "{
    \"repo\": \"$DID\",
    \"collection\": \"app.bsky.feed.post\",
    \"record\": {
      \"\$type\": \"app.bsky.feed.post\",
      \"text\": \"Your post content\",
      \"createdAt\": \"$(date -u +%Y-%m-%dT%H:%M:%SZ)\"
    }
  }"

Mastodon

Credentials Required

Secret Name Description Example
MASTODON_INSTANCE Your Mastodon instance URL https://mastodon.social
MASTODON_ACCESS_TOKEN OAuth access token abc123...

How to Obtain

  1. Create Application:
    • Go to Preferences → Development → New Application
    • Name: “GitHub Actions Publisher”
    • Scopes: read, write, write:statuses
    • Save
  2. Get Access Token:
    • After creating, click your app name
    • Copy “Your access token”

API Usage

# Post a status
curl -X POST "$MASTODON_INSTANCE/api/v1/statuses" \
  -H "Authorization: Bearer $MASTODON_ACCESS_TOKEN" \
  -F "status=Your post content" \
  -F "visibility=public"

Buffer (Aggregator Option)

Buffer allows posting to multiple platforms from a single API.

Credentials Required

Secret Name Description
BUFFER_ACCESS_TOKEN OAuth access token

How to Obtain

  1. Create Buffer Account: buffer.com
  2. Connect Social Accounts: Link LinkedIn, X, etc.
  3. Create Access Token:

API Usage

# Create a post
curl -X POST "https://api.bufferapp.com/1/updates/create.json" \
  -d "access_token=$BUFFER_ACCESS_TOKEN" \
  -d "profile_ids[]=YOUR_PROFILE_ID" \
  -d "text=Your post content" \
  -d "scheduled_at=$(date -d '+1 hour' +%s)"

Storing Secrets in GitHub

Repository Secrets

  1. Go to Repository → Settings → Secrets and variables → Actions
  2. Click “New repository secret”
  3. Add each credential with its name and value
Repository Settings
└── Secrets and variables
    └── Actions
        ├── LINKEDIN_ACCESS_TOKEN
        ├── LINKEDIN_ORG_ID
        ├── TWITTER_API_KEY
        ├── TWITTER_API_SECRET
        ├── TWITTER_ACCESS_TOKEN
        ├── TWITTER_ACCESS_SECRET
        ├── BLUESKY_HANDLE
        ├── BLUESKY_APP_PASSWORD
        ├── MASTODON_INSTANCE
        └── MASTODON_ACCESS_TOKEN

For better security, use environments with protection rules:

  1. Go to Settings → Environments → New environment
  2. Create production environment
  3. Add secrets to environment (not repository)
  4. Enable required reviewers for production deployments
# Workflow using environment
jobs:
  publish:
    runs-on: ubuntu-latest
    environment: production  # Requires approval
    steps:
      - name: Post to LinkedIn
        env:
          LINKEDIN_TOKEN: $
        run: |
          # Use the secret

Organization Secrets

For multi-repo use, store at organization level:

  1. Go to Organization → Settings → Secrets and variables → Actions
  2. Add secrets with repository access policies

Security Best Practices

Do

Don’t

Token Rotation Schedule

Platform Token Lifetime Rotation Frequency
LinkedIn 60 days Every 45 days
X/Twitter Never expires Every 90 days (recommended)
Bluesky Never expires Every 90 days (recommended)
Mastodon Never expires Every 90 days (recommended)

Workflow Integration Example

# .github/workflows/publish-social.yml
name: Publish Social Posts

on:
  workflow_dispatch:
    inputs:
      platform:
        description: 'Platform to publish to'
        required: true
        type: choice
        options:
          - linkedin
          - twitter
          - bluesky
          - mastodon
          - all

jobs:
  publish:
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4

      - name: Publish to LinkedIn
        if: inputs.platform == 'linkedin' || inputs.platform == 'all'
        env:
          LINKEDIN_ACCESS_TOKEN: $
          LINKEDIN_ORG_ID: $
        run: |
          # LinkedIn publishing logic

      - name: Publish to X/Twitter
        if: inputs.platform == 'twitter' || inputs.platform == 'all'
        env:
          TWITTER_API_KEY: $
          TWITTER_API_SECRET: $
          TWITTER_ACCESS_TOKEN: $
          TWITTER_ACCESS_SECRET: $
        run: |
          # Twitter publishing logic

      - name: Publish to Bluesky
        if: inputs.platform == 'bluesky' || inputs.platform == 'all'
        env:
          BLUESKY_HANDLE: $
          BLUESKY_APP_PASSWORD: $
        run: |
          # Bluesky publishing logic

      - name: Publish to Mastodon
        if: inputs.platform == 'mastodon' || inputs.platform == 'all'
        env:
          MASTODON_INSTANCE: $
          MASTODON_ACCESS_TOKEN: $
        run: |
          # Mastodon publishing logic

Troubleshooting

Common Issues

Issue Cause Solution
401 Unauthorized Expired token Refresh or regenerate token
403 Forbidden Insufficient permissions Check app scopes
429 Too Many Requests Rate limit exceeded Add delays between posts
Token not found Secret name mismatch Verify secret names match workflow

Debugging Tips

# Add debug output (remove in production)
- name: Debug
  run: |
    echo "Token length: ${#LINKEDIN_ACCESS_TOKEN}"
    # Never echo the actual token!