simple-jwt-auth
v0.11.1
Published
A simple, convenient, and safe interface for interacting with JSON Web Tokens (JWTs) for authentication and authorization
Downloads
132
Maintainers
Readme
simple-jwt-auth
A simple, convenient, and safe interface for using JSON Web Tokens (JWTs) for authentication and authorization
Simple:
- exposes simple, declarative functions for each supported use case
- throws self explanatory errors when something goes wrong
- leverages open source standards to securely simplify the auth process
- e.g., can automatically lookup the public key required to verify a JWT by using the OAuth2 Discovery Flow
Safe:
- enforces best practices of JWT authentication
- eliminates accidentally using JWTs unsafely, by constraining exposed methods to secure and declarative use cases
In otherwords, it's built to provide a pit of success
Background
JSON Web Token (JWT) authentication is a great way to implement authentication and authorization for user facing applications
Using JWTs to sign requests enables distributed and stateless auth which eliminates latency and reduces costs, for snappy user experiences at scale.
- the request-signature, the jwt, can be authenticated publicly, by anyone
- [distributed] no api calls to issuer-server required, client-public-key is published at wellknown url, cacheable, static
- [stateless] no state to manage, access, or upkeep for authenticating requests
- the request-signature, the jwt, identifies the requester and scope
- [distributed] no api calls to issuer-server required, client identity is embedded and extractable from the request-signature
- [stateless] no state to manage, access, or upkeep for identifying the requester
References:
- JSON Web Token (JWT)
- JSON Web Signature (JWS)
- OAuth 2.0 Authorization Server Metadata
- JSON Web Key (JWK)
- The OAuth 2.0 Authorization Framework: Bearer Token Usage
- JSON Web Token Best Current Practices
Note: if you're looking to implement authentication and authorization for SDK applications, HMAC Key Auth may be a better fit due to their less vulnerable nature
Install
npm install --save simple-jwt-auth
Example
Authenticate and get claims from a JWT
This looks up the public key for the token and authenticates the claims. Useful any time you need to make sure that the claims are accurate (e.g., server side).
import { getAuthedClaims } from 'simple-jwt-auth';
const claims = getAuthedClaims({
token: 'eyJhbGciOiJSUzI1NiIsInR...', // a jwt
issuer: 'https://auth.whodis.io/...', // who you expect to have issued the token, must match `token.claims.iss`
audience: 'ae7f50b0-c762-821...', // the audience the token should be for, must match `token.claims.aud`
});
As long as your token's issuer publishes Authorization Server Metadata (an OAuth2 standard), we can find the public key for you and use it to authenticate your JWT.
Get token from headers
This grabs the token from the standard bearer token header for you. Useful whenever you need to grab a token from an HTTP request.
import { getTokenFromHeaders } from 'simple-jwt-auth';
const token = getTokenFromHeaders({ headers });
Tokens are typically passed to apis through the Authorization
header, according to the OAuth 2.0 Authorization Standard, so this exposes an easy way to grab the token from there.
Alternatively, tokens may also be passed through an Authorization
cookie, in the header. This is useful in browser environments where in order to protect users from XSS you store the JWT in an HTTPOnly cookie, inaccessible from JS. This method exposes an easy way to grab the token from an authorization
cookie, with two layer CSRF protection.
Get claims from a JWT without checking their authenticity
This simply decodes the body of the token and returns the claims, without checking anything. Useful for insecure environments (e.g., client side) where you cant trust data anyway - and debugging.
import { getUnauthedClaims } from 'simple-jwt-auth';
const claims = getUnauthedClaims({
token: 'eyJhbGciOiJSUzI1NiIsInR...', // a jwt
});
Create a secure distributed auth token
This method creates a JWT after checking that requirements for secure distributed authentication with the would be token are met.
import { createSecureDistributedAuthToken } from 'simple-jwt-auth';
const token = createSecureDistributedAuthToken({
headerClaims: { alg: 'RS256', kid: '4.some_directory', typ: 'JWT' },
claims: {
iss: 'https://auth.whodis.io/...',
aud: 'f7326c71-cf5a-4637-9580-8e83c2692e96',
sub: 'e41ea57c-f630-45ba-88fc-8888b06c588e',
exp: 2516239022,
},
privateKey, // rsa pem format private key string
});
Docs
fn:getAuthedClaims({ token: string, issuer: string, audience: string | string[] })
Use this function when you want to authenticate and get the claims that a token is making for use in your applications.
If your token's issuer publishes Authorization Server Metadata (an OAuth2 standard), then we can find the public key for you. We'll cache it up to 5 min by default to speed up subsequent checks.
We check the authenticity of the token in the following ways:
- the token is valid
- by verifying the signature
- check that we can verify the signature comes from the issuer, with the public key
- check that the header/payload have not been tampered with, with the signature
- check that the token uses an asymmetric signing key, for secure decentralized authentication
- by verifying the timestamps
- token is not expired
- token is not used before its allowed to be
- by verifying the signature
- the token comes from the expected issuer
- otherwise, anyone can issue claims to your server
- the token is meant for your application
- otherwise, a token from the same issuer but for a different application could be used to access your application
References:
- https://tools.ietf.org/html/draft-ietf-oauth-jwt-bcp-07
- https://www.sjoerdlangkemper.nl/2016/09/28/attacking-jwt-authentication/
- https://www.cloudidentity.com/blog/2014/03/03/principles-of-token-validation/
Example:
import { getAuthedClaims } from 'simple-jwt-auth';
const claims = getAuthedClaims({
/**
* The JWT that you're checking for authenticity before getting claims
*/
token,
/**
* Who you expect to issue the JWT.
*
* The issuer `string` that you define here is checked against the issuer that the token was issued by (`token.claims.iss`)
*
* This is required because it is critical for security that you only accept tokens from expected issuers.
*
* `getAuthedClaims` will throw an error if `issuer !== token.claims.iss`
*/
issuer,
/**
* The id(s) that the JWT will use to specify that it was created for your application.
*
* The audience `string` (or each `string` in the `string[]`) that you define here is checked against the audience that the token is for (`token.claims.aud`).
*
* This is required, as it is critical for security that you only trust tokens that were intended for you
*
* `getAuthedClaims` will throw an error if `audience !== token.claims.aud`
*/
audience,
});
note: you can check whether your token was issued by an auth service that supports this OAuth2 Discovery Flow by checking whether the auth server exposes Authorization Server Metadata
at expected address: ${token.iss}/.well-known/oauth-authorization-server
fn:getTokenFromHeaders({ token: string, issuer: string, audience: string | string[] })
Use this function when you want to safely extract the token from the headers of the request made to your server.
This function supports two ways of extracting a token from headers:
- through the
Authorization
header- commonly used in native applications (iOS, Android, CLI, etc) where the user has programmatically accessible secure storage for their token
- OAuth 2.0 Authorization Standard
- through the
Authorization
cookie- commonly used in web applications, where users do not have programmatically accessible secure storage for their token, due to XSS, and must rely on HTTPOnly cookies instead
Authorization Header
Extracting the token from an authorization header is very simple. We simply look for a header with the name Authorization
(case insensitive, per spec) and get the token from it.
This function supports the authorization header defining the token with prefix of Bearer
as well as not having a prefix at all:
Bearer __TOKEN__
__TOKEN__
Authorization Cookie
Extracting the token from an authorization cookie is simple, but requires protecting the user against cross-site-request-forgery (CSRF) attempts.
When a request is made with an authorization header, we know that the origin making the request has full programmatic access to the JWT, which confirms that the token owner intended to send the token. However, when a request is made with an authorization cookie, the origin making the request typically does not have programmatic access to the JWT at all. Instead, the browser simply sends the cookie to the target domain any time a request is made to that domain - leaving it susceptible to CSRF.
Cross-site-request-forgery (CSRF) is an attack that leverages the fact that browsers often do not consider the origin of a request when considering whether to send cookies. Specifically, if a user has a cookie from yoursite.com
, visits hakrsite.com
, and hakrsite.com
sends a request to yoursite.com
- the user's browser will happily send yoursite.com
the user's cookie in the request that hakrsite.com
made (e.g., /transfer/funds?from=user&to=hakr&dollars=10000
). Without additional safeguards against CSRF, yoursite.com
will see the cookie and authenticate the request.
This function, getTokenFromHeaders
, leverages a two layer defence from the recommendations of OWASP:
- Verifying Origin with Standard Headers
- "Reliability on these headers comes from the fact that they cannot be altered programmatically (using JavaScript with an XSS vulnerability) as they fall under forbidden headers list, meaning that only the browser can set them"
- Synchronizer Token Based Mitigation
- "CSRF tokens prevent CSRF because without token, attacker cannot create a valid requests to the backend server."
This library leverages the properties of JWTs in order to make the implementation of origin-verification and anti-csrf-tokens seamless from the eyes of the developer.
The first layer, Verify Origin with Standard Headers
, is composed of two parts:
figuring out the
target origin
andsource origin
sourceOrigin = header.origin ?? header.referrer
, as defined by the OWASP recommendations- these can be trusted because they are restricted headers, which can only be set by browsers
if (!sourceOrigin) throw new PotentialCSRFAttackError
- dont allow requests without eitherorigin
orreferrer
defined
targetOrigin = jwt.aud
, i.e. the audience claim of the token- the
aud
claim of a JWT should be theuri
of the target origin that the token is intended to be consumed by
- the
comparing the
target origin
andsource origin
if (!isSameSite(sourceOrigin, targetOrigin)) throw new PotentialCSRFAttackError
- check that
isSameSite(sourceOrigin, targetOrigin)
api.yoursite.com
andwww.yoursite.com
are the same site, since they differ only by subdomainyoursite.github.io
andmysite.github.io
are not the same site, since domains likegithub.io
andcloudfront.net
are a public domains
- check that
The second layer, Synchronizer Token Based Mitigation
, is composed of three parts:
a unique, secure, and random anti-csrf-token is returned by the auth server (so an attacker can't guess or deduce the token)
- the anti-csrf-token is expected to be a signature-redacted form of the auth-token
- signature-redacted meaning the signature of the JWT is replaced with
__REDACTED__
, ensuring that this JWT can not be used for authentication- guarantees the anti-csrf-token is not a risk if stolen by XSS, safe to store in memory or local-storage
- otherwise, the anti-csrf-token would actually present a significant XSS vulnerability
getTokenFromHeaders
checks this withif (antiCsrfTokenSignature !== '__REDACTED__') throw new PotentialXSSVulnerabilityError
- signature-redacted meaning that the header and body claims of the anti-csrf-token are equivalent to the auth-token
- guarantees that the anti-csrf-token is synchronized to the auth-token of this specific session
- otherwise, the anti-csrf-token could not be verified on the serverside in a stateless, distributed way
getTokenFromHeaders
checks this withif (authTokenBody !== antiCsrfTokenBody || authTokenHeader !== antiCsrfTokenBody) throw new PotentialCSRFAttackError
- signature-redacted meaning the signature of the JWT is replaced with
- the auth-token must have a random, unique
jti
claim- guarantees the anti-csrf-token is random and unique per session
- otherwise, the anti-csrf-token could be guessed or deduced, posing a CSRF vulnerability
getTokenFromHeaders
checks this withif (!isUuidV4(jwt.jti)) throw new PotentialCSRFVulnerabilityError
- the authorization cookie, storing the auth-token, must be
HTTPOnly
andSecure
to protect against XSS and MITM attacks- otherwise, not only could the anti-csrf-token be stolen, but worse the auth-token itself could be stolen - making CSRF the least of your concerns
- the anti-csrf-token is expected to be a signature-redacted form of the auth-token
the anti-csrf-token is sent on each request in the body or custom header (proving that the source of the request has programmatic access to the anti-csrf-token)
getTokenFromHeaders
expects that the anti-csrf-token is sent in the authorization header of the request- sending the anti-csrf-token in the authorization header allows browser and native environments to have the same exact code path, simplifying cross platform development.
- sending the anti-csrf-token in the authorization header also proves that the requester has programmatic access to the anti-csrf-token, proving they were given it at some point
the server verifies the anti-csrf-token when processing each request (otherwise an attacker could pass in random values)
- the auth-token, jwt, must be found a cookie named
authorization
(case sensitive) - the anti-csrf-token must be found in the authorization header, as mentioned in part 2
getTokenFromHeaders
verifies that the anti-csrf-token is synchronized, unique, random, and secure - by conducting the checks mentioned in part 1- this verification ensures that this request could only have been made by the origin to which we gave the
jwt
- this verification ensures that this request could only have been made by the origin to which we gave the
- the auth-token, jwt, must be found a cookie named
Important Note: CSRF protection is only useful when the website is not under XSS attack. While storing the auth-token in a cookie prevents XSS attacks from stealing the token directly, it does not prevent an XSS attack from making requests from your site on the users browser. In otherwords, if your site has been attacked with a custom XSS attack, CSRF is the least of your concerns.
Important Note: CSRF protection is only useful when the cookie itself is maximally protected. Please ensure that the cookie storing the token is protected with the following flags:
Secure
: to ensure that the cookie is only transmitted over HTTPS (protects against MITM)HTTPOnly
: to ensure that the cookie is inaccessible to Javascript (protects against XSS)