Example 2

This ASP.NET Core 3.1 website written in C# code implements a simple Request/Response. Each request tries to execute a command written in IQL. Tokens are managed by an asynchronous background process.

We have used the NuGet package IdentityModel (from Dominick Baier and Brock Allen) to assist with the token handling, but the HTTP requests and responses can be created and parsed by other methods if you wish.

This example application demonstrates a multiple user model, and as such uses the Implicit flow. This is typically of interactive applications which act on behalf of a many users.

Download full source.

/Example2.csproj

<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <TargetFramework>netcoreapp3.1</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="IdentityModel" Version="4.1.1" />
    <PackageReference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="3.1.1" />
    <PackageReference Include="Microsoft.Extensions.Logging.Debug" Version="3.1.0" />
    <PackageReference Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="3.1.0" />
  </ItemGroup>
</Project>

/Program.cs

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

namespace IQ.Example2
{
    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>();
                });
    }
}

/SiteSettings.cs

namespace IQ.Example2
{
    public class SiteSettings
    {
        public string AuthorityUrl { get; set; }
        public string LogoutRedirectUrl { get; set; }
        public string ClientId { get; set; }
        public string IQUrl { get; set; }
    }
}

/Startup.cs

using System;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using IdentityModel.AspNetCore.AccessTokenManagement;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.IdentityModel.Tokens;

namespace IQ.Example2
{
    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)
        {
            var siteSection = Configuration.GetSection("Site");
            var siteSettings = siteSection.Get<SiteSettings>();
            services.AddSingleton(siteSettings);

            services.AddHttpContextAccessor();
            services.AddSingleton<IActionContextAccessor, ActionContextAccessor>();

            services.AddHttpClient("apiClient", config =>
            {
                config.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("IQWebExample", "1.0"));
            })
                .AddHttpMessageHandler<UserAccessTokenHandler>();
            services.AddAccessTokenManagement(options =>
            {
                options.User.RefreshBeforeExpiration = TimeSpan.FromSeconds(10);
            })
                .ConfigureBackchannelHttpClient(client =>
            {
                client.Timeout = TimeSpan.FromSeconds(30);
            });

            services
            .AddAuthentication(options =>
            {
                options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = "oidc";
            })
            .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme)
            .AddOpenIdConnect("oidc", options =>
            {
                options.Authority = siteSettings.AuthorityUrl;
                options.RequireHttpsMetadata = true;
                options.ClientId = siteSettings.ClientId;
                options.SaveTokens = true;
                options.GetClaimsFromUserInfoEndpoint = true;
                options.ResponseType = "id_token code";
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    NameClaimType = "name",
                    RoleClaimType = "role"
                };

                options.Scope.Add("openid");
                options.Scope.Add("profile");
                options.Scope.Add("roles");
                options.Scope.Add("offline_access");
                options.Scope.Add("Zenith/News");
                options.Scope.Add("Zenith/OrderPad");
                options.Scope.Add("Zenith/Trading");
                options.Scope.Add("Zenith/Market");
                options.ClaimActions.MapJsonKey("role", "role", "role");

                options.Events.OnTicketReceived = ctx =>
                {
                    var t = ctx.Properties.GetTokens();
                    return Task.CompletedTask;
                };
            });

            services.AddRazorPages();
        }

        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Error");
                app.UseHsts();
            }
            app.UseAuthentication();
            app.UseHttpsRedirection();
            app.UseStaticFiles();
            app.UseRouting();
            app.UseAuthorization();
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapRazorPages();
            });
        }
    }
}

/Pages/Access/Login.cshtml.cs

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace IQ.Example2
{
    [AllowAnonymous]
    [RequireHttps]
    public class LoginModel : PageModel
    {
        public async Task OnGet(string returnUrl = "/")
        {
            await HttpContext.ChallengeAsync("oidc", new AuthenticationProperties() { RedirectUri = returnUrl });
        }
    }
}

/Pages/Access/Logout.cshtml.cs

using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;

namespace IQ.Example2
{
    [AllowAnonymous]
    [RequireHttps]
    public class LogoutModel : PageModel
    {
        private readonly SiteSettings siteSettings;

        public LogoutModel(SiteSettings siteConfig)
        {
            siteSettings = siteConfig;
        }

        public async Task OnGet()
        {
            await HttpContext.SignOutAsync("oidc", new AuthenticationProperties
            {
                // Indicate here where Passport should redirect the user after a logout.
                // Note that the resulting absolute Uri must be whitelisted in the
                // **Allowed Logout URLs** settings for the client.
                RedirectUri = siteSettings.LogoutRedirectUrl
            });
            await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
        }
    }
}

/Pages/Shared/_LoginPartial.cshtml

@if (User.Identity.IsAuthenticated)
{
    <a asp-page="/Access/Logout">[ Log Off ]</a>
}
else
{
    <a asp-page="/Access/Login">[ Log On ]</a>
}

/Pages/Index.cshtml

@page
@model IndexModel
@{
        ViewData["Title"] = "Home page";
}

<div class="text-center">
        <h1 class="display-4">IQ Example 2</h1>
        @if (User.Identity.IsAuthenticated)
        {
                <div class="container">
                        <form method="post">
                                <div class="card">
                                        <div class="card-body">
                                                <div class="input-group mb-3">
                                                        @Html.EditorFor(m => m.CommandText, new { htmlAttributes = new { @class = "form-control", placeholder = "IQL Command", @autofocus = "" }, aria_label = "IQL Command", aria_describedby = "execute-button-addon" })
                                                        <div class="input-group-append">
                                                                <button class="btn btn-primary" type="submit" id="execute-button-addon">Execute</button>
                                                        </div>
                                                </div>
                                        </div>
                                </div>
                                @if (Model.IsSuccessful.HasValue)
                                {
                                        @if (Model.IsSuccessful.Value)
                                        {
                                                <div class="alert alert-success" role="alert">
                                                        Successful executed command.
                                                </div>
                                        }
                                        else
                                        {
                                                <div class="alert alert-danger" role="alert">
                                                        @Model.ErrorText
                                                </div>
                                        }
                                }
                                <div class="row">
                                        <div class="col-12">
                                                @if (Model.IsSuccessful.GetValueOrDefault(false))
                                                {
                                                        <table class="table table-hover table-striped">
                                                                <thead class="thead-dark">
                                                                        <tr>
                                                                                @foreach (var col in Model.Result.Columns)
                                                                                {
                                                                                        <th>@col.Value.ColumnName</th>
                                                                                }
                                                                        </tr>
                                                                </thead>
                                                                <tbody>
                                                                        @foreach (var row in Model.Result.Rows)
                                                                        {
                                                                                <tr>
                                                                                        @foreach (var field in row)
                                                                                        {
                                                                                                <td>@field.Value</td>
                                                                                        }
                                                                                </tr>
                                                                        }
                                                                </tbody>
                                                        </table>
                                                }
                                        </div>
                                </div>
                        </form>
                </div>
        }
        else
        {
                <p>You need to login to use this website.</p>
        }
</div>

/Pages/Index.cshtml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using IQ.Example2.Models;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.RazorPages;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

namespace IQ.Example2.Pages
{
    public class IndexModel : PageModel
    {
        private readonly ILogger<IndexModel> journalLogger;
        private readonly SiteSettings siteSettings;
        private readonly IHttpContextAccessor httpContext;
        private readonly IHttpClientFactory httpClientFactory;

        public IndexModel(ILogger<IndexModel> logger, SiteSettings siteConfig, IHttpContextAccessor httpContextAccessor, IHttpClientFactory httpClients)
        {
            journalLogger = logger;
            siteSettings = siteConfig;
            httpContext = httpContextAccessor;
            httpClientFactory = httpClients;
        }

        [BindProperty]
        public string CommandText { get; set; }

        [BindProperty]
        public bool? IsSuccessful { get; set; }

        [BindProperty]
        public string ErrorText { get; set; }

        [BindProperty]
        public QueryResponse Result { get; set; }

        public async Task<IActionResult> OnPostAsync()
        {
            try
            {
                ModelState.Clear();
                var timeout = new CancellationTokenSource();
                timeout.CancelAfter(TimeSpan.FromSeconds(15));

                var uriQuery = new Uri(new Uri(siteSettings.IQUrl), "query");
                var http = httpClientFactory.CreateClient("apiClient");
                var response = await http.PostAsync(uriQuery, new StringContent(CommandText), timeout.Token);
                IsSuccessful = response.IsSuccessStatusCode;
                if (IsSuccessful.Value)
                {
                    var reply = await response.Content.ReadAsStringAsync();
                    Result = JsonConvert.DeserializeObject<QueryResponse>(reply);
                    ErrorText = "";
                }
                else
                {
                    ErrorText = $"Command failed: {response.StatusCode}";
                    Result = null;
                }
            }
            catch (OperationCanceledException)
            {
                IsSuccessful = false;
                ErrorText = $"Command timed out without response";
                Result = null;
            }
            catch (Exception e)
            {
                IsSuccessful = false;
                ErrorText = $"Command failed: {e} {e.Message}";
                Result = null;
            }
            return Page();
        }
    }
}