Authentication
In this step we're going to extend the server implementation so that it can authenticate itself to the Forge platform, and generate access tokens for different use cases.
It is a good practice to generate an "internal" token with more capabilities (for example, allowing you to create or delete files in the Data Management service) that will only be used by the server, and a "public" token with fewer capabilities that can be safely shared with the client-side logic.
Access tokens
- Node.js & VSCode
- .NET 6 & VSCode
- .NET 6 & VS2022
Create a forge
folder under the services
folder, and inside this new folder, create a file
called auth.js
. This is where we will be implementing all the Forge authentication logic that
will be used in different areas of our server application. Let's start by adding the following
code to the file:
const { AuthClientTwoLegged } = require('forge-apis');
const { FORGE_CLIENT_ID, FORGE_CLIENT_SECRET } = require('../../config.js');
let internalAuthClient = new AuthClientTwoLegged(FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, ['bucket:read', 'bucket:create', 'data:read', 'data:write', 'data:create'], true);
let publicAuthClient = new AuthClientTwoLegged(FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, ['viewables:read'], true);
async function getInternalToken() {
if (!internalAuthClient.isAuthorized()) {
await internalAuthClient.authenticate();
}
return internalAuthClient.getCredentials();
}
async function getPublicToken() {
if (!publicAuthClient.isAuthorized()) {
await publicAuthClient.authenticate();
}
return publicAuthClient.getCredentials();
}
module.exports = {
getInternalToken,
getPublicToken
};
The code creates two authentication clients, one for generating tokens for internal use (giving us read/write access to the Data Management buckets and objects), and one for generating tokens for public use (only giving a read access to the translation outputs from the Model Derivative service), and two helper methods to generate the corresponding tokens for us.
Create a ForgeService.cs
file under the Models
subfolder. That is where we will be implementing
all the Forge-specific logic that will be used in different areas of our server application. Let's
start by adding the following code to the file:
public partial class ForgeService
{
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _bucket;
public ForgeService(string clientId, string clientSecret, string bucket = null)
{
_clientId = clientId;
_clientSecret = clientSecret;
_bucket = string.IsNullOrEmpty(bucket) ? string.Format("{0}-basic-app", _clientId.ToLower()) : bucket;
}
}
Notice that the ForgeService
class is declared as partial
. We're going to extend it
in other *.cs
files later. A ForgeService
singleton will then be provided to our server
through ASP.NET's dependency injection.
Then create another file in the same Models
subfolder, and call it ForgeService.Auth.cs
.
Here we will implement all the authentication logic. Populate the file with the following code:
using System;
using System.Threading.Tasks;
using Autodesk.Forge;
public record Token(string AccessToken, DateTime ExpiresAt);
public partial class ForgeService
{
private Token _internalTokenCache;
private Token _publicTokenCache;
private async Task<Token> GetToken(Scope[] scopes)
{
dynamic auth = await new TwoLeggedApi().AuthenticateAsync(_clientId, _clientSecret, "client_credentials", scopes);
return new Token(auth.access_token, DateTime.UtcNow.AddSeconds(auth.expires_in));
}
public async Task<Token> GetPublicToken()
{
if (_publicTokenCache == null || _publicTokenCache.ExpiresAt < DateTime.UtcNow)
_publicTokenCache = await GetToken(new Scope[] { Scope.ViewablesRead });
return _publicTokenCache;
}
private async Task<Token> GetInternalToken()
{
if (_internalTokenCache == null || _internalTokenCache.ExpiresAt < DateTime.UtcNow)
_internalTokenCache = await GetToken(new Scope[] { Scope.BucketCreate, Scope.BucketRead, Scope.DataRead, Scope.DataWrite, Scope.DataCreate });
return _internalTokenCache;
}
}
This part of the ForgeService
class creates two authentication clients, one for internal use
(giving us read/write access to the Data Management buckets and objects), and one for public use
(only allowing access to the translation outputs from the Model Derivative service), and a couple
of methods for generating the corresponding tokens for us.
Next, let's update our Startup.cs
file to make a singleton instance of the ForgeService
class
available to our server application:
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var ForgeClientID = Configuration["FORGE_CLIENT_ID"];
var ForgeClientSecret = Configuration["FORGE_CLIENT_SECRET"];
var ForgeBucket = Configuration["FORGE_BUCKET"]; // Optional
if (string.IsNullOrEmpty(ForgeClientID) || string.IsNullOrEmpty(ForgeClientSecret))
{
throw new ApplicationException("Missing required environment variables FORGE_CLIENT_ID or FORGE_CLIENT_SECRET.");
}
services.AddSingleton<ForgeService>(new ForgeService(ForgeClientID, ForgeClientSecret, ForgeBucket));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Create a ForgeService.cs
file under the Models
subfolder. That is where we will be implementing
all the Forge-specific logic that will be used in different areas of our server application. Let's
start by adding the following code to the file:
public partial class ForgeService
{
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _bucket;
public ForgeService(string clientId, string clientSecret, string bucket = null)
{
_clientId = clientId;
_clientSecret = clientSecret;
_bucket = string.IsNullOrEmpty(bucket) ? string.Format("{0}-basic-app", _clientId.ToLower()) : bucket;
}
}
Notice that the ForgeService
class is declared as partial
. We're going to extend it
in other *.cs
files later. A ForgeService
singleton will then be provided to our server
through ASP.NET's dependency injection.
Then create another file in the same Models
subfolder, and call it ForgeService.Auth.cs
.
Here we will implement all the authentication logic. Populate the file with the following code:
using System;
using System.Threading.Tasks;
using Autodesk.Forge;
public record Token(string AccessToken, DateTime ExpiresAt);
public partial class ForgeService
{
private Token _internalTokenCache;
private Token _publicTokenCache;
private async Task<Token> GetToken(Scope[] scopes)
{
dynamic auth = await new TwoLeggedApi().AuthenticateAsync(_clientId, _clientSecret, "client_credentials", scopes);
return new Token(auth.access_token, DateTime.UtcNow.AddSeconds(auth.expires_in));
}
public async Task<Token> GetPublicToken()
{
if (_publicTokenCache == null || _publicTokenCache.ExpiresAt < DateTime.UtcNow)
_publicTokenCache = await GetToken(new Scope[] { Scope.ViewablesRead });
return _publicTokenCache;
}
private async Task<Token> GetInternalToken()
{
if (_internalTokenCache == null || _internalTokenCache.ExpiresAt < DateTime.UtcNow)
_internalTokenCache = await GetToken(new Scope[] { Scope.BucketCreate, Scope.BucketRead, Scope.DataRead, Scope.DataWrite, Scope.DataCreate });
return _internalTokenCache;
}
}
This part of the ForgeService
class creates two authentication clients, one for internal use
(giving us read/write access to the Data Management buckets and objects), and one for public use
(only allowing access to the translation outputs from the Model Derivative service), and a couple
of methods for generating the corresponding tokens for us.
Next, let's update our Startup.cs
file to make a singleton instance of the ForgeService
class
available to our server application:
using System;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
var ForgeClientID = Configuration["FORGE_CLIENT_ID"];
var ForgeClientSecret = Configuration["FORGE_CLIENT_SECRET"];
var ForgeBucket = Configuration["FORGE_BUCKET"]; // Optional
if (string.IsNullOrEmpty(ForgeClientID) || string.IsNullOrEmpty(ForgeClientSecret))
{
throw new ApplicationException("Missing required environment variables FORGE_CLIENT_ID or FORGE_CLIENT_SECRET.");
}
services.AddSingleton<ForgeService>(new ForgeService(ForgeClientID, ForgeClientSecret, ForgeBucket));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseDefaultFiles();
app.UseStaticFiles();
app.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
}
Server endpoints
Now we can expose this functionality through the first endpoint of our server.
- Node.js & VSCode
- .NET 6 & VSCode
- .NET 6 & VS2022
Create an auth.js
file under the routes
subfolder with the following content:
const express = require('express');
const { getPublicToken } = require('../services/forge/auth.js');
let router = express.Router();
router.get('/token', async function (req, res, next) {
try {
res.json(await getPublicToken());
} catch (err) {
next(err);
}
});
module.exports = router;
Here we implement a new Express Router that will handle
requests coming to our server, with the URL ending with /token
, by generating a public access token
and sending it back to the client as a JSON response.
Let's "mount" the router to our server application by modifying the server.js
:
const express = require('express');
const { PORT } = require('./config.js');
let app = express();
app.use(express.static('wwwroot'));
app.use('/api/auth', require('./routes/auth.js'));
app.listen(PORT, function () { console.log(`Server listening on port ${PORT}...`); });
Since the router has been attached to the /api/auth
prefix, it will now handle all requests
coming to the endpoint /api/auth/token
.
Create an AuthController.cs
file under the Controllers
subfolder with the following content:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
public record AccessToken(string access_token, long expires_in);
private readonly ForgeService _forgeService;
public AuthController(ForgeService forgeService)
{
_forgeService = forgeService;
}
[HttpGet("token")]
public async Task<AccessToken> GetAccessToken()
{
var token = await _forgeService.GetPublicToken();
return new AccessToken(
token.AccessToken,
(long)Math.Round((token.ExpiresAt - DateTime.UtcNow).TotalSeconds)
);
}
}
The controller will receive the instance of ForgeService
through ASP.NET's
Dependency injection,
and it will handle requests to /api/auth/token
by generating a new access token
and sending it back to the client as a JSON payload.
Create an AuthController.cs
file under the Controllers
subfolder with the following content:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
public record AccessToken(string access_token, long expires_in);
private readonly ForgeService _forgeService;
public AuthController(ForgeService forgeService)
{
_forgeService = forgeService;
}
[HttpGet("token")]
public async Task<AccessToken> GetAccessToken()
{
var token = await _forgeService.GetPublicToken();
return new AccessToken(
token.AccessToken,
(long)Math.Round((token.ExpiresAt - DateTime.UtcNow).TotalSeconds)
);
}
}
The controller will receive the instance of ForgeService
through ASP.NET's
Dependency injection,
and it will handle requests to /api/auth/token
by generating a new access token
and sending it back to the client as a JSON payload.
Try it out
Let's see if our new server endpoint works.
- Node.js & VSCode
- .NET 6 & VSCode
- .NET 6 & VS2022
If the application is still running, restart it (for example, using Run > Restart Debugging,
or by clicking the green restart icon), otherwise start it again (using Run > Start Debugging,
or by pressing F5
). When you navigate to http://localhost:8080/api/auth/token
in the browser, the server should now respond with a JSON object containing the access token data.
If the application is still running, restart it (for example, using Run > Restart Debugging,
or by clicking the green restart icon), otherwise start it again (using Run > Start Debugging,
or by pressing F5
). When you navigate to http://localhost:8080/api/auth/token
in the browser, the server should now respond with a JSON object containing the access token data.
If the application is still running, restart it (for example, using Debug > Restart,
or by pressing Ctrl
+Shift
+F5
), otherwise start it again (using Debug > Start Debugging,
or by pressing F5
). When you navigate to http://localhost:8080/api/auth/token
in the browser, the server should now respond with a JSON object containing the access token data.
If you are using Google Chrome, consider installing JSON Formatter or a similar extension to automatically format JSON responses.