Consume an ABP Framework API from a .NET Core console Application
Introduction
In this article I will show you how to connect to a protected ABP Framework API from a .NET Core console Application using the IdentityModel.OidcClient nuget package.
The sample BookStore ABP Framework application in this article has been developed with Blazor as UI Framework and SQL Server as database provider.
The BookStoreConsole application is a standard .NET Core console application.
As I tried to keep this article as simple as possible, you will see there is still some room for code improvements.
Source code
The source code of both projects is available on GitHub.
Requirements
The following tools are needed to be able to run the solution and follow along.
- .NET 6.0 SDK
- VsCode, Visual Studio 2019 Version 16.10.4+ or another compatible IDE
ABP Framework application
Create a new ABP Framework application
abp new BookStore -u blazor -o BookStore
Implement the Web Application Development tutorial (part1-5)
To follow along make sure you have a protected BookAppService in the BookStore application. For this article I followed the Web Application Development tutorial till part 5: Authorization.
Add the section below in the appsettings.json file of the DbMigrator project
"BookStore_Console": {
"ClientId": "BookStore_Console",
"ClientSecret": "1q2w3e*",
"RootUrl": "https://localhost:44368"
}
Add a BookStoreConsole client in the IdentityServerDataSeedContributor class of the Domain project
// BookStoreConsole Client
var bookStoreConsoleClientId = configurationSection["BookStore_Console:ClientId"];
if (!bookStoreConsoleClientId.IsNullOrWhiteSpace())
{
var bookStoreConsoleRootUrl = configurationSection["BookStore_Console:RootUrl"].TrimEnd('/');
await CreateClientAsync(
name: bookStoreConsoleClientId,
scopes: commonScopes,
grantTypes: new[] { "password", "client_credentials""authorization_code" },
secret: configurationSection["BookStore_Console:ClientSecret"]?.Sha256(),
requireClientSecret: false,
redirectUri: $"{bookStoreConsoleRootUrl}/authentication/login-callback",
corsOrigins: new[] { bookStoreConsoleRootUrl.RemovePostFix("/") }
);
}
Run DbMigrator project
To apply the settings above you need to run the DbMigrator project. After, you can check the IdentityServerClients table of the database to see if the BookStore_Console client has been added.
.NET Core console application
Create a new .NET Core console application
dotnet new console -n BookStoreConsole
Install nuget packages (in terminal window or nuget package manager)
dotnet add package IdentityModel.OidcClient --version 5.0.0-preview.1
dotnet add package Newtonsoft.Json --version 13.0.1
Add a HttpService class in the root of the project
When you want to consume a protected API the user has to be authenticated (username+password) and authorized(has the right permissions). So, when you call the BookAppService GetListAsync method, in the header of the request you need to send the accesstoken with.
To obtain the accesstoken you can make use of the nuget package IdentityModel.OidcClient. All the heavy lifting occurs in the GetTokensFromBookStoreApi method (See below). These method sends a request to the disco.TokenEndpoint of the BookStoreApi and obtains a TokenResponse. If the correct properties are sent and the API is running, you should obtain a TokenResponse (AccessToken, IdentityToken, Scope, ...)
Afterwards the obtained accesstoken is used in the SetBearerToken() of the httpClient.
When you make a request now to the protected BookStore API with the httpClient, the accesstoken is sent with. The BookStore API receives this request and checks the validity of the accesstoken and the permissions. If these conditions are met, the GetListAsync method of the BookAppService returns the list of books.
public class HttpService
{
public async Task<Lazy<HttpClient>> GetHttpClientAsync(bool setBearerToken)
{
var client = new Lazy<HttpClient>(() => new HttpClient());
var accessToken = await GetAccessToken();
if (setBearerToken)
{
client.Value.SetBearerToken(accessToken);
}
client.Value.BaseAddress = new Uri("https://localhost:44388/"); //
return await Task.FromResult(client);
}
private static async Task<TokenResponse> GetTokensFromBookStoreApi()
{
var authority = "https://localhost:44388/";
var discoveryCache = new DiscoveryCache(authority);
var disco = await discoveryCache.GetAsync();
var httpClient = new Lazy<HttpClient>(() => new HttpClient());
var response = await httpClient.Value.RequestPasswordTokenAsync(new PasswordTokenRequest
{
Address = disco.TokenEndpoint, // https://localhost:44388/connect/token
ClientId = "BookStore_Console",
ClientSecret = "1q2w3e",
UserName = "admin",
Password = "1q2w3E*",
Scope = "email openid profile role phone address BookStore",
});
if (response.IsError) throw new Exception(response.Error);
return response;
}
private async Task<string> GetAccessToken()
{
var accessToken = (await GetTokensFromBookStoreApi()).AccessToken;
return accessToken;
}
}
Main Method
Below you see the Main method of the Program.cs file. A new HttpService gets created and the GetHttpClientAsync method is called to get a httpClient.
Next, we make a request to the BookStore API to obtain the list of books.
static async Task Main()
{
// if setBearerToken = false, should throw HttpRequestException: 'Response status code does not indicate success: 401 (Unauthorized).'
// if setBearerToken = true, API should be called an list of books should be returned
const bool setBearerToken = true;
var httpService = new HttpService();
var httpClient = await httpService.GetHttpClientAsync(setBearerToken);
var response = await httpClient.Value.GetAsync("https://localhost:44388/api/app/book");
response.EnsureSuccessStatusCode();
var json = await response.Content.ReadAsStringAsync();
var books = JsonConvert.DeserializeObject<ListResultDto<BookDto>>(json);
Console.WriteLine("====================================");
if (books?.Items != null)
foreach (var book in books.Items)
Console.WriteLine(book.Name);
Console.WriteLine("====================================");
Console.ReadKey();
}
Run API and .NET Core console application
Run the BookStore.HttpApi.Host of the ABP Framework application first. Start the .NET Core console application next. Below is the result when the accesstoken is successfully set.
If you set the variable setBearerToken in the Main method to false, you will get a 401 (Unauthorized)
Congratulations, you can now connect to an ABP Framework API form a .NET Core console application! Check out the source code of this article on GitHub.
Enjoy and have fun!
Comments
TangJun TangJun 169 weeks ago
very helpful, thank you.
parsagachkar@gmail.com 160 weeks ago
What about generated api proxies (that we get in blazor template for instance)?
Don Boutwell 144 weeks ago
So, this is really great stuff and I appreciate this (and all the rest, you've basically taught my more about the ABP Framework than anybody), but I was hoping you could take it a step further and explain how I can use this to extend API access to my tenants. Right now, when I apply this I am able to get a token via OAuth with the correct user details, but when I try to query the API endpoint it just kicks me back to a login screen.
Bart Van Hoey 144 weeks ago
It's difficult to correctly answer the question without seeing your code.
I've used this technique in this article and to obtain the data for a Tenant, I just added the TenantId as a query string parameter to the API endpoint URL.
https://localhost:44368/api/app/book?tenantid=xxx-xxx-xxx-xxx
In the BookAppService it's handled by the corresponding method
public async Task<ListResultDto<BookDto>> GetListAsync(Guid tenantId) { using (CurrentTenant.Change(tenantId)) { var Books = await _BookRepository.GetListAsync(tenantId); return new ListResultDto<BookDto>(ObjectMapper.Map<List<Book>, List<BookDto>>(Books)); } }
Good luck!
Don Boutwell 144 weeks ago
Bart, you are awesome. My issue ended up being the fact that I was using domain-based tenant resolver and a custom URL for my tenant, so the system was failing on the issuer verification part of the JWT validation process. I appreciate your guides and your willingness to provide insight!
Leonardo.Willrich 126 weeks ago
Nice post, thank you for that! I'm just struggling with CurrentTenant and CurrentUser. First, to be able to get the token using the password grant_type, I had to add in the header the key "__tenant" with my tenant name as value. Otherwise, it will return "Invalid username or password". Once I've added DefaultRequestHeaders to my httpClient instance, the CurrentTenant is correct, but, the CurrentUser is still null. Any idea how to initialize the CurrentUser properly?