Skip to content

An ASP.NET Core Web API project using AWS Cognito for authorization, for deployment to AWS Lambda as a demo of SPA with Cognito running serverless.

Notifications You must be signed in to change notification settings

devnull/ServerlessWebApiWithCognito

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

8 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Custom Authorizer for Serverless ASP.NET Core Web APIs with Lambda and Cognito

Warning! This project is from September 2017, prior to .NET Core 2.0 support in Lambda

** If you use this, use the 2. runtime, update the Nuget dependencies, and so on.**

In real-world web-applications, you nearly always want some way to authenticate users – either all of them, or some subset of them – in order to gate access to some features. In ASP.NET Web API, developers are used to adding the [Authorize] attribute to controller methods, and then relying on a membership database to store users and their roles. With Amazon Cognito User Pools, however, we can offload the storage, management and authentication of users and their roles, while still leveraging the [Authorize] attribute plus a custom AuthorizationHandler class, to control access to Web API methods.

In this walk-through, we'll create an Amazon Cognito User Pool to store and authenticate our users, an ASP.NET Core Web API serverless project that will be hosted in AWS Lambda and fronted by API Gateway, and a simple Calendar SPA consisting of HTML and JavaScript, which will be part of the Web API project and served by the Lambda function. Our client-side script will pass a user's login credentials to Cognito, get back a JSON Web Token (JWT), and pass that in the HTTP Authorization header to our Web API methods that require authorization. We'll allow reading calendar events for everybody, but restrict creating and editing them to users in a group that we specify.

Create an Amazon Cognito User Pool

Our Cognito User Pool will contain our users, and the groups to which they belong. For this walk-through, we'll create and configure the User Pool using the AWS Management console, and then add a couple of users manually. For an application with lots of users, you would typically bulk import your users, or allow them to register via your web-application, both of which are supported in Cognito.

In the AWS Management Console, navigate to the AWS Cognito home page, and then click the "Manage your User Pools" button.

Next, create a user pool by clicking the blue, "Create a user pool" button at the top right. Give your user pool a name; for this walk-through, we'll use the name, "WebApi Calendar App Users", and then choose "Review Defaults" to see the default settings. You should see something like the following:

Before we create the pool, let's add an App Client that is allowed to authenticate against our pool. Click the, "Add app client…" link, which takes you to the App Clients list for this User Pool. There aren't any yet, so click the, "Add an app client" link, and specify a name for the JavaScript web-app we'll be creating later. We'll use "CalendarWebClientApp" for this walk-thru. You need to uncheck the "Generate client secret" option, as the Cognito JavaScript SDK doesn't support using secrets (since anyone can view the source code in their browser). You should see something similar to the image below:

Create the app client, then the "Return to pool details" link. We can now see our App Client name next to "App clients". Click the blue "Create Pool" button to create the User Pool. After the pool is successfully created, copy the Pool Id from the top of the page and save it for later. Then click the "App client settings" link in the same navigation menu, and locate the App client ID for CalendarWebClientApp, and save that for later also.

Finally, let's create a Cognito group and a user for it. We'll log into our web application with this user later. Click the "Users and groups" link in the left-hand navigation menu, then on the Groups tab, create a new group called, "CalendarWriter". We don't need to select an IAM role. On the Users tab, create a new user. Pick a username, temporary password, and supply an email address. Uncheck "Mark phone number as verified" unless you want to provide a phone number. Then, add the user you just created to the "CalendarWriter" group. That's the group we'll require to create and edit events. New users created via the admin console this way will have to choose a new password on first login – we'll handle that in JavaScript.

Now we're ready to create the ASP.NET Core Web API project that will run in our Lambda function.

Create AWS Serverless Application (.NET Core)

Ensure you have already installed the latest version of the AWS Toolkit for Visual Studio, which installs the project templates for AWS projects. We're creating a new "AWS Serverless Application (.NET Core)" project. After selecting that project type, we'll choose the "ASP.NET Core Web API" blueprint from the blueprints list. This will generate skeleton code for our project, and also a CloudFormation template that will deploy the solution. This blueprint relies on the Amazon.Lambda.AspNetCoreServer NuGet package to translate calls between API Gateway and the ASP.NET Core framework.

Note that you can do all this on Mac OS X or Linux also. Once you have the .NET Core framework installed, you can install the AWS templates with the dotnet new command:

Bash

@:~$ dotnet new -i Amazon.Lambda.Templates::*

The "Lambda ASP.NET Core Web API" template (short name lambda.AspNetCoreWebAPI) is then available to create new projects. You could then use Visual Studio Code, or any other code editor to write your code, and deploy using the CLI or AWS Management Console. For this walk-through, though, we'll use Visual Studio 2017 and the AWS Toolkit for Visual Studio.

The ASP.NET Core Web API blueprint project template creates two entry point classes, LocalEntryPoint.cs and LambdaEntryPoint.cs. The LocalEntryPoint class is used when running in your local dev environment, and leverages Kestrel, the ASP.NET Core webserver, while the LambdaEntryPoint relies on API Gateway. This makes it really easy to test out your project locally before deploying it. In fact, you should make sure everything is set up properly by building the project and then running it. Verify the endpoint http://localhost:5000/api/values returns the json array ["value1","value2"] by visiting it in a web browser. That will call the ValuesController's Get method.

The blueprint project also creates an S3ProxyController, which we don't need for this project, so we can delete that. We can also remove the CloudFormation template parameters ShouldCreateBucket and BucketName, the conditions CreateS3Bucket and BucketNameGenerated, and all references to them from the serverless.template file.

Before we write any code, let's add the NuGet package, "Microsoft.AspNetCore.Authentication.JwtBearer", v1.0.1. Be careful not to install the latest version , since it requires .NET Standard 2.0, which isn't yet supported in AWS Lambda. You'll notice that some other NuGet packages are already installed as part of the project template. You can optionally remove the AWSSDK.S3 package, and then delete this line from Startup.ConfigureServices so that the project will still build: services.AddAWSService<Amazon.S3.IAmazonS3>();

Once we add the Microsoft.AspNetCore.Authentication.JwtBearer v1.0.1 package, we can add our implementation of IAuthorizationRequirement, and our customer AuthorizationHandler class as in the following code samples.

C#

classCognitoGroupAuthorizationRequirement : IAuthorizationRequirement

{

publicstring CognitoGroup { get; privateset; }

public CognitoGroupAuthorizationRequirement(string cognitoGroup)

{

    CognitoGroup = cognitoGroup;

}

}

C#

classCognitoGroupAuthorizationHandler : AuthorizationHandler<CognitoGroupAuthorizationRequirement>

{

protectedoverrideTask HandleRequirementAsync(AuthorizationHandlerContext context, CognitoGroupAuthorizationRequirement requirement)

{

    if (!context.User.HasClaim(c =&gt; c.Type == &quot;cognito:groups&quot;))

    {

        context.Fail();

        returnTask.CompletedTask;

    }

    var group = context.User.FindFirst(c =&gt; c.Type == &quot;cognito:groups&quot;).Value;

    if (group == requirement.CognitoGroup)

    {

        context.Succeed(requirement);

    }

    else

    {

        context.Fail();

    }

    returnTask.CompletedTask;

}

}

Our custom handler just checks to see if the Cognito group is equal to whatever group we specify in the Authorization Policy we add. Let's register our policy and add a singleton of the handler class by editing the Startup.ConfigureServices method to look like the below example.

C#

publicvoid ConfigureServices(IServiceCollection services)

{

// add our Cognito group authorization requirement, specifying CalendarWriter as the group

services.AddAuthorization(

    options =&gt; options.AddPolicy(&quot;InCalendarWriterGroup&quot;, policy =&gt; policy.Requirements.Add(newCognitoGroupAuthorizationRequirement(&quot;CalendarWriter&quot;)))

);

// add a singleton of our cognito authorization handler

services.AddSingleton&lt;IAuthorizationHandler, CognitoGroupAuthorizationHandler&gt;();

services.AddMvc();

// Pull in any SDK configuration from Configuration object

services.AddDefaultAWSOptions(Configuration.GetAWSOptions());

}

For our simple Calendar app, we'll create one controller, "EventsController", and one model, "CalendarEvent". The model will be serialized and deserialized automatically as it's passed to and from the controller methods. It's just a POCO class that maps to the required properties of a FullCalendar event. FullCalendar is an open-source JavaScript calendar built on JQuery, which we'll use in the client-side portion of the project.

C#

publicclassCalendarEvent

{

publicstring id { get; set; }

publicstring title { get; set; }

publicbool allDay { get; set; }

publicDateTime start { get; set; }

publicDateTime end { get; set; }

}

In our API controller, we'll implement three methods – a Get() method that returns all events in a date range, a Post() method to create new events, and a Put() method to edit existing events. We'll decorate the Put() and Post() methods with our custom [Authorize] attribute and specify the InCalendarWriterGroup policy we registered in Startup.ConfigureServices. To keep things really simple, for this walk-thru we'll just be storing events in a static collection in the controller – in a real application, you would want to persist them somewhere durable and shared between different Lambda invocations, like Amazon DynamoDB, a serverless NoSQL database. Lambda functions will "go away" after about 5 minutes of non-use, so any events we add will disappear when that happens. Also note the "test event" we create in the constructor – that's just to demonstrate the ability to see events when not authenticated.

C#

[Route("api/[controller]")]

publicclassEventsController : Controller

{

privatestaticList&lt;CalendarEvent&gt; events;

public EventsController()

{

    if (events == null)

    {

        var calEvent = newCalendarEvent()

        {

            id = Guid.NewGuid().ToString(),

            title = &quot;Test Event Title&quot;,

            allDay = false,

            start = DateTime.Now,

            end = DateTime.Now.AddHours(2)

        };

        events = newList&lt;CalendarEvent&gt;( new[] { calEvent } );

    }

}

// GET api/events

[HttpGet]

publicIEnumerable&lt;CalendarEvent&gt; Get(DateTime? start, DateTime? end)

{

    start = start ?? newDateTime(DateTime.Now.Year, DateTime.Now.Month, 1);

    end = end ?? newDateTime(DateTime.Now.Year, DateTime.Now.Month, DateTime.DaysInMonth(DateTime.Now.Year, DateTime.Now.Month));

    return events.FindAll(x =&gt; x.end &gt;= start &amp;&amp; x.start &lt;= end);

}

// POST api/events

[HttpPost]

[Authorize(Policy = &quot;InCalendarWriterGroup&quot;)]

publicvoid Post([FromForm]CalendarEvent calEvent)

{

    calEvent.id = Guid.NewGuid().ToString();

    events.Add(calEvent);

}

// PUT api/events/5

[HttpPut(&quot;{id}&quot;)]

[Authorize(Policy = &quot;InCalendarWriterGroup&quot;)]

publicvoid Put(string id, [FromForm]CalendarEvent calEvent)

{

    var index = events.FindIndex(x =&gt; x.id == id);

    events[index] = calEvent;

}

}

Finally, we'll add code to the Startup.Configure method configure the JWT authentication options, and also to enable our app to serve static files (like html, css and JS) as well default files, so we don't have to specify "index.html" in the URL of the main page. Static files support is enabled with the NuGet package, "Microsoft.AspNetCore.StaticFiles", v1.0.0. Again, don't use the latest version since that requires .NET Standard 2.0.

We're just hard-coding the "Audience" (the app client ID from Cognito) and "Authority" (based on User Pool ID) values for simplicity. In a production app, you could pass those values as Environment Variables at deploy time, then read the values with Environment.GetEnvironmentVariable. Your Configure method should look like this.

C#

publicvoid Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)

{

loggerFactory.AddLambdaLogger(Configuration.GetLambdaLoggerOptions());

app.UseDefaultFiles();  //needs to be before the app.UseStaticFiles() call below

app.UseStaticFiles();

app.UseJwtBearerAuthentication(newJwtBearerOptions

{

    Audience = &quot;&lt;the app ID you copied from Cognito earlier&gt;&quot;,

    Authority = &quot;https://cognito-idp.&lt;region&gt;.amazonaws.com/&lt;User Pool Id&gt;&quot;,

    AutomaticAuthenticate = true,

    RequireHttpsMetadata = false//false for dev only, for production the JWT should be sent via https

});

app.UseMvc();

}

To build the "Authority" value, just plug in your region and User Pool Id that you copied earlier. If your region was Oregon (us-west-2) and your User Pool Id was us-west-2_AbcDefGhi, for example, your Authority string would be "https://cognito-idp.us-west-2.amazonaws.com/us-west-2\_AbcDefGhi". For testing locally without worrying about certificates, set "RequireHttpsMetadata" to false. For any production app, you should set this to true so that the JWT token is only sent over HTTPS. The JwtBearer middleware validates the JWT's digital signature, checks the expiration and does other checks, then creates a ClaimsPrincipal for the current user.

Now we're ready to add our client-side application!

Add HTML, CSS and JavaScript

ASP.NET Core uses the folder wwwroot as web root by default, so we'll create that folder in our project, and add our web files to it. You can either install the JavaScript files manually, or using a tool like npm. We'll need the following libraries for this demo app. I installed the Amazon Cognito, FullCalendar and Moment.js files locally, and referenced the JQuery files via URL, but you can change that to suit your needs. I recommend installing with npm to ensure you get all the dependencies, but links to downloads are included below.

Since this demo app is very simple, I embedded the JavaScript used to authenticate and create/edit events inline. You can see the full source code in the Git repo. We set the source for events in FullCalendar to "api/events" (FullCalendar passes the start and end arguments), and add handlers for dayClick and eventClick events. Reading events (the "Get()" method in EventsController) doesn't require authorization.

Creating or editing events will require a valid JWT showing the user in the CalendarWriter group. So we add a login button to our HTML, and wire it up to an authentication flow. When authenticating, we'll handle the onSuccess, onFailure, and newPasswordRequired events. On first login, the newPasswordRequired event is raised. We'll prompt the user for a new password, and then they'll need to log in with it. This will change the user's status in Cognito from FORCE_CHANGE_PASSWORD to CONFIRMED.

The JavaScript in the authenticate() function reads the username and password values from the form, configures the user pool (with the User Pool Id and App Client Id you copied earlier), then calls the CognitoUser.authenticateUser() function which makes the call out to Amazon Cognito. The response (if successful) includes the JWT token, which we put into local storage to use for subsequent Post or Put calls to our calendar API. To keep things simple for this walk-thru, we're not implementing any refresh strategy – when the JWT expires after 1 hour, subsequent PUT/POST calls will fail with a 401 Unauthorized response.

JavaScript

function authenticate() {

    var authenticationData = {

        Username: $(&#39;#username&#39;).val(),

        Password: $(&#39;#password&#39;).val(),

    };

    var CognitoIdentityServiceProvider = AWSCognito.CognitoIdentityServiceProvider;

    var authenticationDetails = newCognitoIdentityServiceProvider.AuthenticationDetails(authenticationData);

    var CognitoUserPool = AmazonCognitoIdentity.CognitoUserPool;

    var poolData = {

        UserPoolId: &#39;us-west-2\_AbcDefGhi&#39;, //user pool id

        ClientId: &#39;&lt;app-client-id&gt;&#39;//app client id

    };

    var userPool = new CognitoUserPool(poolData);

    var userData = {

        Username: authenticationData.Username,

        Pool: userPool

    };

    var cognitoUser = newCognitoIdentityServiceProvider.CognitoUser(userData);

    cognitoUser.authenticateUser(authenticationDetails, {

        onSuccess: function (result) {

            localStorage.setItem(&#39;jwt&#39;, result.getIdToken().getJwtToken())

            console.log(&#39;jwt token: &#39; + localStorage.getItem(&#39;jwt&#39;));

            $(&#39;#loginform&#39;).dialog(&#39;close&#39;);

            $(&#39;#login&#39;).prop(&quot;disabled&quot;, true);

        },

        onFailure: function (err) {

            alert(err);

        },

        newPasswordRequired: function (userAttributes, requiredAttributes) {

            // the api doesn&#39;t accept this field back

            delete userAttributes.email\_verified;

            delete userAttributes.phone\_number\_verified;

            $(&#39;#loginform&#39;).dialog(&#39;close&#39;);

            $(&#39;#changepass&#39;).dialog({ modal: true, title: &#39;1st Login - Change Password&#39;, width: 360 });

            $(&#39;#changebtn&#39;).click(function () {

                if ($(&#39;#newpass&#39;).val() == $(&#39;#confirmpass&#39;).val()) {

                    cognitoUser.completeNewPasswordChallenge($(&#39;#newpass&#39;).val(), userAttributes, this);

                    alert(&#39;Password changed, please log in with the new password&#39;);

                    $(&#39;#changepass&#39;).dialog(&#39;close&#39;);

                    Password: $(&#39;#password&#39;).val(&#39;&#39;);

                    showLogin();

                }

            });

        }

    });

}

One important note: we're implementing the UI and authentication flow ourselves, in HTML and JavaScript. Cognito can actually host the login form itself, and handle the change password flow, and other scenarios as well. You can even customize the UI with your own logo and CSS.

The script above writes the JWT to the console, which makes it easy to test out the EventController API with Postman. Just remember that the Authorization header value is of the form, "bearer <jwt>", and don't forget the single space between them.

You can run the entire project locally from Visual Studio. Once the command-line window shows up, just go to http://localhost:5000/ in a browser (don't forget the trailing slash). In order for API Gateway to handle requests to the root path, however, we need to add a resource corresponding to the "/" path to our serverless.template CloudFormation file, as a child of the "Events" node. Now GET requests to "/" will return our index.html file.

To deploy to AWS, first stop the application (if it's running locally), then right-click the project node in Solution Explorer and select, "Publish to AWS Lambda". The AWS Toolkit's CloudFormation tab will open and track the status of the stack as it's deployed. When it is finished, you can copy the AWS Serverless URL into a browser, and add a trailing slash ("/"). The initial load will take a few seconds, as it is a cold start for the Lambda function. Note that the URL includes the API Gateway stage, which in our case is "PROD". If you use your own domain, you can configure it to point to that stage, so your website would be at www.yourdomain.com/ (for example) instead of the long API Gateway URL and stage name.

![](data:image/*;base64,iVBORw0KGgoAAAANSUhEUgAABtsAAALyCAYAAAC7CmvUAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAAGdYAABnWARjRyu0AAP+lSURBVHhe7P0JeBNXgjb6d09Pz9Lz9UzP0jO9Z1Mmm2ww+272BBLCFvawJBBiIBCWhrBkD1mgSUhCCCQBOpCEkIRAMARjbGMw4B0b7wuWLduyLS9S33zf/eb+73Pv3Pd/TpVKKpVKsiRsluTleX5PgurUqVVFVb065/woJqYniIiIiIiIiIiIiIiIiChyDNuIiIiIiIiIiIiIiIiIouQXtv1oYz4REf1A6K//RERERERERERERBQdhm1ERD9Q+us/EREREREREREREUWHYRsR0Q+U/vpPRERERERERERERNFh2EZE9AOlv/4TERERERERERERUXQYthER/UDpr/9EREREREREREREFB2GbUREP1D66z8RERERERERERERRYdhGxHRD5T++k9ERERERERERERE0WHYRkT0A6W//hMRERERERERERFRdBi2ERH9QOmv/0REREREREREREQUHYZtREQ/UPrrPxERERERERERERFFh2EbEdEPlP76T0RERERERERERETRYdhGRPQDpb/+ExEREREREREREVF0GLYREf1A6a//RERERERERERERBQdhm1ERD9Q+us/EREREREREREREUXnexG2xWX8F+SfxK/yTKeTwVdu7i+9G2l/eNalKqPUfDp9b61NcuBy8/9WfFP2F/zbq5dNy+nN+cLmnSftyne47U/FpuWC0V//f6isa7/Fd999hxNre5hOp+hx3/5wace+ZNc00+lXq7vrD+VGPK/5XSMiIiIiIiK6/qIL2zyBgPHP9QoIGLZFiGGbP4Zt14bhuvHfbc2I26Db5xtsSPzv//ZMDf7nWh2nPfntSohlNq27RBK4XW3QJumv/z9UfEndfbhvf7i6Owzr7vpDuRHPa37XiIiIiIiIiK6/yMK2DaXY1vbf+O///i9se9fwwvsrt19AsKBclDO+TO8mDNsixLDNH/dHt5PfUb/rhidYC/saoV17rtE1RdLCtj47y4Mym+9qhRO4dUXQJumv/z9UN/pL6rUn/oK/lOzGVOvN9xK9K/btzbz91H1utrCtu89jhm1ERERERERE119EYVvYodY1fjHOsC1C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SLdDCnYeh6ormu9mONtORERERERERN0jsrBNhkDGFmNBPpdhh1nrMhkCGF+ia5/JP8aX+No0v8/lMst9QYBpsKKFCybhWKdBgQwbjEGDFuDpQzBd+GAMBeSy9MsJJ/zRysg/puW6anlmQUa4dYezb8Ldf+HwBD/yj/G4adtoGuBFsK+0/SH/+K2fts6GbYnoHI6w7ohDpnD3dXduYxjkvH7XA89xDXY+yGNkdv2grqW//gejBCTGcER7Ua0LwfQvwI0vfOVL8EhfimtlgpXrquWZhgJh1h3Ovgl3/2nraHxJLz83HWsqgu3v6hf6kt+x17bHuJ0my5XLC/aZ1B3bT9ee6fcqxHloer5p4azh+Ac7X8L/3qrljPVq69fZ9zLS7ZCCnsch6mLYRkRERERERHRziawbSV2Aof9j2lrGUFb/cl7/0l7+UV64e176m77E1wUF2h/9S/qgwZJuPjlNK9dpUBBkO4MFIFq9+j/GfRJ0HfWCBTAGV728EIFOp3WHs28i3H8heeoyDV7C2F/h7Cv9/jA9N3X1acI+hyOtO8SxMRXuvu7ObQyT8ViEqkMuO+JWkBQx/fU/GP0LbL1gL3W1l9N6xhBGKxPqxXCwAMfoapcX6iV1Z3WHs2/C3X/asowv6Y3zm73E16Zpwgm9NNG+0NcHHsoydQGFWXn952bzBgspumL76dozO/ahzsNwj78ky2h1GctLnZ0T2vlnNq9xmtn3siu2Q5seqq5ov5v6skRERERERER07XTZmG1EZCLS8Ky73CjrQTcU/fWfqDN8oU9ERERERERERGSOYRtRd2LYRjcw/fWfqDMM24iIiIiIiIiIiMwxbCPqTgzb6Aamv/4TdYZhGxERERERERERkTmGbUTdiWEb3cD013+izjBsIyIiIiIiIiIiMsewjag7MWyjG5j++k/UGYZtRERERERERERE5hi2ERH9QOmv/0REREREREREREQUHYZtREQ/UPrrPxERERERERERERFFh2EbERERERERERERERERUbQYthERERERERERERERERFFiWEbERERERERERERERERUZQYthERERERERERERERERFFSR+2xcePVeg/IyIiIiIiIiIiIiIiIiIffabGsI2IiIiIiIiIiIiIiIgoAgzbiIiIiIiIiIiIiIiIiKLEsI2IiIiIiIiIiIiIiIgoSgzbiIiI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

About

An ASP.NET Core Web API project using AWS Cognito for authorization, for deployment to AWS Lambda as a demo of SPA with Cognito running serverless.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • C# 55.3%
  • HTML 44.7%