Google OAuth2 and JAX-RS
These days users have so many accounts… Google, Facebook, Twitter, LinkedIn, etc. Managing all of these accounts can quickly become a chore. As an application developer, you have the choice of allowing your users to reuse an existing account e.g. Google or creating a new account dedicated to your application. Reusing an existing account clearly has benefits. The user has one less account to maintain. He has one less username/password to remember. He has one less account to potentially have hacked! In this post we will look at how we can enable a user to access our application using an existing account e.g. Google. To do so, I will explain how to authenticate a user using OAuth2. We will also look at how to do this using a JAX-RS REST service.
Agenda:
- What is OAuth2?
- JAX-RS Example: How to authenticate a user’s Google account using OAuth2
- Register you application with Google Developers Console
In this tutorial, I use the following ‘Tools and Frameworks’
- Glassfish 4.0 – Java application server
- Apache OLTU – Apache OAuth implementation
- JsonToken – Sign and Verify datastructures
Prerequisites
Register your application with Google Developers Console
For Registration steps, see the last section of this post
NOTE: Although this post uses Google as the OAuth2 provider, all concepts apply similarly to other OAuth2 providers including Facebook, Twitter, LinkedIn, etc.
What is OAuth2?
OAuth.net defines it as “An open protocol to allow secure authorization in a simple and standard method from web, mobile, and desktop application.”
The OAuth2 specification states that “The OAuth 2.0 authorization framework enables a third-party application to obtain limited access to an HTTP service, either on behalf of a resource owner by orchestrating an approval interaction between the resource owner and the HTTP service, or by allowing the third-party application to obtain access on its own behalf. This specification replaces and obsoletes the OAuth 1.0 protocol described in RFC 5849.”
In this example, we will not use the Google OAuth2 provider to access a ‘Google’ REST API, instead we will use it secure our REST service. Essentially, we can ensure that only valid Google accounts (registered through our application) can access our JAX-RS based REST API. Also, this notion can easily be extended to a web service e.g. JAX-WS, a web application, etc.
JAX-RS Example: How to authenticate a user’s Google account using OAuth2
To authenticate a Google user, we must obtain an ID token
from Google and validate it.
Here’s how:
1. Create an anti-forgery state token.
Google states that “One good choice for a state token is a string of 30 or so characters constructed using a high-quality random-number generator. Another is a hash generated by signing some of your session state variables with a key that is kept secret on your back-end.”
I will skip this step, however skipping this step is not recommended for production. An anti-forgery token is used to ensure the request was really made by you and not a malicious user or script.
2. Send an authentication request to Google
Here, we create a https
request to Google for authentication. As part of this request, we supply a callback url. The callback url is owned by our application. Google responds to our authentication request by invoking our callback url with various information provided in the query string. In this example, we set the response type to code
. This will cause the OAuth2 provider to return a code
which we will use in step 4.
[code language=”Java”]
@Path("google")
public class GoogleResource {
@Context
private UriInfo uriInfo;
@GET
@Produces("text/html")
public Response authenticate() {
try {
OAuthClientRequest request = OAuthClientRequest
.authorizationProvider(OAuthProviderType.GOOGLE)
.setClientId("YOUR_CLIENT_ID_HERE")
.setResponseType("code")
.setScope("openid email https://www.googleapis.com/auth/plus.login")
.setRedirectURI(
UriBuilder.fromUri(uriInfo.getBaseUri())
.path("oauth2callback").build().toString())
.buildQueryMessage();
URI redirect = new URI(request.getLocationUri());
return Response.seeOther(redirect).build();
} catch (OAuthSystemException e) {
throw new WebApplicationException(e);
} catch (URISyntaxException e) {
throw new WebApplicationException(e);
}
}
}
[/code]
3. Confirm the anti-forgery state token.
Here, we validate the anti-forgery token. Since, I skipped the creation of an anti-forgery token in step 2., I will also skip the verification of that anti-forgery token here in step 3. Again, although it is not required to create or validate the anti-forgery token, in practice we must absolutely do so to maintain a high level of security.
To validate the anti-forgery token, we will get the state
parameter present in the query string of our callback url. The OAuth2 provider, Google, will invoke our callback url (plus a query string) in response to our request in step 2. When invoked, we must validate that the value of state
matches our anti-forgery token.
e.g.
[code language=”Java”]
String receivedAntiForgeryStateToken = request.queryParams("state")
// Verify the received token matches your actual token
[/code]
See Step 4. for an example JAX-RS implementation of our callback url. This example, shows how we receive the state
parameter.
4. Exchange the code
for an Access token and ID token
To exchange the code
for an Access token and ID token, we again use the information supplied by our OAuth2 provider, Google, when it invoked our callback url. As we know, the OAuth2 provider invokes this url in response to our request in step 2. As part of this invocation, we are supplied code
as a query parameter. We will then use this code
in exchange for an Access token and ID token. To perform the exchange, we must make another https
request to the OAuth2 provider, Google.
Please, see the code below.
The following REST service is the implementation of our OAuth2 callback. This service represents the callback url invoked by the OAuth2 provider (Google) in response to our request in Step 2. Notice that the Java code below receives both code
and state
values as query parameters from Google. Since we did not supply an anti-forgery key in our request in step 2, the value of state
is be null. This Java code also makes the request to exchange the provided code
for an Access token and ID token.
[code language=”Java”]
@Path("oauth2callback")
public class GoogleAuthorizationResource {
@Context
private UriInfo uriInfo;
@GET
public Response authorize(@QueryParam("code") String code, @QueryParam("state") String state) {
// path to redirect after authorization
final URI uri = uriInfo.getBaseUriBuilder().path("/index.jsp").build();
try {
// Request to exchange code for access token and id token
OAuthClientRequest request = OAuthClientRequest
.tokenProvider(OAuthProviderType.GOOGLE)
.setCode(code)
.setClientId("YOUR_CLIENT_ID_HERE")
.setClientSecret("YOUR_CLIENT_SECRET_HERE")
.setRedirectURI(UriBuilder.fromUri(uriInfo.getBaseUri())
.path("oauth2callback").build().toString())
.setGrantType(GrantType.AUTHORIZATION_CODE)
.buildBodyMessage();
OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
OAuthJSONAccessTokenResponse oAuthResponse = oAuthClient.accessToken(request);
// Get the access token from the response
OAuthToken accessToken = oAuthResponse.getOAuthToken();
// Get the id token from the response
String jwtToken = oAuthResponse.getParam("id_token");
// Insert code from Step 5. here
// Add code to notify application of authenticated user
} catch (OAuthSystemException e) {
throw new WebApplicationException(e);
} catch (OAuthProblemException e) {
throw new WebApplicationException(e);
}
return Response.seeOther(uri).build();
}
}
[/code]
5. Obtain user information from the ID token
Extract the id_token
from the response. The id_token
contains a Json Web Token (JWT). The JWT (pronounced ‘jot’) provides basic information about the user.
What is JWT?
JSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JavaScript Object Notation (JSON) object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or MACed and/or encrypted.
The JWT includes several parameters including:
email
– the users email address.sub
– a unique identifier representing the user.
[code language=”Java”]
// requires import com.google.gson.JsonPrimitive;
OAuthToken accessToken = oAuthResponse.getOAuthToken();
String idToken= oAuthResponse.getParam("id_token");
JsonToken jsonToken = AuthUtil.deserializeJwt(idToken);
JsonPrimitive id = jsonToken.getParamAsPrimitive("sub");
JsonPrimitive id = jsonToken.getParamAsPrimitive("email");
[/code]
6. Authenticate the user
Now that the OAuth (Google) user is authenticated, all that is left to do is to authenticate the Google user within our application.
How do we do this? Well, if the user exists in our application’s database, then we should allow the user to access our services. If the user is not in our application’s database, then we should either 1. start our application’s registration flow or 2. automatically register the user with our application using the information e.g. email
provided by the OAuth provider (Google).
In a JAX-RS application, we might implement a ContainerRequestFilter
to determine whether a user is logged in. This filter could also provide a @SecurityContext
that can be used to protect our RESTful resources.
e.g.
Here is a basic template for what our ContainerRequestFilter
might look like. Here we call it, AuthenticationFilter
[code language=”Java”]
@Provider
public class AuthenticationFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// … some logic
// Get Acesss token from the Authorization header
String accessToken = requestContext.getHeaderString("Authorization");
accessToken = authHeader.replaceFirst("Bearer ", "");
if (MyAuthService.isUserAuthorized(accessToken)l) {
// Authorized
// Create a Security Principal
requestContext.setSecurityContext(new MySecurityContext(accessToken));
} else {
// Unauthorized
requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED)
.entity("Requires authorization.").build());
}
}
[/code]
And finally, we protect our resource by injecting a security context.
[code language=”Java”]
@POST
@Consumes({ MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON })
public Response create(@Context SecurityContext sc, MyResource res) {
// Creation logic
return Response.created(myResourceUri).build();
}
[/code]
- Register you application with Google Developers Console
- The content below was taken from Google documentation:
To create a client ID and client secret, create a Google APIs Console project, enable the Google+ API, and register your origins:
To register a new application, do the following:
- Go to the Google Cloud Console.
- Select a project, or create a new one.
- In the sidebar on the left, select APIs & auth. In the displayed list of APIs, make sure the Google+ API status is set to ON.
- In the sidebar on the left, select Registered apps.
- At the top of the page, select Register App.
- Fill out the form and select Register.
Register the origins where your app is allowed to access the Google APIs. The origin is the unique combination of protocol, hostname, and port. You can enter multiple origins to allow for your app to run on different protocols, domains or subdomains. Wildcards are not allowed.
- Expand the OAuth 2.0 Client ID section.
- In the Web origin field, enter your origin:
http://localhost:8080
Press ENTER to save your origin. You can then click the + symbol to add additional origins.
- Note or copy the client ID and client secret that your app will need to use to access the APIs.
Thank you!
Reblogged this on Sutoprise Avenue, A SutoCom Source.
Instead of AuthUtil.deserializeJwt to get subject and email, you can use jwt artifact of Apache Oltu.
Note that both solutions are working only Google due to jwt support. As i know other providers not supporting jwt yet.
JWT jwt = new JWTReader().read(jwtToken);
ClaimsSet claimsSet = jwt.getClaimsSet();
String subject = claimsSet.getSubject();
String email = claimsSet.getCustomField(“email”, String.class);
org.apache.oltu.oauth2
org.apache.oltu.oauth2.jwt
1.0.1