Service-to-Service Authentication Using OAuth2 in .NET Microservices: Product and Order Services
In microservices architecture, it’s common to have multiple services that need to communicate with each other. This inter-service communication must be secure, and one of the best ways to achieve this is by implementing service-to-service authentication using OAuth2. In this blog, we’ll explore how to secure communication between two microservices — Product and Order services — using OAuth2 in a .NET environment.
Embark on a journey of continuous learning and exploration with DotNet-FullStack-Dev. Uncover more by visiting our https://dotnet-fullstack-dev.blogspot.com reach out for further information.
What is OAuth2?
OAuth2 is an open standard for access delegation commonly used as a way to grant websites or applications limited access to user information without exposing user credentials. It works by issuing access tokens to third-party clients by an authorization server, which the client then uses to access protected resources hosted by resource servers.
Why OAuth2 for Service-to-Service Authentication?
- Security: Ensures that only authenticated services can communicate with each other.
- Granular Access Control: Different tokens can be issued with different scopes, limiting what each service can do.
- Scalability: Centralized token issuing by an authorization server simplifies the authentication process across many services.
Example Scenario: Product and Order Services
Let’s consider two services in a microservice architecture:
- Product Service: Manages product-related data.
- Order Service: Manages customer orders, which need to retrieve product details from the Product Service.
The Order Service needs to securely communicate with the Product Service, ensuring that only authorized services can request product information.
Step 1: Set Up an Authorization Server
The authorization server is responsible for issuing OAuth2 tokens. In a .NET environment, you can use IdentityServer4 or ASP.NET Core Identity with OAuth2 support to act as the authorization server.
For simplicity, we’ll use IdentityServer4 in this example.
Install IdentityServer4 NuGet Package:
dotnet add package IdentityServer4
Configure IdentityServer4 in Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddInMemoryClients(Config.GetClients())
.AddInMemoryApiResources(Config.GetApiResources())
.AddInMemoryApiScopes(Config.GetApiScopes())
.AddDeveloperSigningCredential();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseIdentityServer();
}
Define Clients and Resources:
public static class Config
{
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "order_service",
AllowedGrantTypes = GrantTypes.ClientCredentials,
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedScopes = { "product_api" }
}
};
}
public static IEnumerable<ApiScope> GetApiScopes()
{
return new List<ApiScope>
{
new ApiScope("product_api", "Product API")
};
}
public static IEnumerable<ApiResource> GetApiResources()
{
return new List<ApiResource>
{
new ApiResource("product_api", "Product API")
};
}
}
Step 2: Secure Product Service with OAuth2
The Product Service must be secured so that only authorized services (like the Order Service) can access it.
Install the OAuth2 NuGet Package:
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer
Configure OAuth2 in Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "https://localhost:5000"; // IdentityServer URL
options.RequireHttpsMetadata = false;
options.Audience = "product_api";
});
services.AddControllers();
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
Add Authorization to the Product Controller:
[Authorize]
[ApiController]
[Route("api/[controller]")]
public class ProductController : ControllerBase
{
[HttpGet("{id}")]
public IActionResult GetProduct(int id)
{
// Your logic here
return Ok(new { ProductId = id, ProductName = "Sample Product" });
}
}
Step 3: Order Service Requests a Token and Calls Product Service
The Order Service needs to obtain an access token from the authorization server and use it to call the Product Service.
Add an HTTP Client with OAuth2 Support:
public class ProductServiceClient
{
private readonly HttpClient _httpClient;
private readonly ITokenService _tokenService;
public ProductServiceClient(HttpClient httpClient, ITokenService tokenService)
{
_httpClient = httpClient;
_tokenService = tokenService;
}
public async Task<string> GetProductAsync(int productId)
{
var token = await _tokenService.GetTokenAsync("order_service", "secret");
_httpClient.SetBearerToken(token);
var response = await _httpClient.GetAsync($"https://localhost:5001/api/product/{productId}");
return await response.Content.ReadAsStringAsync();
}
}
Implement Token Service to Obtain the OAuth2 Token:
public class TokenService : ITokenService
{
public async Task<string> GetTokenAsync(string clientId, string clientSecret)
{
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5000");
if (disco.IsError) throw new Exception(disco.Error);
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
ClientId = clientId,
ClientSecret = clientSecret,
Scope = "product_api"
});
if (tokenResponse.IsError) throw new Exception(tokenResponse.Error);
return tokenResponse.AccessToken;
}
}
Use the ProductServiceClient in the Order Service:
public class OrderController : ControllerBase
{
private readonly ProductServiceClient _productServiceClient;
public OrderController(ProductServiceClient productServiceClient)
{
_productServiceClient = productServiceClient;
}
[HttpGet("{id}")]
public async Task<IActionResult> GetOrder(int id)
{
// Simulating fetching product details as part of the order processing
var productDetails = await _productServiceClient.GetProductAsync(id);
// Return order details including product information
return Ok(new { OrderId = id, ProductDetails = productDetails });
}
}
Step 4: Running the Services
To run these services, you’ll need to follow these steps:
Run the Authorization Server:
- Navigate to the directory where your authorization server project is located.
- Use
dotnet run
to start the server. The server will be accessible athttps://localhost:5000
.
Run the Product Service:
- Navigate to the Product Service project directory.
- Run the service with
dotnet run
. It will be available athttps://localhost:5001
.
Run the Order Service:
- Navigate to the Order Service project directory.
- Use
dotnet run
to start the service. It should be accessible athttps://localhost:5002
.
Step 5: Testing the Implementation
You can use tools like Postman to simulate requests from the Order Service to the Product Service. When the Order Service requests product details, it will first obtain a token from the authorization server, then use that token to authenticate with the Product Service.
Request a Token Manually:
- POST request to
https://localhost:5000/connect/token
with client credentials to obtain a token.
Call Product Service:
- Use the obtained token to make a GET request to
https://localhost:5001/api/product/{id}
.
Call Order Service:
- Call
https://localhost:5002/api/order/{id}
and verify that it retrieves the product details correctly.
Conclusion
Implementing service-to-service authentication using OAuth2 in .NET Microservices ensures secure and controlled access between services. By using OAuth2 with JWT tokens, services like the Product and Order services can communicate securely, ensuring that only authenticated services have access to the required resources. This approach not only enhances security but also provides a scalable solution for managing authentication across a distributed microservices environment.