Machine-to-Machine Authentication with Amazon Cognito in .NET
Learn how to implement machine-to-machine authentication with Amazon Cognito using OAuth Client Credentials flow in .NET. Let's explore how applications can securely communicate with each other without human involvement.
When building distributed applications, we often need services to communicate with each other securely without any human interaction.
This is where machine-to-machine (M2M) authentication becomes essential.
Amazon Cognito supports M2M authentication through OAuth Client Credentials flow, allowing applications to securely talk to each other using client credentials instead of user credentials.
Thanks to AWS for sponsoring this article in my .NET on AWS Series.
In this post, let's explore how to implement M2M authentication with Amazon Cognito. We'll use a .NET console application as our client, authenticating with Cognito using the OAuth Client Credentials flow to get an access token, and then use that token to call a secured .NET API.
Amazon Cognito For The .NET Developer: How to Easily Get Started

Understanding Machine-to-Machine Amazon Cognito Authentication
Machine identities in Amazon Cognito user pools are confidential clients that run on application servers and connect to remote APIs without user interaction.
The M2M authorization flow uses the OAuth Client Credentials grant type. This is different from the authorization code flow we typically use for user authentication.
In the Client Credentials flow:
- The client application sends its client ID and client secret to the token endpoint
- Cognito validates the credentials and returns an access token
- The client uses this token to make authenticated requests to the API
The key difference is that there's no user login involved — it's purely application-to-application authentication.
Setting up the .NET Applications
To demonstrate M2M authentication, we'll create two applications: a secured ASP.NET API that requires authentication, and a .NET console application that will act as the M2M client.
Let's start by setting up both applications before we configure Cognito.
Setting Up the ASP NET API
Let's start with an ASP.NET API that's already configured to use Amazon Cognito for authentication. If you're new to securing APIs with Cognito, check out the linked post above for a complete walkthrough.
The API configuration in appsettings.json includes the Cognito user pool details:
{"AmazonCognito": {"Authority": "https://cognito-idp.{region}.amazonaws.com/{userPoolId}","TokenValidationParameters": {"ValidateIssuerSigningKey": true,"ValidateIssuer": true,"ValidateLifetime": true}},"WeatherApi": {"Url": "http://localhost:5264/weatherforecast"}}
In Program.cs, the API requires authorization:
var builder = WebApplication.CreateBuilder(args);builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options =>{options.Authority = builder.Configuration["AmazonCognito:Authority"];options.TokenValidationParameters = new TokenValidationParameters{ValidateIssuerSigningKey = true,ValidateIssuer = true,ValidateLifetime = true};});builder.Services.AddAuthorization();var app = builder.Build();app.UseAuthentication();app.UseAuthorization();app.MapGet("/weatherforecast", () =>{// Weather forecast logic}).RequireAuthorization(); // This endpoint requires authenticationapp.Run();
The /weatherforecast endpoint requires authorization. If we try to call it without a token, we'll get a 401 Unauthorized response.
Setting up the .NET Console Client
Let's create a .NET console application that will act as our M2M client:
var builder = Host.CreateApplicationBuilder(args);builder.Services.AddHttpClient();var host = builder.Build();var clientFactory = host.Services.GetRequiredService<IHttpClientFactory>();var client = clientFactory.CreateClient();var weatherApiUrl = builder.Configuration["WeatherApi:Url"];Console.WriteLine("Press enter to get weather forecast...");Console.ReadLine();var response = await client.GetAsync(weatherApiUrl);if (response.IsSuccessStatusCode){var content = await response.Content.ReadAsStringAsync();Console.WriteLine(content);}else{Console.WriteLine($"Error: {response.StatusCode}");}
If we run both the API and the console application now, the console app will fail with a 401 Unauthorized error because it's not sending any authentication token.
Configuring Machine-to-Machine Client in Cognito
To enable M2M authentication, we need to create a new app client in Amazon Cognito:
- Navigate to your Cognito User Pool in the AWS Console
- Go to App Clients and click Create app client
- Select Machine to Machine as the app type (not "Public client" or "Confidential client")
- Give it a name like
weather-m2m-app - Click Create app client

Once created, you'll see:
- Client ID — The unique identifier for your application
- Client Secret — The secret key used to authenticate
- Token endpoint URL — The URL to exchange credentials for a token (format:
https://{your-domain}/oauth2/token)
Copy all three of these values — we'll need them shortly.
The machine-to-machine app type removes many of the configuration options needed for user authentication (like redirect URIs and scopes) since those aren't relevant for M2M flows.
Testing Token Exchange with Postman
Before updating our .NET client, let's verify the token exchange works using Postman.
Create a new POST request to the token endpoint with the following configuration:
URL: https://{your-cognito-domain}/oauth2/token
Headers:
Content-Type: application/x-www-form-urlencoded
Body (x-www-form-urlencoded):
grant_type:client_credentialsclient_id:{your-client-id}client_secret:{your-client-secret}
Send the request. If everything is configured correctly, you'll receive a response like:
{"access_token": "eyJraWQiOiJ...","token_type": "Bearer","expires_in": 3600}
Copy the access token and paste it into jwt.io to inspect the token contents. You'll see:
- The issuer (
iss) — Your Cognito user pool URL - The client ID (
client_id) — Your app client ID - Expiration time (
exp) - Default scopes
This token can now be used to make authenticated requests to your API.
Implementing Amazon Cognito Client Credentials Token Exchange in .NET
Now let's update our console application to automatically get the token and use it to call the API.
Creating Configuration Classes
First, create classes to hold the OAuth configuration and token response:
public class ClientCredentialsOptions{public string TokenEndpoint { get; set; }public string ClientId { get; set; }public string ClientSecret { get; set; }}public class TokenResponse{[JsonPropertyName("access_token")]public string AccessToken { get; set; }[JsonPropertyName("token_type")]public string TokenType { get; set; }[JsonPropertyName("expires_in")]public int ExpiresIn { get; set; }}
Update your appsettings.json to include the OAuth configuration:
Important: Don't commit secrets to source control! For local development, use .NET User Secrets instead.
{"OAuth2": {"TokenEndpoint": "https://{your-cognito-domain}/oauth2/token","ClientId": "{your-client-id}","ClientSecret": "{your-client-secret}"},"WeatherApi": {"Url": "http://localhost:5264/weatherforecast"}}
🔐 5 Ways to Handle Application Configuration & Secrets With Azure

Creating Client Credentials Token Helper
Create a helper class to handle token retrieval:
public class TokenHelper{public static async Task<string?> GetAccessTokenAsync(HttpClient client,ClientCredentialsOptions options){var content = new FormUrlEncodedContent(new[]{new KeyValuePair<string, string>("grant_type", "client_credentials"),new KeyValuePair<string, string>("client_id", options.ClientId),new KeyValuePair<string, string>("client_secret", options.ClientSecret)});var tokenResponse = await client.PostAsync(options.TokenEndpoint, content);if (!tokenResponse.IsSuccessStatusCode){Console.WriteLine($"Token error: {tokenResponse.StatusCode}");return null;}var tokenJson = await tokenResponse.Content.ReadAsStringAsync();var tokenObj = JsonSerializer.Deserialize<TokenResponse>(tokenJson);return tokenObj?.AccessToken;}}
This method:
- Creates form-encoded content with the grant type, client ID, and client secret
- Posts to the token endpoint
- Deserializes the response to extract the access token
- Returns the token
Updating the .NET Console application to use Client Credentials token
Update Program.cs to get the token and attach it to API requests. Add these changes to the existing console app:
// Add user secrets supportbuilder.Configuration.AddUserSecrets<Program>();// Bind OAuth configurationvar clientCredentialsOptions = new ClientCredentialsOptions();builder.Configuration.GetSection("OAuth2").Bind(clientCredentialsOptions);// Get access token before calling the APIvar accessToken = await TokenHelper.GetAccessTokenAsync(client, clientCredentialsOptions);if (string.IsNullOrEmpty(accessToken)){Console.WriteLine("Failed to get access token");return;}// Add token to authorization headerclient.DefaultRequestHeaders.Authorization =new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
Now when you run both applications:
- The console app requests a token from Cognito using client credentials
- Cognito validates the credentials and returns an access token
- The console app attaches the token to the API request in the Authorization header
- The API validates the token against Cognito
- The API returns the weather forecast data
The console application successfully authenticates and retrieves data from the secured API without any user interaction.
Our current implementation requests a new token on every API call. This is inefficient.
To improve performance, you can implement token caching by storing the access token in memory and only requesting a new one when it expires.
This reduces unnecessary calls to the token endpoint and significantly improves your application's performance.