Search code examples
amazon-cognitoasp.net-core-3.1blazor-client-sideblazor-webassembly

How do I pass OpenID authentication from Blazor WebAssembly to a .NET Core WebApi backend, both using Cognito as the OpenID provider?


Technology Goal:

  • Client-Side: Blazor WebAssembly
  • Backend: .NET Core 3.1 WebApi on Lambda (AWS) via ApiGateway (/{proxy+})
  • Authentication: AWS Cognito

Backend:

  • I have a .NET Core Lambda function that is accessed via AWS ApiGateway (RestApi) by having the ApiGateway /{proxy+} => Lambda. This deploys and works fine. I have configured the WebApi to use AWS Cognito as it's authentication method (code from Startup..cs:ConfigureServices below).
  • I can successfully use the [Authorize] attribute on my routes and when I access an route with [Authorize] in my browser directly, I get sent to the Cognito signing page, I can login and I'm sent back to my Api and the call executes. Works great.

Startup.cs:ConfigureServices

RegionEndpoint region = Configuration.GetAWSOptions().Region;
string CognitoMetadataAddress = $"https://cognito-idp.{region.SystemName}.amazonaws.com/{AppConfig.CognitoPoolId}/.well-known/openid-configuration";

//
// Ref: https://criticalhittech.com/2019/02/19/asp-net-core-with-aws-lambda-and-cognito/
//
services.Configure<OpenIdConnectOptions>(options =>
{
    options.AuthenticationMethod = OpenIdConnectRedirectBehavior.RedirectGet;
    options.ClientId             = AppConfig.CognitoClientId;
    options.MetadataAddress      = CognitoMetadataAddress;
    options.ResponseType         = OpenIdConnectResponseType.Code;
    options.SaveTokens           = true;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true
    };
});
services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
})
.AddCookie()
.AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
{
    options.ClientId        = AppConfig.CognitoClientId;
    options.MetadataAddress = CognitoMetadataAddress;
    options.ResponseType    = OpenIdConnectResponseType.Code;
    options.SaveTokens      = true;
    options.TokenValidationParameters = new TokenValidationParameters()
    {
        ValidateIssuer = true
    };
    options.Events = new OpenIdConnectEvents()
    {
        OnRedirectToIdentityProvider = OnRedirectToIdentityProvider,
        OnRedirectToIdentityProviderForSignOut = OnRedirectToIdentityProviderForSignOut,
        OnAuthenticationFailed = OnAuthenticationFailed,
        OnUserInformationReceived = OnUserInformationReceived
    };
});

Client-Side:

  • I have a basic Blazor project going using WebAssembly (so I can eventually just throw the compiled static output up on AWS S3 and have it hosted there).
  • I have configured the project to use Cognito as it's authentication source (see Program.cs:Main code below) and it works "ok" (it has the dumb Cognito X-Frame-Options issue of course, but it works).

Program.cs:Main

string CognitoPoolId = "ca-central-1_<REMOVED>";
string region = CognitoPoolId.Substring(0, CognitoPoolId.IndexOf('_', StringComparison.InvariantCultureIgnoreCase));
string CognitoAuthority = $"https://cognito-idp.{region}.amazonaws.com/{CognitoPoolId}";
string CognitoMetadataAddress = $"https://cognito-idp.{region}.amazonaws.com/{CognitoPoolId}/.well-known/openid-configuration";

builder.Services.AddOidcAuthentication(options =>
{
    options.ProviderOptions.Authority = CognitoAuthority;
    options.ProviderOptions.MetadataUrl = CognitoMetadataAddress;
    options.ProviderOptions.ClientId = "<REMOVED>";
    options.ProviderOptions.RedirectUri = $"{builder.HostEnvironment.BaseAddress.TrimEnd('/')}/authentication/login-callback";
    options.ProviderOptions.ResponseType = "code";

});

builder.Services.AddOptions();
builder.Services.AddAuthorizationCore();

Problem In my Blazor app, I want to call my back-end API (which requires Authentication) and use the authorization that the Blazor app already has since both the client and backend are using the same Cognito user pool. i.e.

  1. Load up Blazor app
  2. Complete Login
  3. Make Http call to my Backend <-- use the login session from #2
            HttpResponseMessage response = await Http.SendAsync(requestMessage);

How do I configure the HttpClient instance to send along the authorization the Blazor app has via authenticating with Cognito so that it can call the protected Apis?


Solution

  • In the Client projects Program.cs, you can add an HttpClient as follows that will add the neccessary tokens to your http requests. This can then by injected into code as required to make your http calls.

    builder.Services.AddHttpClient("UniqueClientNameHere", client => client.BaseAddress = serverBaseAddress)
                    .AddHttpMessageHandler<BaseAddressAuthorizationMessageHandler>();
    
    // Supply HttpClient instances that include access tokens when making requests to the server project
    builder.Services.AddTransient(sp => sp.GetRequiredService<IHttpClientFactory>().CreateClient("UniqueClientNameHere"));
    

    for example inside a .razor file called Foobar.razor:

    @page "/foobar"
    @inject HttpClient Http
    
    <h1>Hello Foobar</h1>
    
    @code{
        protected override async Task OnInitializedAsync()
        {
            HttpResponseMessage response = await Http.SendAsync(requestMessage);
        }
    }