401 Unauthorized: Consuming web api with JWT authentication in .NET 7 MAUI?

d. simic 41 Reputation points
2022-12-18T16:18:35.96+00:00

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);  
        }  
    }  
}  
Developer technologies .NET Blazor
Developer technologies .NET .NET MAUI
Developer technologies C#
{count} votes

Accepted answer
  1. AgaveJoe 30,126 Reputation points
    2022-12-19T17:36:02.093+00:00

    The Web API code serializes the token twice. I'm guessing the client code does not handle this situation. If you using the Visual Studio debugger to view the response you should see the token is surrounded by double quotes due to the double serialization. The client must deserialize the response twice to get the token.

    string token = await response.Content.ReadFromJsonAsync<string>();  
    

    I recommend returning an object like so...

    public class TokenResponse  
    {  
        public string Token { get; set; } = string.Empty;  
    }  
    

    Minor update to Web API. Edit: Or return a string rather than Ok(string).

    var tokenString = new JwtSecurityTokenHandler().WriteToken(tokeOptions);  
    TokenResponse response = new TokenResponse() { Token = tokenString };  
    return Results.Ok(response);  
    

    The console test code.

    // See https://aka.ms/new-console-template for more information  
    using System;  
    using System.Linq;  
    using System.Net.Http;  
    using System.Net.Http.Headers;  
    using System.Net.Http.Json;  
    using System.Security.Cryptography.X509Certificates;  
      
    //Console.WriteLine("Hello, World!");  
      
    internal class Program  
    {  
        private static HttpClient httpClient = new HttpClient();  
        private static async Task Main(string[] args)  
        {     
            httpClient.BaseAddress = new Uri("https://localhost:7217");  
            httpClient.DefaultRequestHeaders.Clear();  
            httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));  
      
            User user = new User() { AddInfo = "info", Email = "******@email.com", Password = "123", UserName = "user" };  
      
            //Get the JWT  
            string token = await AuthenticateAsync(user);  
            Console.WriteLine(token);  
      
            Console.WriteLine();  
      
            //Call a secured endpoint  
            var message = await Secured(token);  
            Console.WriteLine($"Response: {message}");  
      
        }  
      
        private static async Task<string> AuthenticateAsync(User user)  
        {  
            var response = await httpClient.PostAsJsonAsync(@"security/createToken", user);  
            TokenResponse token = await response.Content.ReadFromJsonAsync<TokenResponse>();  
            return token.Token;  
        }  
      
        private static async Task<string> Secured(string token)  
        {  
            httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("bearer", token);  
            string response = await httpClient.GetStringAsync(@"secret2");  
            return response;  
        }  
    }  
      
      
    public class User  
    {  
        public string UserName { get; set; } = string.Empty;  
        public string Email { get; set; } = string.Empty;  
        public string Password { get; set; } = string.Empty;  
        public string AddInfo { get; set; } = string.Empty;  
    }  
      
    public class TokenResponse  
    {  
        public string Token { get; set; } = string.Empty;  
    }  
    
    2 people found this answer helpful.

1 additional answer

Sort by: Most helpful
  1. P a u l 10,761 Reputation points
    2022-12-19T14:04:02.437+00:00

    It might not be relevant but I know that problems can occur when these middleware are registered in the reverse order:

       builder.Services.AddAuthorization();  
       builder.Services.AddAuthentication();  
    

    AddAuthentication should be first - could you try switching these around and see if it makes any difference?


Your answer

Answers can be marked as Accepted Answers by the question author, which helps users to know the answer solved the author's problem.