JWT sub claim gets ignored during verification - javascript

Well, this question maybe naive as I am implementing JWT in my node app for the first time and I have too many questions about it.
Firstly I am not clear about the iss, sub and aud claims. From my basic understanding I understand that iss is the issuer of the token, so I can assume it to be the company name of the app. sub is the subject of the token or in easy terms probably the user identity/username. And lastly the aud is for audience or in easy terms the api server url/resource server. Please let me know if I got this terms correctly.
Now, with my limited knowledge I have set up the basic JWT signing and verification. A small snippet is as follows:
JWT.js
module.exports = {
sign: (payload, options) => {
let signOptions = {
issuer: config.JWT_ISSUER,
subject: options.subject,
audience: config.JWT_AUDIENCE,
expiresIn: "24h",
};
return jwt.sign(payload, config.JWT_SECRET, signOptions);
},
verify: (token, options) => {
let verifyOptions = {
issuer: config.JWT_ISSUER,
subject: options.subject,
audience: config.JWT_AUDIENCE,
expiresIn: "24h",
};
try {
return jwt.verify(token, config.JWT_SECRET, verifyOptions);
}
catch (err){
return false;
}
},
Now token is issued as follows:
// Issue JWT token
let userData = {
user_name: user.userName,
client_id: user.clientId
};
const token = jwt.sign({ userData }, { subject: user.userName });
Verfication is as follows:
// Verify the token
const authData = jwt.verify(token, { subject: req.body.subject });
MAIN ISSUE
When I send a request to an api endpoint for verification and if I send it without a subject field in the body(the token is issued with a sub field), the token is verified successfully. But if I send the subject field in the body with an correct/incorrect value, it gets success/forbidden respectively.
Why does it happen so?
Why does'nt the token get forbidden when no sub field is passed in the request?
Do I need to manually verify this?

According to JWT standard,
4.1.2. "sub" (Subject) Claim
The "sub" (subject) claim identifies the principal that is the
subject of the JWT. The claims in a JWT are normally statements
about the subject. The subject value MUST either be scoped to be
locally unique in the context of the issuer or be globally unique.
The processing of this claim is generally application specific. The
"sub" value is a case-sensitive string containing a StringOrURI
value. Use of this claim is OPTIONAL.
Thus, when you don't have it, it passes but an incorrect value leads to failure. The jwt package follows the standard.
Now if you plan to make it mandatory, you will have to make it custom but be warned that you may not be then able to use 3rd party authentication (if that's your use case) who do not consider it mandatory. (I don't know if in real life that really is the case)

Related

Error using AWS Cognito for authentication with Hasura

i'm having some problems using lambda enviroment.
Looking to set a function that make a mutation to Hasura so I can relate Auth users of Cognito with my app information.
I set the following function Post Authentication in Lamba but it does not work.
function Add(event, context, callback) {
const userId = event.user_id;
const hasuraAdminSecret = "xxx";
const url = "xxx";
const upsertUserQuery = `
mutation($userId: String!){
insert_RegistroAnimal_users(objects: [{ id: $userId }], on_conflict: { constraint: users_pkey, update_columns: [] }) {
affected_rows
}
}`
const graphqlReq = { "query": upsertUserQuery, "variables": { "userId": userId } }
request.post({
headers: {'content-type' : 'application/json', 'x-hasura-admin-secret': hasuraAdminSecret},
url: url,
body: JSON.stringify(graphqlReq)
}, function(error, response, body){
console.log(body);
callback(null, user, context);
});
}
Followed this tutorial : https://hasura.io/docs/latest/graphql/core/guides/integrations/aws-cognito.html#introduction
What do you think is wrong with the code?
I don't think anything is wrong with the code, but to make it work with Cognito you'd need to provide your Hasura setup with a JWT claims function as shown in that same guide, https://hasura.io/docs/latest/graphql/core/guides/integrations/aws-cognito.html#create-a-lambda-function-to-add-claims-to-the-jwt. If you'd like to do it as the guide suggests, you need to create a lambda function like so;
exports.handler = (event, context, callback) => {
event.response = {
"claimsOverrideDetails": {
"claimsToAddOrOverride": {
"https://hasura.io/jwt/claims": JSON.stringify({
"x-hasura-user-id": event.request.userAttributes.sub,
"x-hasura-default-role": "user",
// do some custom logic to decide allowed roles
"x-hasura-allowed-roles": ["user"],
})
}
}
}
callback(null, event)
}
You then need to pick this function as the PreTokenGeneration trigger from your user pool settings. Then AWS Cognito will trigger this function before token generation, allowing you to add Hasura required claims to your token.
The next step is to tell Hasura where to lookup for the JWT claims by providing HASURA_GRAPHQL_JWT_SECRET during the setup, which is essentially an URL pointing to your Cognito setup, generated using the pool id.
Finally, you can obtain the idToken from your user session after a successful login, and pass that token as an Authentication header for your Hasura requests. Described here.
All of these steps were actually described in the guide you linked, but may not be as clear. I believe the reason your current setup does not work is that your Hasura setup is missing the HASURA_GRAPHQL_ADMIN_SECRET, which needs to be the same as the x-hasura-admin-secret you're using in your requests.
Mind you, if you use x-hasura-admin-secret in your app and expose it to your users which gives them admin access, that creates a potential security issue and anyone with that secret can wipe up your data. x-hasura-admin-secret should be reserved for your admin tasks and not used in an app where AWS Cognito authentication is planned to be used.

Approach to modify the database without logging the user?

I have a React frontend with a Node + MySQL backend, I'm sending an email to an user with two buttons to accept or decline a quote. What I'm trying to achieve is to make the buttons in the email modify the database securely without the user having to log into his account. My idea is to have two routes, one that sends the email containing the buttons which will have a url to my website with the jwt token on its parameters, and another for verifying said token and making the changes to the db. Here's some pseudo-code:
app.post("/email-quote", async function (req, res) {
const payload = {
uid: req.body.user.id,
quoteId: req.body.quote.id,
accepted: // true for the accept button, false for the decline button
}
const secret = ?
const token = jwt.sign(payload, secret);
// ...
// Generate and send email with buttons containing the url + the token
});
When the user clicks one of the buttons, I re-direct him to my website and there I can extract the token and verify its validity:
app.get("/verify-email-quote/:token", async function (req, res) {
const decodedJwt = jwt.decode(req.params.token);
const secret = ?
const verifiedJwt = jwt.verify(req.params.token, secret);
if (verifiedJwt) {
// Make the changes to the db
}
});
I wasn't able to find any examples trying to achieve something similar on the web, so I have these questions:
Would a jwt token be a good approach to achieve this?
If yes, what secret should I use to create the token?
If no, what other solutions could I look into?
Yes, you can do it this way.
The secret does not matter. As long as the secret is secret
It doesn't need to be a jwt token. It can just be a normal token. The incentive to using jwt is that you can embed a payload into the token. For your case, it looks like it is exclusively for verification purposes. It doesn't matter in the grand scheme of things, but if you don't have jwt already implemented, there's no need to go through all that extra work just for this use case.

How to authorize all users with different tokens using jwt in Node JS

I have a node.js app. I'm using Json Web Token for authorization. When I Login with a user who is in my database, it creates a token for the user. But I can use that token for another user and it works too again. I need to have different tokens for all users and I should not use one user's token for another user. (I dont have internet on my work pc so I cant write my codes on my computer here sorry about that)
Here is my verify-token.js (middleware):
const jwt = require("jsonwebtoken");
module.exports = (req, res, next) => {
try {
const token = req.headers.authorization.split(" ")[1];
const decodedToken = jwt.verify(token, "secret_key");
req.userData = decodedToken;
next();
} catch (error) {
return res.status(401).send({
message: "Auth failed"
});
}
};
And here is my login code (im creating the token here)
if password is true:
const token = jwt.sign(
{
email: user.email,
password: user.password
},
"secret_key",
{
expiresIn: "2h"
}
);
return res.status(200).send({ message: "success", token: token });
And in app.js:
const checkAuth = require('../middleware/checkauth');
router.get('/api/company',checkAuth,companyController.list);
I expect that one token should be for just one user, and for every login it should creates a new token for all users. Any suggestion?
You're including the email in your token body; why not also include the user ID as a claim (field)? When you verify the token, if it's successful, it will return the body to you, so then you'll know which user that token was made for and reject if it's not the one who's making the request.
To ensure two people don't use the same token at the same time, you can keep a list of every valid token when you generate it, and when a token expires or is revoked (for instance, when the user signs out or reports an imposter, if it gets that far) remove it from the list. During verification, if the token is not on the list, don't even bother decoding it, just reject it immediately.
If you give your tokens decently small expiration windows (I believe the recommendation is to make them last no longer than 1 hour), you shouldn't have to worry about such things much.
EDIT To clarify, you'll never have a way to know for sure that the person who gave you the token is who they claim to be. You only know if your server created the token and if the token is currently valid. If you really want to prevent replay attacks (that is, make absolutely sure there's no way for two people to use the same token at once), you'll need to generate a new token every time one is used. If you keep that whitelist that I mentioned above, this regeneration ensures every token becomes invalid as soon as it's used once.
You can also, to be EXTRA confident, include a jti claim in the token body; this is a field intended to be filled with a random unique value every time a token is generated, so that you can keep track of the jti's you've received and not allow the same one to come in more than once. It's about the same as just keeping track of the tokens, though.

jwt.verify() returns jwt expired when the expiration is 24h

I used jwt to create a token:
const jwt = require('jsonwebtoken');
const token = jwt.sign({
filePath: "path/to/file"
}, 'secretKey', {
expiresIn: "24h"
});
try {
console.log(token)
var decoded = jwt.verify(token, 'secretKey');
} catch(err) {
console.log(err)
}
jwt.header:
{
"alg": "HS256",
"typ": "JWT"
}
payload:
{
"filePath": "path",
"iat": 1557833831,
"exp": 1557920231
}
When I test the snippet code mentioned above in my real app, I got an error message:
jwt expired
Using the jwt debugger, the token is valid and should expire after 24h.
The error returned by verify() which checks the expiration. How jwt checks the expiration? or it does not check it?
So since the question is, how does jwt check the expiration date, it depends on basically on some properties that may be implemented according to the JWT RFC
One would be exp. In case a token expires before the current datetime, then the JWT cannot be processed
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.
Implementers MAY provide for some small leeway, usually no more than
a few minutes, to account for clock skew. Its value MUST be a number
containing a NumericDate value. Use of this claim is OPTIONAL.
Another one to note would be the iat, which stands for issued at
The "iat" (issued at) claim identifies the time at which the JWT was
issued. This claim can be used to determine the age of the JWT. Its
value MUST be a number containing a NumericDate value. Use of this
claim is OPTIONAL.
A final one that could be used for time verfication, as far as I am aware of would be, nbf, standing for not before
The "nbf" (not before) claim identifies the time before which the JWT
MUST NOT be accepted for processing. The processing of the "nbf"
claim requires that the current date/time MUST be after or equal to
the not-before date/time listed in the "nbf" claim. Implementers MAY
provide for some small leeway, usually no more than a few minutes, to
account for clock skew. Its value MUST be a number containing a
NumericDate value. Use of this claim is OPTIONAL.
Now, for the code at hand, I don't see anything which is of, having following setup, this works perfectly fine for me
const jwt = require('jsonwebtoken');
const token = jwt.sign( {
hello: 'world'
}, 'myverysecretkey', {
expiresIn: '24h'
});
try {
const verify = jwt.verify( token, 'myverysecretkey' );
console.log( verify );
} catch (err) {
console.error( err );
}
which would output
Object {hello: "world", iat: 1557840459, exp: 1557926859}
This can be validated on the codesandbox link

Reset Loopback Password with Access Token

I'm working on a project that uses Loopback as a framework, and includes users and authentication. I added a password reset route generated and sent in an email, and everything seemed to be working correctly. Recently, I discovered that the password reset does not appear to be working. The process for resetting the password here is:
Call password reset method for user
Send email from reset event, including user ID and access token
From reset link, set $http.defaults.headers.common.authorization to the passed token
Call user.prototype$updateAttributes (generated by lb-ng) to update password attribute based on a form
The expected behavior is that the password would be updated on the password reset form. Instead, I get an authorization error as either a 401 or a 500 (seems to go back and forth). I notice that in the actual headers sent to the API, the authorization token does not match what I'm passing from the route. Trying to set it using LoopBackAUth.setUser doesn't work, and neither doesn't updating the authorization property before actually sending the request.
I definitely spent time testing this when it was first added, and I can't figure out what would have changed to break this. I've been following the example from loopback-faq-user-management, but we have an Angular front-end instead of the server side views in that example.
Edit:
I tried opening up the ACLs completely to see if I could update the password (or any properties) of my user object (which inherits from User, but is its own type). I'm still getting a 401 when trying to do this.
Edit #2:
Here are my ACLs and sample code for how I'm calling this.
ACLs from model definition
...
{
"accessType": "*",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW"
},
{
"accessType": "EXECUTE",
"principalType": "ROLE",
"principalId": "$owner",
"permission": "ALLOW",
"property": "updateAttributes"
}
...
auth.js
...
resetPassword: function(user) {
return MyUser.prototype$updateAttributes(user, user).$promise;
}
...
Figured out what the issue was. In our app's server, we were not using Loopback's token middleware. Adding app.use(loopback.token()); before starting the server causes the access token provided in the reset link to work as expected!
While all of the above answers will prove to be helpful, be aware that Loopback destroys a token during validation when it proved it to be invalid . The token will be gone. So when you're working your way through a solution for the 401's, make sure you're creating a new password reset token each time you try a new iteration of your code.
Otherwise you might find yourself looking at perfectly healthy code to change a password, but with a token that's already deleted in a previous iteration of your code, leading you to the false conclusion that you need to work on your code when you see another 401.
In my particular case the access tokens are stored in a SQL Server database and the token would always be immediately expired due to a timezone problem that was introduced, because I had options.useUTC set to false. That cause all newly tokens to be 7200 seconds in the past which is more than the 900 seconds than the password reset tokens are valid. I failed to notice that those tokens were immediately destroyed and concluded I had still problems with my code as I saw 401's in return. Where in fact the 401 was caused by using a token that was already gone on the server.
#OverlappingElvis put me on the right track. Here's a more complete answer for others running into this. The loopback docs are quite limited in this area.
Make sure that you get both the user id and the token in your email and these get populated in the form.
From the form the following code does the job:
function resetPassword(id, token, password) {
$http.defaults.headers.common.authorization = token;
return User
.prototype$updateAttributes({id:id}, {
password: password
})
.$promise;
}
This was way more complicated than it ought to be. Here is my full solution:
1) I expose new method on server side which does the password updating from token.
Member.updatePasswordFromToken = (accessToken, __, newPassword, cb) => {
const buildError = (code, error) => {
const err = new Error(error);
err.statusCode = 400;
err.code = code;
return err;
};
if (!accessToken) {
cb(buildError('INVALID_TOKEN', 'token is null'));
return;
}
Member.findById(accessToken.userId, function (err, user) {
if (err) {
cb(buildError('INVALID_USER', err));
return;
};
user.updateAttribute('password', newPassword, function (err, user) {
if (err) {
cb(buildError('INVALID_OPERATION', err));
return;
}
// successful,
// notify that everything is OK!
cb(null, null);
});
});
}
and I also define the accessibility:
Member.remoteMethod('updatePasswordFromToken', {
isStatic: true,
accepts: [
{
arg: 'accessToken',
type: 'object',
http: function(ctx) {
return ctx.req.accessToken;
}
},
{arg: 'access_token', type: 'string', required: true, 'http': { source: 'query' }},
{arg: 'newPassword', type: 'string', required: true},
],
http: {path: '/update-password-from-token', verb: 'post'},
returns: {type: 'boolean', arg: 'passwordChanged'}
});
From the client-side, I just call it like this:
this.memberApi.updatePasswordFromToken(token, newPassword);

Categories

Resources