Using cloudscribe with Identity Server 4 with a SQL Server store on .Net Core 2 MVC securing APIs

Well that’s a bit of a mouthful of a title – but it does describe what I was trying to do, and I didn’t find it easy. It’s not that complicated when you know how, but there are an awful lot of options and it wasn’t clear (to me at least) which ones to pick, so I hope this saves others some time.

Aim

I wanted a cloudscribe site running allowing users to be able to logon as usual using cookies, but I also wanted to have an API that could be secured for clients external to the site without having to use the cookie login and I wanted to use Identity Server 4 with a SQL data store.

Starting setup

Server

I used Joe Audette's excellent cloudscribe Visual Studio template https://www.cloudscribe.com/blog/2017/09/11/announcing-cloudscribe-project-templates and selected use MSSQL Server and the Include Identity Server integration option. Also selecting the 2 options in the “Expert Zone” gave me an example API to test with.

clip_image002

This gave me the basic website and was the one I wanted to add the secured API into. The VS project has a weather forecast API as an example.

Client

I then setup a separate MVC project using a basic template to act as the client application. This was all done using Visual Studio 2017 and .Net Core 2.

Server application

By default, the weather forecast API is accessible to all users. Try: http://localhost:35668/api/SampleData/WeatherForecasts

You can secure this by adding the [Authorize] statement to the API on the SampleDataController.cs page e.g.

[Authorize]
[HttpGet("[action]")]
public IEnumerable<WeatherForecast> WeatherForecasts()
{
    var rng = new Random();

but you will find this presents the standard cloudscribe logon screen to access it – not exactly what’s wanted for an API.

In order to solve this we need to use JWT authorisation alongside the standard cookie authentication, but tell the API to only secure using the JWT authorisation . This is done by filtering the authentication scheme used by the Authorize statement as below (you will probably have to add the following assemblies to your code)

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authentication.JwtBearer;

and then add the filter to the authorize statement.

[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
[HttpGet("[action]")]
public IEnumerable<WeatherForecast> WeatherForecasts()
{
    var rng = new Random();

We have now told the API to authenticate using JWTBearer but we haven’t yet added JWT authentication to our applications pipeline. So in the startup.cs page we need to add in some assemblies:

using Microsoft.AspNetCore.Authentication.JwtBearer;

and then add the JWT service into the ConfigureServices method. (I added the statement below just above services.AddCors(options =>)

services.AddAuthentication(options =>
{
    options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
})

.AddJwtBearer(options =>
{
    options.Authority = "http://localhost:35668"; //No trailing /
    options.Audience = "api2"; //Name of api
    options.RequireHttpsMetadata = false;

});

Where:

.Authority is the address of the website with the api (note no trailing slash)

.Audience is the name you have given to the api in the Identity server 4 security setup (see more details below)

And then we need to tell our pipeline to use Authentication. So add the app.UseAuthentication() into the end of the ConfigureServices method just above the UseMVC call

app.UseAuthentication();
UseMvc(app, multiTenantOptions.Mode == cloudscribe.Core.Models.MultiTenantMode.FolderName);

Now if you try and access the api - http://localhost:35668/api/SampleData/WeatherForecasts - you should get an unauthorised message - even if you are logged onto the cloudscribe site using cookie authentication.

Identity Server 4 configuration (through cloudscribe)

Identity server has many options – which can be bewildering to start with. Full documentation is here: http://docs.identityserver.io/en/release/index.html

For our purposes here – I’m outlining the bare minimum that we need to setup security for our API, either using:

· A client credential using a secret

· A username and password

API resources

Under the admin menu in cloudscribe select security settings / API resources and create a new API record giving it a name (e.g. api2) making sure it matches the name you entered as the .Audience in the startup.cs .

Then we need to add a single scope record – called “allowapi2” in this example.

clip_image004

Client resources

Under the admin menu in cloudscribe select security settings / API Clients and create a new client (I’ve called it client2 – remember this name for when we make the call from the client application). Edit the new client record and add:

· Allowed Scope record – e.g. allowapi2 – this must match the scope we entered for the api and is used to specify which apis this client can access

clip_image006

· Client Secrets – the value is the SHA56 value of the secret we wish to use (in this example secret2) – at the moment the cloudscribe interface doesn’t do this conversion for us so we have to do it manually somewhere (e.g. I used string s = "secret2".ToSha256();)   

I added the secret using the web page and then pasted the converted secret direct into the relevant field in the record in the csids_ClientSecrets table in the database - but I think it would work equally well just pasting the converted value into the web page.

.ToSha256() is a string extension method in the IdentityModel assembly - this seems to do more than simply convert to sha256 - see https://github.com/IdentityModel/IdentityModel/blob/master/source/IdentityModel.Net45/Extensions/HashStringExtensions.cs.

It’s important that we set the secret type as well – in our example here it must be “SharedSecret”

Joe Audette has updated his nuget packages so saving a client secret now gives you a range of options for the secret type - in our example we need to pick "SharedSecret" and select to encrypt using Sha256 (see Joe's post in comments below for other options) which should make things easier.

clip_image008

· Allowed Grant types – we are entering “password” and “client_credentials”. These determine how we can authenticate from the client app as we see below in the next section. Password means that authentication can use a username / pwd combination (i.e. a cloudscribe login). Client_credentials means we can login using a client secret and don’t have to be a known user on the site.

clip_image010

Client application

To connect securely to the API using a client connection with a secret use:

var tokenClient = new TokenClient(disco.TokenEndpoint, "client2", "secret2");

To connect using a username and password use:

var tokenResponsePassword = await tokenClient.RequestResourceOwnerPasswordAsync("admin", "admin", "allowapi2");

Note that the user name is the user name not the email which can be used to login interactively.

The whole method in the controller looked something like this – the rest of the code is deserializing the JSON return from the API and putting it into an object that can be displayed on a view page

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
using ESDM.Models;
using System.Net.Http;
using Newtonsoft.Json;
using IdentityModel.Client;
using IdentityServerClient.Models;
using IdentityModel;

and then

//Hosted web API REST Service base url  
string Baseurl = "http://localhost:35668";

public async Task<ActionResult> Index()
{
    List<WeatherForecast> ans = new List<WeatherForecast>();
     using (var client = new HttpClient())
    {
        // discover endpoints from metadata
        var disco = await DiscoveryClient.GetAsync(Baseurl);
        var tokenClient = new TokenClient(disco.TokenEndpoint, "client2", "secret2");
        var tokenResponse = await tokenClient.RequestClientCredentialsAsync("allowapi2");

//Example getting alternative token if you want to use username / pwd 
        var tokenResponsePassword = await tokenClient.RequestResourceOwnerPasswordAsync("admin", "admin", "allowapi2");

        // call api - change for tokenResponsePassword if you want to use username / pwd
        client.SetBearerToken(tokenResponse.AccessToken);

        var response = await client.GetAsync(Baseurl + "/api/SampleData/WeatherForecasts");
        if (response.IsSuccessStatusCode)
        {
            var content =  response.Content.ReadAsStringAsync().Result;
            ans = JsonConvert.DeserializeObject<List<WeatherForecast>>(content);
        }
        return View(ans);
    }

The model for the forecast data was:

namespace ESDM.Models
{
    public partial class WeatherForecast
    {
        public string DateFormatted { get; set; }
        public int TemperatureC { get; set; }
        public string Summary { get; set; }

        public int TemperatureF
        {
            get
            {
                return 32 + (int)(TemperatureC / 0.5556);
            }
        }
    }
}

And my view contained

@model IEnumerable<ESDM.Models.WeatherForecast>
<div>
    <ul>
        @foreach (var forecast in Model)
        {
            <li>@forecast.Summary</li>
        }
    </ul>
</div>

Comments

Joe Audette

re: Using cloudscribe with Identity Server 4 with a SQL Server store on .Net Core 2 MVC securing APIs

04 November 2017

Great post! Was looking at the documentation about secrets here: http://docs.identityserver.io/en/release/topics/secrets.html

There is built in support for secret validation using Sha256, Sha512 and X509 certificates. In the samples they pre hash some of the secrets as Sha256 in the seed data. So pondering whether we should do any automatic hashing if secrets are added from the UI, I'm thinking not because if the user enters an X509 secret it should not be hashed.

What I think we could do to make things easier is add a dropdown for the user to select whether we should apply a hash or not, so if you do want a hash you wouldn't need to generate it in code yourself. ie the options could be "Don't Hash", "Apply Sha256 Hash", "Apply Sha512 Hash" when creating the secret from the UI.

Joe Audette

re: Using cloudscribe with Identity Server 4 with a SQL Server store on .Net Core 2 MVC securing APIs

04 November 2017

fyi, thanks to your feedback, I've published updated nugets for our identityserver integration and now we have a dropdown for the secret type and a dropdown if you want a shared secret to be hashed on the way into the database. That should make things a little easier.

re: Using cloudscribe with Identity Server 4 with a SQL Server store on .Net Core 2 MVC securing APIs

04 November 2017

Thanks Joe - I think that will be much easier for people coming to this fresh.  I've updated the post above to reflect the change

Mike Foitzik

re: Using cloudscribe with Identity Server 4 with a SQL Server store on .Net Core 2 MVC securing APIs

03 October 2018

Thank you for this excellent article James. It is exactly what I have been looking for and it helped me a lot.

sabine stein

re: Using cloudscribe with Identity Server 4 with a SQL Server store on .Net Core 2 MVC securing APIs

20 November 2018

Good afternoon,

I am following along your article about cloudscribe and securing APIs. I got stuck on the Client MVC Project. Can you please describe in more detail which basic template you used to act as the client application. Thank you!

 

James

re: Using cloudscribe with Identity Server 4 with a SQL Server store on .Net Core 2 MVC securing APIs

22 November 2018

Hi, It was a while ago now - but the client project was just a vanilla MS DotNet core MVC website.  Nothing to do with cloudscribe at this point as its just consuming the service.

I think the template was:

Visual C# -> Web -> ASP.NET Core Web Application
And then Web Application (MVC)

Find out more