Autodesk Forge is now Autodesk Platform Services

24 Feb 2017

Securing your Forge Viewer token behind a proxy

Default blog image

This post illustrates how to strengthen security of your Forge Viewing Application by using a proxy to avoid passing a token to the client JavaScript code.

The issue 

In order for the viewer to get access to the viewable resources on the Autodesk Cloud and load the models on the client page, it needs to add a bearer token to the requests headers.

To make this possible, the basic approach is to expose a route on your server that returns the token, your client code will then use that route to request a token whenever the viewer needs it. Those steps are described in the basic viewer step-by-step tutorial.

The minimal scope that is required at the moment is "data:read", however this scope also allows to perform GET requests on OSS buckets and objects. It would be relatively trivial for an attacker to analyze the source code of your page or monitor the http traffic, intercept the token and the URN of the model and by base-64-decoding it, he can harvest the seed files stored on that bucket, i.e downloading original CAD files.

 

The workarounds

There are two efficient ways to prevent this from happening:

1/ A Forge Platform solution:

There are internal discussions going on the moment in order to secure that workflow. One possibility would be to provide a "token:read" scope that only allows the viewer to download viewable resources but prevents accessing seed files on OSS. There are other possibilities being evaluated by the development team, but it is too early to share those topics publicly

 

2/ A third party solution: 

Using a proxy on the server application that will securely append the token to the viewer requests, hence protecting it from being exposed to the client at all. This is a solution that third-party applications can implement straight away without waiting implementation on the Forge Platform, so I will expose it today in that post.

The impact on your existing server/client code should be minimal, here is how to proceed:

 

Server Side

I am today exposing the Node.js code to achieve that, later we will show you how to implement that in an ASP.Net application, but the principle remains the same.

Add an additional route to your app that will handle all viewer GET requests:

import LMVProxy from './api/endpoints/lmv-proxy' 
 
var app = express() 
 
app.get('/lmv-proxy/*', LMVProxy.get) 
 
// ... initialize other routes ... 

The code for the proxy route is exposed below:

/////////////////////////////////////////////////////////////////
// Forge Viewer proxy
// By Philippe Leefsma, February 2017
//
/////////////////////////////////////////////////////////////////
import ServiceManager from '../services/SvcManager'
import https from 'https'
import path from 'path'
/////////////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////////////
const EXTENSIONS = {
gzip: [ '.json.gz', '.bin', '.pack' ],
json: [ '.json.gz', '.json' ]
}
const WHITE_LIST = [
'if-modified-since',
'if-none-match',
'accept-encoding',
'x-ads-acm-namespace', // Forge Data Management API
'x-ads-acm-check-groups' // Forge Data Management API
]
/////////////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////////////
function fixContentHeaders (req, res) {
// DS does not return content-encoding header
// for gzip and other files that we know are gzipped,
// so we add it here. The viewer does want
// gzip files uncompressed by the browser
if ( EXTENSIONS.gzip.indexOf (path.extname (req.path)) > -1 ) {
res.set ('content-encoding', 'gzip')
}
if ( EXTENSIONS.json.indexOf (path.extname (req.path)) > -1 ){
res.set ('content-type', 'application/json')
}
}
/////////////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////////////
function setCORSHeaders (res) {
res.set('access-control-allow-origin', '*')
res.set('access-control-allow-credentials', false)
res.set('access-control-allow-headers',
"Origin, X-Requested-With, Content-Type, Accept")
}
/////////////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////////////
function proxyClientHeaders (clientHeaders, upstreamHeaders) {
WHITE_LIST.forEach(h => {
const hval = clientHeaders[h]
if (hval) {
upstreamHeaders[h] = hval
}
})
// fix for OSS issue not accepting the
// etag surrounded with double quotes...
const etag = upstreamHeaders['if-none-match']
if (etag) {
if(etag[0] === '"' && etag[etag.length - 1] === '"') {
upstreamHeaders['if-none-match'] =
etag.substring(1, etag.length - 1);
}
}
}
/////////////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////////////
function Proxy(endpoint, authHeaders) {
this.authHeaders = authHeaders
this.endpoint = endpoint
}
/////////////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////////////
Proxy.prototype.request = function(req, res, url) {
const options = {
host: this.endpoint,
port: 443,
path: url,
method: 'GET', //only proxy GET
headers: this.authHeaders
}
proxyClientHeaders(req.headers, options.headers)
const creq = https.request(options, (cres) => {
// set encoding
//cres.setEncoding('utf8');
for (let h in cres.headers) {
res.set(h, cres.headers[h])
}
fixContentHeaders(req, res)
setCORSHeaders(res)
res.writeHead(cres.statusCode)
cres.pipe(res)
cres.on('error', (e) => {
// we got an error,
// return error 500 to client and log error
debug.error(e.message)
res.end()
})
})
creq.end()
}
/////////////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////////////
function proxyGet (req, res) {
const forgeSvc = ServiceManager.getService(
'ForgeSvc')
forgeSvc.get2LeggedToken().then((token) => {
const url = req.url.replace (/^\/lmv\-proxy/gm, '')
const endpoint = 'developer.api.autodesk.com'
const authHeaders = {
Authorization: `Bearer ${token.access_token}`
}
const proxy = new Proxy(endpoint, authHeaders)
proxy.request(req, res, url)
})
}
/////////////////////////////////////////////////////////////////
//
//
/////////////////////////////////////////////////////////////////
exports.get = proxyGet
view raw lmv-proxy.js hosted with ❤ by GitHub

 

Client Side

A new API has been added in the viewer version 2.13 and will slightly change in version 2.14. Basically you can remove the getAccessToken field in the Initializer options and specify the proxy route the viewer has to use for its GET requests:

const options = { 
  env: 'AutodeskProduction' 
  //getAccessToken: ... //not needed, token provided by proxy on server-side 
) 
 
Autodesk.Viewing.Initializer (options, () => { 
   
  //2.13 
  Autodesk.Viewing.setApiEndpoint( 
    window.location.origin + '/lmv-proxy') 
 
  //2.14 - not yet avail in PROD 
  //Autodesk.Viewing.setEndpointAndApi( 
  //  window.location.origin + '/lmv-proxy', 'modelDerivativeV2') 
 
  // instantiate viewer and load model as before ... 
})

Et voilà, you can now enjoy secure viewing! Complete examples using that approach are available at forge-react-boiler.nodejs and forge-rcdb.nodejs

Related Article