DEV Community

Perry Donham
Perry Donham

Posted on

Using JWTs for Authentication in RESTful Applications

The problem

Applications built using the MEAN stack typically use Node, MongoDB and Express on the back end to implement business logic fronted by a RESTful interface. Most of the work is done on the back end, and Angular serves as an enhanced view in the MVC  (model-view-controller) pattern. Keeping business rules and logic on the back end means that the application is view-agnostic; switching from Angular to React or straight jQuery or PHP should result in the same functionality.

It's often the case that we need to protect some back-end routes, making them available only to authenticated users. The challenge is that our back-end services should be stateless, which means that we need a way for front-end code to provide proof of authentication at each request. At the same time, we can't trust any front-end code, since it is out of our control. We need an irrefutable mechanism for proving authentication that is entirely managed on the back end. We also want the mechanism to be out of the control of the client code, and done in such a way that it would be difficult or impossible to spoof.

The solution

JSON Web Tokens (JWTs) are a good solution for these requirements. The token is basically a JavaScript object in three parts:

  • A header that contains information about the algorithms used to generate the token
  • A body with one or more claims
  • A cryptographic signature based on the header and body

JWTs are formally described in RFC7519. There's nothing inherently authentication-y about them -- they are a mechanism to encapsulate and transmit data between two parties that ensures the integrity of the information. We can leverage this to give clients a way to prove their status without involving the client at all. Here's the flow:

  1. Client authenticates with the server (or via a third-party such as an OAuth provider)
  2. Server creates a signed JWT describing authentication status and authorized roles using a secret that only the server knows
  3. Server returns JWT to client in a session cookie marked httpOnly
  4. At each request the client automatically sends the cookie and the enclosed JWT to the server
  5. Server validates the JWT on each request and decides whether to allow client access to protected resources, returning either the requeseted resource or an error status

Using a cookie to transmit the JWT provides a simple, automated way to pass the token back and forth between the client and the server and also gives the server control over the lifecycle of the cookie. Marking the cookie httpOnly means that it is unavailable to client functions. And, since the token is signed using a secret known only to the server, it is difficult or impossible to spoof the claims in the token.

The implementation discussed in this article uses a simple hash-based signing method. The header and body of the JWT are Base64 encoded, and then the encoded header and body, along with a server-side secret, are hashed to produce a signature. Another option is to use a public/private key pair to sign and verify the JWT. In the example, the JWT is handled only on the server, and so there's no benefit to using a signing key.

JWT authorization in code

Let's take a look at some code that implements our workflow. The application that I'm using in the following examples relies on third-party OAuth authentication from Twitter, and minimal profile information is held over for a user from session to session. The Twitter access token returned after a successful authentication is used as a key to a user record in a mongoDB database. The token exists until the user logs out or the user re-authenticates after having closed the browser window (thus invalidating the session cookie containing the JWT). Note that I've simplified error handling for readability.

Dependencies

Two convenience packages are used in the following code examples:

  • cookie-parser - Express middleware to simplify cookie handling
  • jsonwebtoken - abstracts signing and validation of JWTs, based on the node-jws package

I also use Mongoose as a layer on top of mongoDB; it provides ODM via schemas and also several handy query methods.

Creating the JWT and placing in in a session cookie

Once authentication with Twitter completes, Twitter invokes a callback method on the application, passing back an access token and secret, and information about the user such as their Twitter ID and screen name (passed in the results object). Relevant information about the user is stored in a database document:

User.findOneAndUpdate( {twitterID: twitterID},
 {
     twitterID: twitterID,
     name: results.screen_name,
     username: results.screen_name,
     twitterAccessToken: oauth_access_token,
     twitterAccessTokenSecret: oauth_access_token_secret
 },
 {'upsert': 'true'},
 function (err, result) {
     if (err) {
     console.log(err)
 }
 else {
     console.log("Updated", results.screen_name, "in database.")
 }
})

The upsert option directs mongoDB to create a document if it not present, otherwise it updates an existing document.

Next, a JWT is assembled. The jsonwebtoken package takes care of creating the header of the JWT, so we just fill in the body with the Twitter access token. It is the access token that we'll use to find the user in the database during authorization checks.

const jwtPayload = {
     twitterAccessToken: oauth_access_token
 }

The JWT is then signed.

 const authJwtToken = jwt.sign(jwtPayload, jwtConfig.jwtSecret)

jwtSecret is a string, and can be either a single value used for all users (as it is in this application) or a per-user value, in which case it must be stored along with the user record. A strategy for per-user secrets might be to use the OAuth access token secret returned by Twitter, although it introduces a small risk if the response from Twitter has been intercepted. A concatenation of the Twitter secret and a server secret would be a good option. The secret is used during validation of the signature when authorizing a client's request. Since it is stored on the server and never shared with the client, it is an effective way to verify that a token presented by a client was in fact signed by the server.

The signed JWT is placed on a cookie. The cookie is marked httpOnly, which restricts visibility on the client, and its expiration time is set to zero, making it a session-only cookie.

const cookieOptions = {
  httpOnly: true,
  expires: 0 
 }
 res.cookie('twitterAccessJwt', authJwtToken, cookieOptions)

Keep in mind that the cookie isn't visible to client-side code, so if you need a way to tell the client that the user is authenticated you'll want to add a flag to another, visible, cookie or otherwise pass data indicating authorization status back to the client.

Why a cookie and a JWT?

We certainly could send the JWT back to the client as an ordinary object, and use the data it contains to drive client-side code. The payload is not encrypted, just Base64 encoded, and would thus be accessible to the client. It could be placed on the session for transport to and from the server, though this would have to be done on each request-response pair, on both the sever and the client, since this kind of session variable is not automatically passed back and forth.

Cookies, on the other hand, are automatically sent with each request and each response without any additional action. As long as the cookie hasn't expired or been deleted it will accompany each request back to the server. Further, marking the cookie httpOnly hides it from client-side code, reducing the opportunity for it to be tampered with. This particular cookie is only used for authorization, so there's no need for the client to see it or interact with it.

Authorizing requests

At this point we've handed the client an authorization token that has been signed by the server. Each time the client makes a request to the back-end API, the token is passed inside a session cookie. Remember, the server is stateless, and so we need to verify the authenticity of the token on each request. There are two steps in the process:

  1. Check the signature on the token to prove that the token hasn't been tampered with
  2. Verify that the user associated with the token is in our database
  3. [optionally] Retrieve a set of roles for this user

Simply checking the signature isn't enough -- that just tells us that the information in the token hasn't been tampered with since it left the server, not that the owner is who they say they are; an attacker might have stolen the cookie or otherwise intercepted it. The second step give us some assurance that the user is valid; the database entry was created inside a Twitter OAuth callback, which means that the user had just authenticated with Twitter. The token itself is in a session cookie, meaning that it is not persisted on the client side (it is held in memory, not on disk) and that has the httpOnly flag set, which limits its visibility on the client.

In Express, we can create a middleware function that validates protected requests. Not all requests need such protection; there might be parts of the application that are open to non-logged-in users. A restricted-access POST request on the URI /db looks like this:

// POST Create a new user (only available to logged-in users)
//
router.post('/db', checkAuthorization, function (req, res, next) {
...
}

In this route, checkAuthorization is a function that validates the JWT sent by the client:

const checkAuthorization = function (req, res, next) {

    // 1. See if there is a token on the request...if not, reject immediately
    //
    const userJWT = req.cookies.twitterAccessJwt
    if (!userJWT) {
        res.send(401, 'Invalid or missing authorization token')
    }
    //2. There's a token; see if it is a valid one and retrieve the payload
    //
    else {
        const userJWTPayload = jwt.verify(userJWT, jwtConfig.jwtSecret)
        if (!userJWTPayload) {
            //Kill the token since it is invalid
            //
            res.clearCookie('twitterAccessJwt')
            res.send(401, 'Invalid or missing authorization token')
        }
        else {
            //3. There's a valid token...see if it is one we have in the db as a logged-in user
            //
            User.findOne({'twitterAccessToken': userJWTPayload.twitterAccessToken})
                .then(function (user) {
                    if (!user) {
                        res.send(401, 'User not currently logged in')
                    }
                    else {
                        console.log('Valid user:', user.name)
                        next()
                    }

                })
        }
    }
}

Assuming that the authorization cookie exists (Step 1), it is then checked for a valid signature using the secret stored on the server (Step 2). jwt.verify returns the JWT payload object if the signature is valid, or null if it is not. A missing or invalid cookie or JWT results in a 401 (Not Authorized) response to the client, and in the case of an invalid JWT the cookie itself is deleted.

If steps 1 and 2 are valid, we check the database to see if we have a record of the access token carried on the JWT, using the Twitter access token as the key. If a record is present it is a good indication that the client is authorized, and the call to next() at the end of Step 3 passes control to the next function in the middleware chain, which is in this case the rest of the POST route.

Logging the user out

If the user explicitly logs out, a back-end route is called to do the work:

//This route logs the user out:
//1. Delete the cookie
//2. Delete the access key and secret from the user record in mongo
//
router.get('/logout', checkAuthorization, function (req, res, next) {
    const userJWT = req.cookies.twitterAccessJwt
    const userJWTPayload = jwt.verify(userJWT, jwtConfig.jwtSecret)

    res.clearCookie('twitterAccessJwt')
    User.findOneAndUpdate({twitterAccessToken: userJWTPayload.twitterAccessToken},
        {
            twitterAccessToken: null,
            twitterAccessTokenSecret: null
        },
        function (err, result) {
            if (err) {
                console.log(err)
            }
            else {
                console.log("Deleted access token for", result.name)
            }
            res.render('twitterAccount', {loggedIn: false})
        })

})

We again check to see if the user is logged in, since we need the validated contents of the JWT in order to update the user's database record.

If the user simply closes the browser tab without logging out, the session cookie containing the JWT will be removed on the client. On next access the JWT will not validate in checkAuthorization and the user will be directed to the login page; successful login will update the access token and associated secret in the database.

Comments

In no particular order...

Some services set short expiration times on access tokens, and provide a method to exchange a 'refresh' token for a new access token. In that case an extra step would be necessary in order to update the token stored on the session cookie. Since access to third-party services are handled on the server, this would be transparent to the client.

This application only has one role: a logged-in user. For apps that require several roles, they should be stored in the database and retrieved on each request.

An architecture question comes up in relation to checkAuthorization. The question is, who should be responsible for handling an invalid user? In practical terms, should checkAuthorization return a boolean that can be used by each protecte route? Having checkAuthorization handle invalid cases centralizes this behavior, but at the expense of losing flexibility in the routes. I've leaned both way on this...an unauthorized user is unauthorized, period, and so it makes sense to handle that function in checkAuthorization; however, there might be a use case in which a route passes back a subset of data for unauthenticated users, or adds an extra bit of information for authorized users. For this particular example the centralized version works fine, but you'll want to evaluate the approach based on your won use cases.

The routes in this example simply render a Pug template that displays a user's Twitter accoun information, and a flag (loggedIn) is used to show and hide UI components. A more complex app will need a cleaner way of letting the client know the status of a user.

A gist with sampe code is availabe at gist:bdb91ed5f7d87c5f79a74d3b4d978d3d

Originally posted on my blog at Boston University

Top comments (12)

Collapse
 
orkon profile image
Alex Rudenko

Thanks for the great article! One remark: I would not put the actual twitter token inside the JWT. If the interaction is over plain HTTP, the token can be easily intercepted. I would suggest adding the Secure flag to the cookie. tools.ietf.org/html/rfc6265#sectio... The best would be to store only the user/session id in the JWT because it is unpredictable where the data leaks eventually.

Also, in my opinion, the expiration for JWTs is a must. Otherwise, the token gives a forever access to the service. I would highlight that.

Collapse
 
perrydbucs profile image
Perry Donham

Excellent point, Alex! I'll update my lecture and code on this as it clearly would be a potential leak. What do you think about using a constantly changing identifier on the session as I mentioned in the reply above?

Collapse
 
orkon profile image
Alex Rudenko

I think that would work for the session/client-auth use case. I mostly use JWTs in a distributed system where a token is an authorization to access a specific service (also backend-to-backend). For this use case, I always need the user ID in the token. Constantly changing identifier would be problematic to use because the service does not have access to the session or user data usually.

Collapse
 
convene143 profile image
Conven • Edited

Hi Perry , It's very useful article.
need help to set httpOnly flag to an existing response cookie.
Problem is jwt value is visible to client on browser to restrict we need to add security flag to cookies I couldn't find the right cookie Could you please help out me ,,
Here is the code ,

  1. First API call it returns JWT token[jwtToken]

def consumeAuthToken(authCode: String)(implicit req: Request, w: Wiring): Future[Response] = {
for {
withCode <- Try(json"""{ "code": ${authCode}}""").toFuture
r <- w.postgrest ? (method = Post, path = "/rpc/consume_auth_codes", json = Option(withCode))
json <- r.jsonArrHeadF
resp <- json.consume_auth_codes match {
case json""" { "found": false }""" =>
CANNED(NotFound, s"${authCode} is not a valid code", false)
case json""" { "found": true, "used": true }""" =>
CANNED(NotFound, s"${authCode} is not a valid code", false)
case json""" { "found": true, "used": false, "result": { "email": $email , "principal_id": $principal_id } }""" =>
val token = createJWT(email.as[String], principal_id.as[String]).get
CANNED(Ok, json"""{ "jwtToken": $token }""")
case any =>
CANNED(InternalServerError, any)
}
} yield resp
}
Response : jwtToken : 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkc3QiOiJjb252ZW5lLXFhIiwiZXhwIjoxNTU5OTAyMTU3LCJpYXQiOjE1NTcyMjM3NTcsImlzcyI6ImNvbnZlbmUtcWEiLCJzdWJfZW1haWwiOiJjb252ZW50ZXN0MDk5QGdtYWlsLmNvbSIsInN1YiI6IjMzYTJlZTNkLWQwMGItNGY4YS1hN2NlLTEwYzI0ZTdiNWJjMiIsInN1Yl9uYW1lIjoiIiwic3ViX2dyYXZhdGFyIjoiIiwic3ViX3Byb3ZpZGVyLW5hbWUiOiJjb252ZW5lLWVtYWlsIiwic3ViX3Byb3ZpZGVyLWlkIjoiY29udmVudGVzdDA5OUBnbWFpbC5jb20ifQ.XAEWEhNL92yYjClNsOgjb1tjIgxyBzhwubhaM5iVrwU'

  1. Second call goes to : -

def ensureUserExists(manager:Boolean)(implicit req: Request, w:Wiring): Future[Response] = {
for {
i <- req.parseJWT.toFuture
pr = i.sub_provider-name.map(_.toString).getOrElse("")
resJF <- checkSignOut()
respS=resJF.getContentString()
_ <- if(pr.equals("convene-email") || manager ) if(respS.size>2 && respS.substring(9,13).equals("true")) Future.Done else Future.exception(HTTPError(BadRequest, " new Invalid request body"))
else Future.Done
i <- req.parseJWT.toFuture
pr_provider = i.sub_provider-name.map(pp => json"""{"pr_provider-name": $pp}""").getOrElse(json"{}")
response <- w.models ? (path = "/api/models/users", method = Post,
json = Some(
json"""{
"pr_id": ${i.sub},
"pr_name": ${i.sub_name},
"pr_email": ${i.sub_email},
"manager": $manager
}""" ++ pr_provider))
resp <- response.jsonF
_ <- if (!manager) checkForPendingInvitations(i.sub) else Future.Done
responseCookies <- if (manager) checkManagerInvitation(i) else Future.value(Seq.empty)
_ <- if (resp.created.as[Boolean]) sendWelcomeEmail(i, manager) else Future.Done
} yield mkResp(Ok, Json.format(resp), cookies = responseCookies)
}

///

def checkManagerInvitation(i: TokenInfo)(implicit req: Request, w: Wiring): Future[Seq[Cookie]] = {
req.cookies.get("convene-manager-login") match {
case None => Future.value(Seq.empty)
case Some(c) =>
println(debug"""Login cookie ${c.value}""")
val payload = Some(json"""{
"principalID": ${i.sub},
"principalEmail": ${i.sub_email},
"principalName": ${i.sub_name},
"principalGravatar": ${i.sub_gravatar},
"blob": ${c.value}
}""")
val resp = w.invitations ? (method = Post, path = "/api/invitations/members/redeem", json = payload, propagateFailedResponse = true)
c.maxAge = 0.seconds
c.httpOnly=true // Is this right position way to enable
resp.map(_ => Seq(c))
}
}

How the jwt is been constructed as part of response .where have to add httpOnly true
@perrydbucs please have a look

Collapse
 
dimitarnestorov profile image
Dimitar Nestorov

Collapse
 
perrydbucs profile image
Perry Donham

The httpOnly flag on the cookie is what prevents the client from being able to see it; no reason not to send other cookies on the response that are visible, or an object that contains the info. I tend to treat the front end as just view, so there wouldn't be any front-end permissions checking -- the front end would only receive data appropriate for the role of the authenticated user.

Same with CSRF, from the front end's viewpoint it is only talking the the app on the back end, so there are no CSRF issues.

This reminds me of a demo that I sometimes do where I show how many websites, especially media (newspapers, etc), do a subscriber check on the front end in Javascript. Flipping the isSubscriber variable is trivial (or just turning JS off) to read the 'subscriber only' content.

Can't trust the client.

Collapse
 
deepaprasanna profile image
Deepa Prasanna

Thanks for the detailed article perry! I never knew until now that most of the websites do a subscription check on frontend. I tried turning off the js and it stopped showing the subscriber check.

Collapse
 
imthedeveloper profile image
ImTheDeveloper

Quite timely I have recently asked a question in relation to this here: dev.to/imthedeveloper/critique-my-...

Something that interests me is the storage of JWT. Whilst I appreciate the usage of cookies can aid to remove the accessibility of the cookie via client side javascript, my implementation required some of the data to be accessible. I went with storing the JWT in local storage. This brought me on to thinking deeper around JWT security. Technically even with a secure cookie I can still use chrome extensions to read it's content and thus extract the JWT. Once I have this JWT there is nothing stopping me passing this token to someone else who would have the exact same privileges as myself.

I assume under scenarios where tokens get passed around or an attacker engineers a method to retrieve such token then there really isn't much else you can do? Maybe browser fingerprinting would help as an additional check?

Collapse
 
perrydbucs profile image
Perry Donham

A really good point; maybe one of our security wonks will drop in and offer an opinion.

One approach might be to use the JWT in combination with a session identifier which changes on each request/response pair. The client would need to present both the signed JWT and the correct session identifier; both would be sent on the request automatically and both would be deleted when the browser or tab is closed. On the server side, seeing the same session identifier twice would be an error and would indicate a possible attack.

Also, requiring HTTPS would reduce the chance of a man-in-the-middle or sniffing attack.

In your case, where some data needs to be visible on the client, I think I'd still use the secure JWT and then send a second object back on the response with the data.

Collapse
 
nitingaur6817 profile image
nitingaur6817

Thx for a. great article. I had a quick question on enabling callbacks with JWT/Serverless... since we do not maintain sessions and in some cases we need to implement a callback to client either for redirect or conversational state and not for a web apps but say API services. -- is there anyway to achieve that? or do I need to implement session state?

Collapse
 
jjjjcccjjf profile image
endan • Edited

Hi Perry! Great article!

I'm curious as how would you implement this alongside with local-login and as well as other 3rd Party login such as facebook and google.

Collapse
 
perrydbucs profile image
Perry Donham

Thank you! The technique would be the same for multiple login paths. Once the user is logged in via a local user/password or a third party, generate the JWT. It's independent of the login method used.

If you are using Passport, its serialize / deserialize methods are a good spot to put JWT processing. As Alex pointed out above, sending the actual access token from a third party back and forth is not a secure method, and so you would want to store some identifying data in the JWT such as the user name or other unique value.

The JWT generation in the example code is done inside a Twitter OAuth callback; in your case with multiple providers you'd want to move that out into its own function that each callback (and your local user / password method) would invoke.

Your question has me thinking again about what responsibilities the JWT checking code should have...I'm starting to lean toward that function placing a user object with relevant user info (fetched from the database) onto the request along with an 'isAuthorized' boolean so that each route could decide what to do next.