Skip to content

User Authentication with APS (3-Legged)

Accessing Autodesk Data as a Specific User

Chapter 3 authenticated as your application. Every API call used an app-level token. Your server saw what the application was provisioned to see. 3-legged authentication puts the user into the flow: the user logs in with their Autodesk account, and API calls are made on their behalf with their own permissions.

2-Legged vs 3-Legged

2-Legged (Chapter 3)3-Legged (This Chapter)
Who authenticatesYour applicationA specific user
Credentials usedClient ID + SecretClient ID + Secret + User login
SeesEverything your app is provisioned forEverything that user is permitted to see
Token typeApplication tokenUser access token
Use caseServer-to-server pipelinesUser-specific data, personalised tools

The 3-legged flow has four steps:

1. Your server redirects the user to Autodesk's login page
2. The user logs in at autodesk.com
3. Autodesk redirects back with a short-lived authorisation code
4. Your server exchanges the code for a user access token

Once the token is stored, any MCP tool can use it to call the Autodesk API as that user.

Credentials

The Traditional Web App you created in the Prerequisites supports both 2-legged and 3-legged OAuth, so no new application is needed. The callback URL (http://localhost:3001/auth/callback) was already configured during setup. We reuse the same APS_CLIENT_ID and APS_CLIENT_SECRET from Chapter 3 — your .env file for Node.js, your User Secrets store for .NET — for both flows.

Add 3-Legged OAuth to the APS Server

In Node.js, edit aps-server.js. In .NET, edit ApsServer.cs. Both gain a place to hold the user token, a new get_user_info tool, and two HTTP routes for the OAuth flow.

Section 1 – Capture the user access token

javascript
// After the existing environment-variable check at the top of the file:
const REDIRECT_URI = "http://localhost:3001/auth/callback";

// User access token - null until the user completes the 3-legged login
let userAccessToken = null;
csharp
public sealed class UserTokenProvider
{
    private const string RedirectUri = "http://localhost:3001/auth/callback";

    private readonly AuthenticationClient _authClient;
    private readonly string _clientId;
    private readonly string _clientSecret;
    private string? _userAccessToken;

    public UserTokenProvider(AuthenticationClient authClient, IConfiguration config)
    {
        _authClient = authClient;
        _clientId = config["APS_CLIENT_ID"]
            ?? throw new InvalidOperationException("Missing APS_CLIENT_ID in environment.");
        _clientSecret = config["APS_CLIENT_SECRET"]
            ?? throw new InvalidOperationException("Missing APS_CLIENT_SECRET in environment.");
    }

    public string? CurrentToken => _userAccessToken;

    public async Task ExchangeCodeAsync(string code, CancellationToken cancellationToken = default)
    {
        var token = await _authClient.GetThreeLeggedTokenAsync(
            _clientId,
            code,
            RedirectUri,
            clientSecret: _clientSecret);

        _userAccessToken = token.AccessToken;
        Console.WriteLine("User authenticated via 3-legged OAuth.");
    }
}
  • Node.js — A module-level userAccessToken variable is the cache; tools read it directly.
  • .NETUserTokenProvider mirrors ApsTokenProvider. The user token is set when the OAuth callback exchanges the auth code, and read by the get_user_info tool through DI.

Register the new provider (and HttpClient, used by get_user_info) inside Main, alongside the existing services. Node.js needs no extra wiring — the closure handles it.

csharp
builder.Services.AddHttpClient();
builder.Services.AddSingleton<UserTokenProvider>();

You'll also need an extra using at the top of ApsServer.cs for the Authorization header used by the new tool:

csharp
using System.Net.Http.Headers;

⚠️ Workshop limitation: the user token is cached server-wide — one user's token shared globally. This is fine for a single-user workshop, but in production you must scope tokens by session ID so each user has their own. See Production Checklist for details.

Section 2 – Tool: get_user_info

We add one new tool:

  • get_user_info: returns the Autodesk profile (name, email, and Autodesk ID) of the currently logged-in user. If no user has completed the OAuth flow yet, it returns a message telling the user to open the login URL in their browser.

Register the profile tool — inside createServer() after the existing tools for Node.js, or inside ApsTools for .NET:

javascript
// Tool 3: get_user_info
server.registerTool(
  "get_user_info",
  {
    description:
      "Returns the Autodesk profile of the currently logged-in user. " +
      "The user must first authenticate by visiting http://localhost:3001/auth/login in their browser.",
  },
  async () => {
    if (!userAccessToken) {
      return {
        content: [
          {
            type: "text",
            text: "Not authenticated. Ask the user to open http://localhost:3001/auth/login in their browser to log in with Autodesk.",
          },
        ],
      };
    }

    const response = await fetch(
      "https://api.userprofile.autodesk.com/userinfo",
      {
        headers: { Authorization: `Bearer ${userAccessToken}` },
      },
    );

    if (!response.ok) {
      return {
        content: [{ type: "text", text: `Error: ${response.status}` }],
      };
    }

    const user = await response.json();
    return {
      content: [
        {
          type: "text",
          text: `Name: ${user.name}\nEmail: ${user.email}\nAutodesk ID: ${user.sub}`,
        },
      ],
    };
  },
);
csharp
[McpServerTool(Name = "get_user_info"),
 Description("Returns the Autodesk profile of the currently logged-in user. The user must first authenticate by visiting http://localhost:3001/auth/login in their browser.")]
public static async Task<string> GetUserInfo(UserTokenProvider users, HttpClient http)
{
    if (users.CurrentToken is null)
        return "Not authenticated. Ask the user to open http://localhost:3001/auth/login in their browser to log in with Autodesk.";

    using var request = new HttpRequestMessage(HttpMethod.Get, "https://developer.api.autodesk.com/userinfo");
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", users.CurrentToken);

    using var response = await http.SendAsync(request);
    if (!response.IsSuccessStatusCode)
        return $"Error: {(int)response.StatusCode}";

    await using var stream = await response.Content.ReadAsStreamAsync();
    using var doc = await JsonDocument.ParseAsync(stream);
    var root = doc.RootElement;
    var name = root.TryGetProperty("name", out var n) ? n.GetString() : null;
    var email = root.TryGetProperty("email", out var e) ? e.GetString() : null;
    var sub = root.TryGetProperty("sub", out var s) ? s.GetString() : null;
    return $"Name: {name}\nEmail: {email}\nAutodesk ID: {sub}";
}

Note: The /userinfo endpoint follows the OpenID Connect UserInfo standard. See the OIDC UserInfo reference for full details.

Section 3 – Add auth routes to the HTTP server

The updated server handles three routes:

  • /auth/login: redirects the user's browser to Autodesk's OAuth authorisation page, passing the client ID, callback URL, and requested scopes.
  • /auth/callback: receives the short-lived authorisation code from Autodesk after the user logs in, exchanges it for a user access token, and stores it.
  • /mcp: the existing MCP endpoint, unchanged.

Replace the existing HTTP server block — http.createServer(...) plus httpServer.listen(...) for Node.js, or the section after app.MapMcp("/mcp") for .NET — with one that handles all three:

javascript
const httpServer = http.createServer(async (req, res) => {
  // Route 1: kick off 3-legged login
  if (req.url === "/auth/login") {
    const params = new URLSearchParams({
      response_type: "code",
      client_id: APS_CLIENT_ID,
      redirect_uri: REDIRECT_URI,
      scope: "data:read",
    });
    res.writeHead(302, {
      Location: `https://developer.api.autodesk.com/authentication/v2/authorize?${params}`,
    });
    res.end();
    return;
  }

  // Route 2: receive the OAuth callback, exchange the code for a user token
  if (req.url?.startsWith("/auth/callback")) {
    const url = new URL(req.url, "http://localhost:3001");
    const code = url.searchParams.get("code");

    if (!code) {
      res.writeHead(400).end("Missing authorization code.");
      return;
    }

    try {
      const tokenData = await authClient.getThreeLeggedToken(
        APS_CLIENT_ID,
        code,
        REDIRECT_URI,
        { clientSecret: APS_CLIENT_SECRET },
      );
      userAccessToken = tokenData.access_token;
      console.log("User authenticated via 3-legged OAuth.");

      res
        .writeHead(200, { "Content-Type": "text/html" })
        .end(
          "<h1>Login successful!</h1><p>Close this tab and return to VS Code.</p>",
        );
    } catch (err) {
      res.writeHead(500).end(`Token exchange failed: ${err.message}`);
    }
    return;
  }

  // Route 3: MCP endpoint
  if (req.url !== "/mcp") {
    res.writeHead(404).end("Not found");
    return;
  }

  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: undefined,
  });
  res.on("close", () => transport.close());
  const server = createServer();
  await server.connect(transport);
  await transport.handleRequest(req, res);
});

httpServer.listen(3001, () => {
  console.log("APS MCP server running at http://localhost:3001/mcp");
  console.log("Login at http://localhost:3001/auth/login");
});
csharp
app.MapGet("/auth/login", (IConfiguration cfg) =>
{
    var clientId = cfg["APS_CLIENT_ID"]
        ?? throw new InvalidOperationException("Missing APS_CLIENT_ID in environment.");
    var query = new Dictionary<string, string?>
    {
        ["response_type"] = "code",
        ["client_id"] = clientId,
        ["redirect_uri"] = "http://localhost:3001/auth/callback",
        ["scope"] = "data:read",
    };
    var url = Microsoft.AspNetCore.WebUtilities.QueryHelpers.AddQueryString(
        "https://developer.api.autodesk.com/authentication/v2/authorize", query);
    return Results.Redirect(url);
});

app.MapGet("/auth/callback", async (string? code, UserTokenProvider users) =>
{
    if (code is null)
        return Results.BadRequest("Missing authorization code.");

    try
    {
        await users.ExchangeCodeAsync(code, default);
        return Results.Content(
            "<h1>Login successful!</h1><p>Close this tab and return to VS Code.</p>",
            "text/html");
    }
    catch (Exception ex)
    {
        return Results.Content($"Token exchange failed: {ex.Message}", "text/plain", null, 500);
    }
});

Console.WriteLine("APS MCP server running at http://localhost:3001/mcp");
Console.WriteLine("Login at http://localhost:3001/auth/login");
  • Node.js — A single http.createServer callback dispatches all three routes by req.url. The MCP transport is created fresh per request, as in Chapter 3.
  • .NETapp.MapGet(...) registers each route declaratively on the ASP.NET Core pipeline. The MCP endpoint mounted with app.MapMcp("/mcp") already coexists with these routes — no manual dispatch needed.

View complete aps-server.js in Source Code → · View complete ApsServer.cs in Source Code →

Log In and Test

Start the servers

bash
# Terminal 1 - APS server (now also handles OAuth routes)
node aps-server.js

# Terminal 2 - workshop server
node server.js
bash
# Terminal 1 - APS server (uses the StartupObject set in DevconWorkshop.csproj)
dotnet run

# Terminal 2 - workshop server (override StartupObject to run Server.cs)
dotnet run -p:StartupObject=DevconWorkshop.Server

Log in with Autodesk

Open a browser and go to:

http://localhost:3001/auth/login

You are redirected to the Autodesk login page. Sign in with your Autodesk account. After a successful login, Autodesk calls your callback URL, your server exchanges the code for a user token, and the browser shows:

Login successful! Close this tab and return to VS Code.

Your terminal prints:

User authenticated via 3-legged OAuth.

Ask VS Code Copilot

Run MCP: List Servers → Restart in the Command Palette. Then open Copilot Chat in Agent mode and ask:

Use `#get_user_info` to show my Autodesk profile.

Copilot calls get_user_info on the APS server directly, which fetches the Autodesk /userinfo endpoint with the stored user token.

Expected output:

Name: Your Name
Email: you@company.com
Autodesk ID: AAAAAAAABBBBBBBB0000000

What Changes in Production

The warning above about the globally cached user token is the most critical item. A production system also needs:

  • Token refresh: the access token expires after 1 hour; use the refresh_token returned alongside it to request a new one without forcing a re-login
  • HTTPS callback URL: Autodesk requires HTTPS for production callback URLs (https://yourdomain.com/auth/callback)

DevCon MCP Workshop