Authentication
In this step we're going to extend the server implementation so that it can authenticate itself to the Forge platform, guide the user through a 3-legged OAuth workflow, and generate access tokens for various needs.
It is a good practice to generate an "internal" token with more capabilities (for example, allowing the owner 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 { AuthClientThreeLegged, UserProfileApi } = require('forge-apis');
const { FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, FORGE_CALLBACK_URL, INTERNAL_TOKEN_SCOPES, PUBLIC_TOKEN_SCOPES } = require('../../config.js');
const internalAuthClient = new AuthClientThreeLegged(FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, FORGE_CALLBACK_URL, INTERNAL_TOKEN_SCOPES);
const publicAuthClient = new AuthClientThreeLegged(FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, FORGE_CALLBACK_URL, PUBLIC_TOKEN_SCOPES);
function getAuthorizationUrl() {
return internalAuthClient.generateAuthUrl();
}
async function authCallbackMiddleware(req, res, next) {
const internalCredentials = await internalAuthClient.getToken(req.query.code);
const publicCredentials = await publicAuthClient.refreshToken(internalCredentials);
req.session.public_token = publicCredentials.access_token;
req.session.internal_token = internalCredentials.access_token;
req.session.refresh_token = publicCredentials.refresh_token;
req.session.expires_at = Date.now() + internalCredentials.expires_in * 1000;
next();
}
async function authRefreshMiddleware(req, res, next) {
const { refresh_token, expires_at } = req.session;
if (!refresh_token) {
res.status(401).end();
return;
}
if (expires_at < Date.now()) {
const internalCredentials = await internalAuthClient.refreshToken({ refresh_token });
const publicCredentials = await publicAuthClient.refreshToken(internalCredentials);
req.session.public_token = publicCredentials.access_token;
req.session.internal_token = internalCredentials.access_token;
req.session.refresh_token = publicCredentials.refresh_token;
req.session.expires_at = Date.now() + internalCredentials.expires_in * 1000;
}
req.internalOAuthToken = {
access_token: req.session.internal_token,
expires_in: Math.round((req.session.expires_at - Date.now()) / 1000)
};
req.publicOAuthToken = {
access_token: req.session.public_token,
expires_in: Math.round((req.session.expires_at - Date.now()) / 1000)
};
next();
}
async function getUserProfile(token) {
const resp = await new UserProfileApi().getUserProfile(internalAuthClient, token);
return resp.body;
}
module.exports = {
internalAuthClient,
getAuthorizationUrl,
authCallbackMiddleware,
authRefreshMiddleware,
getUserProfile
};
The code provides a couple of helper functions:
- the
getAuthorizationUrl
function generates a URL for our users to be redirected to when initiating the 3-legged authentication workflow - the
authCallbackMiddleware
function can be used as an Express.js middleware when the user logs in successfully and is redirected back to our application - the
authRefreshMiddleware
function is then used as an Express.js middleware for all requests that will need to make use of the Forge access tokens - the
getUserProfile
function returns additional details about the authenticated user based on an existing access token
Create a ForgeService.cs
file under the Models
subfolder. This is where we will be implementing
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:
using System;
using Autodesk.Forge;
public class Tokens
{
public string InternalToken;
public string PublicToken;
public string RefreshToken;
public DateTime ExpiresAt;
}
public partial class ForgeService
{
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _callbackUri;
private readonly Scope[] InternalTokenScopes = new Scope[] { Scope.DataRead, Scope.ViewablesRead };
private readonly Scope[] PublicTokenScopes = new Scope[] { Scope.ViewablesRead };
public ForgeService(string clientId, string clientSecret, string callbackUri)
{
_clientId = clientId;
_clientSecret = clientSecret;
_callbackUri = callbackUri;
}
}
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.
Next, let's create a ForgeService.Auth.cs
file under the Models
subfolder with the following code:
using System;
using System.Threading.Tasks;
using Autodesk.Forge;
public partial class ForgeService
{
public string GetAuthorizationURL()
{
return new ThreeLeggedApi().Authorize(_clientId, "code", _callbackUri, InternalTokenScopes);
}
public async Task<Tokens> GenerateTokens(string code)
{
dynamic internalAuth = await new ThreeLeggedApi().GettokenAsync(_clientId, _clientSecret, "authorization_code", code, _callbackUri);
dynamic publicAuth = await new ThreeLeggedApi().RefreshtokenAsync(_clientId, _clientSecret, "refresh_token", internalAuth.refresh_token, PublicTokenScopes);
return new Tokens
{
PublicToken = publicAuth.access_token,
InternalToken = internalAuth.access_token,
RefreshToken = publicAuth.refresh_token,
ExpiresAt = DateTime.Now.ToUniversalTime().AddSeconds(internalAuth.expires_in)
};
}
public async Task<Tokens> RefreshTokens(Tokens tokens)
{
dynamic internalAuth = await new ThreeLeggedApi().RefreshtokenAsync(_clientId, _clientSecret, "refresh_token", tokens.RefreshToken, InternalTokenScopes);
dynamic publicAuth = await new ThreeLeggedApi().RefreshtokenAsync(_clientId, _clientSecret, "refresh_token", internalAuth.refresh_token, PublicTokenScopes);
return new Tokens
{
PublicToken = publicAuth.access_token,
InternalToken = internalAuth.access_token,
RefreshToken = publicAuth.refresh_token,
ExpiresAt = DateTime.Now.ToUniversalTime().AddSeconds(internalAuth.expires_in)
};
}
public async Task<dynamic> GetUserProfile(Tokens tokens)
{
var api = new UserProfileApi();
api.Configuration.AccessToken = tokens.InternalToken;
dynamic profile = await api.GetUserProfileAsync();
return profile;
}
}
These helper methods will later be used in our server's controllers to handle various types of requests related to authentication, for example, redirecting the user to the Autodesk login page, processing the callback when the user gets redirected back to our application, or refreshing tokens that have expired.
Finally, 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 ForgeCallbackURL = Configuration["FORGE_CALLBACK_URL"];
if (string.IsNullOrEmpty(ForgeClientID) || string.IsNullOrEmpty(ForgeClientSecret) || string.IsNullOrEmpty(ForgeCallbackURL))
{
throw new ApplicationException("Missing required environment variables FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, or FORGE_CALLBACK_URL.");
}
services.AddSingleton<ForgeService>(new ForgeService(ForgeClientID, ForgeClientSecret, ForgeCallbackURL));
}
// 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. This is where we will be implementing
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:
using System;
using Autodesk.Forge;
public class Tokens
{
public string InternalToken;
public string PublicToken;
public string RefreshToken;
public DateTime ExpiresAt;
}
public partial class ForgeService
{
private readonly string _clientId;
private readonly string _clientSecret;
private readonly string _callbackUri;
private readonly Scope[] InternalTokenScopes = new Scope[] { Scope.DataRead, Scope.ViewablesRead };
private readonly Scope[] PublicTokenScopes = new Scope[] { Scope.ViewablesRead };
public ForgeService(string clientId, string clientSecret, string callbackUri)
{
_clientId = clientId;
_clientSecret = clientSecret;
_callbackUri = callbackUri;
}
}
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.
Next, let's create a ForgeService.Auth.cs
file under the Models
subfolder with the following code:
using System;
using System.Threading.Tasks;
using Autodesk.Forge;
public partial class ForgeService
{
public string GetAuthorizationURL()
{
return new ThreeLeggedApi().Authorize(_clientId, "code", _callbackUri, InternalTokenScopes);
}
public async Task<Tokens> GenerateTokens(string code)
{
dynamic internalAuth = await new ThreeLeggedApi().GettokenAsync(_clientId, _clientSecret, "authorization_code", code, _callbackUri);
dynamic publicAuth = await new ThreeLeggedApi().RefreshtokenAsync(_clientId, _clientSecret, "refresh_token", internalAuth.refresh_token, PublicTokenScopes);
return new Tokens
{
PublicToken = publicAuth.access_token,
InternalToken = internalAuth.access_token,
RefreshToken = publicAuth.refresh_token,
ExpiresAt = DateTime.Now.ToUniversalTime().AddSeconds(internalAuth.expires_in)
};
}
public async Task<Tokens> RefreshTokens(Tokens tokens)
{
dynamic internalAuth = await new ThreeLeggedApi().RefreshtokenAsync(_clientId, _clientSecret, "refresh_token", tokens.RefreshToken, InternalTokenScopes);
dynamic publicAuth = await new ThreeLeggedApi().RefreshtokenAsync(_clientId, _clientSecret, "refresh_token", internalAuth.refresh_token, PublicTokenScopes);
return new Tokens
{
PublicToken = publicAuth.access_token,
InternalToken = internalAuth.access_token,
RefreshToken = publicAuth.refresh_token,
ExpiresAt = DateTime.Now.ToUniversalTime().AddSeconds(internalAuth.expires_in)
};
}
public async Task<dynamic> GetUserProfile(Tokens tokens)
{
var api = new UserProfileApi();
api.Configuration.AccessToken = tokens.InternalToken;
dynamic profile = await api.GetUserProfileAsync();
return profile;
}
}
These helper methods will later be used in our server's controllers to handle various types of requests related to authentication, for example, redirecting the user to the Autodesk login page, processing the callback when the user gets redirected back to our application, or refreshing tokens that have expired.
Finally, 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 ForgeCallbackURL = Configuration["FORGE_CALLBACK_URL"];
if (string.IsNullOrEmpty(ForgeClientID) || string.IsNullOrEmpty(ForgeClientSecret) || string.IsNullOrEmpty(ForgeCallbackURL))
{
throw new ApplicationException("Missing required environment variables FORGE_CLIENT_ID, FORGE_CLIENT_SECRET, or FORGE_CALLBACK_URL.");
}
services.AddSingleton<ForgeService>(new ForgeService(ForgeClientID, ForgeClientSecret, ForgeCallbackURL));
}
// 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 let's expose this functionality via a collection of endpoints in 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 { getAuthorizationUrl, authCallbackMiddleware, authRefreshMiddleware, getUserProfile } = require('../services/forge/auth.js');
let router = express.Router();
router.get('/login', function (req, res) {
res.redirect(getAuthorizationUrl());
});
router.get('/logout', function (req, res) {
req.session = null;
res.redirect('/');
});
router.get('/callback', authCallbackMiddleware, function (req, res) {
res.redirect('/');
});
router.get('/token', authRefreshMiddleware, function (req, res) {
res.json(req.publicOAuthToken);
});
router.get('/profile', authRefreshMiddleware, async function (req, res, next) {
try {
const profile = await getUserProfile(req.internalOAuthToken);
res.json({ name: `${profile.firstName} ${profile.lastName}` });
} catch (err) {
next(err);
}
});
module.exports = router;
Here we implement a new Express.js router that
will handle all the authentication-related endpoints. Let's "mount" the router to our server
application by modifying server.js
:
const express = require('express');
const session = require('cookie-session');
const { PORT, SERVER_SESSION_SECRET } = require('./config.js');
let app = express();
app.use(express.static('wwwroot'));
app.use(session({ secret: SERVER_SESSION_SECRET, maxAge: 24 * 60 * 60 * 1000 }));
app.use('/api/auth', require('./routes/auth.js'));
app.listen(PORT, () => console.log(`Server listening on port ${PORT}...`));
Since we've attached the router to the /api/auth
prefix, the router will now handle the following
requests:
GET /api/auth/login
will redirect the user to the Autodesk login pageGET /api/auth/callback
is the URL our user will be redirected to after logging in successfully, and it is where we're going to generate a new set of tokens for the userGET /api/auth/logout
will remove any cookie-based session data for the given user, effectively logging the user out of our applicationGET /api/auth/token
will generate a public access token that will later be used by Forge Viewer to load our designsGET /api/auth/profile
will return a simple JSON with additional information about the logged in user
Create an AuthController.cs
file under the Controllers
subfolder with the following content:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly ILogger<AuthController> _logger;
private readonly ForgeService _forgeService;
public AuthController(ILogger<AuthController> logger, ForgeService forgeService)
{
_logger = logger;
_forgeService = forgeService;
}
public static async Task<Tokens> PrepareTokens(HttpRequest request, HttpResponse response, ForgeService forgeService)
{
if (!request.Cookies.ContainsKey("internal_token"))
{
return null;
}
var tokens = new Tokens
{
PublicToken = request.Cookies["public_token"],
InternalToken = request.Cookies["internal_token"],
RefreshToken = request.Cookies["refresh_token"],
ExpiresAt = DateTime.Parse(request.Cookies["expires_at"])
};
if (tokens.ExpiresAt < DateTime.Now.ToUniversalTime())
{
tokens = await forgeService.RefreshTokens(tokens);
response.Cookies.Append("public_token", tokens.PublicToken);
response.Cookies.Append("internal_token", tokens.InternalToken);
response.Cookies.Append("refresh_token", tokens.RefreshToken);
response.Cookies.Append("expires_at", tokens.ExpiresAt.ToString());
}
return tokens;
}
[HttpGet("login")]
public ActionResult Login()
{
var redirectUri = _forgeService.GetAuthorizationURL();
return Redirect(redirectUri);
}
[HttpGet("logout")]
public ActionResult Logout()
{
Response.Cookies.Delete("public_token");
Response.Cookies.Delete("internal_token");
Response.Cookies.Delete("refresh_token");
Response.Cookies.Delete("expires_at");
return Redirect("/");
}
[HttpGet("callback")]
public async Task<ActionResult> Callback(string code)
{
var tokens = await _forgeService.GenerateTokens(code);
Response.Cookies.Append("public_token", tokens.PublicToken);
Response.Cookies.Append("internal_token", tokens.InternalToken);
Response.Cookies.Append("refresh_token", tokens.RefreshToken);
Response.Cookies.Append("expires_at", tokens.ExpiresAt.ToString());
return Redirect("/");
}
[HttpGet("profile")]
public async Task<dynamic> GetProfile()
{
var tokens = await PrepareTokens(Request, Response, _forgeService);
if (tokens == null)
{
return Unauthorized();
}
dynamic profile = await _forgeService.GetUserProfile(tokens);
return new
{
name = string.Format("{0} {1}", profile.firstName, profile.lastName)
};
}
[HttpGet("token")]
public async Task<dynamic> GetPublicToken()
{
var tokens = await PrepareTokens(Request, Response, _forgeService);
if (tokens == null)
{
return Unauthorized();
}
return new
{
access_token = tokens.PublicToken,
token_type = "Bearer",
expires_in = Math.Floor((tokens.ExpiresAt - DateTime.Now.ToUniversalTime()).TotalSeconds)
};
}
}
The controller handles several different endpoints:
GET /api/auth/login
will redirect the user to the Autodesk login pageGET /api/auth/callback
is the URL our user will be redirected to after logging in successfully, and it is where we're going to generate a new set of tokens for the userGET /api/auth/logout
will remove any cookie-based session data for the given user, effectively logging the user out of our applicationGET /api/auth/token
will generate a public access token that will later be used by Forge Viewer to load our designsGET /api/auth/profile
will return a simple JSON with additional information about the logged in user
Create an AuthController.cs
file under the Controllers
subfolder with the following content:
using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
[ApiController]
[Route("api/[controller]")]
public class AuthController : ControllerBase
{
private readonly ILogger<AuthController> _logger;
private readonly ForgeService _forgeService;
public AuthController(ILogger<AuthController> logger, ForgeService forgeService)
{
_logger = logger;
_forgeService = forgeService;
}
public static async Task<Tokens> PrepareTokens(HttpRequest request, HttpResponse response, ForgeService forgeService)
{
if (!request.Cookies.ContainsKey("internal_token"))
{
return null;
}
var tokens = new Tokens
{
PublicToken = request.Cookies["public_token"],
InternalToken = request.Cookies["internal_token"],
RefreshToken = request.Cookies["refresh_token"],
ExpiresAt = DateTime.Parse(request.Cookies["expires_at"])
};
if (tokens.ExpiresAt < DateTime.Now.ToUniversalTime())
{
tokens = await forgeService.RefreshTokens(tokens);
response.Cookies.Append("public_token", tokens.PublicToken);
response.Cookies.Append("internal_token", tokens.InternalToken);
response.Cookies.Append("refresh_token", tokens.RefreshToken);
response.Cookies.Append("expires_at", tokens.ExpiresAt.ToString());
}
return tokens;
}
[HttpGet("login")]
public ActionResult Login()
{
var redirectUri = _forgeService.GetAuthorizationURL();
return Redirect(redirectUri);
}
[HttpGet("logout")]
public ActionResult Logout()
{
Response.Cookies.Delete("public_token");
Response.Cookies.Delete("internal_token");
Response.Cookies.Delete("refresh_token");
Response.Cookies.Delete("expires_at");
return Redirect("/");
}
[HttpGet("callback")]
public async Task<ActionResult> Callback(string code)
{
var tokens = await _forgeService.GenerateTokens(code);
Response.Cookies.Append("public_token", tokens.PublicToken);
Response.Cookies.Append("internal_token", tokens.InternalToken);
Response.Cookies.Append("refresh_token", tokens.RefreshToken);
Response.Cookies.Append("expires_at", tokens.ExpiresAt.ToString());
return Redirect("/");
}
[HttpGet("profile")]
public async Task<dynamic> GetProfile()
{
var tokens = await PrepareTokens(Request, Response, _forgeService);
if (tokens == null)
{
return Unauthorized();
}
dynamic profile = await _forgeService.GetUserProfile(tokens);
return new
{
name = string.Format("{0} {1}", profile.firstName, profile.lastName)
};
}
[HttpGet("token")]
public async Task<dynamic> GetPublicToken()
{
var tokens = await PrepareTokens(Request, Response, _forgeService);
if (tokens == null)
{
return Unauthorized();
}
return new
{
access_token = tokens.PublicToken,
token_type = "Bearer",
expires_in = Math.Floor((tokens.ExpiresAt - DateTime.Now.ToUniversalTime()).TotalSeconds)
};
}
}
The controller handles several different endpoints:
GET /api/auth/login
will redirect the user to the Autodesk login pageGET /api/auth/callback
is the URL our user will be redirected to after logging in successfully, and it is where we're going to generate a new set of tokens for the userGET /api/auth/logout
will remove any cookie-based session data for the given user, effectively logging the user out of our applicationGET /api/auth/token
will generate a public access token that will later be used by Forge Viewer to load our designsGET /api/auth/profile
will return a simple JSON with additional information about the logged in user
Try it out
- 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/login
in the browser, you should be redirected to Autodesk login page, and after logging in,
you should be redirected back to your application, for now simply showing Cannot GET /
.
This is expected as we haven't implemented the GET /
endpoint yet. However, if you use
browser dev tools and explore the cookies stored by your browser for the localhost
origin,
you'll notice that the application is already storing the authentication data there.
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/login
in the browser, you should be redirected to Autodesk login page, and after logging in, you should
be redirected back to your application, for now simply returning 404. This is expected as we haven't
implemented the GET /
endpoint yet. However, if you use browser dev tools and explore the cookies
stored by your browser for the localhost
origin, you'll notice that the application is already
storing the authentication data there.
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/login
in the browser, you should be redirected to Autodesk login page, and after logging in, you should
be redirected back to your application, for now simply returning 404. This is expected as we haven't
implemented the GET /
endpoint yet. However, if you use browser dev tools and explore the cookies
stored by your browser for the localhost
origin, you'll notice that the application is already
storing the authentication data there.