.NET Code Sample

.NET sample app for integrating with Business NXT using OAuth flow. Clone or create ASP.NET app, setup configs, run to authenticate.

If you’re using .NET (3.1/5/6) to build your application that integrates with Business NXT, you can set it up as described below to perform the oauth flow.

Note

The code for the application described here is available on GitHub at GraphQLSamples/MvcCode.

This application is a modified version of the MvcCode sample from the IdentityServer project.

Getting started

You can either clone the MvcCode sample repository or create an Asp.Net application from scratch. The sample application described here has the following structure:

Project structure

Application setup

The Program.cs source file has the following content:

using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;

namespace MvcCode
{
   public class Program
   {
      public static void Main(string[] args)
      {
         CreateHostBuilder(args).Build().Run();
      }

      public static IHostBuilder CreateHostBuilder(string[] args) =>
          Host.CreateDefaultBuilder(args)
              .ConfigureWebHostDefaults(webBuilder =>
              {
                 webBuilder.UseStartup<Startup>();
              });
   }
}

The Setup.cs source file contains the following:

using IdentityModel;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.IdentityModel.Tokens;
using System.IdentityModel.Tokens.Jwt;
using System.Net.Http;
using IdentityModel.Client;
using Microsoft.IdentityModel.Logging;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;

namespace MvcCode
{
   public class Startup
   {
      public Startup(IConfiguration configuration)
      {
         Configuration = configuration;
      }

      public IConfiguration Configuration { get; }

      public void ConfigureServices(IServiceCollection services)
      {
         JwtSecurityTokenHandler.DefaultMapInboundClaims = false;

         services.AddControllersWithViews();

         services.AddHttpClient();

         services.AddOptions();

         services.Configure<AppSettings>(Configuration);

         services.AddSingleton<IDiscoveryCache>(r =>
         {
            var factory = r.GetRequiredService<IHttpClientFactory>();
            return new DiscoveryCache(Configuration.GetValue<string>("Authority"), 
                                      () => factory.CreateClient());
         });

         services.AddAuthentication(options =>
         {
            options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = "oidc";
         })
         .AddCookie(options =>
         {
            options.Cookie.Name = "mvccode";
         })
         .AddOpenIdConnect("oidc", options =>
         {
            options.Events.OnTokenResponseReceived = (tokenResponse) =>
            {
               var accessToken = tokenResponse.TokenEndpointResponse.AccessToken;
               return Task.CompletedTask;
            };
            options.Events.OnRemoteFailure = (err) =>
            {
               return Task.CompletedTask;
            };
            options.Events.OnMessageReceived = (msg) =>
            {
               return Task.CompletedTask;
            };

            options.Authority = Configuration.GetValue<string>("Authority");
            options.RequireHttpsMetadata = false;            

            options.ClientId = Configuration.GetValue<string>("ClientId");
            options.ClientSecret = Configuration.GetValue<string>("ClientSecret");

            options.ResponseType = "code id_token";
            options.UsePkce = true;

            options.Scope.Clear();
            options.Scope.Add("openid");
            options.Scope.Add("profile");
            options.Scope.Add("email");
            options.Scope.Add("business-graphql-api:access-group-based");
            options.Scope.Add("offline_access");

            options.GetClaimsFromUserInfoEndpoint = true;
            options.SaveTokens = true;

            options.TokenValidationParameters = new TokenValidationParameters
            {
               NameClaimType = JwtClaimTypes.Name,
               RoleClaimType = JwtClaimTypes.Role,
            };
         });         
      }

      public void Configure(IApplicationBuilder app)
      {
         IdentityModelEventSource.ShowPII = true;

         app.UseDeveloperExceptionPage();
         app.UseHttpsRedirection();
         app.UseStaticFiles();

         app.UseRouting();

         app.UseAuthentication();
         app.UseAuthorization();

         app.UseEndpoints(endpoints =>
         {
            endpoints.MapDefaultControllerRoute()
                   .RequireAuthorization();
         });
      }
   }
}

From this snippet, you should notice the following:

  • Authority, client ID, and client secret are read from the application settings.
  • The response type is code id_token.
  • The requested scopes are openid, profile, email, business-graphql-api:access-group-based, and offline_access. The latter is only needed when offline access is required.

Application Settings

The JSON file with the application settings (appsettings.json) looks as follows:

{
   "ClientId": "...",
   "ClientSecret": "...",
   "Authority": "https://connect.visma.com",
   "SampleApi": "https://localhost:5005/"
}

The client ID is the one you specified when you registered your application with the Visma Developer Portal. The client secret is a value generated in the Credentials tab of the application propertys in the Visma Developer Portal.

Note

Make sure you do not upload the content of this file to a public repository such as on GitHub.

The AppSettings.cs file contains the following class definition used to help with accessing the settings:

namespace MvcCode
{
   public class AppSettings
   {
      public string ClientId { get; set; }
      public string ClientSecret { get; set; }  
      public string Authority { get; set; }
      public string SampleApi { get; set; }  
   }
}

Controllers

The HomeController class is implemented as follows:

using System;
using System.Globalization;
using System.Net.Http;
using System.Threading.Tasks;
using IdentityModel.Client;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
using Newtonsoft.Json.Linq;

namespace MvcCode.Controllers
{
   public class HomeController : Controller
   {
      private readonly IHttpClientFactory _httpClientFactory;
      private readonly IDiscoveryCache _discoveryCache;
      private readonly AppSettings _authSettings;

       public HomeController(IHttpClientFactory httpClientFactory, IDiscoveryCache discoveryCache, IOptions<AppSettings> authSettings)
      {
         _httpClientFactory = httpClientFactory;
         _discoveryCache = discoveryCache;
         _authSettings = authSettings.Value;
      }

      [AllowAnonymous]
      public IActionResult Index() => View();

      public IActionResult Secure() => View();

      public IActionResult Logout() => SignOut("oidc");

      public async Task<IActionResult> CallApi()
      {
         var token = await HttpContext.GetTokenAsync("access_token");

         var client = _httpClientFactory.CreateClient();
         client.SetBearerToken(token);

         var response = await client.GetStringAsync(_authSettings.SampleApi + "identity");
         ViewBag.Json = JArray.Parse(response).ToString();

         return View();
      }

      public async Task<IActionResult> RenewTokens()
      {
         var disco = await _discoveryCache.GetAsync();
         if (disco.IsError) throw new Exception(disco.Error);

         var rt = await HttpContext.GetTokenAsync("refresh_token");
         var tokenClient = _httpClientFactory.CreateClient();

         var tokenResult = await tokenClient.RequestRefreshTokenAsync(new RefreshTokenRequest
         {
            Address = disco.TokenEndpoint,

            ClientId = _authSettings.ClientId,
            ClientSecret = _authSettings.ClientSecret,
            RefreshToken = rt
         });

         if (!tokenResult.IsError)
         {
            var oldIdToken = await HttpContext.GetTokenAsync("id_token");
            var newAccessToken = tokenResult.AccessToken;
            var newRefreshToken = tokenResult.RefreshToken;
            var expiresAt = DateTime.UtcNow + TimeSpan.FromSeconds(tokenResult.ExpiresIn);

            var info = await HttpContext.AuthenticateAsync("Cookies");

            info.Properties.UpdateTokenValue("refresh_token", newRefreshToken);
            info.Properties.UpdateTokenValue("access_token", newAccessToken);
            info.Properties.UpdateTokenValue("expires_at", expiresAt.ToString("o", CultureInfo.InvariantCulture));

            await HttpContext.SignInAsync("Cookies", info.Principal, info.Properties);
            return Redirect("~/Home/Secure");
         }

         ViewData["Error"] = tokenResult.Error;
         return View("Error");
      }
   }
}

In this snippet:

  • CallApi() is a function that makes an HTTP request to https://localhost:5005/identity (or whatever base URL is specified in the SampleApi property in the application settings).
  • RenewTokens() is a function that runs the oauth flow to refesh the access token.

Views

The view files are as follows:

  • Index.cshtml
@{
    ViewData["Title"] = "Home Page";
}

<div class="text-center">
    <h1 class="display-4">Welcome</h1>
    <p>Learn about <a href="https://docs.microsoft.com/aspnet/core">building Web apps with ASP.NET Core</a>.</p>
</div>
  • CallApi.cshtml
<h1>API Response</h1>

<pre>@ViewBag.Json</pre>
  • Secure.cshtml
<h1>API Response</h1>

<pre>@ViewBag.Json</pre>

Running the Application

When you run the application (for instance on port 44303 at https://localhost:44303 or https://app.local:44303) you get the following welcome page:

Home Page

When you click on Secure you are redirected to Visma Connect for authentication:

Authentication

After completing the authentication, the Secure page lists your user identity claims:

Claims

You can also find, on the same page, the authentication properties, such as the ID token, the access token, and the refresh token:

Properties

Last modified September 24, 2024