DanFlakes
DanFlakes

Reputation: 75

Validating AWS Cognito JWT in a ASP.NET Core 6 gRPC service

The JWT (id) token provided by AWS cognito is not passing token validation on my gRPC service, I keep getting unauthenticated as the response.

Does this have something to do with the default JwtBearer options?

gRPC service Program.cs:

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Server.Kestrel.Core;
using Microsoft.IdentityModel.Tokens;

var builder = WebApplication.CreateBuilder();

builder.WebHost.ConfigureKestrel(options =>
{
    options.ListenLocalhost(5000, o => o.Protocols = HttpProtocols.Http2);
});

builder.Services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        ValidIssuer = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_bX1jng7q2",
        ValidateIssuerSigningKey = true,
        ValidateIssuer = true,
        ValidateLifetime = true,
        ValidAudience = "2c744fhbdu94inn8u4sv4kg0ft",
        ValidateAudience = true,
        RoleClaimType = "cognito:groups"
    };
    options.MetadataAddress = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_bX1jng7q2/.well-known/openid-configuration";
});

builder.Services.AddAuthorization();
builder.Services.AddGrpc();

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();
app.MapGrpcService<GreeterService>();

app.Run();

Client:

try
{
    var greeterClient = new Greeter.GreeterClient(channel);
    
   // id token
    string id_token = "eyJraWQiOiJ4Qkk0MUNXYjdPUGtROGk2RWlhK1hQWlpjZ0ZcL0dOSFIwbFYyTTdLNVJhND0iLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiI4M2FhY2U1Mi05NTJjLTQyMmEtODdkYy1iOWI1MDkyNGNmYjkiLCJjb2duaXRvOmdyb3VwcyI6WyJBZG1pbiJdLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZSwiaXNzIjoiaHR0cHM6XC9cL2NvZ25pdG8taWRwLnVzLWVhc3QtMS5hbWF6b25hd3MuY29tXC91cy1lYXN0LTFfYlgxam5nN3EyIiwiY29nbml0bzp1c2VybmFtZSI6IjgzYWFjZTUyLTk1MmMtNDIyYS04N2RjLWI5YjUwOTI0Y2ZiOSIsIm9yaWdpbl9qdGkiOiI0MzQyZWJmNi0zYzQ4LTQ1YjEtODY1NC1kYTg1ZDQ1OGMyMDMiLCJhdWQiOiIyYzc0NGZoYmR1OTRpbm44dTRzdjRrZzBmdCIsImV2ZW50X2lkIjoiMDgyZDNlZDQtZTJlNy00OGRhLWJjOWYtZjgzMjY5NjAxZmQ0IiwidG9rZW5fdXNlIjoiaWQiLCJhdXRoX3RpbWUiOjE2NDg3MjgwMzgsImV4cCI6MTY0ODczMTYzOCwiaWF0IjoxNjQ4NzI4MDM4LCJqdGkiOiJkNzdlZTM3Yi1iMjA3LTRlZDctOTU3ZS1hY2E0OWE1ZmU0YWMiLCJlbWFpbCI6InNoYW1lZWwuZmF6dWxAbWFpbC5jb20ifQ.acGpo3owsd7gEvRtSTCijcRoIz4MP4MN8JUxBgM8mD8Oo-LBQam2uM2NxTtEygfx6MIWJMc9tNylv4GMm53bdrqBXCFeuYGiCdvdP4FvdFKkgwBV6Bzw7t0orN-P0zyrouDKW4NWIz2lUBvaOWE8j_fSdMhSsOlbbByDZH6mrNgugSWIXaF_frwIn2SjhMPnK4VO07uTdXMBiGvgkWH0JJidlU_vc9hjU33f";

// access token
    string access_token = "eyJraWQiOiJPOWlVVWpWVjkrTTdZMXE4c0dieG9RWTNrUXB4S3oyNEZXbERiekN2Nm5zPSIsImFsZyI6IlJTMjU2In0.eyJzdWIiOiI4M2FhY2U1Mi05NTJjLTQyMmEtODdkYy1iOWI1MDkyNGNmYjkiLCJjb2duaXRvOmdyb3VwcyI6WyJBZG1pbiJdLCJpc3MiOiJodHRwczpcL1wvY29nbml0by1pZHAudXMtZWFzdC0xLmFtYXpvbmF3cy5jb21cL3VzLWVhc3QtMV9iWDFqbmc3cTIiLCJjbGllbnRfaWQiOiIyYzc0NGZoYmR1OTRpbm44dTRzdjRrZzBmdCIsIm9yaWdpbl9qdGkiOiIyNTBlNDAzMi1lNmE1LTQwMzYtOWMyMC1hZDhmNTBjYTk1YjUiLCJldmVudF9pZCI6ImE4YmZhYWJlLTBkMjYtNDYzNC04M2YwLWM2YTc5OWM4YTEzZiIsInRva2VuX3VzZSI6ImFjY2VzcyIsInNjb3BlIjoiYXdzLmNvZ25pdG8uc2lnbmluLnVzZXIuYWRtaW4iLCJhdXRoX3RpbWUiOjE2NDkwNjMxOTAsImV4cCI6MTY0OTA2Njc5MCwiaWF0IjoxNjQ5MDYzMTkwLCJqdGkiOiI0YzhlYjg3OC04OTM0LTRkNTAtYWM2ZS0wZDZkYmYwNDY3YjYiLCJ1c2VybmFtZSI6IjgzYWFjZTUyLTk1MmMtNDIyYS04N2RjLWI5YjUwOTI0Y2ZiOSJ9.NTMlSa2xpQvMrmzqWYjK6449G9Hvp97JqhjsSE7dmNY5lo62XypyEpji6mCFCWlyD-b6om0mHmYNNknrG0UuD5dodMEI9AHK2u42jxzeQEndwkIEY827VUAOlHztdO3F4rsvT_P0TZmj4_3CvOladmd9KlW8ppWK5ZoFWUFniaFJOxUdfi6A-lBnJX2TxL1eEvLrLs6M5-HBOWLi8AekMsCc0aUrHPVzVTi9LUIjGXWmd6IkiG6HikC";

    var headers = new Metadata();
    headers.Add("Authorization", $"Bearer {access_token}");

    var greeterResponse = await greeterClient.SayHelloAsync(new HelloRequest { Name = "John Doe" }, headers);

    Console.WriteLine("Response Recieved: {0}", greeterResponse.Message);

}
catch (RpcException ex)
{
    Console.WriteLine("{0} :: {1}", ex.StatusCode, ex.Message);
}

Exception: System.Exception: Status(StatusCode="Unauthenticated", Detail="Bad gRPC response. HTTP status code: 401") ---> Grpc.Core.RpcException: Status(StatusCode="Unauthenticated", Detail="Bad gRPC response. HTTP status code: 401")

Also, just so you know, this is just a test pool and the ids listed above are not sensitive as mentioned here

Upvotes: 3

Views: 5531

Answers (5)

user2410689
user2410689

Reputation:

Like Leo reported... but you don't need to download the jwks.json

 services.AddAuthentication(o =>
    {
        o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options =>
    {
        var awsOptions = config.GetSection("AWS").Get<AWSOptions>();

        var validIssuer = string.Format("https://cognito-idp.{0}.amazonaws.com/{1}", awsOptions.RegionId, awsOptions.UserPoolId);

        options.RequireHttpsMetadata = !builder.Environment.IsDevelopment();

        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidIssuer = validIssuer,
            ValidateIssuerSigningKey = true,
            ValidateIssuer = true,
            ValidateLifetime = true,
            ValidateAudience = true,
            RoleClaimType = "cognito:groups",
            AudienceValidator = (audiences, securityToken, validationParameters) =>
            {
                //This is necessary because Cognito tokens doesn't have "aud" claim. Instead the audience is set in "client_id"
                var castedToken = securityToken as JwtSecurityToken;
                var hasAud = castedToken.Claims.Any(a => a.Type == "client_id" && a.Value == awsOptions.ClientId);
                return hasAud;
            }

        };
        //for debugging
        // options.Events = new JwtBearerEvents()
        // {
        //     OnTokenValidated = async (v) =>
        //   {
        //       return;
        //   },
        //     OnForbidden = async (forbiddenContext) =>
        //     {
        //         System.Diagnostics.Debug.Write(forbiddenContext.ToString());
        //     },

        //     OnAuthenticationFailed = async (AuthenticationFailedContext ctx) =>
        //   {
        //       System.Diagnostics.Debug.Write(ctx.Exception.ToString());
        //       var t = new Random().Next();
        //       var ex = ctx.Exception;
        //   }
        // };

        options.Authority = validIssuer;
        options.Audience = awsOptions.ClientId;

    });
    services.AddAuthorization();

Upvotes: 3

Leo
Leo

Reputation: 106

This is what worked for me on .NET 6

I created an extension method

public static async Task AddAuthenticationSupport(this IServiceCollection services, ConfigurationManager configurationManager)
{
    var validIssuer = configurationManager["ModuleConfiguration:Infrastructure:Cognito:ValidIssuer"];
    var validAudience = configurationManager["ModuleConfiguration:Infrastructure:Cognito:ClientId"];

    var httpClient = new HttpClient();
    var webKeySetJson = await httpClient.GetStringAsync($"{validIssuer}/.well-known/jwks.json");

    services.AddAuthentication(o =>
            {
                o.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
                o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
                o.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            })
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    IssuerSigningKeyResolver = (s, securityToken, identifier, parameters) =>
                    {
                        var jsonWebKeySet = new JsonWebKeySet(webKeySetJson);
                        return jsonWebKeySet?.GetSigningKeys();
                    },
                    ValidIssuer = validIssuer,
                    ValidateIssuerSigningKey = true,
                    ValidateIssuer = true,
                    ValidateLifetime = true,
                    ValidateAudience = true,
                    AudienceValidator = (audiences, securityToken, validationParameters) =>
                    {
                        //This is necessary because Cognito tokens doesn't have "aud" claim. Instead the audience is set in "client_id"
                        var castedToken = securityToken as JwtSecurityToken;
                        var clientId = castedToken?.Payload["client_id"]?.ToString();
                        return validAudience.Equals(clientId);
                    }
            }; 
            });

    services.AddAuthorization();
}

and then just added the following line on Program.cs

await builder.Services.AddAuthenticationSupport(builder.Configuration);

Upvotes: 3

BNG016
BNG016

Reputation: 310

In .NET 6 =>

builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
            .AddJwtBearer(options =>
            {
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    IssuerSigningKeyResolver = (s, securityToken, identifier, parameters) =>
                    {
                        var keys = new HttpClient().GetFromJsonAsync<JsonWebKeySet>(parameters.ValidIssuer + "/.well-known/jwks.json");
                        return (IEnumerable<SecurityKey>)keys;
                    },
                    ValidIssuer = "https://cognito-idp.{REGION}.amazonaws.com/{USER_POOL_ID}",
                    ValidateIssuerSigningKey = true,
                    ValidateIssuer = true,
                    ValidateLifetime = true,
                    ValidAudience = "{APP_CLIENT_ID}",
                    ValidateAudience = true
                };
            });

Upvotes: 3

Danut Radoaica
Danut Radoaica

Reputation: 2000

Use TokenValidationParameters.IssuerSigningKeyResolver:

builder.Services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        IssuerSigningKeyResolver = async (s, securityToken, identifier, parameters) =>
    {
        // get JsonWebKeySet from AWS
        ConfigurationManager<OpenIdConnectConfiguration> configurationManager =
                    new ConfigurationManager<OpenIdConnectConfiguration>(parameters.ValidIssuer + "/.well-known/jwks.json", new OpenIdConnectConfigurationRetriever());
                OpenIdConnectConfiguration openIdConnectConfiguration = await configurationManager.GetConfigurationAsync(CancellationToken.None).ConfigureAwait(false);
        return openIdConnectConfiguration.SigningKeys;
    },
        ValidIssuer = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_bX1jng7q2",
        ValidateIssuerSigningKey = true,
        ValidateIssuer = true,
        ValidateLifetime = true,
        ValidAudience = "2c744fhbdu94inn8u4sv4kg0ft",
        ValidateAudience = true,
        RoleClaimType = "cognito:groups"
    };
});

Or the raw way:

builder.Services.AddAuthentication(x =>
{
    x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
    x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(options =>
{
    options.TokenValidationParameters = new TokenValidationParameters
    {
        IssuerSigningKeyResolver = (s, securityToken, identifier, parameters) =>
    {
        // get JsonWebKeySet from AWS
        var json = new WebClient().DownloadString(parameters.ValidIssuer + "/.well-known/jwks.json");
        // serialize the result
        var keys = JsonConvert.DeserializeObject<JsonWebKeySet>(json).Keys;
        // cast the result to be the type expected by IssuerSigningKeyResolver
        return (IEnumerable<SecurityKey>)keys;
    },
        ValidIssuer = "https://cognito-idp.us-east-1.amazonaws.com/us-east-1_bX1jng7q2",
        ValidateIssuerSigningKey = true,
        ValidateIssuer = true,
        ValidateLifetime = true,
        ValidAudience = "2c744fhbdu94inn8u4sv4kg0ft",
        ValidateAudience = true,
        RoleClaimType = "cognito:groups"
    };
});

See more: How to validate AWS Cognito JWT in .NET Core Web API using .AddJwtBearer()

Upvotes: 5

Jason Pan
Jason Pan

Reputation: 21838

By searching for the error message, it is obvious that there is a problem with the token value, which causes the verification to fail.

And the most important point, when I read the post, I ignored the id token you use to access the service. I'm familiar with Azure AD and I think you should use an access token to access.This should be the key to solving this problem.

Upvotes: 1

Related Questions