Example 1

This C# code shows a console application written for .NET Core 3.1 which implements a simple REPL (Read-Evaluate-Print Loop). Each loop 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 single user model, and as such uses the Password flow. Typically this type of application would not be interactive, and the credentials would be stored in a configuration file.

Download full source.

Example1.csproj

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

  <ItemGroup>
    <PackageReference Include="IdentityModel" Version="4.1.1" />
    <PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.7" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Binder" Version="3.1.1" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.1" />
  </ItemGroup>

  <ItemGroup>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>

Program.cs

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;

namespace IQ.Example1
{
    class Program
    {
        private static SiteSettings siteConfig;
        private static AccessTokenManager tokenManager;

        static async Task Main(string[] args)
        {
            Console.WriteLine("Starting IQ Example #1");
            // Load data from configuration file
            IConfiguration config = new ConfigurationBuilder()
                .AddJsonFile("appsettings.json", false, true)
                .AddJsonFile("appsettings.Development.json", true, true)
                .Build();
            siteConfig = config.GetSection("Site").Get<SiteSettings>();

            tokenManager = new AccessTokenManager(siteConfig.AuthorityUrl, siteConfig.ClientId);
            await Repl();
            tokenManager.Terminate();

            Console.WriteLine();
            Console.WriteLine("Execution complete. Press any key...");
            Console.ReadKey();
        }

        private static async Task Repl()
        {
            // Ask for credentials
            Console.Write("Username: ");
            var username = Console.ReadLine();
            Console.Write("Password: ");
            var password = Console.ReadLine();

            if (await tokenManager.Initialise(username, password))
            {
                Console.WriteLine("Enter IQL command in free text followed by a blank line");

                string iqCommand;
                var uriQuery = new Uri(new Uri(siteConfig.IQUrl), "query");
                while (true)
                {
                    Console.Write("Command: ");
                    iqCommand = "";
                    while (true)
                    {
                        var line = Console.ReadLine();
                        if (string.IsNullOrWhiteSpace(line))
                            break;
                        iqCommand += line + " ";
                    }
                    if (string.IsNullOrWhiteSpace(iqCommand))
                        break;
                    var timeout = new CancellationTokenSource();
                    timeout.CancelAfter(TimeSpan.FromSeconds(15));
                    var http = new HttpClient();
                    http.DefaultRequestHeaders.Authorization = tokenManager.AuthenticationHeader;
                    http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("IQExample", "1.0"));
                    try
                    {
                        var response = await http.PostAsync(uriQuery, new StringContent(iqCommand), timeout.Token);
                        if (response.IsSuccessStatusCode)
                        {
                            var reply = await response.Content.ReadAsStringAsync();
                            Console.WriteLine($"Response: {reply}");
                        }
                        else
                        {
                            Console.WriteLine($"Command failed: {response.StatusCode}");
                        }
                    }
                    catch (OperationCanceledException)
                    {
                        Console.WriteLine("Command timed out without response");
                    }
                    catch (Exception e)
                    {
                        Console.WriteLine($"Command failed: {e} {e.Message}");
                    }
                }
            }
        }
    }
}

SiteSettings.cs

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

AccessTokenManager.cs

using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using IdentityModel.Client;

namespace IQ.Example1
{
    class AccessTokenManager
    {
        public AuthenticationHeaderValue AuthenticationHeader { get; private set; } // immuteable, so no locking required

        private readonly string authorityUrl;
        private readonly string clientId;
        private Task backgroundTokenUpdater;
        private CancellationTokenSource stoppingToken;
        private const int OneSecond = 1000;

        public AccessTokenManager(string tokenAuthorityUrl, string siteClientId)
        {
            authorityUrl = tokenAuthorityUrl;
            clientId = siteClientId;
            stoppingToken = new CancellationTokenSource();
        }

        public async Task<bool> Initialise(string username, string password)
        {
            var discoResponse = await RetrieveDiscoveryDocument(authorityUrl);
            var accessToken = await RetrieveAccessToken(discoResponse.TokenEndpoint, clientId, username, password);
            if (accessToken != null)
            {
                AuthenticationHeader = new AuthenticationHeaderValue(accessToken.TokenType, accessToken.AccessToken);

                // token refresh process
                backgroundTokenUpdater = Task.Run(async () =>
                {
                    while (!stoppingToken.Token.IsCancellationRequested)
                    {
                        // refresh shortly before expiry
                        var timeout = (accessToken.ExpiresIn - 10) * OneSecond;
                        await Task.Delay(timeout, stoppingToken.Token);
                        accessToken = await RefreshAccessToken(discoResponse.TokenEndpoint, clientId, accessToken.RefreshToken);
                        // replace current authentication header with new one
                        AuthenticationHeader = new AuthenticationHeaderValue(accessToken.TokenType, accessToken.AccessToken);
                    }
                }, stoppingToken.Token)
                .ContinueWith(t =>
                {
                    // absorb OperationCanceledException
                    t.Exception?.Handle(e => true);
                }, TaskContinuationOptions.OnlyOnCanceled);

                return true;
            }
            return false;
        }

        public void Terminate()
        {
            if (backgroundTokenUpdater != null)
            {
                // terminate background task cleanly
                stoppingToken.Cancel(false);
                backgroundTokenUpdater.Wait();
            }
        }

        private static async Task<DiscoveryDocumentResponse> RetrieveDiscoveryDocument(string tokenAuthority)
        {
            // Retrieve discovery document from token authority
            var discoCache = new DiscoveryCache(tokenAuthority);
            var discoResponse = await discoCache.GetAsync();
            if (discoResponse.IsError)
            {
                throw new Exception($"Error accessing Discovery Document: {discoResponse.Error}");
            }
            return discoResponse;
        }

        private static async Task<TokenResponse> RetrieveAccessToken(string tokenEndpoint, string clientId, string username, string password)
        {
            // Retrieve the access token from token authority (password flow)
            var http = new HttpClient();
            var token = await http.RequestPasswordTokenAsync(new PasswordTokenRequest()
            {
                Address = tokenEndpoint,
                ClientId = clientId,
                UserName = username,
                Password = password
            });
            if (token.IsError)
            {
                throw new Exception($"Error obtaining AccessToken: {token.Error} {token.ErrorDescription}");
            }
            return token;
        }

        private static async Task<TokenResponse> RefreshAccessToken(string tokenEndpoint, string clientId, string refreshToken)
        {
            // Refresh the access token from token authority
            var http = new HttpClient();
            var token = await http.RequestRefreshTokenAsync(new RefreshTokenRequest()
            {
                Address = tokenEndpoint,
                ClientId = clientId,
                RefreshToken = refreshToken
            });
            if (token.IsError)
            {
                throw new Exception($"Error refreshinging AccessToken: {token.Error} {token.ErrorDescription}");
            }
            return token;
        }
    }
}