15 Jun 2025
Proxying APS Viewer

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
- Node.js - https://github.com/yiskang/aps-viewer-proxy
- .NET - https://github.com/yiskang/aps-utility-service-dotnet
If you have any questions or feedback, please don't hesitate to contact us through our APS support channel.