While working with .net core I needed to create API. For this what I aimed to have was proper authentication. Therefore I decided to use JSON Web Token (JWT) authentication.
However I wanted to avoid creating any of this logic by myself or spending too much time on it. That’s why I decided to use AWS Cognito User Pools to provide me with user management and to generate JWT I need.
It took me some time to gather information how to wire it all together so I will try to outline the most important.
AWS setup
- Create user pool in AWS Cognito
- Get the newly created user pool ID and run the following command
curl https://cognito-idp.<region>.amazonaws.com/<user-pool-id>/.well-known/jwks.json > result.json
* if you want to you can also just navigate to the URL (https://cognito-idp.<region>.amazonaws.com/<user-pool-id>/.well-known/jwks.json ) . Just replace region and user pool ID with correct information.
- The information you receive will be used by us to validate the tokens given by AWS.
- Save the results for later use.
.Net core API project
- Create new .net core webapi project
dotnet new webapi
- Install additional packages
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearer dotnet add package Microsoft.IdentityModel.Tokens dotnet add package Microsoft.AspNetCore.Identity
- add JWT authentication policy ( we will decorate our controllers with it )
services.AddAuthorization(auth => { auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder() .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser() .Build()); });
- Before making further modifications we will add 2 methods used which will be used to validate the signature and issuer ( this has potential to be made much better 🙂 )Key is the “n” value and Expo is the “e” value in the keys you got form the url in AWS setup
public RsaSecurityKey SigningKey(string Key, string Expo) { RSA rrr = RSA.Create(); rrr.ImportParameters( new RSAParameters() { Modulus = Base64UrlEncoder.DecodeBytes(Key), Exponent = Base64UrlEncoder.DecodeBytes(Expo) } ); return new RsaSecurityKey(rrr); } public TokenValidationParameters TokenValidationParameters(string issuer) { // Basic settings - signing key to validate with, audience and issuer. return new TokenValidationParameters { // Basic settings - signing key to validate with, IssuerSigningKey and issuer. IssuerSigningKey = this.SigningKey(<key-comes-here>,<expo-comes-here>), ValidIssuer = issuer, // when receiving a token, check that the signing key ValidateIssuerSigningKey = true, // When receiving a token, check that we've signed it. ValidateIssuer = true, // When receiving a token, check that it is still valid. ValidateLifetime = true, // Do not validate Audience on the "access" token since Cognito does not supply it but it is on the "id" ValidateAudience = false, // This defines the maximum allowable clock skew - i.e. provides a tolerance on the token expiry time // when validating the lifetime. As we're creating the tokens locally and validating them on the same // machines which should have synchronised time, this can be set to zero. Where external tokens are // used, some leeway here could be useful. ClockSkew = TimeSpan.FromMinutes(0) }; }
- Modify Configure method to enable JWT
app.UseJwtBearerAuthentication(new JwtBearerOptions() { TokenValidationParameters = this.TokenValidationParameters(<issuer-comes-here>) });
The issuer format has the following format : https://cognito-idp.<region>.amazonaws.com/<user-pool-id>
- Modify controller and enable the authentication by using the following decorator
[Authorize(Policy = "Bearer")]
Testing the solution
With the authentication enabled we get the following while requesting controller
> http http://localhost:5000/api/values HTTP/1.1 401 Unauthorized Content-Length: 0 Date: Sun, 30 Jul 2017 11:41:33 GMT Server: Kestrel WWW-Authenticate: Bearer
And if we pass the JWT 🙂
http --auth-type=jwt -v http://localhost:5000/api/values GET /api/values HTTP/1.1 Accept: */* Accept-Encoding: gzip, deflate Authorization: Bearer ey..... Host: localhost:5000 User-Agent: HTTPie/0.9.9 HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Date: Sun, 30 Jul 2017 11:45:41 GMT Server: Kestrel Transfer-Encoding: chunked
Code
Full gist below 🙂
Very nice example. I want to use similar approach for Cognito authenticating my ASP.NET Core web client razor pages. So user log in using a log in page (this needs to be my log in page not aws).. entered username/password are authenticated against AWS Cognito user pool, using .net sdk. I reached this point where aws sdk returns encoded id token and access token in encoded format, now how I can use this token as .net identity user or logged in user and allow to navigate to razor pages decorated with authorized attribute not much documentation on this on AWS……
thanks in advance
Havent been investigating that option. Surely you can leverage AWS Cognito user details from login and use that across your application.
If you come across some useful solution post some info
thank for this… please could you also post an example of how to call the web api from .net core web application using httpclient passing the token in request header? how to build the token?
Thanks for posting, this helped me out!
Im getting invalid audience. I did used the client’s id..
Can you post your code which if you are using modified one ?
sorry for the late reply. i fix it by not validating the audience.
You need to dispose RSA object to avoid Memory Leak:
public RsaSecurityKey SigningKey(string key, string expo)
{
using (RSA rsa = RSA.Create())
{
rsa.ImportParameters(
new RSAParameters
{
Modulus = Base64UrlEncoder.DecodeBytes(key),
Exponent = Base64UrlEncoder.DecodeBytes(expo)
}
);
return new RsaSecurityKey(rsa);
}
}
Thanks Vladimir! That is really good point! I have not looked at optimisation of the code. This one was the “v1” to get things working!
Great article, thank you!
Have you tried a similar approach for an Asp.net API instead of .netCore?
Thanks.
Nope – only .netCore
Hello Robert
have you tried this for asp.net web api?
I’ve found that you can simply just use the following in .NET Core 1.1.x
app.UseJwtBearerAuthentication(new JwtBearerOptions()
{
Authority = “https://cognito-idp..amazonaws.com/”,
Audience = “”
});
Have not tested this approach 🙂 Will take a look
With ASP.NET Core 2.0 …
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.IncludeErrorDetails = true;
// ID of the client application (get it from your cognito user pool config); either hardcoded or configureable via IConfiguration if needed
options.Audience = “876jkgjkhgkjhgkjhg”;
// URL of Identity Server; use IConfiguration instead of hardcoding
options.Authority = “https://cognito-idp.us-east-1.amazonaws.com/us-east-1_asdasdasd”;
// require HTTPS (may be disabled in development, but I advise against it)
options.RequireHttpsMetadata = false;
});
im getting this: www-authenticate →Bearer error=”invalid_token”, error_description=”The audience is invalid”.
Any idea why? also, how do you set up cognito to use it like this?
Thank you for the exellent example! it has worked for me just fine.
I am trying to work out the other part of the equation – client auth.
Are you able to share any resources you found that guide you through setting up cognito as client_credentials flow provider?
The AWS documentation is a bit vague on this one.
Specifically, this guide http://docs.aws.amazon.com/cognito/latest/developerguide/token-endpoint.html
mentioning the following:
client_id
Must be a preregistered client in the user pool. The client must be enabled for Amazon Cognito federation.
how did you manage to enable your client for Amazon Cognito federation?
Thanks in advance!
Igal
What is the Token to send via the Authorization header? I am trying out but it keeps saying 401 Unauthorized.
If you authenticate to cognito you will get response with JWT which can be used as Authorization header for requests