22 Jun 2017
Securing your Forge Viewer token behind a proxy (.NET)
First, take a look at this blog post for NodeJS apps, sections "The issue" and "The workarounds", which explains why we need this. Note that we recently released a viewables:read scope, that improves the workaround, but not yet an optimal solution.
But how should we implement this for ASP.NET webapps?
Regardless how your app was implemented, the WebApi approach can be used (or you can integrate on a MVC). The following class is the core piece. Note the AccessToken property needs to be implemented according to your project design. In summary, for each request, it makes the same request on Autodesk Forge URL adding the Authorization header.
public class ViewerProxyController : ApiController
{
private const string FORGE_BASE_URL = "https://developer.api.autodesk.com";
private const string PROXY_ROUTE = "api/forge/viewerproxy/";
private string AccessToken
{
get
{
// how are you keeping the access_token?
// this piece will vary
return your_access_token;
}
}
// HttpClient has been designed to be re-used for multiple calls.
// Even across multiple threads.
// https://stackoverflow.com/a/22561368/4838205
private static HttpClient _httpClient;
[HttpGet]
[Route(PROXY_ROUTE + "{*.}")]
public async Task<HttpResponseMessage> Get()
{
if (_httpClient == null)
{
_httpClient = new HttpClient(
// this should avoid HttpClient seaching for proxy settings
new HttpClientHandler()
{
UseProxy = false,
Proxy = null
}, true);
_httpClient.BaseAddress = new Uri(FORGE_BASE_URL);
ServicePointManager.DefaultConnectionLimit = int.MaxValue;
}
string url = Request.RequestUri.AbsolutePath.Replace(PROXY_ROUTE, string.Empty);
string resourceUrl = url + Request.RequestUri.Query;
try
{
HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, resourceUrl);
// add our Access Token
request.Headers.Add("Authorization", "Bearer " + AccessToken);
HttpResponseMessage response = await _httpClient.SendAsync(request,
// this ResponseHeadersRead force the SendAsync to return
// as soon as the header is ready, faster
HttpCompletionOption.ResponseHeadersRead);
return response;
}
catch
{
return new HttpResponseMessage(System.Net.HttpStatusCode.InternalServerError);
}
}
}
An ApiController must also be registered during Global.asax Application_Start (but you may already have it):
public class Global : System.Web.HttpApplication
{
protected void Application_Start(object sender, EventArgs e)
{
GlobalConfiguration.Configure(WebApiConfig.Register);
}
}
Where the WebApiConfig for the above should be something like:
public class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes();
}
}
Done! The above should take care of the proxy implementation.
But will not work just yet. Why?
The .NET engine security will not accept requests with parameters containing special chars, like % and . (dot), which are used when viewer3d.js library requests a resource from Forge. So we need a few changes on web.config. Now I would like to thank Andriiv Litvinov, who helped me debug this piece, and suggested to use runAllManagedModulesForAllRequests, which tells the .NET engine to ignore the . (dot) on the query string and run the module regardless, but as described here, this can waste resources. Below is the optimized version:
<configuration>
<system.web>
<httpRuntime requestPathInvalidCharacters="<,>,*,%,&,\,?"/>
</system.web>
<system.webServer>
<!--<modules runAllManagedModulesForAllRequests="true"/>-->
<modules>
<remove name="UrlRoutingModule-4.0" />
<add name="UrlRoutingModule-4.0" type="System.Web.Routing.UrlRoutingModule"
preCondition="" />
</modules>
</system.webServer>
</configuration>
Finally, on the JavaScript client while initializing the Viewer, you should have something like the following (if using the basic tutorial):
Autodesk.Viewing.Initializer(options, function onInitialized() {
// add this setApiEndpoint to enable our custom Proxy
Autodesk.Viewing.setApiEndpoint(window.location.origin + '/api/forge/viewerproxy', '', true);
viewerApp = new Autodesk.Viewing.ViewingApplication('MyViewerDiv');
viewerApp.registerViewer(viewerApp.k3D, Autodesk.Viewing.Private.GuiViewer3D);
viewerApp.loadDocument(documentId, onDocumentLoadSuccess, onDocumentLoadFailure);
});
All done!
And a question tips & tricks: if you have enabled a verbose output to debug HttpClient request, this code will run extremely slow. If you see this problem, please review this reply.