I would like to use Azure AD B2C but have several difficulties using it.
One problem I have is to validate the signature of the token.
First I wanted to validate the token "manually" using jwt.io.
According to the Microsoft Docs, validating the signature should work like this:
Your app can use the kid claim in the JWT header to select the public key in the JSON document that is used to sign a particular token. It can then perform signature validation by using the correct public key and the indicated algorithm.
My understandig: Grab the kid value out of the header, lookup the key in the metadata under the location of jwks_uri, (assumption) use the value of "n" to verify the signature.
But Jwt.io, jsonwebtoken.io, and jose-jwt all say, that the signature is invalid.
What am I missing?
Jwt.io seems to only support HS265 with a string secret and RS256 with a string secret or a certificate.
Azure AD B2C uses the more native form of RS256 which as per RFC 3447, section 3.1 defines that the public key consists of two components: n and e. The JWK contains both n and e which can be used to generate public key and validate the token signature.
In order to use Jwt.io, you'll need to convert Azure AD B2C's n + e format for the key to a cert format. See this example for a reference on how to do this: Go Language Convert Modulus exponent to X.509 certificate
There is now a way to validate tokens using two npm packages.
Resources
The information was made available at: Microsoft - Example-verifying-validation-tokens
Package Versions Used
jwks-rsa: "1.8.1"
jsonwebtoken: "8.5.1"
How To
The example below was modified from example in Microsoft - Example-verifying-validation-tokens
Retrieving "Issuer" and "User Flow" Name Values From Azure
Retrieving Application Id
Code Example
import jwt from 'jsonwebtoken';
import jkwsClient from 'jwks-rsa';
// Variables that need to be defined based on your Azure B2C Configuration.
const jwksUri = 'https://MY-B2C-TENANT.b2clogin.com/MY-B2C-TENANT.onmicrosoft.com/MY-USER-FLOW-NAME/discovery/v2.0/keys';
const client = jkwsClient({
jwksUri
});
export function getKey(header, callback) {
client.getSigningKey(header.kid, (err, key) => {
var signingKey = key.getPublicKey();
callback(null, signingKey);
});
}
export function isTokenValid(token, applicationId, issuerUri) {
return new Promise((resolve) => {
const options = {
audience: [applicationId],
issuer: [issuerUri]
};
jwt.verify(token, getKey, options, (err, decoded) => {
if (err) {
// eslint-disable-next-line no-console
console.error('Jwt Validation Failed', err);
resolve(false);
} else {
// eslint-disable-next-line no-console
console.debug(decoded)
resolve(true);
}
});
});
}
Usage
const applicationId = 'APPLICATION ID OF B2C TENANT'
const issuerUri = 'In the User Flow Properties'
const valid = isTokenValid('some token value without "Bearer", applicationId, issuerUri);
Notes
The jwksUri contains the user "User Policy". If you have multiple "User Policies", you will need to figure out how to instantiate the jkwsClient with correct jwksUri based on the each "User Policy" flow.
While you can leverage jwt.io or jwt.ms, a token can be decoded in the debugger console. This will give you some key information such as the "iss" (Issuer) and the "kid":
'e3R5cDogIkpXVCIsIGFsZzogIlJTMjU2Iiwga2lkOiAiWU9VX1NORUFLWV9SQUJCSVQifQ==.e2V4cDogMTU5NjYwMTc4NiwgbmJmOiAxNTk2NTk4MTg2LCB2ZXI6ICIxLjAiLCBpc3M6ICJodHRwczovL3doaXRlLXJhYmJpdC5iMmNsb2dpbi5jb20vZjIzNDZhMzBhLTk1ODEtNGU0ZC04MWQwLWQyZjk4NTQ3MWJhOS92Mi4wLyIsIHN1YjogImYzMmNjNmJhLWE5MTctNGE1Ni1hYjhmLWIyNGZmMTg1ODUyOCIsICIuLi4iOiAibW9yZSB2YWx1ZXMuLi4ifQ==.UNODECODEME'.split('.').map((value, index) => {
// The signature can't be decoded
if(index > 1) { return value; }
return atob(value);
});
You can use https://play.golang.org/p/7wWMBOp10R to get a public key from modulus(n) and exponent(e). Then copy the entire output in jwt.io and you'll see the signature is valid.
Related
I am aware of the answer given here which is about using Forms Authentication & SAML. In My case I am using Asp.net core Identity on .Net 5. Also I am using two authentication schemes (Cookies & JWT).
My auth pipeline goes as;
//include identity
services.AddIdentity<ApplicationUser, ApplicationRole>(SetupIdentityOptions)
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultTokenProviders();
//configure cookie and Jwt scheme
services.ConfigureApplicationCookie(...)
services.AddAuthentication(...) //configures default Identity
.AddJwtBearer(options => {...})
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
What I want to know is where should I add SAML2 in this pipeline.
In general the application should;
Be able to login with Cookie authentication (Identity takes care of this)
Jwt tokens should work as well for Apis (this also works with Jwt scheme)
SSO clients should get authenticated from their IdP and when redirected to AssertionConsumer() I will create additional claims create new ClaimsIdentity, create a JWT token(pass it to client) and get the user to dashboard.
I am stuck at 3rd point which is to properly add the SAML scheme without overriding cookie/jwt schemes.
The error No sign-in authentication handler is registered for the scheme 'saml2' probably occurs bedause you need to add services.AddSaml2() and app.UseSaml2()
You can use the setup from the provided example project. Newly added middleware should not interfere with what you already have.
When a SAML2 IdP redirects back to your application, you are given a result that identifies the authenticated user, e.g. Email Address or SSN (in case it is a government Id Provider).
You can combine that information with a Role (e.g. SpecialCustomer, Citizen, or an existing Role that you already have) into a cookie or JWT Token as you probably already do for other users. This can always be distinguished from other cookies and tokens by the Role. Regular [Authorize(....)] attributes will also work just fine.
I was stuck at the same point.. The solution I found:
If you check the source code of IFOXTEC.IDENTITY.SAML2, the method AddSaml2 overrides your AddAuthentication method and adds the AddCookie section.
public static IServiceCollection AddSaml2(this IServiceCollection services, string loginPath = "/Auth/Login", bool slidingExpiration = false, string accessDeniedPath = null, ITicketStore sessionStore = null, SameSiteMode cookieSameSite = SameSiteMode.Lax, string cookieDomain = null, CookieSecurePolicy cookieSecurePolicy = CookieSecurePolicy.SameAsRequest)
{
services.AddAuthentication(Saml2Constants.AuthenticationScheme)
.AddCookie(Saml2Constants.AuthenticationScheme, o =>
{
o.LoginPath = new PathString(loginPath);
o.SlidingExpiration = slidingExpiration;
if(!string.IsNullOrEmpty(accessDeniedPath))
{
o.AccessDeniedPath = new PathString(accessDeniedPath);
}
if (sessionStore != null)
{
o.SessionStore = sessionStore;
}
o.Cookie.SameSite = cookieSameSite;
o.Cookie.SecurePolicy = cookieSecurePolicy;
if (!string.IsNullOrEmpty(cookieDomain))
{
o.Cookie.Domain = cookieDomain;
}
});
return services;
}
So, to add SAML to your pipeline, you can remove the services.AddSaml2(), add the AddCookie section and, inside your policy, you can add the verification of any cookie with name saml2 to forward to your SAML schema.
services
.AddAuthentication(sharedOptions =>
{
sharedOptions.DefaultScheme = "custom-schema";
sharedOptions.DefaultChallengeScheme = "custom-schema";
})
.AddPolicyScheme("custom-schema", null, options =>
{
options.ForwardDefaultSelector = context =>
{
if (context.Request.Headers["Authorization"].Any(x => x.StartsWith("Bearer ")))
return JwtBearerDefaults.AuthenticationScheme;
else if (context.Request.Headers["Cookie"].Any(x => x.Contains(".saml2=")))
return Saml2Constants.AuthenticationScheme;
return "Identity.Application";
};
})
.AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, null, options =>
{
//...
})
.AddCookie(Saml2Constants.AuthenticationScheme, o =>
{
o.LoginPath = new PathString("/Auth/Login");
o.SlidingExpiration = false;
o.Cookie.SameSite = SameSiteMode.Lax;
o.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest;
});
I am using the SignalR .net core client in my project with JWT Tokens.
In the sample code below, the string variable "tokenString" has already been configured as an actual token and therefore i don't need to call upon an external method to create the token, that part has already been done before I reach this method. Using debug, and also testing the "tokenString" value on JWT website, I know the token is working, its just the fact I dont know how to use the ready made token in the SignalR connection method.
How do I configure the SignalR client connection to use this tokenString?
localConConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:44372/LocalConnectorHub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(tokenString); // Not working
// Need a solution like this: options.Token = tokenString
})
.WithAutomaticReconnect()
.Build();
The issue was the fact that the [Authorize] attribute I had configured in the SignalR Hub class needed to define the authentication scheme to use, [Authorize] attribute alone was not enough.
SignalR Hub Class:
[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]
public class LocalConnectorHub : Hub
{
public async Task SendToMacros(string serverName, string data)
{
await Clients.All.SendAsync("MacrosInbound", serverName, data);
}
public async Task ConnectorStatus(string serverName, string data)
{
await Clients.All.SendAsync("UpdateConnectorStatus", serverName, data);
}
}
SignalR .NET Core Client Connection:
localConConnection = new HubConnectionBuilder()
.WithUrl("https://localhost:44372/LocalConnectorHub", options =>
{
options.AccessTokenProvider = () => Task.FromResult(tokenString);
})
.WithAutomaticReconnect()
.Build();
await localConConnection.StartAsync();
Further example code from the startup.cs class (inside configure services method), this is posted to help one of our fellow members in the comments below:
// Retrieve the secret key from the appsettings.json file used for encryption
// when generating the JWT token for REST API authentication.
var key = Encoding.ASCII.GetBytes(Configuration.GetSection("AppSettings:Token").Value);
// Added to original .net core template.
// The preceding code configures multiple authentication methods. The app uses cookie-based authentication to log in
// via the browser using the identity manager. The second methid uses JWT bearer authentication for the REST API.
// The preceding cookie configuration configures Identity with default option values.
// Services are made available to the app through dependency injection.
// Cookie configuration MUST be called after calling AddIdentity or AddDefaultIdentity.
// IMPORTANT NOTE:
// When we decorate controllers or classes with use the [Authorize] attribute, it actually binds to the first authentication
// system by default (in this case cookie authentication) The trick is to change the attribute to specify which authorization
// service we want to use. Anexample for a protected respurce for a REST API controller would be to decorate using:
// "[Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme)]"
services.AddAuthentication()
.AddCookie(options =>
{
// Cookie settings
options.Cookie.Name = "MyCompanyName";
// HttpOnly is a flag that can be used when setting a cookie to block access to the cookie from client side scripts.
// Javascript for example cannot read a cookie that has HttpOnly set. This helps mitigate a large part of XSS attacks
// as many of these attempt to read cookies and send them back to the attacker, possibly leaking sensitive information
// or worst case scenario, allowing the attacker to impersonate the user with login cookies.
options.Cookie.HttpOnly = true;
// CookieAuthenticationOptions.ExpireTimespan is the option that allows you to set how long the issued cookie is valid for.
// The cookie is valid for (XX) minutes from the time of creation. Once those XX minutes are up the user will have to sign
// back in becuase if the SlidingExpiration is set to false.
// If SlidingExpiration is set to true then the cookie would be re-issued on any request half way through the ExpireTimeSpan.
// For example, if the user logged in and then made a second request half way through the permitted timespan then the cookie
// would be re-issued for another (XX) minutes. If the user logged in and then made a second request AFTER (XX) minutes later
// then the user would be prompted to log in.
// You can also change the units i.e. TimeSpan.FromHours(10); OR TimeSpan.FromDays(10);
// In a nutshell, setting the options.ExpireTimeSpan is equivalent to setting an idle time out period...
options.ExpireTimeSpan = TimeSpan.FromMinutes(10);
options.LoginPath = "/Identity/Account/Login";
options.AccessDeniedPath = "/Identity/Account/AccessDenied";
// Sliding expiration resets the expiration time for a valid authentication cookie if a request is made and more than half of the
// timeout interval has elapsed.If the cookie expires, the user must re - authenticate.Setting the SlidingExpiration property to
// false can improve the security of an application by limiting the time for which an authentication cookie is valid, based on the
// configured timeout value.
options.SlidingExpiration = true;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
// The "iss" (issuer) claim identifies the principal that issued the JWT. The processing of this
// claim is generally application specific. The "iss" value is a case-sensitive string containing
// a StringOrURI value. Use of this claim is OPTIONAL.
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(key),
// The "iss" (issuer) claim identifies the principal that issued the JWT.The processing of this
// claim is generally application specific. The "iss" value is a case-sensitive string containing
// a StringOrURI value.Use of this claim is OPTIONAL.
ValidateIssuer = false,
// Usually, this is your application base URL
ValidIssuer = "http://localhost:45092/",
// The "aud" (audience) claim identifies the recipients that the JWT is intended for. Each principal
// intended to process the JWT MUST identify itself with a value in the audience claim. If the principal
// processing the claim does not identify itself with a value in the "aud" claim when this claim is present,
// then the JWT MUST be rejected. In the general case, the "aud" value is an array of case-sensitive strings,
// each containing a StringOrURI value. In the special case when the JWT has one audience, the "aud" value
// MAY be a single case-sensitive string containing a StringOrURI value. The interpretation of audience
// values is generally application specific. Use of this claim is OPTIONAL.
ValidateAudience = false,
//Here, we are creating and using JWT within the same application.
//In this case, base URL is fine.
//If the JWT is created using a web service, then this would be the consumer URL.
ValidAudience = "http://localhost:45092/",
// The "exp" (expiration time) claim identifies the expiration time on or after which the JWT MUST NOT be accepted
// for processing. The processing of the "exp" claim requires that the current date/time MUST be before the
// expiration date/time listed in the "exp" claim.
RequireExpirationTime = true,
// Check if token is not expired and the signing key of the issuer is valid (ValidateLifetime = true)
ValidateLifetime = true,
};
// We have to hook the OnMessageReceived event in order to
// allow the JWT authentication handler to read the access
// token from the query string when a WebSocket or
// Server-Sent Events request comes in.
// Sending the access token in the query string is required due to
// a limitation in Browser APIs. We restrict it to only calls to the
// SignalR hub in this code.
// See https://learn.microsoft.com/aspnet/core/signalr/security#access-token-logging
// for more information about security considerations when using
// the query string to transmit the access token.
options.Events = new JwtBearerEvents
{
OnMessageReceived = context =>
{
var accessToken = context.Request.Query["access_token"];
// If the request is for our hub...
var path = context.HttpContext.Request.Path;
if (!string.IsNullOrEmpty(accessToken) &&
(path.StartsWithSegments("/hubs")))
{
// Read the token out of the query string
context.Token = accessToken;
}
return Task.CompletedTask;
}
};
});
Appsettings.json file (dont store keys here for production :)
"AppSettings": {
"Token": "secret key for jwt"
}
I'm developing an Outlook web add-in.
I'm trying to verify a token that I'm passing to server side, through a node.js library, but it's failing and I can't understand why.
This is what I'm doing to retrieve the user identity token.
Office.context.mailbox.getUserIdentityTokenAsync(function(result) {
result.value // contains the token.
// send this value to server side, which I can see that it's working.
})
On the server side, I retrieve the token and do below:
token; // contains the token passed from the web-app.
const jwt = require('jsonwebtoken');
const request = require('request');
let decoded = jwt.decode(token, {complete: true});
// Get the key, we'll need this later since we'll have to
// find the matching key from the config file.
let key = decoded.header.x5t;
let amurl = JSON.parse(decoded.payload.appctx).amurl;
// Make a request to the url to get the configuration json file.
request(amurl, {}, (err, response, body) => {
let keys = JSON.parse(body).keys;
// Filter the keys so we get the one which we can verify.
let s = keys.filter(t => t.keyinfo.x5t === key);
let cert = s[0].keyvalue.value;
// Fails the verification.
console.log(jwt.verify(token, cert));
});
Just to clarify, I'm retrieving the token correctly and this npm package seems to be functioning fine for other jwt tokens. (So it's not really a configuration issue)
I have now found the answer to this question.
Just to re-iterate the problem was:
Office.context.mailbox.getUserIdentityToken method returns a jwt token.
When decoded this token contains an amurl field which points to the public certificate as a text.
When jsonwebtoken.verify(token, certText) is called, it's failing with the message invalid algorithm (even if you specify the algorithm from the token's header)
The problem was the formatting of the certificate text. jsonwebtoken package was looking for a particular formatting (splitted with 64 characters across each line along with the certificate begin and certificate end lines, so when formatted with method below - it started working properly.
The original code is taken from here: https://github.com/auth0/node-jsonwebtoken/issues/68 and formatted slightly to fit the needs.
/**
* #param {string} key - The certificate value retrieved from amurl property.
*/
formatKey: function(key) {
const beginKey = "-----BEGIN CERTIFICATE-----";
const endKey = "-----END CERTIFICATE-----";
const sanitizedKey = key.replace(beginKey, '').replace(endKey, '').replace('\n', '')
const keyArray = sanitizedKey.split('').map((l, i) => {
const position = i + 1
const isLastCharacter = sanitizedKey.length === position
if(position % 64 === 0 || isLastCharacter) {
return l + '\n'
}
return l
})
return `${beginKey}\n${keyArray.join('')}${endKey}\n`
}
In AppSync, when you use Cognito User Pools as your auth setting your identity you get
identity:
{ sub: 'bcb5cd53-315a-40df-a41b-1db02a4c1bd9',
issuer: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_oicu812',
username: 'skillet',
claims:
{ sub: 'bcb5cd53-315a-40df-a41b-1db02a4c1bd9',
aud: '7re1oap5fhm3ngpje9r81vgpoe',
email_verified: true,
event_id: 'bb65ba5d-4689-11e8-bee7-2d0da8da81ab',
token_use: 'id',
auth_time: 1524441800,
iss: 'https://cognito-idp.us-west-2.amazonaws.com/us-west-2_oicu812',
'cognito:username': 'skillet',
exp: 1524459387,
iat: 1524455787,
email: 'myemail#nope.com' },
sourceIp: [ '11.222.33.200' ],
defaultAuthStrategy: 'ALLOW',
groups: null }
However when you use AWS_IAM auth you get
identity:
{ accountId: '12121212121', //<--- my amazon account ID
cognitoIdentityPoolId: 'us-west-2:39b1f3e4-330e-40f6-b738-266682302b59',
cognitoIdentityId: 'us-west-2:a458498b-b1ac-46c1-9c5e-bf932bad0d95',
sourceIp: [ '33.222.11.200' ],
username: 'AROAJGBZT5A433EVW6O3Q:CognitoIdentityCredentials',
userArn: 'arn:aws:sts::454227793445:assumed-role/MEMORYCARDS-CognitoAuthorizedRole-dev/CognitoIdentityCredentials',
cognitoIdentityAuthType: 'authenticated',
cognitoIdentityAuthProvider: '"cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob","cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob:CognitoSignIn:1a072f08-5c61-4c89-807e-417d22702eb7"' }
The Docs says that this is expected, https://docs.aws.amazon.com/appsync/latest/devguide/resolver-context-reference.html .
However, if you use AWS_IAM connected to Cognito (which is required to have unauthenticated access), how are you supposed to get at the User's username, email, sub, etc? I need access to the user's claims when using AWS_IAM type Auth.
For making User's username, email, sub etc. accessible through AppSync API, there's an answer for that: https://stackoverflow.com/a/42405528/1207523
To sum it up, you want to send User Pools ID token to your API (e.g. AppSync or API Gateway). Your API request is IAM authenticated. Then you validate the ID token in a Lambda function and now you have your validated IAM user and User Pools data together.
You want to use the IAM's identity.cognitoIdentityId as primary key for you User table. Add the data included in ID token (username, email, etc.) as attributes.
This way you can make user's claims available through you API. Now, for example, you can set $ctx.identity.cognitoIdentityId as the owner of an item. Then maybe other users can see the name of the owner via GraphQL resolvers.
If you need to access the user's claims in your resolver I'm afraid that doesn't seems to be possible at the moment. I have made a question about this as it would be very helpful for authorization: Group authorization in AppSync using IAM authentication
In this case, instead of using a resolver you could use Lambda as a data source and retrieve the user's claims from the above-mentioned User table.
It's all a bit difficult at the moment :)
Here is bad answer that works. I notice that cognitoIdentityAuthProvider: '"cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob","cognito-idp.us-west-2.amazonaws.com/us-west-2_HighBob:CognitoSignIn:1a072f08-5c61-4c89-807e-417d22702eb7" contains the Cognito user's sub (the big after CognitoSignIn). You can extract that with a regex and use the aws-sdk to get the user's info from cognito user pool.
///////RETRIEVE THE AUTHENTICATED USER'S INFORMATION//////////
if(event.context.identity.cognitoIdentityAuthType === 'authenticated'){
let cognitoidentityserviceprovider = new AWS.CognitoIdentityServiceProvider();
//Extract the user's sub (ID) from one of the context indentity fields
//the REGEX in match looks for the strings btwn 'CognitoSignIn:' and '"', which represents the user sub
let userSub = event.context.identity.cognitoIdentityAuthProvider.match(/CognitoSignIn:(.*?)"/)[1];
let filter = 'sub = \"'+userSub+'\"' // string with format = 'sub = \"1a072f08-5c61-4c89-807e-417d22702eb7\"'
let usersData = await cognitoidentityserviceprovider.listUsers( {Filter: filter, UserPoolId: "us-west-2_KsyTKrQ2M",Limit: 1}).promise()
event.context.identity.user=usersData.Users[0];
}
It's a bad answer because you are pinging the User Pool database instead of just decoding a JWT.
Here is my answer. There was a bug in the appSync client library that would overwrite all custom headers. That has since been fixed. Now you can pass down custom headers that will make it all the way to you resolvers, which I pass to my lambda functions (again, note I am using lambda datasourcres and not using dynamoDB).
So I attach my logged in JWT on the client side and, server side in my lambda function, I decode it. You need the public key created by cognito to validate the JWT. (YOU DO NOT NEED A SECRET KEY.) There is a "well known key" url associated with every user pool which I ping the first time my lambda is spun up but, just like my mongoDB connection, it is persisted between lambda calls (at least for a while.)
Here is lambda resolver...
const mongoose = require('mongoose');
const jwt = require('jsonwebtoken');
const jwkToPem = require('jwk-to-pem');
const request = require('request-promise-native');
const _ = require('lodash')
//ITEMS THAT SHOULD BE PERSISTED BETWEEN LAMBDA EXECUTIONS
let conn = null; //MONGODB CONNECTION
let pem = null; //PROCESSED JWT PUBLIC KEY FOR OUR COGNITO USER POOL, SAME FOR EVERY USER
exports.graphqlHandler = async (event, lambdaContext) => {
// Make sure to add this so you can re-use `conn` between function calls.
// See https://www.mongodb.com/blog/post/serverless-development-with-nodejs-aws-lambda-mongodb-atlas
lambdaContext.callbackWaitsForEmptyEventLoop = false;
try{
////////////////// AUTHORIZATION/USER INFO /////////////////////////
//ADD USER INFO, IF A LOGGED IN USER WITH VALID JWT MAKES THE REQUEST
var token = _.get(event,'context.request.headers.jwt'); //equivalen to "token = event.context.re; quest.headers.alexauthorization;" but fails gracefully
if(token){
//GET THE ID OF THE PUBLIC KEY (KID) FROM THE TOKEN HEADER
var decodedToken = jwt.decode(token, {complete: true});
// GET THE PUBLIC KEY TO NEEDED TO VERIFY THE SIGNATURE (no private/secret key needed)
if(!pem){
await request({ //blocking, waits for public key if you don't already have it
uri:`https://cognito-idp.${process.env.REGION}.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`,
resolveWithFullResponse: true //Otherwise only the responce body would be returned
})
.then(function ( resp) {
if(resp.statusCode != 200){
throw new Error(resp.statusCode,`Request of JWT key with unexpected statusCode: expecting 200, received ${resp.statusCode}`);
}
let {body} = resp; //GET THE REPSONCE BODY
body = JSON.parse(body); //body is a string, convert it to JSON
// body is an array of more than one JW keys. User the key id in the JWT header to select the correct key object
var keyObject = _.find(body.keys,{"kid":decodedToken.header.kid});
pem = jwkToPem(keyObject);//convert jwk to pem
});
}
//VERIFY THE JWT SIGNATURE. IF THE SIGNATURE IS VALID, THEN ADD THE JWT TO THE IDENTITY OBJECT.
jwt.verify(token, pem, function(error, decoded) {//not async
if(error){
console.error(error);
throw new Error(401,error);
}
event.context.identity.user=decoded;
});
}
return run(event)
} catch (error) {//catch all errors and return them in an orderly manner
console.error(error);
throw new Error(error);
}
};
//async/await keywords used for asynchronous calls to prevent lambda function from returning before mongodb interactions return
async function run(event) {
// `conn` is in the global scope, Lambda may retain it between function calls thanks to `callbackWaitsForEmptyEventLoop`.
if (conn == null) {
//connect asyncoronously to mongodb
conn = await mongoose.createConnection(process.env.MONGO_URL);
//define the mongoose Schema
let mySchema = new mongoose.Schema({
///my mongoose schem
});
mySchema('toJSON', { virtuals: true }); //will include both id and _id
conn.model('mySchema', mySchema );
}
//Get the mongoose Model from the Schema
let mod = conn.model('mySchema');
switch(event.field) {
case "getOne": {
return mod.findById(event.context.arguments.id);
} break;
case "getAll": {
return mod.find()
} break;
default: {
throw new Error ("Lambda handler error: Unknown field, unable to resolve " + event.field);
} break;
}
}
This is WAY better than my other "bad" answer because you are not always querying a DB to get info that you already have on the client side. About 3x faster in my experience.
If you are using AWS Amplify, what I did to get around this was to set a custom header username as explained here, like so:
Amplify.configure({
API: {
graphql_headers: async () => ({
// 'My-Custom-Header': 'my value'
username: 'myUsername'
})
}
});
then in my resolver I would have access to the header with:
$context.request.headers.username
As explained by the AppSync's docs here in the section Access Request Headers
Based on Honkskillets answer, I have written a lambda function that will return you the user attributes. You just supply the function with the JWT.
const jwt = require("jsonwebtoken");
const jwkToPem = require("jwk-to-pem");
const request = require("request-promise");
exports.handler = async (event, context) => {
try {
const { token } = event;
const decodedToken = jwt.decode(token, { complete: true });
const publicJWT = await request(
`https://cognito-idp.${process.env.REGION}.amazonaws.com/${process.env.USER_POOL_ID}/.well-known/jwks.json`
);
const keyObject = JSON.parse(publicJWT).keys.find(
key => key.kid == decodedToken.header.kid
);
const pem = jwkToPem(keyObject);
return {
statusCode: 200,
body: jwt.verify(token, pem)
};
} catch (error) {
console.error(error);
return {
statusCode: 500,
body: error.message
};
}
};
I use it in Appsync where I create Pipeline resolvers and add this function whenever I need user attributes. I supply the JWT by grabbing it from the header in the resolver using $context.request.
I am working with OpenID Connect in my application, i got the JWT token correctly from Google and I need to validate it using Signature.
To achieve this i need the public key, Google provide an URL which contains all its public certificate.
https://www.googleapis.com/oauth2/v3/certs
but it returns this :
{
"keys": [
{
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"kid": "f86c80f329b3ac69232463382fc1644167211d23",
"n": "wItpB2JpNKNgBM-xjgFbMGLYySu0SvaSA8Ag_MpqWrlWOvWvd3JQFrNKdw1nCGrGSczP6FdCRptogSEO51UB3n1h2quH-YW3NPGt0JGqXdRARJ1I1cOVq3dvrPaZhtDcEQCBAdqEmix_ngQM5vD1t8J22JO_v_JzJlTkzfYu5dPeSoXZymtgGeofdu38L1y-FlFqD09p6IP6Fxza22cv3ST3Dsw3eQ1yzGi5YuO0scTpds0jqPAslddclo22zapqB1_6qplwunpT3qAuObYR5Xn3gPseyQiwDtIk7MpEkb_AA_r4bpUGIh9-1SX3ev8urVZJ1Sg1Y_Rr-u7oQO9pdQ",
"e": "AQAB"
},
what does this mean ? where is the public key or the certificate here?
Thanks for your Help !
This looks like a JSON Web Key Set containing a description for an RSA public signing key. Parameters e and n for such keys are in turn described in JSON Web Algorithms.
In addition to Pieter's answer, you can find the PEM X.509 certificate representation of those RSA keys here: https://www.googleapis.com/oauth2/v1/certs
Using this output, you can build a public key which can be used for verifying JWT. Below is the link,
https://stackoverflow.com/a/59191103/3337028
Here, public key is build using n and e properties. You can then use library such as JJWT to verify the JWT using this public key.
You can use jose library to convert that to a key and verify the signature later https://github.com/panva/jose
const rsaPublicKey = await jose.importJWK({
kty: 'RSA',
e: 'AQAB',
n: '12oBZRhCiZFJLcPg59LkZZ9mdhSMTKAQZYq32k_ti5SBB6jerkh-WzOMAO664r_qyLkqHUSp3u5SbXtseZEpN3XPWGKSxjsy-1JyEFTdLSYe6f9gfrmxkUF_7DTpq0gn6rntP05g2-wFW50YO7mosfdslfrTJYWHFhJALabAeYirYD7-9kqq9ebfFMF4sRRELbv9oi36As6Q9B3Qb5_C1rAzqfao_PCsf9EPsTZsVVVkA5qoIAr47lo1ipfiBPxUCCNSdvkmDTYgvvRm6ZoMjFbvOtgyts55fXKdMWv7I9HMD5HwE9uW839PWA514qhbcIsXEYSFMPMV6fnlsiZvQQ'
}, 'PS256')
Then when you got the publicKey
const jwt = 'eyJhbGciOiJFUzI1NiJ9.eyJ1cm46ZXhhbXBsZTpjbGFpbSI6dHJ1ZSwiaWF0IjoxNjA0MzE1MDc0LCJpc3MiOiJ1cm46ZXhhbXBsZTppc3N1ZXIiLCJhdWQiOiJ1cm46ZXhhbXBsZTphdWRpZW5jZSJ9.hx1nOfAT5LlXuzu8O-bhjXBGpklWDt2EsHw7-MDn49NrnwvVsstNhEnkW2ddauB7eSikFtUNeumLpFI9CWDBsg'
const { payload, protectedHeader } = await jose.jwtVerify(jwt, publicKey, {
issuer: 'urn:example:issuer',
audience: 'urn:example:audience'
})
console.log(protectedHeader)
console.log(payload)
Note that the values in this example are explainatory, they are not the actual values, and you need to change the algorith from PS256 to RS256