Securing your .NET Core API with AWS Cognito
Francois de Wet
Senior .NET Software Engineer (Consultant) // Killer Coding Ninja Monkey - 23+ Years of designing, building & deploying solutions.
This is the first article in a series where I'm planning to have a look at Authentication & Authorization, using Cloud Providers, in .NET Core.
In this article we'll first have a look at Setting up and connecting to AWS Cognito.
What will be covered in this article:
I'm assuming that most reading this article has some level of experience with creating projects in Visual Studio IDE, and will not be covering that step by step. All the source code is available on my github, and will be updated as we progress with the articles.
AWS Cognito
In order to setup the User Pool you will need an AWS Console account. If you do not have one already you can signup here.
Setting up tine User Pool
Setup a User Pool using the AWS console.
Select Review Defaults
Select Add app client
Create the App Client
Make sure to select the options as per the screen shot above. (I did find that I had to go back and reapply some of the selections)
Get AWS Ids & Secrets
Under the General setting tab we'll find the User Pool Id
Copy the Pool Id, as we'll need that to connect later.
Under the App clients tab we'll find the App Client Id & the App Client Secret (Note: Keep these secure)
While on the App client setting confirm that the Auth Flows Configuration settings are correct:
Visual Studio Project
We begin by creating a new project using the ASP.NET Core Web API template.
Nuget Packages
The following nuget packages needs to be install in the project:
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer -Version 5.0.14
Install-Package Amazon.AspNetCore.Identity.Cognito
Install-Package Amazon.Extensions.CognitoAuthentication
Install-Package AWSSDK.CognitoIdentityProvider
App Settings
In development the settings can be configure in appsettings.Development.json and for the purposes of this example that is where we will set them; however is is not recommended or in any way best practice to store secrets in app setting. Rather make use of secrets management.
Add the following to the appsettings.Development.json file.
领英推荐
"AWS": {
? ? "Region": "<your region id goes here>",
? ? "UserPoolClientId": "<your user pool client id goes here>",
? ? "UserPoolClientSecret": "<your user pool client secret goes here>",
? ? "UserPoolId": "<your user pool id goes here>"
}
Replace all the values with the Id's & Keys from AWS. The region id can be found in the Cognito url or in the region selector in AWS Console.
The Code
Firstly we need to setup Cognito in our Startup.cs file
ConfigureServices Method:
public void ConfigureServices(IServiceCollection services)
{
// Adds Amazon Cognito as Identity Provider
services.AddCognitoIdentity();
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.Authority = $"https://cognito-idp.{this.config.AWS.Region}.amazonaws.com/{this.config.AWS.UserPoolId}";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidIssuer = $"https://cognito-idp.{this.config.AWS.Region}.amazonaws.com/{this.config.AWS.UserPoolId}",
ValidateLifetime = true,
LifetimeValidator = (before, expires, token, param) => expires > DateTime.UtcNow,
ValidateAudience = false,
};
});
...
}
Configure Method
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
// If not already enabled, you will need to enable ASP.NET Core authentication
app.UseAuthentication();
...
}
The Models
For now we'll need 3 Models that will be used in the controller methods.
The User Login Model:
public class UserLoginModel
{
? ? public string Email { get; set; }
? ? public string Password { get; set; }
}
The Update Password Model
public class UpdatePasswordModel : UserLoginModel
{
? ? public string NewPassword { get; set; }
}
The Token Response Model
public class TokenResponseModel
{
? ? public string RefreshToken { get; set; }
? ? ? ??
? ? public string Token { get; set; }
? ? public DateTimeOffset Expiry { get; set; }
}
The Controller
In the controllers folder add a new controller called AccountController.
We'll make use of constructor Dependency Injection to get access to the Identity Provider, User Pool & Cognito Configuration.
public AccountController(IAmazonCognitoIdentityProvider identityProvider, CognitoUserPool userPool, AwsCognitoConfig cognitoConfig)
We'll add two methods to the controller, LoginAsync & ChangePasswordAsync.
LoginAsync
[HttpPost]
[Route("login")]
public async Task<TokenResponseModel> LoginAsync([FromBody]UserLoginModel model)
{
? ? var user = new CognitoUser(
? ? ? ? model.Email,
? ? ? ? this.cognitoConfig.UserPoolClientId,
? ? ? ? this.userPool,
? ? ? ? this.identityProvider,
? ? ? ? clientSecret: this.cognitoConfig.UserPoolClientSecret
? ? );
? ? var authRequest = new InitiateSrpAuthRequest()
? ? {
? ? ? ? Password = model.Password,
? ? };
? ? var authResponse = await user.StartWithSrpAuthAsync(authRequest);
? ? if (authResponse.ChallengeName == ChallengeNameType.NEW_PASSWORD_REQUIRED)
? ? {
? ? ? ? throw new Exception("Update your temporary password.");
? ? }
? ? var timespan = TimeSpan.FromSeconds(authResponse.AuthenticationResult.ExpiresIn);
? ? var expiry = DateTimeOffset.UtcNow + timespan;
? ? return new TokenResponseModel
? ? {
? ? ? ? RefreshToken = authResponse.AuthenticationResult.RefreshToken,
? ? ? ? Token = authResponse.AuthenticationResult.AccessToken,
? ? ? ? Expiry = expiry,
? ? };
}
ChangePasswordAsync
[HttpPost]
[Route("updatepassword")]
public async Task<string> ChangePasswordAsync([FromBody]UpdatePasswordModel model)
{
var user = new CognitoUser(
model.Email,
this.cognitoConfig.UserPoolClientId,
this.userPool,
this.identityProvider,
clientSecret: this.cognitoConfig.UserPoolClientSecret
);
var authRequest = new InitiateSrpAuthRequest()
{
Password = model.Password,
};
var authResponse = await user.StartWithSrpAuthAsync(authRequest);
var response = await user.RespondToNewPasswordRequiredAsync( new RespondToNewPasswordRequiredRequest
{
SessionID = authResponse.SessionID,
NewPassword = model.NewPassword,
});
return "Password changed successfully.";
}
With that we can now use the Authorize attribute on the controller classes.
In order for the token to be used in the Swagger definition we need to setup swagger that it know about the authentication and how it should be applied.
In the ConfigureServices method in Startup.cs add the following code:
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "CognitoSampleApi", Version = "v1" });
c.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme {
In = ParameterLocation.Header,?
Description = "Please enter into field the word 'Bearer' followed by a space and the JWT value",
Name = "Authorization",
Type = SecuritySchemeType.ApiKey?
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement {
{?
new OpenApiSecurityScheme?
{?
Reference = new OpenApiReference?
{?
Type = ReferenceType.SecurityScheme,
Id = "Bearer"?
}?
},
new string[] { }?
}?
});
});
Running the App
When we now run the application we get the following swagger display:
We can now use the login endpoint the get the JWT token that can be used to Authorize the protected API calls.
Conclusion
Working with AWS Cognito makes it easy to implement authentication, that is secure, in your system. Additionally methods for create user, forgot password, refresh token etc. can also be implemented, that I'll leave for another article, which will build on this existing code.
All the source code is available on my github. Feel free to head over there and download the code.