diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index 5a380b0d2fe75..71b6d4a521769 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -39,7 +39,7 @@ 1.0.4 1.3.4 4.3.0 - 2.1.2 + 2.2.0 1.0.13 1.0.13 1.0.13 diff --git a/docs/src/main/asciidoc/security-jwt.adoc b/docs/src/main/asciidoc/security-jwt.adoc index 6cfe005a4f1b5..89ad1a4c26de7 100644 --- a/docs/src/main/asciidoc/security-jwt.adoc +++ b/docs/src/main/asciidoc/security-jwt.adoc @@ -11,7 +11,7 @@ include::./attributes.adoc[] This guide explains how your Quarkus application can utilize MicroProfile JWT (MP JWT) to verify https://tools.ietf.org/html/rfc7519[JSON Web Token]s, represent them as MP JWT `org.eclipse.microprofile.jwt.JsonWebToken` and provide secured access to the Quarkus HTTP endpoints using Bearer Token Authorization and https://en.wikipedia.org/wiki/Role-based_access_control[Role-Based Access Control]. -Note that Quarkus OpenId Connect extension also supports Bearer Token Authorization and uses `smallrye-jwt` to represent the bearer tokens as `JsonWebToken`, please read the link:security-openid-connect[Using OpenID Connect to Protect Service Applications] guide for more information. +NOTE: Quarkus OpenId Connect extension also supports Bearer Token Authorization and uses `smallrye-jwt` to represent the bearer tokens as `JsonWebToken`, please read the link:security-openid-connect[Using OpenID Connect to Protect Service Applications] guide for more information. OpenId Connect extension has to be used if the Quarkus application needs to authenticate the users using OIDC Authorization Code Flow, please read link:security-openid-connect-web-authentication[Using OpenID Connect to Protect Web Applications] guide for more information. == Solution @@ -23,7 +23,7 @@ Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {q The solution is located in the `security-jwt-quickstart` {quickstarts-tree-url}/security-jwt-quickstart[directory]. -== Creating the Maven project +=== Creating the Maven project First, we need a new project. Create a new project with the following command: @@ -85,9 +85,6 @@ public class TokenSecuredResource { This is a basic REST endpoint that does not have any of the {extension-name} specific features, so let's add some. -NOTE: The {mp-jwt} 1.1.1 specification details the annotations and behaviors we will make use of in -this quickstart. See https://github.com/eclipse/microprofile-jwt-auth/releases/download/1.1.1/microprofile-jwt-auth-spec.html[HTML] - and https://github.com/eclipse/microprofile-jwt-auth/releases/download/1.1.1/microprofile-jwt-auth-spec.pdf[PDF] versions of the specification for the details. .REST Endpoint V1 [source,java] @@ -100,6 +97,7 @@ import javax.annotation.security.PermitAll; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.ws.rs.GET; +import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; @@ -108,9 +106,6 @@ import javax.ws.rs.core.SecurityContext; import org.eclipse.microprofile.jwt.JsonWebToken; -/** - * Version 1 of the TokenSecuredResource - */ @Path("/secured") @RequestScoped // <1> public class TokenSecuredResource { @@ -122,12 +117,28 @@ public class TokenSecuredResource { @Path("permit-all") @PermitAll // <3> @Produces(MediaType.TEXT_PLAIN) - public String hello(@Context SecurityContext ctx) { // <4> - Principal caller = ctx.getUserPrincipal(); <5> - String name = caller == null ? "anonymous" : caller.getName(); - boolean hasJWT = jwt.getClaimNames() != null; - String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s, hasJWT: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJWT); - return helloReply; // <6> + public String hello(@Context SecurityContext ctx) { + return getResponseString(ctx); // <4> + } + + private String getResponseString(SecurityContext ctx) { + String name; + if (ctx.getUserPrincipal() == null) { // <5> + name = "anonymous"; + } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { // <6> + throw new InternalServerErrorException("Principal and JsonWebToken names do not match"); + } else { + name = ctx.getUserPrincipal().getName(); // <7> + } + return String.format("hello + %s," + + " isHttps: %s," + + " authScheme: %s," + + " hasJWT: %s", + name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); // <8> + } + + private boolean hasJwt() { + return jwt.getClaimNames() != null; } } ---- @@ -135,11 +146,13 @@ public class TokenSecuredResource { will produce undesirable behavior since JWT claims are naturally request scoped. <2> Here we inject the JsonWebToken interface, an extension of the java.security.Principal interface that provides access to the claims associated with the current authenticated token. <3> @PermitAll is a JSR 250 common security annotation that indicates that the given endpoint is accessible by any caller, authenticated or not. -<4> Here we inject the JAX-RS SecurityContext to inspect the security state of the call. -<5> Here we obtain the current request user/caller `Principal`. For an unsecured call this will be null, so we build the user name by checking `caller` against null. -<6> The reply we build up makes use of the caller name, the `isSecure()` and `getAuthenticationScheme()` states of the request `SecurityContext`, and whether a non-null `JsonWebToken` was injected. +<4> Here we inject the JAX-RS SecurityContext to inspect the security state of the call and use a `getResponseString()` function to populate a response string. +<5> Here we check if the call is insecured by checking the request user/caller `Principal` against null. +<6> Here we check that the Principal and JsonWebToken have the same name since JsonWebToken does represent the current Principal. +<7> Here we get the Principal name. +<8> The reply we build up makes use of the caller name, the `isSecure()` and `getAuthenticationScheme()` states of the request `SecurityContext`, and whether a non-null `JsonWebToken` was injected. -== Run the application +=== Run the application Now we are ready to run our application. Use: @@ -161,10 +174,9 @@ $ ./mvnw compile quarkus:dev [INFO] --------------------------------[ jar ]--------------------------------- ... Listening for transport dt_socket at address: 5005 -2019-03-03 07:23:06,988 INFO [io.qua.dep.QuarkusAugmentor] (main) Beginning quarkus augmentation -2019-03-03 07:23:07,328 INFO [io.qua.dep.QuarkusAugmentor] (main) Quarkus augmentation completed in 340ms -2019-03-03 07:23:07,493 INFO [io.quarkus] (main) Quarkus started in 0.769s. Listening on: http://127.0.0.1:8080 -2019-03-03 07:23:07,493 INFO [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jsonb, security, smallrye-jwt, vertx, vertx-web] +2020-07-15 16:09:50,883 INFO [io.quarkus] (Quarkus Main Thread) security-jwt-quickstart 1.0-SNAPSHOT on JVM (powered by Quarkus 999-SNAPSHOT) started in 1.073s. Listening on: http://0.0.0.0:8080 +2020-07-15 16:09:50,885 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated. +2020-07-15 16:09:50,885 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, mutiny, resteasy, resteasy-jsonb, security, smallrye-context-propagation, smallrye-jwt, vertx, vertx-web] ---- Now that the REST endpoint is running, we can access it using a command line tool like curl: @@ -173,14 +185,14 @@ Now that the REST endpoint is running, we can access it using a command line too [source,shell] ---- $ curl http://127.0.0.1:8080/secured/permit-all; echo -hello + anonymous, isSecure: false, authScheme: null, hasJWT: false +hello + anonymous, isHttps: false, authScheme: null, hasJWT: false ---- We have not provided any JWT in our request, so we would not expect that there is any security state seen by the endpoint, and the response is consistent with that: * user name is anonymous -* isSecure is false as https is not used +* isHttps is false as https is not used * authScheme is null * hasJWT is false @@ -193,13 +205,12 @@ So now let's actually secure something. Take a look at the new endpoint method ` ---- package org.acme.security.jwt; -import java.security.Principal; - import javax.annotation.security.PermitAll; import javax.annotation.security.RolesAllowed; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.ws.rs.GET; +import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; @@ -208,43 +219,55 @@ import javax.ws.rs.core.SecurityContext; import org.eclipse.microprofile.jwt.JsonWebToken; -/** - * Version 2 of the TokenSecuredResource - */ @Path("/secured") @RequestScoped public class TokenSecuredResource { @Inject - JsonWebToken jwt; + JsonWebToken jwt; // <1> - @GET() + @GET @Path("permit-all") @PermitAll @Produces(MediaType.TEXT_PLAIN) public String hello(@Context SecurityContext ctx) { - Principal caller = ctx.getUserPrincipal(); - String name = caller == null ? "anonymous" : caller.getName(); - String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme()); - return helloReply; + return getResponseString(ctx); } - @GET() - @Path("roles-allowed") // <1> - @RolesAllowed({"Echoer", "Subscriber"}) // <2> + @GET + @Path("roles-allowed") // <2> + @RolesAllowed({ "User", "Admin" }) // <3> @Produces(MediaType.TEXT_PLAIN) public String helloRolesAllowed(@Context SecurityContext ctx) { - Principal caller = ctx.getUserPrincipal(); - String name = caller == null ? "anonymous" : caller.getName(); - boolean hasJWT = jwt.getClaimNames() != null; - String helloReply = String.format("hello + %s, isSecure: %s, authScheme: %s, hasJWT: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJWT); - return helloReply; + return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString(); // <4> + } + + private String getResponseString(SecurityContext ctx) { + String name; + if (ctx.getUserPrincipal() == null) { + name = "anonymous"; + } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { + throw new InternalServerErrorException("Principal and JsonWebToken names do not match"); + } else { + name = ctx.getUserPrincipal().getName(); + } + return String.format("hello + %s," + + " isHttps: %s," + + " authScheme: %s," + + " hasJWT: %s", + name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); + } + + private boolean hasJwt() { + return jwt.getClaimNames() != null; } } ---- -<1> This new endpoint will be located at /secured/roles-allowed -<2> @RolesAllowed is a JSR 250 common security annotation that indicates that the given endpoint is accessible by a caller if -they have either a "Echoer" or "Subscriber" role assigned. +<1> Here we inject `JsonWebToken` +<2> This new endpoint will be located at /secured/roles-allowed +<3> @RolesAllowed is a JSR 250 common security annotation that indicates that the given endpoint is accessible by a caller if +they have either a "User" or "Admin" role assigned. +<4> Here we build the reply the same way as in the `hello` method but also add a value of the JWT `birthdate` claim by directly calling the injected `JsonWebToken`. After you make this addition to your `TokenSecuredResource`, rerun the `./mvnw compile quarkus:dev` command, and then try `curl -v http://127.0.0.1:8080/secured/roles-allowed; echo` to attempt to access the new endpoint. Your output should be: @@ -272,25 +295,18 @@ Not authorized Excellent, we have not provided any JWT in the request, so we should not be able to access the endpoint, and we were not. Instead we received an HTTP 401 Unauthorized error. We need to obtain and pass in a valid JWT to access that endpoint. There are two steps to this, 1) configuring our {extension-name} extension with information on how to validate a JWT, and 2) generating a matching JWT with the appropriate claims. -== Configuring the {extension-name} Extension Security Information - -In the <> section we introduce the `application.properties` file that affect the {extension-name} extension. - -=== Setting up application.properties +=== Configuring the {extension-name} Extension Security Information -For part A of step 1, create a `security-jwt-quickstart/src/main/resources/application.properties` with the following content: +Create a `security-jwt-quickstart/src/main/resources/application.properties` with the following content: .application.properties for TokenSecuredResource [source, properties] ---- mp.jwt.verify.publickey.location=META-INF/resources/publicKey.pem #<1> mp.jwt.verify.issuer=https://quarkus.io/using-jwt-rbac #<2> -quarkus.smallrye-jwt.enabled=true #<3> ---- <1> We are setting public key location to point to a classpath publicKey.pem resource location. We will add this key in part B, <>. <2> We are setting the issuer to the URL string `https://quarkus.io/using-jwt-rbac`. -<3> We are enabling the {extension-name}. Also not required since this is the default, -but we are making it explicit. === Adding a Public Key @@ -301,6 +317,8 @@ key to use to verify the JWT sent along with requests. The `mp.jwt.verify.public previously expects that the public key is available on the classpath as `publicKey.pem`. To accomplish this, copy the following content to a `security-jwt-quickstart/src/main/resources/META-INF/resources/publicKey.pem` file. +NOTE: Adding `publicKey.pem` to `resources/META-INF/resources` ensures that it is available in the native image without having to provide a GraalVM resource file. + .RSA Public Key PEM Content [source, text] ---- @@ -317,164 +335,46 @@ nQIDAQAB === Generating a JWT -Often one obtains a JWT from an identity manager like https://www.keycloak.org/[Keycloak], but for this quickstart we will generate our own using the JWT generation API provided by `smallrye-jwt` (see <> for more information) and the TokenUtils class shown in the following listing. Take this source and place it into `security-jwt-quickstart/src/test/java/org/acme/security/jwt/TokenUtils.java`. +Often one obtains a JWT from an identity manager like https://www.keycloak.org/[Keycloak], but for this quickstart we will generate our own using the JWT generation API provided by `smallrye-jwt` (see <> for more information). +Take the code from the following listing and place into `security-jwt-quickstart/src/main/java/org/acme/security/jwt/GenerateToken.java`: -.JWT utility class +.GenerateToken main Driver Class [source, java] ---- package org.acme.security.jwt; -import java.io.InputStream; -import java.security.KeyFactory; -import java.security.KeyPair; -import java.security.KeyPairGenerator; -import java.security.NoSuchAlgorithmException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.spec.PKCS8EncodedKeySpec; -import java.security.spec.X509EncodedKeySpec; -import java.util.Base64; -import java.util.Map; +import java.util.Arrays; +import java.util.HashSet; import org.eclipse.microprofile.jwt.Claims; import io.smallrye.jwt.build.Jwt; -import io.smallrye.jwt.build.JwtClaimsBuilder; -/** - * Utilities for generating a JWT for testing - */ -public class TokenUtils { - - private TokenUtils() { - // no-op: utility class - } - - /** - * Utility method to generate a JWT string from a JSON resource file that is signed by the privateKey.pem - * test resource key, possibly with invalid fields. - * - * @param jsonResName - name of test resources file - * @param timeClaims - used to return the exp, iat, auth_time claims - * @return the JWT string - * @throws Exception on parse failure - */ - public static String generateTokenString(String jsonResName, Map timeClaims) - throws Exception { - // Use the test private key associated with the test public key for a valid signature - PrivateKey pk = readPrivateKey("/privateKey.pem"); - return generateTokenString(pk, "/privateKey.pem", jsonResName, timeClaims); - } - - public static String generateTokenString(PrivateKey privateKey, String kid, - String jsonResName, Map timeClaims) throws Exception { - - JwtClaimsBuilder claims = Jwt.claims(jsonResName); - long currentTimeInSecs = currentTimeInSecs(); - long exp = timeClaims != null && timeClaims.containsKey(Claims.exp.name()) - ? timeClaims.get(Claims.exp.name()) : currentTimeInSecs + 300; - - claims.issuedAt(currentTimeInSecs); - claims.claim(Claims.auth_time.name(), currentTimeInSecs); - claims.expiresAt(exp); - - return claims.jws().signatureKeyId(kid).sign(privateKey); - } - - /** - * Read a PEM encoded private key from the classpath - * - * @param pemResName - key file resource name - * @return PrivateKey - * @throws Exception on decode failure - */ - public static PrivateKey readPrivateKey(final String pemResName) throws Exception { - try (InputStream contentIS = TokenUtils.class.getResourceAsStream(pemResName)) { - byte[] tmp = new byte[4096]; - int length = contentIS.read(tmp); - return decodePrivateKey(new String(tmp, 0, length, "UTF-8")); - } - } - - /** - * Decode a PEM encoded private key string to an RSA PrivateKey - * - * @param pemEncoded - PEM string for private key - * @return PrivateKey - * @throws Exception on decode failure - */ - public static PrivateKey decodePrivateKey(final String pemEncoded) throws Exception { - byte[] encodedBytes = toEncodedBytes(pemEncoded); - - PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedBytes); - KeyFactory kf = KeyFactory.getInstance("RSA"); - return kf.generatePrivate(keySpec); - } - - private static byte[] toEncodedBytes(final String pemEncoded) { - final String normalizedPem = removeBeginEnd(pemEncoded); - return Base64.getDecoder().decode(normalizedPem); - } - - private static String removeBeginEnd(String pem) { - pem = pem.replaceAll("-----BEGIN (.*)-----", ""); - pem = pem.replaceAll("-----END (.*)----", ""); - pem = pem.replaceAll("\r\n", ""); - pem = pem.replaceAll("\n", ""); - return pem.trim(); - } - - /** - * @return the current time in seconds since epoch - */ - public static int currentTimeInSecs() { - long currentTimeMS = System.currentTimeMillis(); - return (int) (currentTimeMS / 1000); - } - -} ----- - -Next take the code from the following listing and place into `security-jwt-quickstart/src/test/java/org/acme/security/jwt/GenerateToken.java`: - -.GenerateToken main Driver Class -[source, java] ----- -package org.acme.security.jwt; - -import java.util.HashMap; - -import org.eclipse.microprofile.jwt.Claims; -/** - * A simple utility class to generate and print a JWT token string to stdout. Can be run with: - * mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test - */ public class GenerateToken { /** - * - * @param args - [0]: optional name of classpath resource for json document of claims to add; defaults to "/JwtClaims.json" - * [1]: optional time in seconds for expiration of generated token; defaults to 300 - * @throws Exception + * Generate JWT token */ - public static void main(String[] args) throws Exception { - String claimsJson = "/JwtClaims.json"; - if (args.length > 0) { - claimsJson = args[0]; - } - HashMap timeClaims = new HashMap<>(); - if (args.length > 1) { - long duration = Long.parseLong(args[1]); - long exp = TokenUtils.currentTimeInSecs() + duration; - timeClaims.put(Claims.exp.name(), exp); - } - String token = TokenUtils.generateTokenString(claimsJson, timeClaims); + public static void main(String[] args) { + String token = + Jwt.issuer("https://quarkus.io/using-jwt-rbac") // <1> + .upn("jdoe@quarkus.io") // <2> + .groups(new HashSet<>(Arrays.asList("User", "Admin"))) // <3> + .claim(Claims.birthdate.name(), "2001-07-13") // <4> + .sign(); System.out.println(token); } } ---- -Now we need the content of the RSA private key that corresponds to the public key we have in the TokenSecuredResource application. Take the following PEM content and place it into `security-jwt-quickstart/src/test/resources/privateKey.pem`. +<1> The `iss` claim is the issuer of the JWT. This needs to match the server side `mp.jwt.verify.issuer`. +in order for the token to be accepted as valid. +<2> The `upn` claim is defined by the {mp-jwt} spec as preferred claim to use for the +`Principal` seen via the container security APIs. +<3> The `group` claim provides the groups and top-level roles associated with the JWT bearer. +<4> The `birthday` claim. It can be considered to be a sensitive claim so you may want to consider encrypting the claims, see <>. + +Note for this code to work we need the content of the RSA private key that corresponds to the public key we have in the TokenSecuredResource application. Take the following PEM content and place it into `security-jwt-quickstart/src/main/resources/META-INF/resources/privateKey.pem`: .RSA Private Key PEM Content [source, text] @@ -509,158 +409,22 @@ f3cg+fr8aou7pr9SHhJlZCU= -----END PRIVATE KEY----- ---- -And finally, we need to define what claims to include in the JWT. The `TokenUtils` class uses a json resource on the classpath -to define the non-time sensitive claims, so take the content from the following listing and place it into -`security-jwt-quickstart/src/test/resources/JwtClaims.json`: - -.JwtClaims.json claims document -[source, json] ----- -{ - "iss": "https://quarkus.io/using-jwt-rbac", - "jti": "a-123", - "sub": "jdoe-using-jwt-rbac", - "upn": "jdoe@quarkus.io", - "preferred_username": "jdoe", - "aud": "using-jwt-rbac", - "birthdate": "2001-07-13", - "roleMappings": { - "group1": "Group1MappedRole", - "group2": "Group2MappedRole" - }, - "groups": [ - "Echoer", - "Tester", - "Subscriber", - "group2" - ] -} ----- - -Let's explore the content of this document in more detail to understand how the claims will affect our application security. - -.JwtClaims.json claims document -[source, json, linenums, highlight="2,6,10,14"] ----- -{ - "iss": "https://quarkus.io/using-jwt-rbac", <1> - "jti": "a-123", - "sub": "jdoe-using-jwt-rbac", - "upn": "jdoe@quarkus.io", <2> - "preferred_username": "jdoe", - "aud": "using-jwt-rbac", - "birthdate": "2001-07-13", - "roleMappings": { <3> - "group1": "Group1MappedRole", - "group2": "Group2MappedRole" - }, - "groups": [ <4> - "Echoer", - "Tester", - "Subscriber", - "group2" - ] -} ----- -<1> The `iss` claim is the issuer of the JWT. This needs to match the server side `mp.jwt.verify.issuer` -in order for the token to be accepted as valid. -<2> The `upn` claim is defined by the {mp-jwt} spec as preferred claim to use for the -`Principal` seen via the container security APIs. -<3> The `roleMappings` claim can be used to map from a role defined in the `groups` claim -to an application level role defined in a `@RolesAllowed` annotation. We won't use this -feature in this quickstart, but it can be useful when the IDM providing the token has -roles that do not directly align with those defined by the application. -<4> The `group` claim provides the groups and top-level roles associated with the JWT bearer. -In this quickstart we are only using the top-level role mapping which means the JWT will -be seen to have the roles "Echoer", "Tester", "Subscriber" and "group2". The full set of roles would -also include a "Group2MappedRole" due to the `roleMappings` claim having a mapping from -"group2" to "Group2MappedRole". +We will use a `smallrye.jwt.sign.key-location` property to point to this private signing key. Now we can generate a JWT to use with `TokenSecuredResource` endpoint. To do this, run the following command: .Command to Generate JWT -[source,shell] ----- -mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test ----- - -TIP: You may need to run `./mvnw test-compile` before this if you are working strictly from the command line and not an IDE that -automatically compiles code as you write it. .Sample JWT Generation Output [source,shell] ---- -$ mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -[INFO] Scanning for projects... -[INFO] -[INFO] ----------------------< org.acme:security-jwt-quickstart >----------------------- -[INFO] Building security-jwt-quickstart 1.0-SNAPSHOT -[INFO] --------------------------------[ jar ]--------------------------------- -[INFO] -[INFO] --- exec-maven-plugin:1.6.0:java (default-cli) @ security-jwt-quickstart --- -Setting exp: 1551659976 / Sun Mar 03 16:39:36 PST 2019 - Added claim: sub, value: jdoe-using-jwt-rbac - Added claim: aud, value: [using-jwt-rbac] - Added claim: upn, value: jdoe@quarkus.io - Added claim: birthdate, value: 2001-07-13 - Added claim: auth_time, value: 1551659676 - Added claim: iss, value: https://quarkus.io/using-jwt-rbac - Added claim: roleMappings, value: {"group2":"Group2MappedRole","group1":"Group1MappedRole"} - Added claim: groups, value: ["Echoer","Tester","Subscriber","group2"] - Added claim: preferred_username, value: jdoe - Added claim: exp, value: Sun Mar 03 16:39:36 PST 2019 - Added claim: iat, value: Sun Mar 03 16:34:36 PST 2019 - Added claim: jti, value: a-123 -eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjU5Njc2LCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1OTk3NiwiaWF0IjoxNTUxNjU5Njc2LCJqdGkiOiJhLTEyMyJ9.O9tx_wNNS4qdpFhxeD1e7v4aBNWz1FCq0UV8qmXd7dW9xM4hA5TO-ZREk3ApMrL7_rnX8z81qGPIo_R8IfHDyNaI1SLD56gVX-NaOLS2OjfcbO3zOWJPKR_BoZkYACtMoqlWgIwIRC-wJKUJU025dHZiNL0FWO4PjwuCz8hpZYXIuRscfFhXKrDX1fh3jDhTsOEFfu67ACd85f3BdX9pe-ayKSVLh_RSbTbBPeyoYPE59FW7H5-i8IE-Gqu838Hz0i38ksEJFI25eR-AJ6_PSUD0_-TV3NjXhF3bFIeT4VSaIZcpibekoJg0cQm-4ApPEcPLdgTejYHA-mupb8hSwg -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 1.682 s -[INFO] Finished at: 2019-03-03T16:34:36-08:00 -[INFO] ------------------------------------------------------------------------ ----- - -The JWT string is the base64 encoded string that has 3 parts separated by '.' characters: -`eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjUyMDkxLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1MjM5MSwiaWF0IjoxNTUxNjUyMDkxLCJqdGkiOiJhLTEyMyJ9.aPA4Rlc4kw7n_OZZRRk25xZydJy_J_3BRR8ryYLyHTO1o68_aNWWQCgpnAuOW64svPhPnLYYnQzK-l2vHX34B64JySyBD4y_vRObGmdwH_SEufBAWZV7mkG3Y4mTKT3_4EWNu4VH92IhdnkGI4GJB6yHAEzlQI6EdSOa4Nq8Gp4uPGqHsUZTJrA3uIW0TbNshFBm47-oVM3ZUrBz57JKtr0e9jv0HjPQWyvbzx1HuxZd6eA8ow8xzvooKXFxoSFCMnxotd3wagvYQ9ysBa89bgzL-lhjWtusuMFDUVYwFqADE7oOSOD4Vtclgq8svznBQ-YpfTHfb9QEcofMlpyjNA` +$ mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -Dsmallrye.jwt.sign.key-location=privateKey.pem -If you start playing around with the code and/or the solution code, you will only be able -to use a given token for 5-6 minutes because that is the default expiration period + grace period. To use -a longer expiration, pass in the lifetime of the token in seconds as the second argument to the `GenerateToken` class using -`-Dexec.args=...`. The first argument is the classpath resource name of the json document containing the claims to add to -the JWT, and should be '/JwtClaims.json' for this quickstart. - -.Example Command to Generate JWT with Lifetime of 3600 Seconds -[source,shell] ----- -$ mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -Dexec.args="/JwtClaims.json 3600" -[INFO] Scanning for projects... -[INFO] -[INFO] ----------------------< org.acme: >----------------------- -[INFO] Building security-jwt-quickstart 1.0-SNAPSHOT -[INFO] --------------------------------[ jar ]--------------------------------- -[INFO] -[INFO] --- exec-maven-plugin:1.6.0:java (default-cli) @ security-jwt-quickstart --- - Added claim: iss, value: https://quarkus.io/using-jwt-rbac - Added claim: jti, value: a-123 - Added claim: sub, value: jdoe-using-jwt-rbac - Added claim: upn, value: jdoe@quarkus.io - Added claim: preferred_username, value: jdoe - Added claim: aud, value: using-jwt-rbac - Added claim: birthdate, value: 2001-07-13 - Added claim: roleMappings, value: {group1=Group1MappedRole, group2=Group2MappedRole} - Added claim: groups, value: [Echoer, Tester, Subscriber, group2] - Added claim: iat, value: 1571329458 - Added claim: auth_time, value: NumericDate{1571329458 -> Oct 17, 2019 5:24:18 PM IST} - Added claim: exp, value: 1571333058 -eyJraWQiOiIvcHJpdmF0ZUtleS5wZW0iLCJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3F1YXJrdXMuaW8vdXNpbmctand0LXJiYWMiLCJqdGkiOiJhLTEyMyIsInN1YiI6Impkb2UtdXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJqZG9lIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwicm9sZU1hcHBpbmdzIjp7Imdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUiLCJncm91cDIiOiJHcm91cDJNYXBwZWRSb2xlIn0sImdyb3VwcyI6WyJFY2hvZXIiLCJUZXN0ZXIiLCJTdWJzY3JpYmVyIiwiZ3JvdXAyIl0sImlhdCI6MTU3MTMyOTQ1OCwiYXV0aF90aW1lIjoiTnVtZXJpY0RhdGV7MTU3MTMyOTQ1OCAtPiBPY3QgMTcsIDIwMTkgNToyNDoxOCBQTSBJU1R9IiwiZXhwIjoxNTcxMzMzMDU4fQ.Hn6f0qSk6wbbqOM-q9zo1KQ91VwIAdhJqdMmNK3pQrgSv68Ljdi75nSKvDmQwhtvEnHbZvoZy4BqbQagLT05JYcAWaT4NrtFLaqtJ_k8HD39_HosObF43u-vpEwisen0U219R0hpo9jx8Qohj4gzM-YL1sIFgqZSgsxH6YEorVLS70vkizTqfcclMvyrmkUq0nA4p4ST7jq987RkqXtY7U6jNc0rVnu7XmalA26VtfcqSgz9fwk_b-TmwqA6jgLvO6Rdovh0Q6tRDOW1VugQ_11-3k34ImdD3HG8gpdGatulHKWoxg9MhIcbrFWftlk7Ts97tkljp8ysfFzwFELnkg -[INFO] ------------------------------------------------------------------------ -[INFO] BUILD SUCCESS -[INFO] ------------------------------------------------------------------------ -[INFO] Total time: 1.685 s -[INFO] Finished at: 2019-03-03T16:32:35-08:00 -[INFO] ------------------------------------------------------------------------ +eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjU5Njc2LCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1OTk3NiwiaWF0IjoxNTUxNjU5Njc2LCJqdGkiOiJhLTEyMyJ9.O9tx_wNNS4qdpFhxeD1e7v4aBNWz1FCq0UV8qmXd7dW9xM4hA5TO-ZREk3ApMrL7_rnX8z81qGPIo_R8IfHDyNaI1SLD56gVX-NaOLS2OjfcbO3zOWJPKR_BoZkYACtMoqlWgIwIRC-wJKUJU025dHZiNL0FWO4PjwuCz8hpZYXIuRscfFhXKrDX1fh3jDhTsOEFfu67ACd85f3BdX9pe-ayKSVLh_RSbTbBPeyoYPE59FW7H5-i8IE-Gqu838Hz0i38ksEJFI25eR-AJ6_PSUD0_-TV3NjXhF3bFIeT4VSaIZcpibekoJg0cQm-4ApPEcPLdgTejYHA-mupb8hSwg ---- +The JWT string is the Base64 URL encoded string that has 3 parts separated by '.' characters. +First part - JWT headers, second part - JWT claims, third part - JWT signature. == Finally, Secured Access to /secured/roles-allowed Now let's use this to make a secured request to the /secured/roles-allowed endpoint. Make sure you have the Quarkus server running using the `./mvnw compile quarkus:dev` command, and then run the following command, making sure to use your version of the generated JWT from the previous step: @@ -674,7 +438,7 @@ curl -H "Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUI [source,shell] ---- $ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed; echo -hello + jdoe@quarkus.io, isSecure: false, authScheme: MP-JWT, hasJWT: true +hello + jdoe@quarkus.io, isHttps: false, authScheme: MP-JWT, hasJWT: true, birthdate: 2001-07-13 ---- Success! We now have: @@ -682,8 +446,9 @@ Success! We now have: * a non-anonymous caller name of jdoe@quarkus.io * an authentication scheme of Bearer * a non-null JsonWebToken +* birthdate claim value -== Using the JsonWebToken and Claim Injection +=== Using the JsonWebToken and Claim Injection Now that we can generate a JWT to access our secured REST endpoints, let's see what more we can do with the `JsonWebToken` interface and the JWT claims. The `org.eclipse.microprofile.jwt.JsonWebToken` interface extends the `java.security.Principal` @@ -694,191 +459,101 @@ hold of the caller `JsonWebToken` interface by casting the `SecurityContext#getU The `JsonWebToken` interface defines methods for accessing claims in the underlying JWT. It provides accessors for common claims that are required by the {mp-jwt} specification as well as arbitrary claims that may exist in the JWT. -Let's expand our `TokenSecuredResource` with another endpoint /secured/winners. The `winners()` method, some hypothetical lottery - winning number generator, whose code is shown in the following list: +All the JWT claims can also be injected. Let's expand our `TokenSecuredResource` with another endpoint /secured/roles-allowed-admin which users the injected `birthdate` claim +(as opposed to getting it from `JsonWebToken`): -.TokenSecuredResource#winners Method Addition [source, java] ---- package org.acme.security.jwt; -import java.security.Principal; -import java.time.LocalDate; -import java.util.ArrayList; - import javax.annotation.security.PermitAll; import javax.annotation.security.RolesAllowed; import javax.enterprise.context.RequestScoped; import javax.inject.Inject; import javax.ws.rs.GET; +import javax.ws.rs.InternalServerErrorException; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.SecurityContext; +import org.eclipse.microprofile.jwt.Claim; import org.eclipse.microprofile.jwt.Claims; import org.eclipse.microprofile.jwt.JsonWebToken; - -/** - * Version 3 of the TokenSecuredResource - */ @Path("/secured") @RequestScoped -public class TokenSecuredResourceV3 { +public class TokenSecuredResource { @Inject JsonWebToken jwt; - -... + @Inject + @Claim(standard = Claims.birthdate) + String birthdate; // <1> @GET - @Path("winners") + @Path("permit-all") + @PermitAll @Produces(MediaType.TEXT_PLAIN) - @RolesAllowed("Subscriber") - public String winners() { - int remaining = 6; - ArrayList numbers = new ArrayList<>(); - - // If the JWT contains a birthdate claim, use the day of the month as a pick - if (jwt.containsClaim(Claims.birthdate.name())) { // <1> - String bdayString = jwt.getClaim(Claims.birthdate.name()); // <2> - LocalDate bday = LocalDate.parse(bdayString); - numbers.add(bday.getDayOfMonth()); // <3> - remaining --; - } - // Fill remaining picks with random numbers - while(remaining > 0) { // <4> - int pick = (int) Math.rint(64 * Math.random() + 1); - numbers.add(pick); - remaining --; - } - return numbers.toString(); + public String hello(@Context SecurityContext ctx) { + return getResponseString(ctx); } -} ----- -<1> Here we use the injected `JsonWebToken` to check for a `birthday` claim. -<2> If it exists, we obtain the claim value as a `String`, and then convert it to a `LocalDate`. -<3> The day of month value of the `birthday` claim is inserted as the first winning number pick. -<4> The remainder of the winning number picks are random numbers. - -This illustrates how you can use the JWT to not only provide identity and role based authorization, but as a stateless container -of information associated with the authenticated caller that can be used to alter you business method logic. -Add this `winners` method to your `TokenSecuredResource` code, and run the following command, replacing _YOUR_TOKEN_ with -a new JWT or a long lived JWT you generated previously: -.curl command for /secured/winners -[source,shell] ----- -curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8080/secured/winners; echo ----- - -Example output using my generated token is shown in the following example output. Note that the first pick corresponds to the day of month of -the birthdate claim from the `JwtClaims.json` content. - -.Example Output for /secured/winners -[source,shell] ----- -$ curl -H "Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjY2MDMzLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY2NjMzMywiaWF0IjoxNTUxNjY2MDMzLCJqdGkiOiJhLTEyMyJ9.LqJ5LlCrVIbCcRAus4aNHv7UfvuUgrcEVOwBxwfPY4c-YCaUxK0owxbtP2WtR5__yTFXpdplR6gVJWwv4Hw8c_sP8MRQi_5bdnTqZt3TeJsepx0cm7AIwJCopmpbuNjIgLVLZ_6VP3ZkZ2VK9SDO-9yBMPWWp2bnLILdwfYsOuJbFB_bWxSQYnTioms7NZjVefVY8eqawwfRq75PhB7W2iw-Ni2puVFjnpTiAZeCUCur-zjQ50QG6zSCZpVqPcI5JZ2-KeJKheiglYCYp0cauTdVgXjdlXCGQbAU0xirLxJXNsxg2GZxgV9luGwy1y3BdezwoM2m4mXviuHJP-lziA" http://localhost:8080/secured/winners; echo -[13, 47, 42, 45, 19, 25] ----- - -=== Claims Injection - -In the previous `winners()` method we accessed the `birthday` claim through the `JsonWebToken` interface. {mp-jwt} also supports -the direct injection of claim values from the JWT using CDI injection and the {mp-jwt} `@Claim` qualifier. Here is an alternative -version of the `winners()` method that injects the `birthday` claim value as an `Optional`: - -.TokenSecuredResource#winners2 Method Addition -[source, java] ----- -package org.acme.security.jwt; - -import java.security.Principal; -import java.time.LocalDate; -import java.util.ArrayList; -import java.util.Optional; - -import javax.annotation.security.PermitAll; -import javax.annotation.security.RolesAllowed; -import javax.enterprise.context.RequestScoped; -import javax.inject.Inject; -import javax.json.JsonString; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.Context; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.SecurityContext; - -import org.eclipse.microprofile.jwt.Claim; -import org.eclipse.microprofile.jwt.Claims; -import org.eclipse.microprofile.jwt.JsonWebToken; - -/** - * Version 4 of the TokenSecuredResource - */ -@Path("/secured") -@RequestScoped -public class TokenSecuredResource { - - @Inject - JsonWebToken jwt; - @Inject // <1> - @Claim(standard = Claims.birthdate) // <2> - Optional birthdate; // <3> - - ... + @GET + @Path("roles-allowed") + @RolesAllowed({ "User", "Admin" }) + @Produces(MediaType.TEXT_PLAIN) + public String helloRolesAllowed(@Context SecurityContext ctx) { + return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString(); + } @GET - @Path("winners2") + @Path("roles-allowed-admin") + @RolesAllowed("Admin") @Produces(MediaType.TEXT_PLAIN) - @RolesAllowed("Subscriber") - public String winners2() { - int remaining = 6; - ArrayList numbers = new ArrayList<>(); - - // If the JWT contains a birthdate claim, use the day of the month as a pick - if (birthdate.isPresent()) { // <4> - String bdayString = birthdate.get().getString(); <5> - LocalDate bday = LocalDate.parse(bdayString); - numbers.add(bday.getDayOfMonth()); - remaining --; - } - // Fill remaining picks with random numbers - while(remaining > 0) { - int pick = (int) Math.rint(64 * Math.random() + 1); - numbers.add(pick); - remaining --; + public String helloRolesAllowedAdmin(@Context SecurityContext ctx) { + return getResponseString(ctx) + ", birthdate: " + birthdate; // <2> + } + + private String getResponseString(SecurityContext ctx) { + String name; + if (ctx.getUserPrincipal() == null) { + name = "anonymous"; + } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { + throw new InternalServerErrorException("Principal and JsonWebToken names do not match"); + } else { + name = ctx.getUserPrincipal().getName(); } - return numbers.toString(); + return String.format("hello + %s," + + " isHttps: %s," + + " authScheme: %s," + + " hasJWT: %s", + name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); + } + + private boolean hasJwt() { + return jwt.getClaimNames() != null; } } ---- -<1> We use CDI `@Inject` along with... -<2> an {mp-jwt} `@Claim(standard = Claims.birthdate)` qualifier to inject the `birthdate` claim directly as -<3> an `Optional` value. -<4> Now we check whether the injected `birthdate` field is present -<5> and if it is, get its value. +<1> Here we use the injected `birthday` claim. +<2> Here we use the injected `birthday` claim to build the final reply. -The remainder of the code is the same as before. Update your `TokenSecuredResource` to either add or replace the current -`winners()` method, and then invoke the following command with _YOUR_TOKEN_ replaced: +Now generate the token again and run: -.curl command for /secured/winners2 [source,shell] ---- -curl -H "Authorization: Bearer YOUR_TOKEN" http://localhost:8080/secured/winners2; echo +curl -H "Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjUyMDkxLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY1MjM5MSwiaWF0IjoxNTUxNjUyMDkxLCJqdGkiOiJhLTEyMyJ9.aPA4Rlc4kw7n_OZZRRk25xZydJy_J_3BRR8ryYLyHTO1o68_aNWWQCgpnAuOW64svPhPnLYYnQzK-l2vHX34B64JySyBD4y_vRObGmdwH_SEufBAWZV7mkG3Y4mTKT3_4EWNu4VH92IhdnkGI4GJB6yHAEzlQI6EdSOa4Nq8Gp4uPGqHsUZTJrA3uIW0TbNshFBm47-oVM3ZUrBz57JKtr0e9jv0HjPQWyvbzx1HuxZd6eA8ow8xzvooKXFxoSFCMnxotd3wagvYQ9ysBa89bgzL-lhjWtusuMFDUVYwFqADE7oOSOD4Vtclgq8svznBQ-YpfTHfb9QEcofMlpyjNA" http://127.0.0.1:8080/secured/roles-allowed-admin; echo ---- -.Example Output for /secured/winners2 [source,shell] ---- -$ curl -H "Authorization: Bearer eyJraWQiOiJcL3ByaXZhdGVLZXkucGVtIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiJqZG9lLXVzaW5nLWp3dC1yYmFjIiwiYXVkIjoidXNpbmctand0LXJiYWMiLCJ1cG4iOiJqZG9lQHF1YXJrdXMuaW8iLCJiaXJ0aGRhdGUiOiIyMDAxLTA3LTEzIiwiYXV0aF90aW1lIjoxNTUxNjY3MzEzLCJpc3MiOiJodHRwczpcL1wvcXVhcmt1cy5pb1wvdXNpbmctand0LXJiYWMiLCJyb2xlTWFwcGluZ3MiOnsiZ3JvdXAyIjoiR3JvdXAyTWFwcGVkUm9sZSIsImdyb3VwMSI6Ikdyb3VwMU1hcHBlZFJvbGUifSwiZ3JvdXBzIjpbIkVjaG9lciIsIlRlc3RlciIsIlN1YnNjcmliZXIiLCJncm91cDIiXSwicHJlZmVycmVkX3VzZXJuYW1lIjoiamRvZSIsImV4cCI6MTU1MTY3MDkxMywiaWF0IjoxNTUxNjY3MzEzLCJqdGkiOiJhLTEyMyJ9.c2QJAK3a1VOYL6vOt40VSEAy9wXPBEjVbqApTTNG8V8UDkQZ6HiOR9-rKOFX3WmTtQVru3O9zDu2_T2_v8kTmCkT-ThxodqC4VxD_QVx1v6BaSJ9-MX1Q7nrkD0Mk1V6x0Cqd6jmHxtJy0Ep8IgeMw2Y5gL9a1NgWVeldXP6cdHrHcYKYGnZKmYp7VpqZBoONPIS_QmWXm-JerwVpwt0juEtZUQoGCJdp7-GZA31QyEN64gCMKfdhYNnLuWQaom3i0uF_LfXtlMHdRU0kzDnLrnGw99ynTAex7ah7zG10ZbanK-PI-nD6wcTbE9WqriwohHM9BFJoBmF81RRk5uMsw" http://localhost:8080/secured/winners2; echo -[13, 38, 36, 38, 36, 22] +$ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed-admin; echo +hello + jdoe@quarkus.io, isHttps: false, authScheme: MP-JWT, hasJWT: true, birthdate: 2001-07-13 ---- -== Package and run the application +=== Package and run the application As usual, the application can be packaged using `./mvnw clean package` and executed using the `-runner.jar` file: .Runner jar Example [source,shell] @@ -920,7 +595,7 @@ Scotts-iMacPro:security-jwt-quickstart starksm$ ./target/security-jwt-quickstart 2019-03-28 14:31:37,316 INFO [io.quarkus] (main) Installed features: [cdi, resteasy, resteasy-jsonb, security, smallrye-jwt] ---- -== Explore the Solution +=== Explore the Solution The solution repository located in the `security-jwt-quickstart` {quickstarts-tree-url}/security-jwt-quickstart[directory] contains all of the versions we have worked through in this quickstart guide as well as some additional endpoints that illustrate subresources with injection @@ -964,6 +639,8 @@ SmallRye JWT provides more properties which can be used to customize the token p |=== |Property Name|Default|Description |smallrye.jwt.verify.algorithm|`RS256`|Signature algorithm. Set it to `ES256` to support the Elliptic Curve signature algorithm. +|smallrye.jwt.verify.key-format|`ANY`|Set this property to a specific key format such as `PEM_KEY`, `PEM_CERTIFICATE`, `JWK` or `JWK_BASE64URL` to optimize the way the verification key is loaded. +|smallrye.jwt.verify.relax-key-validation|false|Relax the validation of the verification keys, setting this property to `true` will allow public RSA keys with the length less than 2048 bit. |smallrye.jwt.token.header|`Authorization`|Set this property if another header such as `Cookie` is used to pass the token. |smallrye.jwt.token.cookie|none|Name of the cookie containing a token. This property will be effective only if `smallrye.jwt.token.header` is set to `Cookie`. |smallrye.jwt.always-check-authorization|false|Set this property to true for Authorization header be checked even if the smallrye.jwt.token.header is set to Cookie but no cookie with a smallrye.jwt.token.cookie name exists. @@ -980,14 +657,18 @@ SmallRye JWT provides more properties which can be used to customize the token p |smallrye.jwt.jwks.forced-refresh-interval|30|Forced JWK cache refresh interval in minutes which is used to restrict the frequency of the forced refresh attempts which may happen when the token verification fails due to the cache having no JWK key with a kid property matching the current token's kid header. It will be ignored unless the mp.jwt.verify.publickey.location points to the HTTPS URL based JWK set. |smallrye.jwt.expiration.grace|60|Expiration grace in seconds. By default an expired token will still be accepted if the current time is no more than 1 min after the token expiry time. |smallrye.jwt.verify.aud|none|Comma separated list of the audiences that a token `aud` claim may contain. +|smallrye.jwt.required.claims|none|Comma separated list of the claims that a token must contain. +|smallrye.jwt.decrypt.key-location|none|Config property allows for an external or internal location of Private Decryption Key to be specified. +|smallrye.jwt.decrypt.algorithm|`RSA_OAEP`|Decryption algorithm. +|smallrye.jwt.token.decryption.kid|none|Decryption Key identifier. If it is set then the decryption JWK key as well every JWT token must have a matching `kid` header. |=== == Create JsonWebToken with JWTParser If the JWT token can not be injected, for example, if it is embedded in the service request payload or the service endpoint acquires it out of band, then one can use `JWTParser`: ``` -import io.smallrye.jwt.auth.principal.JWTParser; import org.eclipse.microprofile.jwt.JsonWebToken; +import io.smallrye.jwt.auth.principal.JWTParser; ... @Inject JWTParser parser; @@ -997,6 +678,54 @@ String token = getTokenFromOidcServer(); JsonWebToken jwt = parser.parse(token); ``` +You can also use it to customize the way the token is verified or decrypted. For example, one can supply a local `SecretKey`: + +``` +import javax.crypto.SecretKey; +import javax.ws.rs.GET; +import javax.ws.rs.core.NewCookie; +import javax.ws.rs.core.Response; +import org.eclipse.microprofile.jwt.JsonWebToken; +import io.smallrye.jwt.auth.principal.JWTParser; +import io.smallrye.jwt.build.Jwt; + +@Path("/secured") +public class SecuredResource { + @Inject JWTParser parser; + private SecretKey key = createSecretKey(); + + @GET + @Produces("text/plain") + public Response getUserName(@CookieParam("jwt") String jwtCookie) { + Response response = null; + if (jwtCookie == null) { + String newJwtCookie = Jwt.upn("Alice").sign(key); + // or newJwtCookie = Jwt.upn("alice").encrypt(key); + return Response.ok("Alice").cookie(new NewCookie("jwt", newJwtCookie)).build(); + else { + // All mp.jwt and smallrye.jwt properties are still effective, only the verification key is customized. + JsonWebToken jwt = parser.verify(jwtCookie, key); + // or jwt = parser.decrypt(jwtCookie, key); + return Response.ok(jwt.getName()).build(); + } +} +``` + +== Token Decryption + +If your application needs to accept the tokens with the encrypted claims or with the encrypted inner signed claims then all you have to do is to set +`smallrye.jwt.decrypt.key-location` pointing to the decryption key. + +If this is the only key property which is set then the incoming token is expected to contain the encrypted claims only. +If either `mp.jwt.verify.publickey` or `mp.jwt.verify.publickey.location` verification properties are also set then the incoming token is expected to contain +the encrypted inner-signed token. + +See <> and learn how to generate the encrypted or inner-signed and then encrypted tokens fast. + +== How to check the errors in the logs == + +Set `quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".level=TRACE` to see more details about the token verification or decryption errors. + [[generate-jwt-tokens]] == Generate JWT tokens with SmallRye JWT @@ -1023,6 +752,8 @@ import org.eclipse.microprofile.jwt.JsonWebToken; // Create an empty builder and add some claims JwtClaimsBuilder builder1 = Jwt.claims(); builder1.claim("customClaim", "custom-value").issuer("https://issuer.org"); +// Or start typing the claims immediately: +// JwtClaimsBuilder builder1 = Jwt.upn("Alice"); // Builder created from the existing claims JwtClaimsBuilder builder2 = Jwt.claims("/tokenClaims.json"); @@ -1041,7 +772,10 @@ JwtClaimsBuilder builder4 = Jwt.claims(json); JwtClaimsBuilder builder5 = Jwt.claims(token); ---- -The API is fluent so the builder initialization can be done as part of the fluent API sequence. The builder will also set `iat` (issued at) to the current time, `exp` (expires at) to 5 minutes away from the current time and `jti` (unique token identifier) claims if they have not already been set, so one can skip setting them when possible. +The API is fluent so the builder initialization can be done as part of the fluent API sequence. + +The builder will also set `iat` (issued at) to the current time, `exp` (expires at) to 5 minutes away from the current time (it can be customized with the `smallrye.jwt.new-token.lifespan` property) and `jti` (unique token identifier) claims if they have not already been set. +One can also configure `smallrye.jwt.new-token.issuer` property and skip setting the issuer directly with the builder API. The next step is to decide how to secure the claims. @@ -1059,7 +793,7 @@ import io.smallrye.jwt.build.Jwt; String jwt1 = Jwt.claims("/tokenClaims.json").sign(); // Set the headers and sign the claims with an RSA private key loaded in the code (the implementation of this method is omitted). Note a 'jws()' transition to a 'JwtSignatureBuilder'. -String jwt2 = Jwt.claims("/tokenClaims.json").jws().signatureKeyId("kid1").header("custom-header", "custom-value").sign(getPrivateKey()); +String jwt2 = Jwt.claims("/tokenClaims.json").jws().keyId("kid1").header("custom-header", "custom-value").sign(getPrivateKey()); ---- Note the `alg` (algorithm) header is set to `RS256` by default. @@ -1095,6 +829,21 @@ import io.smallrye.jwt.build.Jwt; String jwt = Jwt.claims("/tokenClaims.json").innerSign().encrypt(); ---- +=== Fast JWT Generation + +If `smallrye.jwt.sign.key-location` or/and `smallrye.jwt.encrypt.key-location` properties are set then one can secure the existing claims (resources, maps, JsonObjects) with a single call: +``` +// More compact than Jwt.claims("/claims.json").sign(); +Jwt.sign("/claims.json"); + +// More compact than Jwt.claims("/claims.json").jwe().encrypt(); +Jwt.encrypt("/claims.json"); + +// More compact than Jwt.claims("/claims.json").innerSign().encrypt(); +Jwt.signAndEncrypt("/claims.json"); +``` +As mentioned above, `iat`, `exp`, `jti` and `iss` claims will be added if needed. + === SmallRye JWT Builder configuration Smallrye JWT supports the following properties which can be used to customize the way claims are signed and encrypted: @@ -1104,11 +853,14 @@ Smallrye JWT supports the following properties which can be used to customize th |Property Name|Default|Description |smallrye.jwt.sign.key-location|`none`|Location of a private key which will be used to sign the claims when either a no-argument `sign()` or `innerSign()` method is called. |smallrye.jwt.encrypt.key-location|`none`|Location of a public key which will be used to encrypt the claims or inner JWT when a no-argument `encrypt()` method is called. +|smallrye.jwt.new-token.lifespan|300|Token lifespan in seconds which will be used to calculate an exp (expiry) claim value if this claim has not already been set. +|smallrye.jwt.new-token.issuer|none|Token issuer which can be used to set an iss (issuer) claim value if this claim has not already been set. |=== == References -* link:https://github.com/eclipse/microprofile-jwt-auth/releases/download/1.1.1/microprofile-jwt-auth-spec.html[MP JWT 1.1.1] +* link:https://github.com/eclipse/microprofile-jwt-auth/releases/download/1.1.1/microprofile-jwt-auth-spec.html[MP JWT 1.1.1 HTML] +* link:https://github.com/eclipse/microprofile-jwt-auth/releases/download/1.1.1/microprofile-jwt-auth-spec.pdf[MP JWT 1.1.1 PDF] * link:https://github.com/smallrye/smallrye-jwt[SmallRye JWT] * link:https://tools.ietf.org/html/rfc7519[JSON Web Token] * link:https://tools.ietf.org/html/rfc7515[JSON Web Signature] diff --git a/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/TokenUtils.java b/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/TokenUtils.java index ff1dead457cee..35ac63a9d6303 100644 --- a/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/TokenUtils.java +++ b/extensions/smallrye-jwt/deployment/src/test/java/io/quarkus/jwt/test/TokenUtils.java @@ -150,10 +150,10 @@ public static String generateTokenString(PrivateKey pk, String kid, String jsonR pk = generateKeyPair(2048).getPrivate(); } - JwtSignatureBuilder jws = claims.jws().signatureKeyId(kid); + JwtSignatureBuilder jws = claims.jws().keyId(kid); if (invalidClaims.contains(InvalidClaims.ALG)) { - jws.signatureAlgorithm(SignatureAlgorithm.HS256); + jws.algorithm(SignatureAlgorithm.HS256); SecretKey sk = KeyGenerator.getInstance("HMACSHA256").generateKey(); return jws.sign(sk); } else {