Enrico
Enrico

Reputation: 6286

Secure API with IdentityServer and C#

I have my ASP.NET Core (.NET5) project with API controllers. I want to secure my APIs with Identity Server. My goal is to give to some clients access to the APIs based on client_id and client_secret and based on that define what APIs they can call. For that reason, I added in the Startup.cs the following code

public void ConfigureServices(IServiceCollection services)
{
    // ...
    services.AddAuthentication(
        IdentityServerAuthenticationDefaults.AuthenticationScheme)
        .AddIdentityServerAuthentication(options =>
        {
            options.Authority = apiSettings.Authority;
            options.ApiName = apiSettings.ApiName;
            options.ApiSecret = apiSettings.ApiSecret;
        });
}

So, the in each controller I added the [Authorize] attribute. Now, I want to call this APIs from a Console Application or Web Application using HttpClient.

private static async Task<string> GetAccessToken()
{
    using (var client = new HttpClient())
    {
        client.BaseAddress = new Uri(baseUrl);

        // We want the response to be JSON.
        client.DefaultRequestHeaders.Accept.Clear();
        client.DefaultRequestHeaders.Accept.Add(
            new MediaTypeWithQualityHeaderValue("application/json"));

        // Build up the data to POST.
        List<KeyValuePair<string, string>> postData = 
            new List<KeyValuePair<string, string>>();
        postData.Add(new KeyValuePair<string, string>("scope", "myscope"));
        postData.Add(new KeyValuePair<string, string>("client_id", clientId));
        postData.Add(new KeyValuePair<string, string>("client_secret", 
            clientSecret));

        FormUrlEncodedContent content = new FormUrlEncodedContent(postData);

        // Post to the Server and parse the response.
        HttpResponseMessage response = await 
            client.PostAsync("/api/v1/Test", content);
        string jsonString = await response.Content.ReadAsStringAsync();
        object responseData = JsonConvert.DeserializeObject(jsonString);

        // return the Access Token.
        return ((dynamic)responseData).data;
    }
}

The call always returns 401Unauthorized. What is the correct way to call the APIs with client_id and client_secret? Is scope required?

Update

To clarify, I think a scenario machine-to-machine. So, there is not user involved. When the HttpClient calls the API, it has to pass the authentication. In the machine-to-machine scenario, I want to check if the request has a particular scope, for example api_read, api_write, api_full and based on that the call has or has not access to a function.

For example, in Blazor, I created a function to pass the scope but not the clinet_id and client_secret.

public class MyAuthorizationMessageHandler : 
       AuthorizationMessageHandler
{
    public MyAuthorizationMessageHandler(IAccessTokenProvider provider, 
        NavigationManager navigation, IConfiguration configuration) :
        base(provider, navigation)
    {
        string apiEndpoint = configuration["Api:EndpointsUrl"];
        string apiScope = configuration["Api:Scope"];

        ConfigureHandler(authorizedUrls: new[] { apiEndpoint },
            scopes: new[] { apiScope });
    }
}

Upvotes: 0

Views: 1319

Answers (2)

Andrew Halil
Andrew Halil

Reputation: 1319

Accessing a secured API requires passing an access token to the header of the API request.

Passing a client id and secret will not provide access. A client id and secret is only for use in non-interactive clients.

There are two recommendations to obtain access to a secure web API method:

Method 1

  1. Call the identity server with either the client id/secret
  2. Client obtains the access token from 1)
  3. Call your API method from the client passing the access token into the request header.

Method 2

  1. Call the identity server with user credentials (user name / password)
  2. Identity server validates user credentials and generate an access token.
  3. Client obtains the access token from 2)
  4. Call your API method from the client passing the access token into the request header.

Method 1 only requires the client id/secret. This is recommended for a non-interactive service that needs to obtain a token from the identity server.

Method 2 requires a custom validation of credentials. Either a third party solution or your own backend secure store. This is a recommended solution for an interactive client application.

Upvotes: 0

eocron
eocron

Reputation: 7546

If you want access control to some operations, I would recommend you to use policies/roles.

Authentication is not supposed to restrict control, it is supposed to check who you working with (and by who I mean concrete entity, with identifier, not his titles, like "Manager", "Administrator", etc, which can be added/removed from entity). Important to mention that Authentication is not preventing user to log in, if he doesn't have some role - it is checked that user correct, and thats it, it shouldn't ban them out of service, Authorization will do it for you.

Authorization and specifically ASP NET Policy/Role usage on the other hand is about checking "permissions"/"titles". Or banning someone entirely.

For permissions/policies which I don't recommend to use, but it can be useful if you want dynamic roles in your service, like specifying them yourself in database:

//you decide which functionality should be restricted
[Authorize(Policy="CanAddStuff")]
public IActionResult AddSomeStuff(){...}

[Authorize(Policy="CanGetStuff")]
public IActionResult GetSomeStuff(){...}

//you decide what concrete role can do.
services.AddAuthorization(options =>
{
    options.AddPolicy("CanAddStuff", policy => policy.RequireRole("Manager"));
    options.AddPolicy("CanGetStuff", policy => policy.RequireRole("Reader"));
});

//somewhere in authentication step of some user you decide it is manager  logged in, and manager in YOUR hierarchy of titles is also a Reader
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Role, "Manager", ClaimValueTypes.String, Issuer));
claims.Add(new Claim(ClaimTypes.Role, "Reader", ClaimValueTypes.String, Issuer));

For titles/roles:

[Authorize(Role="Manager")]
public IActionResult AddSomeStuff(){...}

[Authorize(Role="Reader")]
public IActionResult GetSomeStuff(){...}

which will simply check if your user has Claim (you can add those on authentication step) on it. Which you can freely add in any amount you want, even multiple based on your own hierarchy of Kings/Managers/Readers/Slaves etc.

More here - https://github.com/blowdart/AspNetAuthorizationWorkshop#step-6-multiple-handlers-for-a-requirement

Upvotes: 1

Related Questions