Service Fabric: Secure your ASP.NET Core WebApi with IdentityServer4

ไทย/Eng

OAuth2 is an authorization framework that enables applications to grant limited access to user. It allows resource owner to manage the limited access to the clients. Clients use their credential to grant the access token to gather the protected resource. The following figure is a Flow of OAuth2 that came from Harsha Kumara

IdentityServer4 is a middleware that adds OpenId and OAuth2 endpoints to ASP.NET Core Application. In the previous article I discussed about the secured cluster which can protected your service fabric cluster from the anonymous user but for the OpenAPI which everyone with the url endpoint can gather it. So we need to limit the access to the authorized user.

Prerequisite

  1. Azure Portal Subscription Account – If you don’t have one. Try it for free
  2. Visual Studio – This article used VS2017 Preview 2 with .NET Version 4.6.1
  3. Service Fabric SDK – See How
  4. Secure Cluster – See how
  5. ASP.NET Core WebApi Setup – See how

Full source code of this project is availabled on GitHub. But it’s locate in the “IdentityServer4” branch. You have to checkout to that branch.

Let’s Start

    1. This article is a further work from this post
    2. There are 3 NuGet packages are required for this post: “IdentityServer4”, “IdentityServer4AccessTokenValidation”, “WindowAzure.Storage”


    1. Create new file named “Config.cs”. This file will provide the ApiResource for IdentityServer4.
public static IEnumerable GetApiResources()
{
    return new List
    {
        new ApiResource("api1", "My API")
    };
}
    1. Create new file named “StorageClient.cs”. This file will be the Azure Storage Client that stores our ClientId and ClientSecret. This article use a local storage that means you have to run Azure Storage Emulator or you can use a remote server as well.
public class StorageClient : IDisposable
{
    private string ConnectionString => "UseDevelopmentStorage=true";
    private string PrefixTable => "IDx";

    public CloudTable ClientStore { get { ThrowIfDisposed(); return _ClientStore; } set { _ClientStore = value; } }
    private CloudTable _ClientStore;

    private CloudTableClient CloudStorageClient = null;

    public StorageClient()
    {
        CloudStorageClient = CloudStorageAccount.Parse(ConnectionString).CreateCloudTableClient();
        _ClientStore = CloudStorageClient.GetTableReference(PrefixTable + "ClientStore");
        _ClientStore.CreateIfNotExistsAsync();
    }
    private bool _disposed = false;
    ~StorageClient()
    {
        this.Dispose(false);
    }
    private void ThrowIfDisposed()
    {
        if (this._disposed)
        {
            throw new ObjectDisposedException(base.GetType().Name);
        }
    }
    public void Dispose()
    {
        this.Dispose(true);
    }
    protected virtual void Dispose(bool disposing)
    {
        if (!_disposed && disposing)
        {
            CloudStorageClient = null;
            _ClientStore = null;
            _disposed = true;
        }
    }
}
    1. Create new file named “ClientEntity.cs”. This is the Azure Storage Table Entity Model.
public class ClientEntity : TableEntity
{
    // PK = "ClientData"
    // RK = {ClientId}

    public string Secret { get; set; }
    public string Scope { get; set; }
}

    1. Create new file named “ClientStore.cs”. This file will provide the ClientId and ClientSecret from Azure Storage for IdentityServer4.
public StorageClient Db { get; set; }
public ClientStore()
{
    Db = new StorageClient();
}
public async Task FindClientByIdAsync(string clientId)
{
    TableResult RetrieveResult = await Db.ClientStore.ExecuteAsync(TableOperation.Retrieve("ClientData", clientId));
    var Entity = (ClientEntity)RetrieveResult.Result;
    return new Client()
    {
        ClientId = clientId,
        AllowedGrantTypes = GrantTypes.ClientCredentials,

        // secret for authentication
        ClientSecrets =
        {
            new Secret(Entity.Secret.Sha256())
        },

        // scopes that client has access to
        AllowedScopes = { Entity.Scope }
    };
}
    1. Open “Startup.cs” file. In the “ConfigureServices” method, Insert these lines of code.
services.AddIdentityServer()
                .AddDeveloperSigningCredential()
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddClientStore();

services.AddAuthentication("Bearer")
                .AddIdentityServerAuthentication(options =>
                {
                    options.Authority = "http://localhost:8994";
                    options.RequireHttpsMetadata = false;
                    options.ApiName = "api1";
                });
    1. In the “Configure” method, Insert this line of code.
app.UseIdentityServer();
    1. I had mocked some client data like this.

    1. Create some ApiController named “PersonController.cs”
[Produces("application/json")]
[Route("api/Person")]
[Authorize]
public class PersonController : Controller
{
    [HttpGet]
    public List GetPersons()
    {
        return new List()
        {
            new Person()
            {
                Firstname = "Jakkrit",
                Lastname = "Junrat",
                Age = 27
            },
            new Person()
            {
                Firstname = "Apinya",
                Lastname = "Pimpisan",
                Age = 27
            }
        };
    }
}

public class Person
{
    public string Firstname { get; set; }
    public string Lastname { get; set; }
    public int Age { get; set; }
}
    1. Deploy the project and go to “http://localhost:8994/api/Person”. You will get response 401 (Unauthorized), that means your APIs have been protected.

    1. Now, your WebApi has been protected. Let’s implement the client app.
    2. First, create a new ConsoleApp project and install a NuGet package called “IdentityModel”.

    1. In the “Main” method, Insert these lines of code.
// discover endpoints from metadata
var disco = DiscoveryClient.GetAsync("http://localhost:8994").GetAwaiter().GetResult();
if (disco.IsError)
{
    Console.WriteLine(disco.Error);
    return;
}

// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "ClientId", "ClientSecret");
var tokenResponse = tokenClient.RequestClientCredentialsAsync("api1").GetAwaiter().GetResult();

if (tokenResponse.IsError)
{
    Console.WriteLine(tokenResponse.Error);
    return;
}

Console.WriteLine(tokenResponse.Json);

// call api
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);

var response = client.GetAsync("http://localhost:8994/api/Person/").GetAwaiter().GetResult();
if (!response.IsSuccessStatusCode)
{
    Console.WriteLine(response.StatusCode);
}
else
{
    var content = response.Content.ReadAsStringAsync().GetAwaiter().GetResult();
    Console.WriteLine(JArray.Parse(content));
}
    1. Run the project and the result should look like this.

Thank you for reading. See you on next post.
Happy Coding.

2 thoughts on “Service Fabric: Secure your ASP.NET Core WebApi with IdentityServer4

  1. Jun Reply

    Hi,

    I also need IdentityServer to run in my Service Fabric app. For some reason I cannot fathom I cannot get your code to run. I get an error when trying to access the disco from the client app.

    I greatly appreciate any help you can extend.

    Thanks

    • Jakkrit Junrat Post authorReply

      Hi Jun,

      Sorry for late reply. Can you provide me the error log?

      Or you can go to the openid-configuration by go to this url: “http(s):///.well-known/openid-configuration” and see what you get.

      The disco class will go to check this url to get the json of openid configuration of your server.

      Hope this help 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *