I have a minimal API .NET 7 installed on an external web server and use JWT for authentication. Everything works as expected. I can log in via Postman, then I get JWT and if I enter JWT in Postman, then I can also access protected endpoint and get the data from the Web API.
Now I have created a desktop application in MAUI .NET 7 and I want to use this web API. Also here the access to unprotected endpoint works as well as logging in with receiving the JWT. Only the last part of the whole thing does not work anymore and that is access to a protected endpoint with the delivery of JWT for which I constantly get the message 401 Unauthorized. If I then put the same JWT into Postman, then the request goes through Posstman and I get the data from Web API!
I Use some code examples from net:
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri("http://api.mywebsite.com:64591/secret")
};
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token.token);
var response = await _httpClient.SendAsync(requestMessage);
I also installed Wireshark software and analyzed Network Transfer between my MAUI App and my Web Api (ninimal Api NET 7) on Server. Here is what I found:
- the token I receive from Web Api after authentication, I also send in the same form when requesting to an Authorized Endpoint (so received and sent token from client are identical). And if I use the same token in Postman, everything works! My conclusion here is that the token is correct.
- When the request with the JWT is sent from the client to the server (Web Api), I then get the error message 401. In the log I see below information with the reason Bearer error="invalid_token": Hypertext Transfer Protocol
HTTP/1.1 401 Unauthorized\r\n
[Expert Info (Chat/Sequence): HTTP/1.1 401 Unauthorized\r\n]
[HTTP/1.1 401 Unauthorized\r\n]
[Severity level: Chat]
[Group: Sequence]
Response Version: HTTP/1.1
Status Code: 401
[Status Code Description: Unauthorized]
Response Phrase: Unauthorized
Transfer Encoding: chunked\r\n
Server: Microsoft-IIS/10.0\r\n
WWW-Authenticate: Bearer error="invalid_token"\r\n
X-Powered-By: ASP.NET\r\n
Date: Sun, 18 Dec 2022 09:07:00 GMT\r\n
\r\n
[HTTP response 1/1]
[Time since request: 0.047969000 seconds]
[Request in frame: 790]
[Request URI: http://api.myserver.com:64591/secret2]
HTTP chunked response
End of chunked encoding
Chunk size: 0 octets
\r\n
File Data: 0 bytes
There are only two error reasons I could think of:
- I still have a bug in my minimal API (Web Api) and that is regarding the JWT I get from the client and somehow still need to convert/crimp the JWT maybe (where I then could not explain why the whole thing works in Postman)!? By the fact that I may use JWT in exactly the form that is sent to client, then it may be that it is wrong and that is why this error message "invalid_token" comes.
- the second cause could be NET 7 / MAUI / Minimal API, so an error that occurs not because my code is wrong but because it is implemented incorrectly in NET 7 / MAUI / Minimal API (is of course not probable but not impossible).
Software versions
Microsoft Visual Studio Community 2022 (64-Bit) - Current
Version 17.4.2
.NET 7
Minimal API for Web Application in NET 7
MAUI for Windows Desktop
Thanks
tpc
EDIT
Here is the completely code from Web Api (for the generation and request of JWT, as well as endpoint for a request via JWT).).
using Microsoft.AspNetCore.Authorization;
using Microsoft.IdentityModel.Tokens;
using System.Security.Claims;
using System.Text;
using System.IdentityModel.Tokens.Jwt;
using Microsoft.OpenApi.Models;
using Microsoft.AspNetCore.Authentication.JwtBearer;
namespace WebApplication1
{
public class User
{
public string UserName { get; set; } = "";
public string Email { get; set; } = "";
public string Password { get; set; } = "";
public string AddInfo { get; set; } = "";
}
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAuthorization();
builder.Services.AddAuthentication().AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superSecretKey@345"))
};
});
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.MapGet("/api/test", [AllowAnonymous] () => "Hello you!");
app.MapGet("/secret2", [Authorize] () => $"Hello You. This is a secret!!!");
app.MapPost("/security/createToken",
[AllowAnonymous] (User user) =>
{
if (user.UserName == "user" && user.Password == "123")
{
var claims = new[]
{
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
new Claim(JwtRegisteredClaimNames.Iat, DateTime.UtcNow.ToString()),
new Claim(JwtRegisteredClaimNames.GivenName, user.UserName),
new Claim(JwtRegisteredClaimNames.Email, "******@test.com"),
new Claim(ClaimTypes.Role, "Administrator"),
new Claim("Role1", "Administrator"),
new Claim("Role2", "Standard"),
new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
};
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("superSecretKey@345"));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
var tokeOptions = new JwtSecurityToken(
issuer: "https://api.mysite.com:64591",
audience: "https://api.mysite.com:64591",
claims: claims,
expires: DateTime.Now.AddMinutes(50),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);
return Results.Ok(tokenString);
}
return Results.Unauthorized();
});
app.UseHttpsRedirection();
app.Run();
}
}
}
EDIT
Here is the client app that has a class that has only two methods. The first method fetches the JWT from the web api and the second requests the data from the web api by sending JWT to the server. Right here at this point you will get the 401 response from Wen Api server.
using Microsoft.AspNetCore.Components;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Pages.ViewModel.Stamm
{
public class UserAuthentication
{
public string UserName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public string AddInfo { get; set; } = "";
}
public class CounterViewModel : LayoutComponentBase
{
private protected string token = "";
private protected string InfoToken = "";
private protected string InfoData = "";
private protected async Task GetToken()
{
try
{
// Set User
UserAuthentication user = new UserAuthentication();
user.UserName = "user";
user.Email= "******@email.com";
user.Password = "123";
// Server-Login
HttpClient httpClient = new HttpClient();
var response = await httpClient.PostAsJsonAsync("https://api.mysite.com/security/createToken", user);
if (response.IsSuccessStatusCode)
{
var authContent = await response.Content.ReadAsStringAsync();
token = authContent;
var payload = authContent.Split('.')[1];
var jsonBytes = ParseBase64WithoutPadding(payload);
var keyValuePairs = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(jsonBytes);
string GivenName = keyValuePairs["given_name"].ToString();
string Email = keyValuePairs["email"].ToString();
string Role1 = keyValuePairs["Role1"].ToString();
string Role2 = keyValuePairs["Role2"].ToString();
InfoToken = "Token!";
}
else
InfoToken = "No Token!";
}
catch (Exception ex)
{
throw;
}
}
private protected async Task GetData()
{
try
{
// Methode 1
HttpClient httpClient = new HttpClient();
var requestMessage = new HttpRequestMessage
{
Method = HttpMethod.Get,
RequestUri = new Uri(@"https://api.mysite.com/secret2")
};
requestMessage.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
requestMessage.Content = new StringContent(string.Empty, Encoding.UTF8, "application/json");
var response = await httpClient.SendAsync(requestMessage);
//// Methode 2
//HttpClient httpClient = new HttpClient();
//var request = new HttpRequestMessage(HttpMethod.Get, "https://api.mysite.com/secret2");
////request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
//httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
//HttpResponseMessage response = await httpClient.SendAsync(request, HttpCompletionOption.ResponseHeadersRead);
if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) // NOTE: THEN TOKEN HAS EXPIRED
{
InfoData = "Unauthorized!";
}
else if (response.StatusCode == System.Net.HttpStatusCode.NoContent)
{
InfoData = "NoContent!";
}
else if (response.IsSuccessStatusCode)
{
InfoData = "Success!";
}
}
catch (Exception ex)
{
throw;
}
}
public byte[] ParseBase64WithoutPadding(string base64)
{
switch (base64.Length % 4)
{
case 2:
base64 += "==";
break;
case 3:
base64 += "=";
break;
}
return Convert.FromBase64String(base64);
}
}
}