Skip to main content
编辑本页

The JWT auth provider

这个组件包含了一个开箱即用的JWT实现。 要使用这个项目,将下面的依赖添加到构建描述符里的`dependencies`部分。

  • Maven (在 pom.xml 文件里):

<dependency>
 <groupId>io.vertx</groupId>
 <artifactId>vertx-auth-jwt</artifactId>
 <version>3.6.2</version>
</dependency>
  • Gradle ( 在 build.gradle 文件里 ):

compile 'io.vertx:vertx-auth-jwt:3.6.2'

JSON Web 令牌是一种简单的方法来发送明文信息(通常是URL),其内容可以被验证为是可信的。像下面的这些场景JWT是非常适用的:

  • 在一个单一的报名场景中,你想要有一个隔离的认证服务用可被信任的方式发送用户信息

  • 无状态的Server API,非常适用于简单的页面应用

  • 等等

在决定使用JWT之前,需要重点注意的是JWT并不加密payload,它仅对payload签名。 你不应该使用JWT发送任何私密信息,相反你应该发送是不是私密的但要被验证的信息。 举个例子,使用JWT发送一个签名过的用户id来表明这个用户已经登录了的做法非常对的。 相反发送一个用户的密码的做法是非常非常错误的。

JWT主要的优点有:

  • 它可以让你验证令牌的真实性

  • 它有一个JSON结构,可以包含任何你想要的变量和大量的数据

  • 它完全是无状态的

你可以使用 JWTAuth 来创建一个提供者的实例,并指定一个JSON对象的配置。

这是创建一个JWT auth提供者的示例代码:

JWTAuthOptions config = new JWTAuthOptions()
  .setKeyStore(new KeyStoreOptions()
    .setPath("keystore.jceks")
    .setPassword("secret"));

AuthProvider provider = JWTAuth.create(vertx, config);

JWT用法的典型流程是,在您的应用程序有一个终点颁发令牌,这个终点应在SSL模式下运行,终点这通过用户名和密码验证完请求用户之后,表示你将这样做生成令牌:

JWTAuthOptions config = new JWTAuthOptions()
  .setKeyStore(new KeyStoreOptions()
    .setPath("keystore.jceks")
    .setPassword("secret"));

JWTAuth provider = JWTAuth.create(vertx, config);

// on the verify endpoint once you verify the identity of the user by its username/password
if ("paulo".equals(username) && "super_secret".equals(password)) {
  String token = provider.generateToken(new JsonObject().put("sub", "paulo"), new JWTOptions());
  // now for any request to protected resources you should pass this string in the HTTP header Authorization as:
  // Authorization: Bearer <token>
}

Loading Keys

Loading keys can be performed in 3 different ways:

  • Using secrets (symmetric keys)

  • Using OpenSSL pem formatted files (pub/sec keys)

  • Using Java Keystore files (both symmetric and pub/sec keys)

It is recommended to avoid java keystores, as java keystores are complex to generate and make many assumptions in order to choose the right key for the authenticator.

Using Symmetric Keys

The default signature method for JWT’s is known as HS256. HS stands in this case for HMAC Signature using SHA256.

This is the simplest key to load. All you need is a secret that is shared between you and the 3rd party, for example assume that the secret is: keyboard cat then you can configure your Auth as:

JWTAuth provider = JWTAuth.create(vertx, new JWTAuthOptions()
  .addPubSecKey(new PubSecKeyOptions()
    .setAlgorithm("HS256")
    .setPublicKey("keyboard cat")
    .setSymmetric(true)));

String token = provider.generateToken(new JsonObject());

In this case the secret is configured as a public key, as it is a token that is known to both parties and you configure your PubSec key as being symmetric.

Using RSA keys

This section is by no means a manual on OpenSSL and a read on OpenSSL command line usage is advised. We will cover how to generate the most common keys and how to use them with JWT auth.

Imagine that you would like to protect your application using the very common RS256 JWT algorithm. Contrary to some belief, 256 is not the key length but the hashing algorithm signature length. Any RSA key can be used with this JWT algorithm. Here is an information table:

"alg" Param Value Digital Signature Algorithm

RS256

RSASSA-PKCS1-v1_5 using SHA-256

RS384

RSASSA-PKCS1-v1_5 using SHA-384

RS512

RSASSA-PKCS1-v1_5 using SHA-512

If you would like to generate a 2048bit RSA key pair, then you would do (please remember not to add a passphrase otherwise you will not be able to read the private key in the JWT auth):

openssl genrsa -out private.pem 2048

You can observe that the key is correct as the file content is similar to this:

-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEAxPSbCQY5mBKFDIn1kggvWb4ChjrctqD4nFnJOJk4mpuZ/u3h
...
e4k0yN3F1J1DVlqYWJxaIMzxavQsi9Hz4p2JgyaZMDGB6kGixkMo
-----END RSA PRIVATE KEY-----

The standard JDK cannot read this file as is, so we must convert it to PKCS8 format first:

openssl pkcs8 -topk8 -inform PEM -in private.pem -out private_key.pem -nocrypt

Now the new file private_key.pem which resembles the original one contains:

-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDE9JsJBjmYEoUM
...
0fPinYmDJpkwMYHqQaLGQyg=
-----END PRIVATE KEY-----

If we are verifying tokens only (you will only need the private_key.pem file) however at some point you will need to issue tokens too, so you will a public key. In this case you need to extract the public key from the private key file:

openssl rsa -in private.pem -outform PEM -pubout -out public.pem

And you should see that the content of the file is similar to this:

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxPSbCQY5mBKFDIn1kggv
...
qwIDAQAB
-----END PUBLIC KEY-----

Now you can use this to issue or validate tokens:

JWTAuth provider = JWTAuth.create(vertx, new JWTAuthOptions()
  .addPubSecKey(new PubSecKeyOptions()
    .setAlgorithm("RS256")
    .setPublicKey(
      "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxPSbCQY5mBKFDIn1kggv\n" +
        "Wb4ChjrctqD4nFnJOJk4mpuZ/u3h2ZgeKJJkJv8+5oFO6vsEwF7/TqKXp0XDp6IH\n" +
        "byaOSWdkl535rCYR5AxDSjwnuSXsSp54pvB+fEEFDPFF81GHixepIbqXCB+BnCTg\n" +
        "N65BqwNn/1Vgqv6+H3nweNlbTv8e/scEgbg6ZYcsnBBB9kYLp69FSwNWpvPmd60e\n" +
        "3DWyIo3WCUmKlQgjHL4PHLKYwwKgOHG/aNl4hN4/wqTixCAHe6KdLnehLn71x+Z0\n" +
        "SyXbWooftefpJP1wMbwlCpH3ikBzVIfHKLWT9QIOVoRgchPU3WAsZv/ePgl5i8Co\n" +
        "qwIDAQAB")
    .setSecretKey(
      "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDE9JsJBjmYEoUM\n" +
        "ifWSCC9ZvgKGOty2oPicWck4mTiam5n+7eHZmB4okmQm/z7mgU7q+wTAXv9Oopen\n" +
        "RcOnogdvJo5JZ2SXnfmsJhHkDENKPCe5JexKnnim8H58QQUM8UXzUYeLF6khupcI\n" +
        "H4GcJOA3rkGrA2f/VWCq/r4fefB42VtO/x7+xwSBuDplhyycEEH2Rgunr0VLA1am\n" +
        "8+Z3rR7cNbIijdYJSYqVCCMcvg8cspjDAqA4cb9o2XiE3j/CpOLEIAd7op0ud6Eu\n" +
        "fvXH5nRLJdtaih+15+kk/XAxvCUKkfeKQHNUh8cotZP1Ag5WhGByE9TdYCxm/94+\n" +
        "CXmLwKirAgMBAAECggEAeQ+M+BgOcK35gAKQoklLqZLEhHNL1SnOhnQd3h84DrhU\n" +
        "CMF5UEFTUEbjLqE3rYGP25mdiw0ZSuFf7B5SrAhJH4YIcZAO4a7ll23zE0SCW+/r\n" +
        "zr9DpX4Q1TP/2yowC4uGHpBfixxpBmVljkWnai20cCU5Ef/O/cAh4hkhDcHrEKwb\n" +
        "m9nymKQt06YnvpCMKoHDdqzfB3eByoAKuGxo/sbi5LDpWalCabcg7w+WKIEU1PHb\n" +
        "Qi+RiDf3TzbQ6TYhAEH2rKM9JHbp02TO/r3QOoqHMITW6FKYvfiVFN+voS5zzAO3\n" +
        "c5X4I+ICNzm+mnt8wElV1B6nO2hFg2PE9uVnlgB2GQKBgQD8xkjNhERaT7f78gBl\n" +
        "ch15DRDH0m1rz84PKRznoPrSEY/HlWddlGkn0sTnbVYKXVTvNytKSmznRZ7fSTJB\n" +
        "2IhQV7+I0jeb7pyLllF5PdSQqKTk6oCeL8h8eDPN7awZ731zff1AGgJ3DJXlRTh/\n" +
        "O6zj9nI8llvGzP30274I2/+cdwKBgQDHd/twbiHZZTDexYewP0ufQDtZP1Nk54fj\n" +
        "EpkEuoTdEPymRoq7xo+Lqj5ewhAtVKQuz6aH4BeEtSCHhxy8OFLDBdoGCEd/WBpD\n" +
        "f+82sfmGk+FxLyYkLxHCxsZdOb93zkUXPCoCrvNRaUFO1qq5Dk8eftGCdC3iETHE\n" +
        "6h5avxHGbQKBgQCLHQVMNhL4MQ9slU8qhZc627n0fxbBUuhw54uE3s+rdQbQLKVq\n" +
        "lxcYV6MOStojciIgVRh6FmPBFEvPTxVdr7G1pdU/k5IPO07kc6H7O9AUnPvDEFwg\n" +
        "suN/vRelqbwhufAs85XBBY99vWtxdpsVSt5nx2YvegCgdIj/jUAU2B7hGQKBgEgV\n" +
        "sCRdaJYr35FiSTsEZMvUZp5GKFka4xzIp8vxq/pIHUXp0FEz3MRYbdnIwBfhssPH\n" +
        "/yKzdUxcOLlBtry+jgo0nyn26/+1Uyh5n3VgtBBSePJyW5JQAFcnhqBCMlOVk5pl\n" +
        "/7igiQYux486PNBLv4QByK0gV0SPejDzeqzIyB+xAoGAe5if7DAAKhH0r2M8vTkm\n" +
        "JvbCFjwuvhjuI+A8AuS8zw634BHne2a1Fkvc8c3d9VDbqsHCtv2tVkxkKXPjVvtB\n" +
        "DtzuwUbp6ebF+jOfPK0LDuJoTdTdiNjIcXJ7iTTI3cXUnUNWWphYnFogzPFq9CyL\n" +
        "0fPinYmDJpkwMYHqQaLGQyg=")
  ));

String token = provider.generateToken(new JsonObject(), new JWTOptions().setAlgorithm("RS256"));

Do note that all the lines -----BEGIN …​ and -----END…​ should be stripped from the string to be passed to the configuration.

Using EC keys

Elliptic Curse keys are also supported, however the default JDK has some limitations on the features that can be used.

The usage is very similar to RSA, first you create a private key:

openssl ecparam -name secp256r1 -genkey -out private.pem

So you will get something similar to this:

-----BEGIN EC PARAMETERS-----
BggqhkjOPQMBBw==
-----END EC PARAMETERS-----
-----BEGIN EC PRIVATE KEY-----
MHcCAQEEIMZGaqZDTHL+IzFYEWLIYITXpGzOJuiQxR2VNGheq7ShoAoGCCqGSM49
AwEHoUQDQgAEG1O9LCrP6hg3Y9q68+LF0q48UcOkwVKE1ax0b56wjVusf3qnuFO2
/+XHKKhtzEavvFMeXRQ+ZVEqM0yGNb04qw==
-----END EC PRIVATE KEY-----

However the JDK prefers PKCS8 format so we must convert:

openssl pkcs8 -topk8 -nocrypt -in private.pem -out private_key.pem

Which will give you a key similar to this:

-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgxkZqpkNMcv4jMVgR
YshghNekbM4m6JDFHZU0aF6rtKGhRANCAAQbU70sKs/qGDdj2rrz4sXSrjxRw6TB
UoTVrHRvnrCNW6x/eqe4U7b/5ccoqG3MRq+8Ux5dFD5lUSozTIY1vTir
-----END PRIVATE KEY-----

Using the private key you can already generate tokens:

JWTAuth provider = JWTAuth.create(vertx, new JWTAuthOptions()
  .addPubSecKey(new PubSecKeyOptions()
    .setAlgorithm("ES256")
    .setSecretKey(
      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgeRyEfU1NSHPTCuC9\n" +
        "rwLZMukaWCH2Fk6q5w+XBYrKtLihRANCAAStpUnwKmSvBM9EI+W5QN3ALpvz6bh0\n" +
        "SPCXyz5KfQZQuSj4f3l+xNERDUDaygIUdLjBXf/bc15ur2iZjcq4r0Mr")
  ));

String token = provider.generateToken(new JsonObject(), new JWTOptions().setAlgorithm("ES256"));

So in order to validate the tokens you will need a public key:

openssl ec -in private.pem -pubout -out public.pem

So you can do all operations with it:

JWTAuth provider = JWTAuth.create(vertx, new JWTAuthOptions()
  .addPubSecKey(new PubSecKeyOptions()
    .setAlgorithm("ES256")
    .setPublicKey(
      "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEraVJ8CpkrwTPRCPluUDdwC6b8+m4\n" +
        "dEjwl8s+Sn0GULko+H95fsTREQ1A2soCFHS4wV3/23Nebq9omY3KuK9DKw==\n")
    .setSecretKey(
      "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgeRyEfU1NSHPTCuC9\n" +
        "rwLZMukaWCH2Fk6q5w+XBYrKtLihRANCAAStpUnwKmSvBM9EI+W5QN3ALpvz6bh0\n" +
        "SPCXyz5KfQZQuSj4f3l+xNERDUDaygIUdLjBXf/bc15ur2iZjcq4r0Mr")
  ));

String token = provider.generateToken(new JsonObject(), new JWTOptions().setAlgorithm("ES256"));

The JWT keystore file

If you prefer to use Java Keystores, then you can do it either.

auth提供者需要在类路径或文件系统上加载一个keystore文件,keystore文件要么有一个 javax.crypto.Mac 要么有一个 java.security.Signature 用于签名和验证生成的令牌。

默认的情况下,实践将会查找这些别名,然而并不是所有的算法都需要存在。作为一个好的实践 HS256 应该存在:

`HS256`:: HMAC using SHA-256 hash algorithm
`HS384`:: HMAC using SHA-384 hash algorithm
`HS512`:: HMAC using SHA-512 hash algorithm
`RS256`:: RSASSA using SHA-256 hash algorithm
`RS384`:: RSASSA using SHA-384 hash algorithm
`RS512`:: RSASSA using SHA-512 hash algorithm
`ES256`:: ECDSA using P-256 curve and SHA-256 hash algorithm
`ES384`:: ECDSA using P-384 curve and SHA-384 hash algorithm
`ES512`:: ECDSA using P-521 curve and SHA-512 hash algorithm

当keystore没有被提供,执行将会回滚到一个不安全的模式同时签名并不会被验证,如果payload被外部的方法签名或加密后的情况下这是非常有用的。

生成一个新的Keystore文件

生成keystore文件唯一需要的工具是 keytool ,运行的时候,你可以指定你需要使用的算法:

keytool -genseckey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg HMacSHA256 -keysize 2048 -alias HS256 -keypass secret
keytool -genseckey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg HMacSHA384 -keysize 2048 -alias HS384 -keypass secret
keytool -genseckey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg HMacSHA512 -keysize 2048 -alias HS512 -keypass secret
keytool -genkey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg RSA -keysize 2048 -alias RS256 -keypass secret -sigalg SHA256withRSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
keytool -genkey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg RSA -keysize 2048 -alias RS384 -keypass secret -sigalg SHA384withRSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
keytool -genkey -keystore keystore.jceks -storetype jceks -storepass secret -keyalg RSA -keysize 2048 -alias RS512 -keypass secret -sigalg SHA512withRSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
keytool -genkeypair -keystore keystore.jceks -storetype jceks -storepass secret -keyalg EC -keysize 256 -alias ES256 -keypass secret -sigalg SHA256withECDSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
keytool -genkeypair -keystore keystore.jceks -storetype jceks -storepass secret -keyalg EC -keysize 384 -alias ES384 -keypass secret -sigalg SHA384withECDSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360
keytool -genkeypair -keystore keystore.jceks -storetype jceks -storepass secret -keyalg EC -keysize 521 -alias ES512 -keypass secret -sigalg SHA512withECDSA -dname "CN=,OU=,O=,L=,ST=,C=" -validity 360

Read only tokens

If you need to consume JWT tokens issues by third parties you probably won’t have the private key with you, in that case all you need to have is a public key im PEM format.

JWTAuthOptions config = new JWTAuthOptions()
  .addPubSecKey(new PubSecKeyOptions()
    .setAlgorithm("RS256")
    .setPublicKey("BASE64-ENCODED-PUBLIC_KEY"));

AuthProvider provider = JWTAuth.create(vertx, config);

AuthN/AuthZ with JWT

A common scenario when developing for example micro services is that you want you application to consume APIs. These api’s are not meant to be consumed by humans so we should remove all the interactive part of authenticating the consumer out of the picture.

In this scenario one can use HTTP as the protocol to consume this API and the HTTP protocol already defines that there is a header Authorization that should be used for passing authorization information. In most cases you will see that tokens are sent as bearer tokens, i.e.: Authorization: Bearer some+base64+string.

Authenticating (AuthN)

For this provider a user is authenticated if the token passes the signature checks and that the token is not expired. For this reason it is imperative that private keys are kept private and not copy pasted across project since it would be a security hole.

jwtAuth.authenticate(new JsonObject().put("jwt", "BASE64-ENCODED-STRING"), res -> {
  if (res.succeeded()) {
    User theUser = res.result();
  } else {
    // Failed!
  }
});

In a nutshell the provider is checking for several things:

  • token signature is valid against internal private key

  • fields: exp, iat, nbf, audience, issuer are valid according to the config

If all these are valid then the token is considered good and a user object is returned.

While the fields exp, iat and nbf are simple timestamp checks only exp can be configured to be ignored:

jwtAuth.authenticate(new JsonObject()
  .put("jwt", "BASE64-ENCODED-STRING")
  .put("options", new JsonObject()
    .put("ignoreExpiration", true)), res -> {
  if (res.succeeded()) {
    User theUser = res.result();
  } else {
    // Failed!
  }
});

In order to verify the aud field one needs to pass the options like before:

jwtAuth.authenticate(new JsonObject()
  .put("jwt", "BASE64-ENCODED-STRING")
  .put("options", new JsonObject()
    .put("audience", new JsonArray().add("paulo@server.com"))), res -> {
  if (res.succeeded()) {
    User theUser = res.result();
  } else {
    // Failed!
  }
});

And the same for the issuer:

jwtAuth.authenticate(new JsonObject()
  .put("jwt", "BASE64-ENCODED-STRING")
  .put("options", new JsonObject()
    .put("issuer", "mycorp.com")), res -> {
  if (res.succeeded()) {
    User theUser = res.result();
  } else {
    // Failed!
  }
});

Authorizing (AuthZ)

Once a token is parsed and is valid we can use it to perform authorization tasks. The most simple is to verify if a user has a specific authority. In this case one needs to to:

user.isAuthorized("create-report", res -> {
  if (res.succeeded() && res.result()) {
    // Yes the user can create reports
  }
});

By default the provider will lookup under the key permissions but like the other providers one can extend the concept to authorities to roles by using the : as a splitter, so role:authority can be used to lookup the token.

Since JWT are quite free form and there is no standard on where to lookup for the claims the location can be configured to use something else than permissions, for example one can even lookup under a path like this:

JsonObject config = new JsonObject()
  .put("public-key", "BASE64-ENCODED-PUBLIC_KEY")
  // since we're consuming keycloak JWTs we need to locate the permission claims in the token
  .put("permissionsClaimKey", "realm_access/roles");

AuthProvider provider = JWTAuth.create(vertx, new JWTAuthOptions(config));

So in this example we configure the JWT to work with Keycloak token format. In this case the claims will be checked under the path realm_access/roles rather than permissions.

Validating Tokens

When the method authenticate is invoked, the token is validated against the JWTOptions provided during the initialization. The validation performs the following steps:

  1. if ignoreExpiration (default is false) is false then the token is checked for expiration, this will check the fields: exp, iat and nbf. Since sometimes clocks are not reliable, it is possible to configure some leeway to be applied to the dates so we allow some grace period if the dates are outside the required limits.

  2. if audience is provided, then the token aud is checked against the configured one and all configured audiences must be in the token.

  3. if issuer is configured, then the tokens iss is checked against the configured one.

Once these validations complete a JWTUser object is then returned, the object is configured with a reference to the permission claims key provided in the configuration. This value is used later when doing authorization. The value corresponds to the json path where authorities should be checked.

Customizing Token Generation

In the same way tokens are validated, the generation is initially configured during the initialization.

When generating a token an optional extra parameter can be supplied to control the token generation, this is a JWTOptions object. The token signature algorithm (default HS256) can be configured using the property algorithm. In this case a lookup for a key that corresponds to the algorithm is performed and used to sign.

Token headers can be added by specifying any extra headers to be merged with the default ones using the options headers property.

Sometimes it might be useful to issue tokens without a timestamp (test, development time for example) in this case the property noTimestamp should be set to true (default false). This means that there is no iat field in the token.

Token expiration is controlled by the property expiresInSeconds, by default there is no expiration. Other control fields audience, issuer and subject are then picked from the config is available and added to the token metadata.

Finally the token is signed and encoded in the correct format.

@author <a href="mailto:julien@julienviet.com">Julien Viet</a> @author <a href="http://tfox.org">Tim Fox</a> @author <a href="mailto:pmlopes@gmail.com">Paulo Lopes</a>