/** * @copyright nhcarrigan * @license Naomi's Public License * @author Naomi Carrigan * * Simple local server to authenticate with LinkedIn and obtain a Company Page Access Token. * Run with: node linkedinAuth.js * Make sure to set LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET environment variables. */ import http from "http"; import { URL } from "url"; const PORT = 3001; // Different port from Facebook auth server const REDIRECT_URI = `http://localhost:${PORT}/callback`; /** * Creates the LinkedIn OAuth authorization URL. * @param {string} clientId - The LinkedIn Client ID. * @returns {string} The authorization URL. */ const getAuthUrl = (clientId) => { const params = new URLSearchParams({ client_id: clientId, redirect_uri: REDIRECT_URI, // LinkedIn requires OpenID Connect scopes as base, plus organization permission scope: "openid profile email w_organization_social", response_type: "code", state: "linkedin-auth-state", // CSRF protection }); return `https://www.linkedin.com/oauth/v2/authorization?${params.toString()}`; }; /** * Exchanges an authorization code for an access token. * @param {string} code - The authorization code from LinkedIn. * @param {string} clientId - The LinkedIn Client ID. * @param {string} clientSecret - The LinkedIn Client Secret. * @returns {Promise<{access_token: string, expires_in?: number}>} The access token response. */ const exchangeCodeForToken = async (code, clientId, clientSecret) => { const params = new URLSearchParams({ grant_type: "authorization_code", code: code, redirect_uri: REDIRECT_URI, client_id: clientId, client_secret: clientSecret, }); const response = await fetch("https://www.linkedin.com/oauth/v2/accessToken", { body: params.toString(), headers: { "Content-Type": "application/x-www-form-urlencoded", }, method: "POST", }); return await response.json(); }; /** * Gets the authenticated user's profile information. * @param {string} accessToken - The access token. * @returns {Promise} The user profile. */ const getUserProfile = async (accessToken) => { const response = await fetch( "https://api.linkedin.com/v2/userinfo", { headers: { "Authorization": `Bearer ${accessToken}`, }, }, ); return await response.json(); }; /** * Gets the organizations/companies the user manages. * @param {string} accessToken - The access token. * @returns {Promise} Array of organizations. */ const getUserOrganizations = async (accessToken) => { // First, get the user's profile to get their ID const profile = await getUserProfile(accessToken); if (!profile.sub) { return []; } // Get organizations using the Organization API // Note: This requires the organization to be associated with your app const response = await fetch( `https://api.linkedin.com/v2/organizationalEntityAcls?q=roleAssignee&role=ADMINISTRATOR&state=APPROVED`, { headers: { "Authorization": `Bearer ${accessToken}`, }, }, ); const data = await response.json(); if (data.elements && data.elements.length > 0) { // Get organization details for each const orgDetails = []; for (const element of data.elements) { const orgId = element.organizationalTarget?.split(":")[1]; if (orgId) { try { const orgResponse = await fetch( `https://api.linkedin.com/v2/organizations/${orgId}`, { headers: { "Authorization": `Bearer ${accessToken}`, }, }, ); const orgData = await orgResponse.json(); orgDetails.push({ id: orgId, name: orgData.localizedName || orgData.name || `Organization ${orgId}`, accessToken: accessToken, // Same token works for organization }); } catch (error) { // Skip if we can't get org details console.error(`Failed to get org details for ${orgId}:`, error); } } } return orgDetails; } return []; }; /** * Sends an HTML response. * @param {http.ServerResponse} res - The HTTP response object. * @param {number} statusCode - The HTTP status code. * @param {string} html - The HTML content to send. */ const sendHtml = (res, statusCode, html) => { res.writeHead(statusCode, { "Content-Type": "text/html" }); res.end(html); }; const clientId = process.env.LINKEDIN_CLIENT_ID; const clientSecret = process.env.LINKEDIN_CLIENT_SECRET; if (!clientId || !clientSecret) { console.error( "Error: LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET environment variables must be set.", ); console.error( "Example: LINKEDIN_CLIENT_ID=your_client_id LINKEDIN_CLIENT_SECRET=your_secret node linkedinAuth.js", ); process.exit(1); } const server = http.createServer(async (req, res) => { const url = new URL(req.url, `http://localhost:${PORT}`); // Root route - show auth link if (url.pathname === "/") { const authUrl = getAuthUrl(clientId); const html = ` LinkedIn Company Page Token Generator

šŸ” LinkedIn Company Page Token Generator

Click the button below to authenticate with LinkedIn and get your Company Page Access Token.

Authenticate with LinkedIn
Note: Make sure you're an administrator of the LinkedIn Company Page you want to post to.
āš ļø Important: Your LinkedIn app must be associated with the Company Page. This requires:
  • The Company Page super admin must approve the app association
  • Your app must have "Sign In with LinkedIn using OpenID Connect" enabled in Products
  • The w_organization_social permission requires App Review approval
  • Business verification may be required

Note: If you get an invalid_scope_error, make sure OpenID Connect is enabled in your app settings.

`; return sendHtml(res, 200, html); } // Callback route - handle OAuth callback if (url.pathname === "/callback") { const code = url.searchParams.get("code"); const error = url.searchParams.get("error"); const errorDescription = url.searchParams.get("error_description"); if (error) { const html = ` Authentication Error

āŒ Authentication Error

Error: ${error}

${errorDescription || ""}

Try again

`; return sendHtml(res, 400, html); } if (!code) { return sendHtml( res, 400, "

Error

No authorization code received.

Try again", ); } try { // Step 1: Exchange code for access token const tokenResponse = await exchangeCodeForToken(code, clientId, clientSecret); if (tokenResponse.error) { throw new Error( tokenResponse.error_description || tokenResponse.error || "Failed to exchange code for token", ); } const accessToken = tokenResponse.access_token; const expiresIn = tokenResponse.expires_in; // Step 2: Get user's organizations const organizations = await getUserOrganizations(accessToken); if (organizations.length === 0) { return sendHtml( res, 200, ` No Organizations Found

āš ļø No Organizations Found

You don't have administrator access to any LinkedIn Company Pages, or your app isn't associated with any pages.

Troubleshooting:

  • Make sure you're an administrator of the Company Page
  • Ensure your LinkedIn app is associated with the Company Page (requires super admin approval)
  • Check that your app has been approved for the w_organization_social permission
  • Verify your app is in Live mode if required

Try again

`, ); } // Display results const orgsHtml = organizations .map( (org) => `

${org.name}

Organization ID: ${org.id}

Access Token:

Expires in: ${expiresIn ? `${Math.floor(expiresIn / 86400)} days` : "Check token expiration"}

`, ) .join(""); const html = ` Success! Your Organization Tokens

āœ… Success!

Your Organization Access Tokens:

Copy these tokens and add them to your environment variables. Use the Access Token for the organization you want to post to.

${orgsHtml}

āš ļø Important:

  • Store these tokens securely (like your other API credentials)
  • LinkedIn access tokens typically expire after 60 days
  • Add the token to your environment variables as LINKEDIN_ACCESS_TOKEN
  • You'll also need the Organization ID as LINKEDIN_ORG_ID
  • Make sure your app is associated with the Company Page before posting

Start over

`; return sendHtml(res, 200, html); } catch (error) { const html = ` Error

āŒ Error

Error: ${error.message}

Try again

`; return sendHtml(res, 500, html); } } // 404 sendHtml(res, 404, "

Not Found

Go home

"); }); server.listen(PORT, () => { console.log(`\nšŸš€ LinkedIn Auth Server running at http://localhost:${PORT}`); console.log(`\nšŸ“‹ Make sure you've set:`); console.log(` - LINKEDIN_CLIENT_ID`); console.log(` - LINKEDIN_CLIENT_SECRET`); console.log(`\nšŸ”— Open http://localhost:${PORT} in your browser to start!\n`); });