Quick Start and Use of JWT

Posted by tinker on Wed, 01 Dec 2021 21:06:32 +0100

Use of JWT(JSON Web Token)

Preface

This post is correct shiro uses JWT Complete complementary extensions to provide full sample code for detailed use in JWT.

If there is a discrepancy between your understanding of RSA and your own reading of this blog, please read the part of this blog explaining RSA. If there are other inconsistencies after reading, you are welcome to leave a message for discussion.

Role and basic formatting of JWT

Just read this question and answer , which explains in detail what JWT is, what JWT can be used for, the format of JWT, how JWT works, why JWT should be used, and so on.

Basic Format

In fact, after reading the above document, you will understand its basic format. To put it simply here, JWT is divided into three sections

xxxxx.yyyyy.zzzzz

Represents the Header, which contains the signature algorithm used, and the type of token

{
  "alg": "HS256",
  "typ": "JWT"
}

Payload,Payload mainly contains token-related information such as signing time, expiration time, and some customized information. A more complete example of payload is as follows

{
  "sub": "1000",
  "aud": [
    "https://app-one.com",
    "https://app-two.com"
  ],
  "nbf": 1638357246,
  "iss": "http://localhost:18080",
  "exp": 1638357846,
  "iat": 1638357246,
  "jti": "17fcfe5e-f705-481d-bf55-23368988f8d6",
  "scope": "read write"
}
  • iss can be understood as where token s are generated
  • exp expiration time
  • iat can be interpreted as generating token time
  • nbf means token must be no earlier than this time
  • aud can be understood as the scope of this token
  • jti JWT ID
  • subject is stored information, such as userId
  • scope is our custom field, if you want to set other fields basic actual scene settings

See full explanation Here

Signature is the signature of the JWT. Encrypt the encryption algorithm used by Header and Payload+ after base64 encoding

The final output is a base64-encoded string, separated by three dots (...), so the JWT is decoded by Base64 to see the corresponding payload content. So sensitive information isn't recommended in payload.

Why do I need to sign?

In fact, the second red part of the above screenshot has already been explained. Signatures are added to prevent tampering with JWT and, if encrypted using RSA private keys, the correct decrypted content can only be obtained by decrypting using RSA public keys on the server side. It is also important to use this to prove that the current JWT signature has not been tampered with or is currently signed on the server side (similarly, for HMAC, HMAC secret s are only on the server side, and the results of the calculation agree that the JWT is indeed from its own server).

JWT-related Libraries

The JWT libraries used more in Java are nimbus-jose-jwt and io.jsonwebtoken. Can be used in Complete list of JWT Libraries More libraries supported by JWT are found in.

stay Using JWT in Shiro In this post, use the io.jsonwebtoken library. It simply pastes some code without making a detailed distinction between the class library choices.

This article describes using the nimbus-jose-jwt Library

Selection of nimbus-jose-jwt and jsonwebtoken

The stackoverflow has already given the answer, and here are two reference links that you can use to make your own decisions

In short, nimbus-jose-jwt supports more features

nimbus-jose-jwt use

nimbus-jose-jwt Official Web There are a number of examples from which you can quickly get started using JWT. In the official website, nimbus is basically divided into two parts: JWS and JWE for the use of JWT. This paper will mainly explain JWS,JWE as a guide, readers can explore the practice themselves.

JWS

JSON Web Signature

Sign the JWT. The role of signing content has been described previously

  • Prevent tampering with JWT content and ensure its integrity
  • You can verify that the JWT is actually signed from the current server

Note: In fact, signing is not only used in JWT, but also in the practical application of SSL. You can learn more if you are interested

There are HMAC, RSA, EC, etc. on the encryption algorithm of the signature. For the usage scenario of each encryption algorithm, please move on Official Documents , which explains in detail some of the objectives of encrypting data and the corresponding goals and scenarios for various encryption algorithms

HMAC encryption algorithm
Scenarios using HMAC

For example, the second red-colored part of a message's verification code explains its best use when data is sent outside and eventually applied for identification. Its core idea is to ensure that the data is not tampered with and that the data is generated by us.

Here's a simple springboot application for demonstration

Generate jwt using HMAC encryption algorithm
@Configuration
@Component
public class SignerAndVerifierConfiguration {

    private static final String sharedSecret = "31611159e7e6ff7843ea4627745e89225fc866621cfcfdbd40871af4413747cc";
    @Bean(name = "HmacSigner")
    @SneakyThrows
    public JWSSigner generateHmacJwsSigner() {
        SecureRandom random = new SecureRandom();
        random.nextBytes(sharedSecret.getBytes());
        return new MACSigner(sharedSecret);
    }

    @Bean(name = "HmacVerifier")
    @SneakyThrows
    public JWSVerifier getHmacJwsVerifier() {
        SecureRandom random = new SecureRandom();
        random.nextBytes(sharedSecret.getBytes());
        return new MACVerifier(sharedSecret);
    }
}

Configure HMAC's signer-JWSSigner and verifier-JWSVerifier first, where HMAC's secret uses a random string

Next, we begin to construct the JWT, starting with our payload, which uses the ==JWTClaimsSet.Builder()==method

@Component
public class JWTClaimsSetFactory {

    public JWTClaimsSet buildJWTClaimsSet(String userId) {
        Calendar signTime = Calendar.getInstance();
        Date signTimeTime = signTime.getTime();
        signTime.add(Calendar.MINUTE, 10);
        Date expireTime = signTime.getTime();
        return new JWTClaimsSet.Builder()
                .issuer("http://localhost:18080")
                .subject(userId)
                .audience(Arrays.asList("https://app-one.com", "https://app-two.com"))
                .expirationTime(expireTime)
                .notBeforeTime(signTimeTime)
                .issueTime(signTimeTime)
                .jwtID(UUID.randomUUID().toString())
                .claim("scope", "read write")
                .build();
    }
}

Once you get payload, add a Header and use signer to encrypt. This uses SignedJWT, which means the JWS we use

@RestController
@RequestMapping("generate")
@Log
public class GenerateTokenController {

    @Autowired
    @Qualifier("HmacSigner")
    private JWSSigner hmacSigner;

    @Autowired
    private JWTClaimsSetFactory jwtClaimsSetFactory;

    @GetMapping("hmac")
    @SneakyThrows
    public String generateHMACToken() {
      // Incoming header and payload
        SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.HS256).type(JOSEObjectType.JWT).build(), jwtClaimsSetFactory.buildJWTClaimsSet("ADMIN"));
        // Sign
        signedJWT.sign(hmacSigner);
        String result = signedJWT.serialize();
        log.info("HMAC token is: \n" + result);
        return result;
    }
}
Resolving jwt of HMAC encryption algorithm
@RestController
@RequestMapping("verify")
public class VerifyTokenController {

    @Autowired
    @Qualifier("HmacVerifier")
    private JWSVerifier hmacVerifier;

    @GetMapping("hmac")
    @SneakyThrows
    public boolean verifyHMACToken(@RequestHeader("Authorization") String token) {
        SignedJWT parse = SignedJWT.parse(token);
        if (!parse.verify(hmacVerifier)) {
            throw new RuntimeException("invalid token");
        }
        verifyClaimsSet(parse.getJWTClaimsSet());
        return true;
    }

    /**
     * All validations are done here.
     * @param jwtClaimsSet
     */
    private void verifyClaimsSet(final JWTClaimsSet jwtClaimsSet) {
        boolean result = false;
        if (Calendar.getInstance().getTime().before(jwtClaimsSet.getExpirationTime())) {
            result = true;
        }
        if (!result) {
            throw new RuntimeException("token expired");
        }
    }
}
RSA Encryption Algorithm
Scenarios using RSA

For example, when OAuth2.0 servers send access token s. When used, the public and private keys need to be generated, then signed by the private key and verified by the public key.

Online generation of RSA public and private keys

Use during presentations Online RAS Generation Web site to generate RSA public and private keys. In practice, openSSL can be used to generate on the server

Put the generated public and private keys into the publish-key.pem and private-key.pem files, respectively. If not, create a new one.

Generate jwt using RSA encryption algorithm

Like HMAC, we also configure signer and verifier

@Configuration
@Component
public class SignerAndVerifierConfiguration {

    private static final String sharedSecret = "31611159e7e6ff7843ea4627745e89225fc866621cfcfdbd40871af4413747cc";

    @Bean(name = "RsaSigner")
    @SneakyThrows
    public JWSSigner generateRsaJwsSigner(){
        // Read private key content
        String pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString("rsa/private-key.pem");
        RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey);
        return new RSASSASigner(rsaKey.toRSAPrivateKey());
    }

    @Bean(name = "RsaVerifier")
    @SneakyThrows
    public JWSVerifier getRsaJWSVerifier() {
      // Read Public Key Content
        String pemEncodedRSAPublicKey = PEMKeyUtils.readKeyAsString("rsa/publish-key.pem");
        RSAKey rsaPublicKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPublicKey);
        return new RSASSAVerifier(rsaPublicKey);
    }
}

Here we put the two files we just built under the resources file of the project and read them when initializing signer and verifier

@RestController
@RequestMapping("generate")
@Log
public class GenerateTokenController {

    @Autowired
    @Qualifier("RsaSigner")
    private JWSSigner rsaSigner;

    @Autowired
    private JWTClaimsSetFactory jwtClaimsSetFactory;

    @GetMapping("{userId}")
    @SneakyThrows
    public String generateRSAToken(@PathVariable String userId) {
      // header Select RS256 here
        SignedJWT signedJWT = new SignedJWT(new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build(), jwtClaimsSetFactory.buildJWTClaimsSet(userId));
      // autograph
        signedJWT.sign(rsaSigner);
        String result = signedJWT.serialize();
        log.info("token is: \n" + result);
        return result;
    }
}

Construct the payload section here, as in the example, without giving duplicate code

Resolving jwt of RAS encryption algorithm
@RestController
@RequestMapping("verify")
public class VerifyTokenController {

    @Autowired
    @Qualifier("RsaVerifier")
    private JWSVerifier rsaVerifier;

    @GetMapping
    @SneakyThrows
    public boolean verifyRSAToken(@RequestHeader("Authorization") String token) {
        SignedJWT parse = SignedJWT.parse(token);
        if (!parse.verify(rsaVerifier)) {
            throw new RuntimeException("invalid token");
        }
        verifyClaimsSet(parse.getJWTClaimsSet());
        return true;
    }
}

JWE

JWS uses signatures to ensure that data content is not tampered with, but the pauload content is still visible after the final base64 decoding. In practice, we also have a userId in our JWT. If it comes with OAuth 2.0, there may be scope s and so on, but in fact these contents will have little impact on us after they are decoded.

However, if you want to encrypt the contents of payload so that the data cannot be decoded, then you need a JWE-JSON Web Encryption.

Here's another reminder:

  • JWS is to sign content without encrypting it
  • JWE encrypts but does not sign content
Encrypt content using RSA
@Component
@Configuration
public class EncryptAndDecryptConfiguration {
    
    @Bean
    @SneakyThrows
    public JWEEncrypter generateRsaJweEncrypter() {
        String pemEncodedRSAPublicKey = PEMKeyUtils.readKeyAsString("rsa/publish-key.pem");
        RSAKey rsaPublicKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPublicKey);
        return new RSAEncrypter(rsaPublicKey);
    }
    
    @Bean
    @SneakyThrows
    public JWEDecrypter getRsaJweDecrypter() {
        String pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString("rsa/private-key.pem");
        RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey);
        return new RSADecrypter(rsaKey);
    }
}

Configure JWEEncrypter and JWEDecrypter

@RestController
@RequestMapping("secret")
@Log
public class SecretController {
    
    @Autowired
    private JWEEncrypter jweEncrypter;
    
    @Autowired
    private JWTClaimsSetFactory jwtClaimsSetFactory;
    
    @GetMapping("{userId}")
    @SneakyThrows
    public String secretToken(@PathVariable final String userId) {
      // Set up JWE header
        JWEHeader header = new JWEHeader(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM);
        EncryptedJWT encryptedJWT = new EncryptedJWT(header, jwtClaimsSetFactory.buildJWTClaimsSet(userId));
      // Encrypt with publishKey
        encryptedJWT.encrypt(jweEncrypter);
        String result = encryptedJWT.serialize();
        log.info("encrypt token is: \n" + result);
        return result;
    }
}

Decrypt the base64 string of the generated JWT and find that the payload content is not visible

Decrypt

    @GetMapping("decrypt")
    @SneakyThrows
    public void decryptRSASecretToken(@RequestHeader("Authorization") String token) {
        EncryptedJWT encryptedJWT = EncryptedJWT.parse(token);
       // Decrypt using private
        encryptedJWT.decrypt(jweDecrypter);
    }

JWS + JWE

After the previous introduction, some people may wonder if I can sign and encrypt, of course. Here you can refer to the official nimbus About Example of using signature with encryption . Don't explain much here

The order of official suggestions is to sign first and then encrypt. For why, look at the first line of the official example above to explain

Some explanations of RSA

Those who know RSA in general may be confused with the way I use RSA in JWS. Specific doubts may be as follows:

  1. RSA asymmetric encryption is both a pair of RSA public and private keys. Why do only the server side have public and private keys here?

    A: The server that sends token down corresponds to the browser. The browser itself does not need to decrypt the token, just put the next request in the request header. So here the browser does not actually need to maintain its own set of RSA public and private keys

  2. Why use RSA private key encryption, public key decryption. Aren't both public key encryption and private key decryption?

    A: Scenarios for public key encryption and private key decryption are for content encryption. The scenario we use with JWS is signing content. So in the use of RSA practice, we need to look at our needs. Simple explanation is as follows

    The first usage is public key encryption and private key decryption. For encryption and decryption
    Second usage: private key signature, public key verification. For signature

    It's a bit confusing. Don't try to remember it. Make a summary:
    All you have to do is:
    Since it is encrypted, you certainly don't want others to know my message, so only I can decrypt it, so it can be concluded that the public key is responsible for encryption and the private key is responsible for decryption.
    Since it's a signature, you certainly don't want anyone to pass me off as a message. Only I can publish this signature, so you can get the private key to sign and the public key to verify.

    In the same way, I'm saying something different:
    The private key and the public key are a pair. Anyone can encrypt and decrypt it, but who encrypts and decrypts it depends on the scenario:
    The first scenario is signing, encrypting with a private key, and decrypting the public key, which allows all public key owners to verify the identity of the private key owner and prevents content published by the private key owner from being tampered with. However, it is not used to guarantee that content is not acquired by others.
    The second scenario is encryption, encryption with a public key, and decryption with a private key, which is used to publish information to the public key owner that may have been tampered with but cannot be obtained by others.

    For example, encryption scenarios:
    If A wants to send B a secure and confidential data, then A and B should each have a private key. A first encrypts the data with B's public key, then encrypts the encrypted data with its own private key. Finally, it is sent to B, which ensures that the content will not be read and tampered with.

    This is a very simple and clear explanation of how public and private keys work together in different scenarios. This is from This Blogger Thank you very much for your explanation

  3. In OAtuh2.0, is the public or private key used when Authorization Server issues token s?

    A: In fact, the above examples and the second question have been explained. For token signatures, we need to use a private key signature to ensure that the signature comes from Authorization Server. At the same time, in OAuth2.0, the server is responsible for issuing tokens. Judging whether this token is legal can actually be transferred to other servers, with less pressure on the authentication server. Then you just need to copy the corresponding public key to the server cluster that resolves the token. This can also be seen where the RSA algorithm implements JWS where the truncation is red

  4. Recommendations for digital signatures and content encryption?

    A: You can read some explanations on reliable websites, such as Ruan Yifeng's website, which should be covered by relevant content, although I haven't looked for it yet. Or use Google more, use less or don't use Baidu

Remarks

Topics: Shiro Spring Boot jwt rsa oauth2