1. Overview
In this tutorial, we’re going to provide an implementation for the OAuth 2.0 Authorization Framework using Java EE And MicroProfile. Most importantly, we’re going to implement the interaction of the OAuth 2.0 roles through the Authorization Code grant type. The motivation behind this writing is to give support for projects that are implemented using Java EE as this doesn’t yet provide support for OAuth.
For the most important role, the Authorization Server, we’re going to implement the Authorization Endpoint, the Token Endpoint and additionally, the JWK Key Endpoint, which is useful for the Resource Server to retrieve the public key.
As we want the implementation to be simple and easy for a quick setup, we’re going to use a pre-registered store of clients and users, and obviously a JWT store for access tokens.
2. OAuth 2.0 Overview
In this section, we’re going to give a brief overview of the OAuth 2.0 roles and the Authorization Code grant flow.
2.1. Roles
The OAuth 2.0 framework implies the collaboration between the four following roles:
- Resource Owner: Usually, this is the end-user – it’s the entity that has some resources worth protecting
- Resource Server: An service that protects the resource owner’s data, usually publishing it through a REST API
- Client: An application that uses the resource owner’s data
- Authorization Server: An application that grants permission – or authority – to clients in the form of expiring tokens
2.2. Authorization Grant Types
A grant type is how a client gets permission to use the resource owner’s data, ultimately in the form of an access token.
Naturally, different types of clients prefer different types of grants:
- Authorization Code: Preferred most often – whether it is a web application, a native application, or a single-page application, though native and single-page apps require additional protection called PKCE
- Refresh Token: A special renewal grant, suitable for web applications to renew their existing token
- Client Credentials: Preferred for service-to-service communication, say when the resource owner isn’t an end-user
- Resource Owner Password: Preferred for first-party authentication of native applications, say when the mobile app needs its own login page
In addition, the client can use the implicit grant type. However, it’s usually more secure to use the authorization code grant with PKCE.
2.3. Authorization Code Grant Flow
Since the authorization code grant flow is the most common, let’s also review how that works, and that’s actually what we’ll build in this tutorial.
An application – a client – requests permission by redirecting to the authorization server’s /authorize endpoint. To this endpoint, the application gives a callback endpoint.
The authorization server will usually ask the end-user – the resource owner – for permission. If the end-user grants permission, then the authorization server redirects back to the callback with a code.
The application receives this code and then makes an authenticated call to the authorization server’s /token endpoint. By “authenticated”, we mean that the application proves who it is as part of this call. If all appears in order, the authorization server responds with the token.
With the token in hand, the application makes its request to the API – the resource server – and that API will verify the token. It can ask the authorization server to verify the token using its /introspect endpoint. Or, if the token is self-contained, the resource server can optimize by locally verifying the token’s signature, as is the case with JWT.
2.4. What Does Java EE support?
Not much, yet. In this tutorial, we’ll build most things from the ground up.
3. OAuth 2.0 Authorization Server
In this implementation, we’ll focus on the most commonly used grant type: Authorization Code.
3.1. Client and User Registration
An authorization server would, of course, need to know about the clients and users before it can authorize their requests. And it’s common for an authorization server to have a UI for this.
For simplicity, though, we’ll use a pre-configured client:
INSERT INTO clients (client_id, client_secret, redirect_uri, scope, authorized_grant_types) VALUES ('webappclient', 'webappclientsecret', 'http://localhost:9180/callback', 'resource.read resource.write', 'authorization_code refresh_token');
@Entity @Table(name = "clients") public class Client { @Id @Column(name = "client_id") private String clientId; @Column(name = "client_secret") private String clientSecret; @Column(name = "redirect_uri") private String redirectUri; @Column(name = "scope") private String scope; // ... }
And a pre-configured user:
INSERT INTO users (user_id, password, roles, scopes) VALUES ('appuser', 'appusersecret', 'USER', 'resource.read resource.write');
@Entity @Table(name = "users") public class User implements Principal { @Id @Column(name = "user_id") private String userId; @Column(name = "password") private String password; @Column(name = "roles") private String roles; @Column(name = "scopes") private String scopes; // ... }
Note that for the sake of this tutorial, we’ve used passwords in plain text, but in a production environment, they should be hashed.
For the rest of this tutorial, we’ll show how appuser – the resource owner – can grant access to webappclient – the application – by implementing Authorization Code.
3.2. Authorization Endpoint
The main role of the authorization endpoint is to first authenticate the user and then ask for the permissions – or scopes – that the application wants.
As instructed by the OAuth2 specs, this endpoint should support the HTTP GET method, although it can also support the HTTP POST method. In this implementation, we’ll support only the HTTP GET method.
First, the authorization endpoint requires that the user be authenticated. The spec doesn’t require a certain way here, so let’s use Form Authentication from the Java EE 8 Security API:
@FormAuthenticationMechanismDefinition( loginToContinue = @LoginToContinue(loginPage = "/login.jsp", errorPage = "/login.jsp") )
The user will be redirected to /login.jsp for authentication and then will be available as a CallerPrincipal through the SecurityContext API:
Principal principal = securityContext.getCallerPrincipal();
We can put these together using JAX-RS:
@FormAuthenticationMechanismDefinition( loginToContinue = @LoginToContinue(loginPage = "/login.jsp", errorPage = "/login.jsp") ) @Path("authorize") public class AuthorizationEndpoint { //... @GET @Produces(MediaType.TEXT_HTML) public Response doGet(@Context HttpServletRequest request, @Context HttpServletResponse response, @Context UriInfo uriInfo) throws ServletException, IOException { MultivaluedMap<String, String> params = uriInfo.getQueryParameters(); Principal principal = securityContext.getCallerPrincipal(); // ... } }
At this point, the authorization endpoint can start processing the application’s request, which must contain response_type and client_id parameters and – optionally, but recommended – the redirect_uri, scope, and state parameters.
The client_id should be a valid client, in our case from the clients database table.
The redirect_uri, if specified, should also match what we find in the clients database table.
And, because we’re doing Authorization Code, response_type is code.
Since authorization is a multi-step process, we can temporarily store these values in the session:
request.getSession().setAttribute("ORIGINAL_PARAMS", params);
And then prepare to ask the user which permissions the application may use, redirecting to that page:
String allowedScopes = checkUserScopes(user.getScopes(), requestedScope); request.setAttribute("scopes", allowedScopes); request.getRequestDispatcher("/authorize.jsp").forward(request, response);
3.3. User Scopes Approval
At this point, the browser renders an authorization UI for the user, and the user makes a selection. Then, the browser submits the user’s selection in an HTTP POST:
@POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.TEXT_HTML) public Response doPost(@Context HttpServletRequest request, @Context HttpServletResponse response, MultivaluedMap<String, String> params) throws Exception { MultivaluedMap<String, String> originalParams = (MultivaluedMap<String, String>) request.getSession().getAttribute("ORIGINAL_PARAMS"); // ... String approvalStatus = params.getFirst("approval_status"); // YES OR NO // ... if YES List<String> approvedScopes = params.get("scope"); // ... }
Next, we generate a temporary code that refers to the user_id, client_id, and redirect_uri, all of which the application will use later when it hits the token endpoint.
So let’s create an AuthorizationCode JPA Entity with an auto-generated id:
@Entity @Table(name ="authorization_code") public class AuthorizationCode { @Id @GeneratedValue(strategy=GenerationType.AUTO) @Column(name = "code") private String code; //... }
And then populate it:
AuthorizationCode authorizationCode = new AuthorizationCode(); authorizationCode.setClientId(clientId); authorizationCode.setUserId(userId); authorizationCode.setApprovedScopes(String.join(" ", authorizedScopes)); authorizationCode.setExpirationDate(LocalDateTime.now().plusMinutes(2)); authorizationCode.setRedirectUri(redirectUri);
When we save the bean, the code attribute is auto-populated, and so we can get it and send it back to the client:
appDataRepository.save(authorizationCode); String code = authorizationCode.getCode();
Note that our authorization code will expire in two minutes – we should be as conservative as we can with this expiration. It can be short since the client is going to exchange it right away for an access token.
We then redirect back to the application’s redirect_uri, giving it the code as well as any state parameter that the application specified in its /authorize request:
StringBuilder sb = new StringBuilder(redirectUri); // ... sb.append("?code=").append(code); String state = params.getFirst("state"); if (state != null) { sb.append("&state=").append(state); } URI location = UriBuilder.fromUri(sb.toString()).build(); return Response.seeOther(location).build();
Note again that redirectUri is whatever exists in the clients table, not the redirect_uri request parameter.
So, our next step is for the client to receive this code and exchange it for an access token using the token endpoint.
3.4. Token Endpoint
As opposed to the authorization endpoint, the token endpoint doesn’t need a browser to communicate with the client, and we’ll, therefore, implement it as a JAX-RS endpoint:
@Path("token") public class TokenEndpoint { List<String> supportedGrantTypes = Collections.singletonList("authorization_code"); @Inject private AppDataRepository appDataRepository; @Inject Instance<AuthorizationGrantTypeHandler> authorizationGrantTypeHandlers; @POST @Produces(MediaType.APPLICATION_JSON) @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public Response token(MultivaluedMap<String, String> params, @HeaderParam(HttpHeaders.AUTHORIZATION) String authHeader) throws JOSEException { //... } }
The token endpoint requires a POST, as well as encoding the parameters using the application/x-www-form-urlencoded media type.
As we discussed, we’ll be supporting only the authorization code grant type:
List<String> supportedGrantTypes = Collections.singletonList("authorization_code");
So, the received grant_type as a required parameter should be supported:
String grantType = params.getFirst("grant_type"); Objects.requireNonNull(grantType, "grant_type params is required"); if (!supportedGrantTypes.contains(grantType)) { JsonObject error = Json.createObjectBuilder() .add("error", "unsupported_grant_type") .add("error_description", "grant type should be one of :" + supportedGrantTypes) .build(); return Response.status(Response.Status.BAD_REQUEST) .entity(error).build(); }
Next, we check the client authentication through via HTTP Basic authentication. That is, we check if the received client_id and client_secret, through the Authorization header, matches a registered client:
String[] clientCredentials = extract(authHeader); String clientId = clientCredentials[0]; String clientSecret = clientCredentials[1]; Client client = appDataRepository.getClient(clientId); if (client == null || clientSecret == null || !clientSecret.equals(client.getClientSecret())) { JsonObject error = Json.createObjectBuilder() .add("error", "invalid_client") .build(); return Response.status(Response.Status.UNAUTHORIZED) .entity(error).build(); }
Finally, we delegate the production of the TokenResponse to a corresponding grant type handler:
public interface AuthorizationGrantTypeHandler { TokenResponse createAccessToken(String clientId, MultivaluedMap<String, String> params) throws Exception; }
As we’re more interested in the authorization code grant type, we’ve provided an adequate implementation as a CDI bean and decorated it with the Named annotation:
@Named("authorization_code")
At runtime, and according to the received grant_type value, the corresponding implementation is activated through the CDI Instance mechanism:
String grantType = params.getFirst("grant_type"); //... AuthorizationGrantTypeHandler authorizationGrantTypeHandler = authorizationGrantTypeHandlers.select(NamedLiteral.of(grantType)).get();
It’s now time to produce /token‘s response.
3.5. RSA Private And Public Keys
Before generating the token, we need an RSA private key for signing tokens.
For this purpose, we’ll be using OpenSSL:
# PRIVATE KEY openssl genpkey -algorithm RSA -out private-key.pem -pkeyopt rsa_keygen_bits:2048
The private-key.pem is provided to the server through the MicroProfile Config signingKey property using the file META-INF/microprofile-config.properties:
signingkey=/META-INF/private-key.pem
The server can read the property using the injected Config object:
String signingkey = config.getValue("signingkey", String.class);
Similarly, we can generate the corresponding public key:
# PUBLIC KEY openssl rsa -pubout -in private-key.pem -out public-key.pem
And use the MicroProfile Config verificationKey to read it:
verificationkey=/META-INF/public-key.pem
The server should make it available for the resource server for the purpose of verification. This is done through a JWK endpoint.
Nimbus JOSE+JWT is a library that can be a big help here. Let’s first add the nimbus-jose-jwt dependency:
<dependency> <groupId>com.nimbusds</groupId> <artifactId>nimbus-jose-jwt</artifactId> <version>7.7</version> </dependency>
And now, we can leverage Nimbus’s JWK support to simplify our endpoint:
@Path("jwk") @ApplicationScoped public class JWKEndpoint { @GET public Response getKey(@QueryParam("format") String format) throws Exception { //... String verificationkey = config.getValue("verificationkey", String.class); String pemEncodedRSAPublicKey = PEMKeyUtils.readKeyAsString(verificationkey); if (format == null || format.equals("jwk")) { JWK jwk = JWK.parseFromPEMEncodedObjects(pemEncodedRSAPublicKey); return Response.ok(jwk.toJSONString()).type(MediaType.APPLICATION_JSON).build(); } else if (format.equals("pem")) { return Response.ok(pemEncodedRSAPublicKey).build(); } //... } }
We’ve used the format parameter to switch between the PEM and JWK formats. The MicroProfile JWT which we’ll use for implementing the resource server supports both these formats.
3.6. Token Endpoint Response
It’s now time for a given AuthorizationGrantTypeHandler to create the token response. In this implementation, we’ll support only the structured JWT Tokens.
For creating a token in this format, we’ll again use the Nimbus JOSE+JWT library, but there are numerous other JWT libraries, too.
So, to create a signed JWT, we first have to construct the JWT header:
JWSHeader jwsHeader = new JWSHeader.Builder(JWSAlgorithm.RS256).type(JOSEObjectType.JWT).build();
Then, we build the payload which is a Set of standardized and custom claims:
Instant now = Instant.now(); Long expiresInMin = 30L; Date in30Min = Date.from(now.plus(expiresInMin, ChronoUnit.MINUTES)); JWTClaimsSet jwtClaims = new JWTClaimsSet.Builder() .issuer("http://localhost:9080") .subject(authorizationCode.getUserId()) .claim("upn", authorizationCode.getUserId()) .audience("http://localhost:9280") .claim("scope", authorizationCode.getApprovedScopes()) .claim("groups", Arrays.asList(authorizationCode.getApprovedScopes().split(" "))) .expirationTime(in30Min) .notBeforeTime(Date.from(now)) .issueTime(Date.from(now)) .jwtID(UUID.randomUUID().toString()) .build(); SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaims);
In addition to the standard JWT claims, we’ve added two more claims – upn and groups – as they’re needed by the MicroProfile JWT. The upn will be mapped to the Java EE Security CallerPrincipal and the groups will be mapped to Java EE Roles.
Now that we have the header and the payload, we need to sign the access token with an RSA private key. The corresponding RSA public key will be exposed through the JWK endpoint or made available by other means so that the resource server can use it to verify the access token.
As we’ve provided the private key as a PEM format, we should retrieve it and transform it into an RSAPrivateKey:
SignedJWT signedJWT = new SignedJWT(jwsHeader, jwtClaims); //... String signingkey = config.getValue("signingkey", String.class); String pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString(signingkey); RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey);
Next, we sign and serialize the JWT:
signedJWT.sign(new RSASSASigner(rsaKey.toRSAPrivateKey())); String accessToken = signedJWT.serialize();
And finally we construct a token response:
return Json.createObjectBuilder() .add("token_type", "Bearer") .add("access_token", accessToken) .add("expires_in", expiresInMin * 60) .add("scope", authorizationCode.getApprovedScopes()) .build();
which is, thanks to JSON-P, serialized to JSON format and sent to the client:
{ "access_token": "acb6803a48114d9fb4761e403c17f812", "token_type": "Bearer", "expires_in": 1800, "scope": "resource.read resource.write" }
4. OAuth 2.0 Client
In this section, we’ll be building a web-based OAuth 2.0 Client using the Servlet, MicroProfile Config, and JAX RS Client APIs.
More precisely, we’ll be implementing two main servlets: one for requesting the authorization server’s authorization endpoint and getting a code using the authorization code grant type, and another servlet for using the received code and requesting an access token from the authorization server’s token endpoint.
Additionally, we’ll be implementing two more servlets: One for getting a new access token using the refresh token grant type, and another for accessing the resource server’s APIs.
4.1. OAuth 2.0 Client Details
As the client is already registered within the authorization server, we first need to provide the client registration information:
- client_id: Client Identifier and it’s usually issued by the authorization server during the registration process.
- client_secret: Client Secret.
- redirect_uri: Location where to receive the authorization code.
- scope: Client requested permissions.
Additionally, the client should know the authorization server’s authorization and token endpoints:
- authorization_uri: Location of the authorization server authorization endpoint that we can use to get a code.
- token_uri: Location of the authorization server token endpoint that we can use to get a token.
All this information is provided through the MicroProfile Config file, META-INF/microprofile-config.properties:
# Client registration client.clientId=webappclient client.clientSecret=webappclientsecret client.redirectUri=http://localhost:9180/callback client.scope=resource.read resource.write # Provider provider.authorizationUri=http://127.0.0.1:9080/authorize provider.tokenUri=http://127.0.0.1:9080/token
4.2. Authorization Code Request
The flow of getting an authorization code starts with the client by redirecting the browser to the authorization server’s authorization endpoint.
Typically, this happens when the user tries to access a protected resource API without authorization, or by explicitly by invoking the client /authorize path:
@WebServlet(urlPatterns = "/authorize") public class AuthorizationCodeServlet extends HttpServlet { @Inject private Config config; @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { //... } }
In the doGet() method, we start by generating and storing a security state value:
String state = UUID.randomUUID().toString(); request.getSession().setAttribute("CLIENT_LOCAL_STATE", state);
Then, we retrieve the client configuration information:
String authorizationUri = config.getValue("provider.authorizationUri", String.class); String clientId = config.getValue("client.clientId", String.class); String redirectUri = config.getValue("client.redirectUri", String.class); String scope = config.getValue("client.scope", String.class);
We’ll then append these pieces of information as query parameters to the authorization server’s authorization endpoint:
String authorizationLocation = authorizationUri + "?response_type=code" + "&client_id=" + clientId + "&redirect_uri=" + redirectUri + "&scope=" + scope + "&state=" + state;
And finally, we’ll redirect the browser to this URL:
response.sendRedirect(authorizationLocation);
After processing the request, the authorization server’s authorization endpoint will generate and append a code, in addition to the received state parameter, to the redirect_uri and will redirect back the browser http://localhost:9081/callback?code=A123&state=Y.
4.3. Access Token Request
The client callback servlet, /callback, begins by validating the received state:
String localState = (String) request.getSession().getAttribute("CLIENT_LOCAL_STATE"); if (!localState.equals(request.getParameter("state"))) { request.setAttribute("error", "The state attribute doesn't match!"); dispatch("/", request, response); return; }
Next, we’ll use the code we previously received to request an access token through the authorization server’s token endpoint:
String code = request.getParameter("code"); Client client = ClientBuilder.newClient(); WebTarget target = client.target(config.getValue("provider.tokenUri", String.class)); Form form = new Form(); form.param("grant_type", "authorization_code"); form.param("code", code); form.param("redirect_uri", config.getValue("client.redirectUri", String.class)); TokenResponse tokenResponse = target.request(MediaType.APPLICATION_JSON_TYPE) .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeaderValue()) .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE), TokenResponse.class);
As we can see, there’s no browser interaction for this call, and the request is made directly using the JAX-RS client API as an HTTP POST.
As the token endpoint requires the client authentication, we have included the client credentials client_id and client_secret in the Authorization header.
The client can use this access token to invoke the resource server APIs which is the subject of the next subsection.
4.4. Protected Resource Access
At this point, we have a valid access token and we can call the resource server’s /read and /write APIs.
To do that, we have to provide the Authorization header. Using the JAX-RS Client API, this is simply done through the Invocation.Builder header() method:
resourceWebTarget = webTarget.path("resource/read"); Invocation.Builder invocationBuilder = resourceWebTarget.request(); response = invocationBuilder .header("authorization", tokenResponse.getString("access_token")) .get(String.class);
5. OAuth 2.0 Resource Server
In this section, we’ll be building a secured web application based on JAX-RS, MicroProfile JWT, and MicroProfile Config. The MicroProfile JWT takes care of validating the received JWT and mapping the JWT scopes to Java EE roles.
5.1. Maven Dependencies
In addition to the Java EE Web API dependency, we need also the MicroProfile Config and MicroProfile JWT APIs:
<dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>8.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.eclipse.microprofile.config</groupId> <artifactId>microprofile-config-api</artifactId> <version>1.3</version> </dependency> <dependency> <groupId>org.eclipse.microprofile.jwt</groupId> <artifactId>microprofile-jwt-auth-api</artifactId> <version>1.1</version> </dependency>
5.2. JWT Authentication Mechanism
The MicroProfile JWT provides an implementation of the Bearer Token Authentication mechanism. This takes care of processing the JWT present in the Authorization header, makes available a Java EE Security Principal as a JsonWebToken which holds the JWT claims, and maps the scopes to Java EE roles. Take a look at the JAVA EE Security API for more background.
To enable the JWT authentication mechanism in the server, we need to add the LoginConfig annotation in the JAX-RS application:
@ApplicationPath("/api") @DeclareRoles({"resource.read", "resource.write"}) @LoginConfig(authMethod = "MP-JWT") public class OAuth2ResourceServerApplication extends Application { }
Additionally, MicroProfile JWT needs the RSA public key in order to verify the JWT signature. We can provide this either by introspection or, for simplicity, by manually copying the key from the authorization server. In either case, we need to provide the location of the public key:
mp.jwt.verify.publickey.location=/META-INF/public-key.pem
Finally, the MicroProfile JWT needs to verify the iss claim of the incoming JWT, which should be present and match the value of the MicroProfile Config property:
mp.jwt.verify.issuer=http://127.0.0.1:9080
Typically, this is the location of the Authorization Server.
5.3. The Secured Endpoints
For demonstration purposes, we’ll add a resource API with two endpoints. One is a read endpoint that’s accessible by users having the resource.read scope and another write endpoint for users with resource.write scope.
The restriction on the scopes is done through the @RolesAllowed annotation:
@Path("/resource") @RequestScoped public class ProtectedResource { @Inject private JsonWebToken principal; @GET @RolesAllowed("resource.read") @Path("/read") public String read() { return "Protected Resource accessed by : " + principal.getName(); } @POST @RolesAllowed("resource.write") @Path("/write") public String write() { return "Protected Resource accessed by : " + principal.getName(); } }
6. Running All Servers
To run one server, we just need to invoke the Maven command in the corresponding directory:
mvn package liberty:run-server
The authorization server, the client and the resource server will be running and available respectively at the following locations:
# Authorization Server http://localhost:9080/ # Client http://localhost:9180/ # Resource Server http://localhost:9280/
So, we can access the client home page and then we click on “Get Access Token” to start the authorization flow. After receiving the access token, we can access the resource server’s read and write APIs.
Depending on the granted scopes, the resource server will respond either by a successful message or we’ll get an HTTP 403 forbidden status.
7. Conclusion
In this article, we’ve provided an implementation of an OAuth 2.0 Authorization Server that can be used with any compatible OAuth 2.0 Client and Resource Server.
To explain the overall framework, we have also provided an implementation for the client and the resource server. To implement all these components, we’ve used using Java EE 8 APIs, especially, CDI, Servlet, JAX RS, JAVA EE Security. Additionally, we have used the pseudo-Java EE APIs of the MicroProfile: MicroProfile Config and MicroProfile JWT.
The full source code for the examples is available over on GitHub. Note that the code includes an example of both the authorization code and refresh token grant types.