ASP.NET Core authentication using Cognito User Pools on the ALB

Viewed 1027

I am porting a legacy ASP.NET MVC application to .NET Core, I am also trying to replace the existing Identity framework to use AWS Cognito User Pools which are handled by the application load balancer.

I have the majority setup and working, when requests hit the ALB and unauthorised user is redirected to the Cognito hosted login page where they can signin. This all works OK but I want to hook up the information in the JWT (passed back from the ALB) to the ASP.NET Identity framework so I can access the populated "User" object.

The response header header containing the JWT is also non-stadard "X-Amzn-Oidc-Data" rather than in the standard Authorization. I have looked at using the Microsoft.AspNetCore.Authentication.JwtBearer but I beleive this expects the standard Authorization header.

I did come across this project https://github.com/awslabs/aws-alb-identity-aspnetcore which appears to be exactly the kind of thing I need but it appears to have been abandoned. Does anyone have any similar experience in acheiving the same thing which they could share?

UPDATE: this is our updated Startup code based on the comment from Mickaël:

services.AddAuthentication("Cognito")
            .AddJwtBearer("Cognito", options =>
                {
                    options.Events = options.Events ?? new JwtBearerEvents();
                    options.Events.OnMessageReceived = context =>
                        {
                            string amazonOidcDataHeader = context.Request.Headers["X-Amzn-Oidc-Data"];
                            if (!string.IsNullOrEmpty(amazonOidcDataHeader))
                            {
                                context.Token = amazonOidcDataHeader.Trim();
                            }

                            return Task.CompletedTask;
                        };

                    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 = this.Configuration["Authentication:Cognito:ValidIssuer"],
                        ValidateIssuerSigningKey = true,
                        ValidateIssuer = true,
                        ValidateLifetime = true,
                        ValidAudience = this.Configuration["Authentication:Cognito:ClientId"],
                        ValidateAudience = true
                    };
                });

The token is now retrieved from the header and pushed into the context.Token property, the Identity User object still contains no claims.

1 Answers

The built-in JWT provider has an extensibility point to source the token from virtually anywhere through the JwtBearerEvents.OnMessageReceived event.

You could do something like this:

services
    .AddAuthentication("Cognito")
    .AddJwtBearer("Cognito", options =>
    {
        options.Events ??= new JwtBearerEvents();
        options.Events.OnMessageReceived = context =>
        {
            const string bearerPrefix = "Bearer ";
            string amazonOidcDataHeader = context.Request.Headers["X-Amzn-Oidc-Data"];
            if (!string.IsNullOrEmpty(amazonOidcDataHeader) && amazonOidcDataHeader.StartsWith(bearerPrefix, StringComparison.OrdinalIgnoreCase))
            {
                context.Token = amazonOidcDataHeader.Substring(bearerPrefix.Length).Trim();
            }

            return Task.CompletedTask;
        };
    });

For reference, see this extract of code on GitHub which shows the implementation of the handler, and how it uses the token assigned during the MessageReceived event if there is one, or tries to extract the one from the standard Authorization header otherwise: https://github.com/dotnet/aspnetcore/blob/20f5f6fbc3db4a66faeb941e56db6ace333c1817/src/Security/Authentication/JwtBearer/src/JwtBearerHandler.cs#L58-L91

Related