According to my understanding a JWT can be broken down into a Header, Payload and a Signature. The signature is created using Header+Payload+Secret. The secret is stored by the server and is never shared.
When a client sends a JWT to the server. The server can authorize it by accessing the Header , Payload received from JWT and combine it with the secret to create a Test Signature. Then the server can check if Test Signature = Signature received from the client JWT and make sure the data is not altered.
• Does this mean that the entire JWT authorization is dependent on the Secret and the whole authorization process is compromised if it is leaked?
Secret is matter to verify JWT
This diagram is normal sequence to use JWT for accessing resource by API call.
I would like to demo with real code.
Using jsonwebtoken java-script library on node-js
1. JWT generate step.
function name: generate_token()
first function parameter: secret
second function parameter: user name (or payload)
const jwt = require("jsonwebtoken")
const jwtExpirySeconds = 600
const generate_token = (jwtKey, username) => {
const token = jwt.sign({ username }, jwtKey, {
algorithm: "HS256",
expiresIn: jwtExpirySeconds,
})
console.log("JWT token:", token, '\n')
const [header, payload, signature] = token.split(".")
console.log("1) header:", header)
console.log(JSON.parse(Buffer.from(header, "base64").toString("utf8")), '\n')
console.log("2) payload:", payload)
console.log(JSON.parse(Buffer.from(payload, "base64").toString("utf8")), '\n')
console.log("3) signature:", signature)
}
const my_args = process.argv.slice(2);
generate_token(my_args[0], my_args[1]);
generate token with secret and user name
$ node generate_token.js my-secret user1
JWT token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwiaWF0IjoxNjU5NDA3NzQ0LCJleHAiOjE2NTk0MDgzNDR9.MMIxotVJjMBLFOg0nusePz4L62n7VZ4Q-fW_u392qDQ
1) header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
{ alg: 'HS256', typ: 'JWT' }
2) payload: eyJ1c2VybmFtZSI6InVzZXIxIiwiaWF0IjoxNjU5NDA3NzQ0LCJleHAiOjE2NTk0MDgzNDR9
{ username: 'user1', iat: 1659407744, exp: 1659408344 }
3) signature: MMIxotVJjMBLFOg0nusePz4L62n7VZ4Q-fW_u392qDQ
2. Verify JWT step.
function name: verify_token()
first function parameter: secret
second function parameter: JWT token
const jwt = require("jsonwebtoken")
const verify_token = (jwtKey, token) => {
try {
payload = jwt.verify(token, jwtKey)
console.log("JWT token verify:", payload, '\n')
} catch (e) {
if (e instanceof jwt.JsonWebTokenError) {
console.log("JWT token error: 401")
return
}
// otherwise, return a bad request error
console.log("JWT token error: 400")
}
}
const my_args = process.argv.slice(2);
verify_token(my_args[0], my_args[1]);
2.1 verify token with correct secret and JWT token
$ node verify_token.js my-secret eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwiaWF0IjoxNjU5NDA3NzQ0LCJleHAiOjE2NTk0MDgzNDR9.MMIxotVJjMBLFOg0nusePz4L62n7VZ4Q-fW_u392qDQ
JWT token verify: { username: 'user1', iat: 1659407744, exp: 1659408344 }
2.2 if verify token with wrong secret and same JWT token
$node verify_token.js wrong-secret eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InVzZXIxIiwiaWF0IjoxNjU5NDA3NzQ0LCJleHAiOjE2NTk0MDgzNDR9.MMIxotVJjMBLFOg0nusePz4L62n7VZ4Q-fW_u392qDQ
JWT token error: 401
Related
I have two simple authentication callback functions "jwt" and "session" which check if the user object exists and create the session if so.
callbacks: {
jwt: async ({ token, user }) => {
if(user) {
token.id = user.id
}
return token
},
session: ({ session, token }) => {
if(token) {
session.id = token.id
}
return session
},
}
My issue is, and I have been searching a lot to find information concerning this, why isn't this jwt automatically saved to cookies?
I find that my session is created and I am successfully "logged in", however if I look into my local storage there are no jwt cookies saved.
Do I have to manually save the jwt to my cookies in the jwt callback? Or is the jwt cookie not even required in the case of jwt session strategy? I need jwt cookies because from what I've read about middleware most solutions use cookies and decrypt the jwt to see if the user is logged in, instead of checking the getSession() hook.
You might need to to explain your problem in more detail since I can´t really tell what you already implemented and left out for simplicity sake. I hope this helps you anyway:
The steps to add the cookie look roughly like this:
Create / sign a jwt with a npm package like jose or jsonwebtoken
Set the header of your response and add your signed jwt to it; return it to the client
import { SignJWT, jwtVerify, JWTVerifyResult } from "jose";
async function setCookie(response, user: {id: number}) {
const token = await generateJwtToken(user);
response.setHeader("Set-Cookie", [
`user=${token};` +
"expires=" + new Date(new Date().getTime() + 1 * 86409000).toUTCString() + ";"
]);
response.status(200).json({message: "Successfully set cookie"})
}
async function generateJwtToken(user: { id: number }) {
return await new SignJWT({id: user.id})
.setProtectedHeader({ alg: "HS256" })
.setIssuedAt()
.setExpirationTime("24h")
.sign(new TextEncoder().encode(process.env.YOUR_JWT_TOKEN_ENV_VAR));
}
Verify the jwt on further requests with the same package as in 1.
export async function verifyJwt(request) {
const token = request.cookies["yourCustomUser"];
const verified: JWTVerifyResult = await jwtVerify(
token,
new TextEncoder().encode(process.env.YOUR_JWT_TOKEN_ENV_VAR),
);
verified.payload.status = 200;
return verified.payload;
}
In addition to that you might wanna add sameSite=Strict, secure or path=/. For further information you should have a look at developers.mozilla
Also make sure to add error handling for expired Jwts etc.
Is it safe for my lambda function to trust the accessToken passed by the user and checked by the lambda authorizer to perform CRUD db operations?
For example:
const authToken = event.headers['Authorization'];
if (!authToken) throw new Error('No auth token found so no username');
var decodedToken = jwt_decode(authToken);
const userName = decodedToken.username; //---- BUT CAN WE TRUST THIS? ----
let params = {
TableName: "myTable",
IndexName: 'userName-gsi',
KeyConditionExpression: 'userName = :userName',
ExpressionAttributeValues: {
':userName': userName,
},
Limit: 1,
};
let data = await dynamodb.query(params).promise();
return {
statusCode: 200,
headers: utils.getResponseHeaderApplicantifyCors(),
body: JSON.stringify(data.Items[0]),
};
In your example you're only decoding the JWT, so the only verification made is that the token is in JWT form. That is not enough to guarantee, that the JWT is originating from trusted party.
The minimum you should do, is to also verify the contents of the JWT token. Steps for that are listed here: https://docs.aws.amazon.com/cognito/latest/developerguide/amazon-cognito-user-pools-using-tokens-verifying-a-jwt.html
I have a small ExpressJS server with a login feature. Some pages are secured with self-written authenticate middleware that checks if your Json WebToken is correct.
I read that just one Json WebToken isn't secure enough and that you need a refresh token as well. So I added a Refresh Token. This is all verified on the server.
Now when the token is expired, I check if the user has a refreshToken and if so I create a new token and set it as a cookie. Like this:
const jwt = require('jsonwebtoken');
// Simple array holding the issued refreshTokens
const { refreshTokens } = require('../lib/auth.js');
module.exports.authenticateToken = function(req, res, next) {
const token = req.cookies.token;
const refreshToken = req.cookies.refreshToken;
if(token === null) return res.redirect('/login');
try {
const verified = jwt.verify(token, process.env.TOKEN_SECRET);
if(verified) return next();
} catch(err) {
try {
const refreshInDb = refreshTokens.find(token => token === refreshToken);
const refreshVerified = refreshInDb && jwt.verify(refreshToken, process.env.REFRESHTOKEN_SECRET);
const newToken = jwt.sign({ email: refreshVerified.email }, process.env.TOKEN_SECRET, { expiresIn: 20 });
res.cookie('token', newToken, { maxAge: 900000, httpOnly: true });
return next();
} catch(err) {
return res.redirect('/login');
}
}
};
Now is this code correct & secure enough for a small webapplication? Am I missing stuff? It feels so... easy?
Seems like you are veryfing both tokens at the same endpoint. This approach is wrong.
In your login endpoint, validate the user and password against database. If credentials are correct we respond with an access token and a refresh token
router.post('/login',
asyncWrap(async (req, res, next) => {
const { username, password } = req.body
await validateUser(username, password)
return res.json({
access_token: generateAccessToken(), // expires in 1 hour
refresh_token: generateRefreshToken(), // expires in 1 month
})
})
)
In your authenticated routes, you should validate only the access token (the user should send ONLY this one)
// middleware to validate the access token
export const validateToken = asyncWrap(async (req, res, next) => {
const data = await verifyAccessToken(req.headers.authorization)
req.auth = data
next()
})
If the access token expires, the user should refresh its token. In this endpoint we will validate the refresh token and respond with two new tokens:
router.post('/refresh',
asyncWrap(async (req, res, next) => {
const { refresh_token } = req.body
await verifyRefreshToken(refresh_token)
return res.json({
access_token: generateAccessToken(), // expires in 1 hour
refresh_token: generateRefreshToken(), // expires in 1 month
})
})
)
I read that just one Json WebToken isn't secure enough and that you need a refresh token as well. So I added a Refresh Token. This is all verified on the server.
Using refresh tokens has nothing to do with security of the JWT or access token. Refresh tokens are just a UX feature. They allow you to get new access tokens without asking the user to authorize again. Having a refresh token in your app doesn't automatically make it more secure.
Now when the token is expired, I check if the user has a refreshToken and if so I create a new token and set it as a cookie. Like this:
When implemented this way the refresh token doesn't grant any more security to your application. You could as well keep the access tokens in the db and refresh them when they are expired.
Are you sure that you need JWTs at all? It looks like you're using them as you would use a session based on cookies. It should be simpler to deal with sessions. You are using http-only cookies for your tokens so you already use it pretty much like a session.
Now is this code correct & secure enough for a small webapplication?
Secure enough is a concept that depends on the data that your application has access to. If it's nothing sensitive, and you know that your app can't really be abused by an attacker, then it is fine to have only some basic security in place.
I'm working on a nuxt.js application with amplify providing the backend, more specifically: Cognito for user/identity management and AppSync for the API.
AppSync uses IAM as authorization on the client side and API KEY on the server side.
When the initial request is made to the server, I can parse the tokens included in the cookie and check if they are valid.
If the token is valid, everything is great: I can grab the user from my database and update my Vuex store before the initial page is sent back to the client.
If the token is expired however, I have a problem because the client/browser then receives an initial page where no user is logged in. On the client side, when the web app is initialized, the amplify library "kicks-in", sees the expired tokens and proceeds to automatically refresh them in the background. When I hit refresh, the new tokens are sent to the server and this time, are valid. Not a great experience.
Therefore, my question is how do I refresh the token on the server-side the first time when the parsed token is expired ?
nuxtServerInit(store, context) {
const cookie = context.req.headers.cookie
if (!cookie) { return console.log("no cookie received") }
const parsedCookie = cookieParser.parse(cookie)
var idToken = ""
var refreshToken = ""
var userData: Object | null = null
Object.keys(parsedCookie).forEach((key, index) => {
if (key.includes("idToken")) idToken = parsedCookie[key]
if (key.includes("refreshToken")) refreshToken = parsedCookie[key]
if (key.includes("userData")) userData = JSON.parse(parsedCookie[key])
}
return context.$axios
// validate token against cognito
.get("https://cognito-idp.us-east-1.amazonaws.com/us-east-xxx/.well-known/jwks.json")
.then(res => jwt.verify(idToken, jwkToPem(res.data.keys[0]), { algorithms: ["RS256"] }))
.then(decodedToken => {
// token is valid, proceed to grab user data
var user = { attributes: {} }
;(userData!["UserAttributes"] as Array<any>).forEach(element => {
user.attributes[element.Name] = element.Value
})
const sub = user.attributes["sub"]
return sub
})
.then(sub => {
// fetch user data from API
})
.then(res => {
// update store with user data
})
.catch(e => {
// TODO: CASE WHERE TOKEN IS INVALID OR EXPIRED
// THIS IS WHERE I WOULD NEED TO REFRESH THE TOKEN
console.log("[nuxtServerInit] error", e.message)
})
}
In express-jwt home page introduce a function for getting json web token from header or query that we can use as express middle wear and this is the function :
app.use(jwt({
secret: 'hello world !',
credentialsRequired: false,
getToken: function fromHeaderOrQuerystring (req) {
if (req.headers.authorization && req.headers.authorization.split(' ')
[0] === 'Bearer') {
return req.headers.authorization.split(' ')[1];
} else if (req.query && req.query.token) {
return req.query.token;
}
return null;
}
}));
And I use express.Route() like this :
app.use('/user',userRoute);
app.use('/apps',appsRouter);
My question is how can I use getToken() function or how can I access to the token in authorization of header.
Thanks in advance.
finally i find the solution. with middle ware (jwt) can verify the token in header and if can it set in req.user so , in req.user we have all information of deceded jwt, according to this:
By default, the decoded token is attached to req.user but can be
configured with the requestProperty option.
jwt({ secret: publicKey, requestProperty: 'auth' });
I think , if i am not wrong, you need the token or the decoded one in your routes. Here's how i did it.
I Have a middle ware function that will decode the token for me which has the user information in it and then it will add the decoded object to the req object.
e.g route
.put('/update', Middleware.decodeToken, yourCallBackfunction)
decodeToken(req, res, next) {
authorization = req.headers.authorization.replace('Bearer ', ''),
decodeToken = Jwt.verify(authorization);
//verifies the token
req.tokenInfo = decodeToken
next();
}