+
Protected Servlet
+
principal name: %s
+
access token (type = %s):
+
%s
+
preferred_username: %s
+
roles: %s
+
claims:
+
%s
+
+ """.formatted(
+ name,
+ context.getTokenType(),
+ context.getAccessToken(),
+ context.getClaimsJson().get("preferred_username").toString(),
+ context.getClaimsJson().get("http://www.jakartaee.demo/roles").toString(),
+ context.getClaimsJson()
+ );
+
+ response.setContentType("text/html");
+ response.getWriter().print(html.toString());
+ }
+}
+```
+
+The `@OpenIdAuthenticationMechanismDefinition` is the new feature added by Jakarta EE 10 and Security 3.0. The docs for this annotation [are here](https://jakarta.ee/specifications/security/3.0/jakarta-security-spec-3.0.html#openid-connect-annotation).
+
+The first four parameters set the required OIDC values. I had to increase the timeout values to avoid an intermittent error. The `extraParameters` param is used to send the `audience` value as the Auth0 custom API (without which, Auth0 will return an opaque token). The `claimsDefinition` param is used to configure reading the roles from the custom claim.
+
+The `@OpenIdAuthenticationMechanismDefinition` annotation alone does not protect the resource. It activates OIDC and configures a provider. It could just as easily have been included in another class file.
+
+The security constraint is added by `@ServletSecurity`, which is used to only allow users with the role (or group) `Everyone`.
+
+The other annotation, `@WebServlet("/protected")`, defines the class as a web servlet and defines the path. You can see [the spec for this annotation here](https://docs.oracle.com/javaee/7/api/javax/servlet/annotation/WebServlet.html).
+
+CDI (Context and Dependency Injection) is used to inject two dependencies: the `OpenIdContext` and the `SecurityContext`. These are both used to retrieve and return some details about the authenticated person. They are not required for authentication itself.
+
+When a user that is not authenticated attempts to load this resource, they are redirected to Auth0 for authentication. From a browser, the user sees Auth0's login screen. After successfully logging in, the user is redirected back to the `/callback` servlet with an authentication code. Jakarta EE's security framework intercepts this redirect and sends the code back to Auth0 to exchange it for an authentication token before passing control back to the `/callback` endpoint.
+
+At this point, the user is successfully authenticated. If you look at the callback servlet (shown below), you'll see that it simply redirects the user back to the `/protected` servlet.
+
+## Log In to the App Using Auth0 SSO and OpenID Connect
+
+Give it a try. Start the app.
+
+```bash
+./mvnw wildfly:run
+```
+
+Wait a few seconds for it to finish loading.
+
+Open a browser to the protected page at `http://localhost:8080/protected`.
+
+You'll have to authorize the app with Auth0. You may also have to log in if you are not already logged in. After that you should be redirected back to the protected page, which will print out some information from the token.
+
+Success! You've got a working Jakarate EE application secured with OIDC and OAuth 2.0.
+
+```
+Protected Servlet
+
+principal name: andrewcarterhughes+test@gmail.com
+
+access token (type = Bearer):
+
+eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im5yMWZwWVlkb3JkalEybzRlREp6MiJ9.eyJodHRwOi8vd3d3Lmpha2FydGFlZS5kZW1vL3JvbGVzIjpbIkV2ZXJ5b25lIl0sImlzcyI6Imh0dHBzOi8vZGV2LTByYTk5anJwLnVzLmF1dGgwLmNvbS8iLCJz...
+
+preferred_username: "andrewcarterhughes+test@gmail.com"
+
+roles: ["Everyone"]
+
+claims:
+
+{"sub":"auth0|638e36302e342504ae92b911","nickname":"andrewcarterhughes+test","preferred_username":"andrewcarterhughes+test@gmail.com","name":"andrewcarterhughes+test@gmail.com","picture":"https://s.gravatar.com/avatar/146a9ec7b0773b3edc6a299d7ad5dbb0?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fan.png","updated_at":"2023-02-18T04:37:30.403Z","email":"andrewcarterhughes+test@gmail.com","email_verified":true,"http://www.jakartaee.demo/roles":["Everyone"]}
+```
+
+Make sure you see `roles: ["Everyone"]`. This is coming from the claim `http://www.jakartaee.demo/roles":["Everyone"]`, which is what is being injected by the action you created on Auth0. If that's not there, something is misconfigured.
+
+Next you'll see how to secure the an API method on the app and use the token you just retrieved to access the secured API method. Directly below, however, is a summary of the OIDC login flow for people not already familiar with it.
+
+## OpenID Connect Authentication Flow Summary
+
+For people new to OAuth and OIDC, this is a summary of what just happened when you accessed the `protected` endpoint.
+
+- Client requests `/protected`.
+- Jakarta EE Security 3.0 intercepts this request based on OIDC configuration and authentication requirement for the endpoint and redirects to Auth0 for authentication.
+- Upon successful authentication, Auth0 redirects back to `/callback` endpoint, sending the authorization code.
+- Jakarta EE Security 3.0 intercepts the request to the `/callback` endpoint and sends the authorization code back to Auth0.
+- Auth0 accepts the authorization code, verifies it, and returns an access token (and possibly an identity token) to the Jakarta EE Security 3.0 framework.
+- The client receives the access token, unpacks it, and verifies. Once the token is verified, the user is authenticated. The `callback` method is run, which programmattically redirects back to the `/protected` endpoint.
+- Before the `/protected` endpoint is run, the `@ServletSecurity` annotation requirement is checked. If the user is a member of the `Everyone` group, the `ProtectedServlet.doGet()` method is called.
+- Finally, the `ProtectedServlet.doGet()` method is called.
+
+All of that happened above when you logged into Auth0 and loaded the protected servlet. Since this servlet handily prints out the JWT, I thought it would be nice to see how to secure a web API using a JWT, which is what you'll see in the next section.
+
+## Use the JWT to access the protected API
+
+Your secured API method will not perform all of the redirecting of the OIDC flow. Instead, it will simply decode and validate the JWT. Take a look at the `ApiServlet.java` file. This is what defines the API servlet. This is what you'll access using the JWT and a simple HTTP request using HTTPie.
+
+`src/main/java/com/demo/ApiServlet.java`
+
+```java
+package com.demo;
+
+...
+
+@WebServlet("/api/protected")
+public class ApiServlet extends HttpServlet {
+
+ @Override
+ protected void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
+
+ DecodedJWT jwt = (DecodedJWT) request.getAttribute("accessToken");
+ IdToken idToken = (IdToken) request.getAttribute("idToken");
+ response.setContentType("text/plain");
+ response.getWriter().println("Welcome, " + idToken.email);
+ response.getWriter().println("accessToken claims:" + jwt.getClaims());
+ response.getWriter().println("idToken claims:" + idToken.toString());
+ }
+}
+```
+
+**By itself, this servlet is not secured.** It would be public without the `JwtFilter` class, which is shown below. The filter intercepts any requests matching the `/api/*` URL pattern and denies them if they do not have a valid JWT. Notice that this is a totally different authentication and authorization method from the client login OIDC example above.
+
+`src/main/java/com/demo/JwtFilter.java`
+
+```java
+package com.demo;
+
+...
+
+@WebFilter(filterName = "jwtFilter", urlPatterns = "/api/*")
+public class JwtFilter implements Filter {
+
+ private static final Logger LOGGER = Logger.getLogger(JwtFilter.class.getName());
+
+ @Inject
+ OidcConfig oidcConfig;
+
+ @Override
+ public void init(FilterConfig filterConfig) {
+ LOGGER.info("Auth0 jwtVerifier initialized for issuer:" + oidcConfig.getIssuerUri());
+ }
+
+ @Override
+ public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
+ FilterChain chain) throws IOException, ServletException {
+
+ HttpServletRequest request = (HttpServletRequest) servletRequest;
+ HttpServletResponse response = (HttpServletResponse) servletResponse;
+
+ LOGGER.info("In JwtFilter, path: " + request.getRequestURI());
+
+ // Get access token from authorization header
+ String authHeader = request.getHeader("authorization");
+ if (authHeader == null) {
+ response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+ response.getOutputStream().print("Unauthorized");
+ return;
+ } else {
+ // Get the access token from the header
+ String accessToken = authHeader.substring(authHeader.indexOf("Bearer ") + 7);
+ LOGGER.info("accesstoken: " + accessToken);
+ JwkProvider provider = new UrlJwkProvider(oidcConfig.getIssuerUri());
+ try {
+ // Decode the access token
+ DecodedJWT jwt = JWT.decode(accessToken);
+ // Get the kid from received JWT token
+ Jwk jwk = provider.get(jwt.getKeyId());
+
+ Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null);
+
+ // Verify the access token
+ JWTVerifier verifier = JWT.require(algorithm)
+ .withIssuer(oidcConfig.getIssuerUri())
+ .build();
+
+ jwt = verifier.verify(accessToken);
+ LOGGER.info("JWT decoded. sub=" + jwt.getClaims().get("sub"));
+
+ // Save the access token in a request attribute
+ request.setAttribute("accessToken", jwt);
+
+ // Get the ID Token
+ String issuerUri = oidcConfig.getIssuerUri();
+ String userinfoUri = issuerUri + "userinfo";
+ LOGGER.info("userinfoUri: " + userinfoUri);
+
+ HttpClient client = HttpClient.newHttpClient();
+ HttpRequest requestIdToken = HttpRequest.newBuilder(
+ URI.create(userinfoUri))
+ .header("Authorization", "Bearer " + accessToken)
+ .build();
+ HttpResponse