15 Jun 2025

Proxying APS Viewer

Default blog image

A few years ago, we discussed how to proxy the viewer to hide the APS access token from the frontend/client side in this blog post by my colleague Petr: Proxying Forge Viewer. However, the solution we proposed was specific to SVF models, so it doesn't support SVF2 models. But don't worry. Now, we've got you covered. Here comes a solution to support both SVF and SVF2.

With the new solution, the options are passing to Autodesk.Viewing.Initializer will become the following one:

Autodesk.Viewing.Initializer({
    env: "AutodeskProduction2",
    api: "streamingV2",
    useCookie: false,
    shouldInitializeAuth: false,
    endpoint: "<custom url the viewer will use to request each derivative's data>",
});

Regarding the endpoint, we set it to http://localhost:3000/proxy for example. The viewer will request each viewable's manifest and assets from http://localhost:3000/proxy/derivativeservice/v2/... instead of the usual https://cdn.derivative.autodesk.com/derivativeservice/v2/... 

On the server side, you can then intercept requests to any URL starting with the proxy path (in our example, /proxy), run your custom auth checks, and finally make the request to https://cdn.derivative.autodesk.com as usual. Here we use a third-party package `http-proxy-middleware` to enable this example to support proxying WebSocket more easily in Node.js as follows:

// ...

import { createProxyMiddleware } from 'http-proxy-middleware';

const DS_HOST = 'https://cdn.derivative.autodesk.com';

const proxyOptions = {
    target: DS_HOST, // target host
    changeOrigin: true,
    pathRewrite: {
        '^/proxy': ''
    },
    proxyTimeout: 600 * 1000, // this is matching to tandem proxy server
    onProxyReq: function (proxyReq, req, res) {
        // console.debug(`headers: ${req.rawHeaders}`);
        console.debug(`  path: ${proxyReq.path}`);
        // add custom header to request
        const token = authToken;

        if (token) {
            proxyReq.setHeader('Authorization', `Bearer ${token.access_token}`)
        }
    },
    onProxyReqWs: function (proxyReq, req, socket, options, head) {
        // console.debug(`headers: ${req.rawHeaders}`);
        // add custom header to request
        const token = authToken;

        if (token) {
            proxyReq.setHeader('Authorization', `Bearer ${token.access_token}`)
        }
    },
    logger: console,
    ws: true
};

const proxy = createProxyMiddleware(proxyOptions);

app.use('/proxy', checkAuthTokenMiddleware, async (req, res, next) => {
    if (!authToken) {
        const token = await createToken('viewables:read');

        authToken = token;
    }
    next();
}, proxy);

 

Additionally, except for Node.js version, we also have a similar one in .NET 8 this time. The backend logic is identical, just here we use another third-party package `AspNetCore.Proxy` to build the Web Proxy service in .NET, which also supports proxying WebSocket.

public ProxyController(ApsTokenService tokenService, IOptions<ApsServiceOptions> apsOpts)
{
    this.tokenService = tokenService;
    this.apsProxyConfig = apsOpts.Value;
    this.httpProxyOptions = HttpProxyOptionsBuilder.Instance
                    .WithBeforeSend((context, message) =>
                    {
                        var token = this.tokenService.Token;

                        // Set something that is needed for the downstream endpoint.
                        message.Headers.Add("X-Forwarded-Host", context.Request.Host.Host);
                        message.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token.AccessToken);
                        context.Response.Headers.Remove("Content-Type");
                        context.Response.Headers.Append("Content-Type", "application/json; chartset=utf-8");

                        return Task.CompletedTask;
                    })
                    .WithHandleFailure(async (context, exception) =>
                    {
                        // Return a custom error response.
                        context.Response.StatusCode = 403;
                        var result = new
                        {
                            message = "Request cannot be proxied",
                            reason = exception.ToString()
                        };
                        await context.Response.WriteAsync(JsonConvert.SerializeObject(result));
                    }).Build();

    this.wsProxyOptions = WsProxyOptionsBuilder.Instance
        .WithBeforeConnect((context, wso) =>
        {
            var token = this.tokenService.Token;
            wso.SetRequestHeader("X-Forwarded-Host", context.Request.Host.Host);
            wso.SetRequestHeader("Authorization", new AuthenticationHeaderValue("Bearer", token.AccessToken).ToString());

            return Task.CompletedTask;
        })
        .WithHandleFailure(async (context, exception) =>
        {
            context.Response.StatusCode = 599;
            var result = new
            {
                message = "Request cannot be proxied",
                reason = exception.ToString()
            };
            await context.Response.WriteAsync(JsonConvert.SerializeObject(result));
        }).Build();
}

[Route("{**rest}")]
public Task ProxyCatchAll(string rest)
{
    var apsConfig = this.apsProxyConfig;

    HostString host = rest.Contains("manifest") && !rest.Contains("modeldata") ? apsConfig.Host : apsConfig.DerivativeHost;
    var apsURL = UriHelper.BuildAbsolute(apsConfig.Scheme, host);

    var queries = this.Request.QueryString;
    if (queries.HasValue)
    {
        return this.HttpProxyAsync($"{apsURL}{rest}{queries.Value}", this.httpProxyOptions);
    }
    return this.HttpProxyAsync($"{apsURL}{rest}", this.httpProxyOptions);
}

[Route("cdnws")]
public Task ProxyWs()
{
    var apsConfig = this.apsProxyConfig;
    var path = this.Request.Path;

    var request = this.Request;

    var hostUrl = request.Host.ToUriComponent();
    var pathBase = request.PathBase.ToUriComponent();

    HostString host = apsConfig.DerivativeHost;

    var pathResult = path.ToString().Split('/');

    string rest = String.Join('/', pathResult.Take(3));
    rest = path.ToString().Replace(rest, "");
    var apsWsURL = UriHelper.BuildAbsolute(apsConfig.SchemeWs, host, rest);

    var queries = this.Request.QueryString;
    if (queries.HasValue)
    {
        return this.WsProxyAsync($"{apsWsURL}{queries.Value}", this.wsProxyOptions);
    }
    return this.WsProxyAsync(apsWsURL, this.wsProxyOptions);
}

 

The complete, working version of this sample can be found in

 

If you have any questions or feedback, please don't hesitate to contact us through our APS support channel. 

Related Article