Safety Cribs: JWT



    Many applications use JSON Web Tokens (JWT) to allow the client to identify themselves for further information exchange after authentication.

    JSON Web Token is an open standard (RFC 7519) that defines a compact and autonomous way to securely transfer information between parties as a JSON object.


    This information is verified and reliable because it is digitally signed.
    JWTs can be signed using secret (using the HMAC algorithm) or public / private key pairs using RSA or ECDSA.

    JSON Web Token is used to transmit information regarding the identity and characteristics of the client. This “container” is signed by the server so that the client does not interfere with it and cannot change, for example, identification data or any characteristics (for example, the role from a simple user to an administrator or change the client’s login).

    This token is created in case of successful authentication and is checked by the server before starting each client request. The token is used by the application as an “identity card” of the client (a container with all information about him). The server has the ability to verify the validity and integrity of the token in a safe way. This allows the application to be stateless (a stateless application does not save client data generated in one session for use in the next session with this client (each session is independent)), and the authentication process is independent of the services used (in the sense that client and server technologies may vary, including even the transport channel, although HTTP is most commonly used).

    Considerations for Using JWT


    Even if the JWT token is easy to use and allows you to provide services (mainly REST) ​​without statefulness (stateless), this solution is not suitable for all applications, because it comes with some caveats, such as the issue of storing the token.

    If the application does not have to be completely stateless, then you can consider using the traditional session system provided by all web platforms. However, for stateless applications, JWT is a good option if implemented correctly.

    JWT Issues and Attacks


    Using the NONE Hash Algorithm


    A similar attack occurs when an attacker changes the token and also changes the hashing algorithm (“alg” field) to indicate through the none keyword that the token integrity has already been verified. Some libraries considered tokens signed using the none algorithm as a valid token with a verified signature, so an attacker could change the payload of the token, and the application would trust the token.

    To prevent an attack, you must use the JWT library, which is not affected by this vulnerability. Also, during the validation of the token, you must explicitly request the use of the expected algorithm.

    Implementation Example:

    // Ключ HMAC хранится как String в памяти JVM
    private transient byte[] keyHMAC = ...;
    ...
    // Создание контекста верификации для запроса к токену
    // Явно указывается использование HMAC-256 хеш-алгоритма
    JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC)).build();
    // Верификация токена
    DecodedJWT decodedToken = verifier.verify(token);

    Token interception


    The attack occurs when a token has been intercepted or stolen by an attacker and he uses it to gain access to the system using the credentials of a specific user.

    Protection consists in adding a “user context” to the token. The user context will consist of the following information:

    1. A random string that is generated at the authentication stage and included in the token, and also sent to the client as a more secure cookie (flags: HttpOnly + Secure + SameSite + cookie prefixes).
    2. The SHA256 hash from the random string will be stored in the token so that any XSS problem would not allow the attacker to read the value of the random string and set the expected cookie.

    The IP address will not be used in context, because there are situations in which the IP address can change during one session, for example, when a user accesses the application through his mobile phone. Then the IP address is constantly legitimately changing. Moreover, using an IP address can potentially cause problems at the level of compliance with European GDPR.

    If during the token verification the received token does not contain the correct context, it must be rejected.
    Implementation example:

    Code for creating a token after successful authentication:

    // Ключ HMAC хранится как String в памяти JVM
    private transient byte[] keyHMAC = ...;
    // Генератор случайных данных
    private SecureRandom secureRandom = new SecureRandom();
    ...
    // Генерация случайной строки, которая является фингерпринтом пользователя
    byte[] randomFgp = new byte[50];
    secureRandom.nextBytes(randomFgp);
    String userFingerprint = DatatypeConverter.printHexBinary(randomFgp);
    // Добавление фингерпринта в cookie
    String fingerprintCookie = "__Secure-Fgp=" + userFingerprint
                               + "; SameSite=Strict; HttpOnly; Secure";
    response.addHeader("Set-Cookie", fingerprintCookie);
    // SHA256 хеш от фингерпринта для хранения хеша фингерпринта в токене
    // (вместо открытых данных) чтобы XSS не мог прочитать фингерпринт и
    // самостоятельно установить нужное значение cookie
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    byte[] userFingerprintDigest = digest.digest(userFingerprint.getBytes("utf-8"));
    String userFingerprintHash = DatatypeConverter.printHexBinary(userFingerprintDigest);
    // Создание токена с временем валидности 15 минут и контекстом клиента
    Calendar c = Calendar.getInstance();
    Date now = c.getTime();
    c.add(Calendar.MINUTE, 15);
    Date expirationDate = c.getTime();
    Map headerClaims = new HashMap<>();
    headerClaims.put("typ", "JWT");
    String token = JWT.create().withSubject(login)
       .withExpiresAt(expirationDate)
       .withIssuer(this.issuerID)
       .withIssuedAt(now)
       .withNotBefore(now)
       .withClaim("userFingerprint", userFingerprintHash)
       .withHeader(headerClaims)
       .sign(Algorithm.HMAC256(this.keyHMAC));


    Code to verify the validity of the token:
    
    // Ключ HMAC хранится как String в памяти JVM
    private transient byte[] keyHMAC = ...;
    ...
    // Получение фингерпринта пользователя из cookie
    String userFingerprint = null;
    if (request.getCookies() != null && request.getCookies().length > 0) {
     List cookies = Arrays.stream(request.getCookies()).collect(Collectors.toList());
     Optional cookie = cookies.stream().filter(c -> "__Secure-Fgp"
                                                .equals(c.getName())).findFirst();
     if (cookie.isPresent()) {
       userFingerprint = cookie.get().getValue();
     }
    }
    // Вычисление SHA256 хеша от полученного фингерпринта из cookie для
    // сравнения с хешем фингерпринта в токене
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    byte[] userFingerprintDigest = digest.digest(userFingerprint.getBytes("utf-8"));
    String userFingerprintHash = DatatypeConverter.printHexBinary(userFingerprintDigest);
    // Создание контекста для верификации токена
    JWTVerifier verifier = JWT.require(Algorithm.HMAC256(keyHMAC))
                                  .withIssuer(issuerID)
                                  .withClaim("userFingerprint", userFingerprintHash)
                                  .build();
    // Верификация токена
    DecodedJWT decodedToken = verifier.verify(token);

    Explicit revocation of token by user


    Since the token becomes invalid only after its expiration, the user does not have a built-in function that allows you to explicitly cancel the token. Thus, in case of theft, the user cannot withdraw the token himself and then block the attacker.

    One of the protection methods is the introduction of a blacklist of tokens, which will be suitable for simulating the "log out" function that exists in a traditional session system.

    The collection (in SHA-256 encoding in HEX) of the token with the cancellation date, which should exceed the validity period of the issued token, will be stored in the black list.

    When the user wants to "log out", he calls a special service that adds the provided user token to the black list, which leads to the immediate cancellation of the token for further use in the application.

    Implementation example:

    Blacklist storage:
    For centralized storage of the blacklist, a database with the following structure will be used:

    create table if not exists revoked_token(jwt_token_digest varchar(255) primary key,
    revokation_date timestamp default now());

    Token revocation management:

    // Контролирование отката токена (logout).
    // Используйте БД, чтобы разрешить нескольким экземплярам проверять
    // отозванный токен и разрешить очистку на уровне централизованной БД.
    public class TokenRevoker {
     // Подключение к БД
     @Resource("jdbc/storeDS")
     private DataSource storeDS;
     // Проверка является ли токен отозванным
     public boolean isTokenRevoked(String jwtInHex) throws Exception {
         boolean tokenIsPresent = false;
         if (jwtInHex != null && !jwtInHex.trim().isEmpty()) {
             // Декодирование токена
             byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex);
             // Вычисление SHA256 от токена
             MessageDigest digest = MessageDigest.getInstance("SHA-256");
             byte[] cipheredTokenDigest = digest.digest(cipheredToken);
             String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest);
             // Поиск токена в БД
             try (Connection con = this.storeDS.getConnection()) {
                 String query = "select jwt_token_digest from revoked_token where jwt_token_digest = ?";
                 try (PreparedStatement pStatement = con.prepareStatement(query)) {
                     pStatement.setString(1, jwtTokenDigestInHex);
                     try (ResultSet rSet = pStatement.executeQuery()) {
                         tokenIsPresent = rSet.next();
                     }
                 }
             }
         }
         return tokenIsPresent;
     }
    // Добавление закодированного в HEX токена в таблица отозванных токенов
    public void revokeToken(String jwtInHex) throws Exception {
         if (jwtInHex != null && !jwtInHex.trim().isEmpty()) {
             // Декодирование токена
             byte[] cipheredToken = DatatypeConverter.parseHexBinary(jwtInHex);
             // Вычисление SHA256 от токена
             MessageDigest digest = MessageDigest.getInstance("SHA-256");
             byte[] cipheredTokenDigest = digest.digest(cipheredToken);
             String jwtTokenDigestInHex = DatatypeConverter.printHexBinary(cipheredTokenDigest);
             // Проверка на наличие токена уже в БД и занесение в БД в
    	   // обратном случае
             if (!this.isTokenRevoked(jwtInHex)) {
                 try (Connection con = this.storeDS.getConnection()) {
                     String query = "insert into revoked_token(jwt_token_digest) values(?)";
                     int insertedRecordCount;
                     try (PreparedStatement pStatement = con.prepareStatement(query)) {
                         pStatement.setString(1, jwtTokenDigestInHex);
                         insertedRecordCount = pStatement.executeUpdate();
                     }
                     if (insertedRecordCount != 1) {
                         throw new IllegalStateException("Number of inserted record is invalid," + " 1 expected but is " + insertedRecordCount);
                     }
                 }
             }
         }
     }
    

    Token Disclosure


    This attack occurs when an attacker gains access to a token (or a set of tokens) and extracts the information stored in it (information about the JWT token is encoded using base64) to obtain information about the system. Information may be, for example, such as security roles, login format, etc.

    The protection method is quite obvious and consists in encrypting the token. It is also important to protect encrypted data from attacks using cryptanalysis. To achieve all these goals, the AES-GCM algorithm is used, which provides Authenticated Encryption with Associated Data (AEAD). AEAD primitive provides symmetric authenticated encryption functionality. Implementations of this primitive are protected from adaptive attacks based on selected ciphertext. When encrypting plaintext, you can optionally specify related data that must be authenticated but not encrypted.

    That is, encryption with the relevant data ensures the authenticity and integrity of the data, but not their secrecy.

    However, it should be noted that encryption is added mainly to conceal internal information, but it is very important to remember that the initial protection against counterfeiting the JWT token is a signature, therefore, the token's signature and its verification should always be used.

    Client side token storage


    If the application stores the token so that one or more of the following situations occurs:

    • the token is automatically sent by the browser (cookie storage);
    • the token is obtained even if the browser is restarted (using the browser localStorage container);
    • the token is obtained in the case of an XSS attack (cookie available for JavaScript code or a token that is stored in localStorage or sessionStorage).

    To prevent an attack:

    1. Store the token in the browser using the sessionStorage container.
    2. Add it to the Authorization header using the Bearer scheme. The title should look like this:

      Authorization: Bearer 
    3. Add fingerprint information to the token.

    By storing the token in the sessionStorage container, it provides a token for theft in the case of XSS. However, a fingerprint added to the token prevents an attacker from reusing the stolen token on his computer. To close the maximum usage areas for an attacker, add a Content Security Policy to limit the execution context.

    There remains a case where an attacker uses the user's browsing context as a proxy server to use the target application through a legitimate user, but the Content Security Policy can prevent communication with unexpected domains.

    It is also possible to implement an authentication service so that the token is issued inside a secure cookie, but in this case, protection against CSRF should be implemented.

    Using a weak key to create a token


    If the secret used in the case of the HMAC-SHA256 algorithm, necessary for signing the token, is weak, then it can be hacked (picked up using brute force attack). As a result, an attacker can fake an arbitrary valid token in terms of a signature.

    To prevent this problem, you must use a complex secret key: alphanumeric (mixed case) + special characters.

    Since the key is needed only for computer calculations, the size of the secret key can exceed 50 positions.

    For instance:

    A&'/}Z57M(2hNg=;LE?~]YtRMS5(yZ2j:ZeX-BGftaVk`)jKP~q?,jk)EMbgt*kW'

    To assess the complexity of the secret key used for your token signing, you can apply a password dictionary attack to the token in combination with the JWT API.

    Also popular now: