ASP.NET Core with AWS Lambda and Cognito

We are wrapping up a project for a client consisting of mobile apps (Android and iOS, built with Xamarin Forms), and a fairly small management server deployed to AWS. For handling user account management, AWS Cognito seemed like the way to go, but there are a few different ways you can use it with ASP.NET Core:

1. Manual calls to APIs, for instance, for instance as described here https://aws.amazon.com/blogs/mobile/use-csharp-to-register-and-authenticate-with-amazon-cognito-user-pools/ and https://docs.aws.amazon.com/sdk-for-net/v3/developer-guide/cognito-authentication-extension.html.

2. Using the new (as of this post) Identity Provider, as described https://aws.amazon.com/blogs/developer/introducing-the-asp-net-core-identity-provider-preview-for-amazon-cognito/. This has the advantage of nicely integrating with typical ASP.NET Core Identity coding patterns.

3. Using OpenId Connect (OIDC) and Cognito UI.  With both methods 1 and 2, AWS Cognito is in charge of the user database and integrating with the AWS roles and permissions infrastructure, but you are still responsible for all of the UI flow around account creation and management.  This option has Cognito handle all of the UI flow around account creation and login (though you have some ability to customize).  For a recent project, we needed to provide server-side access, but didn’t have any kind of fancy needs for account management, so this really seemed to fit the bill.

There area bunch of steps to go through to get everything set up, but after that, you can rely on Cognito for a lot of the mechanics of user account management. I did rely on some previously-written articles to get everything working (see References at end), but I haven’t found an end-to-end guide. Hope you find it useful.

Setting up ASP.Net Core in Lambda

There are several good descriptions out there about getting an ASP.NET Core app running as an AWS Lambda function, including https://aws.amazon.com/blogs/developer/serverless-asp-net-core-2-0-applications/.

You’ll end up with a website hosted at some Url like https://<something>.execute-api.<region>.amazonaws.com/Prod (or something else instead of Prod, depending on aliases you set up).  After getting your site set up, create a page called SignedOut which will not require authentication, displaying a message like “You’re now signed out.”  We’ll use this later as the callback after signing out.

Setting up Cognito

 There are a few key parts of the Cognito setup that have to be completed correctly, for this to work, and some of the information will be needed to configure the ASP.NET Core app.

Configuring AWS Cognito App Clients section

We’ll use the App Client Id and App Client Secret later.  Make sure to also check ADMIN_NO_SRP_AUTH.

Next, we’ll configure OAuth settings and callbacks to your ASP.NET Core app.

App Client Settings page
App Client Settings page

The URLs need to refer to:

Callback URL(s): <your ASP.NET Core App website URL>/signin-oidc 

Sign out URL(s): <your ASP.NET Core App website URL>/SignedOut

If you have multiple deployments, list them all in these settings. The route ‘signin-oidc’ is automatically provided by ASP.NET Core.

If you called your signed-out page something else from the previous section, substitute that here.  Cognito uses this settings as the acceptable redirects after logging out.

 The last thing you need to configure on the Cognito side is the domain:

Select domain for sign-in pages

Select an available domain name which will host all the sign-in and account management pages.  Failing to do this will result in very unhelpful error messages. We’ll refer to this as <CognitoAuthUrl>.

Log in and out of the ASP.NET Core App

Then there’s a big chunk of code to set up the OpenId Connect configuration in Startup.ConfigureServices. This chunk of code includes the call to services.AddMvc(); it only needs to be called once.

This uses the Cognito Client Id and Client Secret from above.  It also uses a Cognito metadata URL, which is constructed as follows from your Cognito region and pool Id:

https://cognito-idp.{cognitoRegion}.amazonaws.com/{cognitoPoolId}/.well-known/openid-configuration

This Url returns information needed to tell OpenId Connect how to interact with Cognito.

            services.Configure(options =>
            {
                options.ClientId = {cognitoClientId};
                options.ClientSecret = {clientSecret};
                options.MetadataAddress = {clientMetadata};
                options.ResponseType = "code";
                options.SaveTokens = true;
                options.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidateIssuer = true
                };
            });
            var serviceProvider = services.BuildServiceProvider();
            var authOptions = serviceProvider.GetService<ioptions>();
            services.AddMvc();
            services.AddAuthentication(options =>
            {
                options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
                options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
            })
            .AddCookie()
            .AddOpenIdConnect(OpenIdConnectDefaults.AuthenticationScheme, options =>
            {
                options.ResponseType = authOptions.Value.ResponseType;
                options.MetadataAddress = authOptions.Value.MetadataAddress;
                options.ClientId = authOptions.Value.ClientId;
                options.ClientSecret = authOptions.Value.ClientSecret;
                options.SaveTokens = authOptions.Value.SaveTokens;
                options.Events = new OpenIdConnectEvents()
                {
                    OnRedirectToIdentityProviderForSignOut = OnRedirectToIdentityProviderForSignOut
                };
                options.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuer = authOptions.Value.TokenValidationParameters.ValidateIssuer
                };
            });

At this point (if you comment out the options.Events assignment above), you could add an [Authorize] attribute to a Page or Controller, and you’d be automatically redirected to Cognito to login, then redirected back upon successful authentication.

Unfortunately, the metadata Url doesn’t return the information OpenId Connect wants in order to be able to do logout out of the box, which is what the options.Events assignment starts resolving.

This block of code uses the Sign Out Url that was configured above in the Cognito configuration.

        private static Task OnRedirectToIdentityProviderForSignOut(RedirectContext context)
        {
            context.ProtocolMessage.Scope = "openid";
            context.ProtocolMessage.ResponseType = "code";

            context.ProtocolMessage.IssuerAddress = $"{CognitoAuthUrl}/logout?client_id={CognitoClientId}&logout_uri={Signed-Out-Url}&redirect_uri={Signed-Out-Url}";

            context.Properties.Items.Remove(CookieAuthenticationDefaults.AuthenticationScheme);
            context.Properties.Items.Remove(OpenIdConnectDefaults.AuthenticationScheme);

            return Task.CompletedTask;
        }

Now that all of that is configured, we need to tell ASP.NET to actually start the sign out procedure. At this point, this is pretty much standard ASP.NET Core. I defined a Post action, redirecting to the /SignedOut page that was configured with Cognito for the post-signout redirect:

public IActionResult OnPost()
{
    return SignOut(
        new AuthenticationProperties()
            { RedirectUri = Url.Page("/SignedOut", pageHandler: null,
                              values: null, protocol: Request.Scheme) },
        new[] {
            CookieAuthenticationDefaults.AuthenticationScheme,
            OpenIdConnectDefaults.AuthenticationScheme
        });
}

Groups and Policies

At this point, you can use [Authorize] to ensure that only logged-in users can access the Page/Controller/Route. However you’d probably like more fine-grained control, so for that you can add users to Cognito Groups. On the ASP.NET Core app, those groups are sent as part of the user Claims. You can create authorization polices during the Startup.Configure method as follows:

            services.AddAuthorization(options =>
            {
                options.AddPolicy("AdminOnly", policy =>
                    policy.RequireAssertion(context =>
                        context.User.HasClaim(c => c.Type == "cognito:groups" && c.Value == "Admin")));
            });

This creates a policy requiring the logged in user to be part of the Cognito group Admin. To use this on a Page/Controller/Route, refer to the policy name in the Authorize attribute:

[Authorize(Policy = "AdminOnly")]

References

Many thanks to these articles that contributed to getting this working:

  1. https://dzone.com/articles/identity-as-a-service-idaas-aws-cognito-and-aspnet
  2. https://lbadri.wordpress.com/2018/02/25/asp-net-core-2-0-oidc-authentication-using-aws-cognito/
  3. https://github.com/gianibob82/aspnet-core-cognito-integration

.


Author

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.