Skip to content

Instantly share code, notes, and snippets.

@aaronanderson
Last active March 10, 2018 15:05
Show Gist options
  • Save aaronanderson/84fc6f15e5693eda1695 to your computer and use it in GitHub Desktop.
Save aaronanderson/84fc6f15e5693eda1695 to your computer and use it in GitHub Desktop.
JAX-RS 2.0 ContainerRequestFilter that performs the LinkedIn Exchange of JSAPI Tokens for REST API OAuth Tokens
import java.io.IOException;
import java.io.StringReader;
import java.net.URLDecoder;
import java.security.Principal;
import java.util.Base64;
import java.util.Collections;
import java.util.Map;
import java.util.Map.Entry;
import javax.annotation.Priority;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.json.Json;
import javax.json.JsonArray;
import javax.json.JsonObject;
import javax.json.JsonReader;
import javax.json.JsonString;
import javax.json.JsonValue;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import javax.ws.rs.Priorities;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.container.ContainerRequestContext;
import javax.ws.rs.container.ContainerRequestFilter;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.Form;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
import javax.ws.rs.ext.Provider;
import net.oauth.OAuth;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthMessage;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* https://developer.linkedin.com/documents/exchange-jsapi-tokens-rest-api-oauth
* -tokens
*/
@Provider
@Priority(Priorities.AUTHENTICATION)
public class ContainerSecurityFilter implements ContainerRequestFilter {
public static final Logger log = LoggerFactory.getLogger(ContainerSecurityFilter.class);
public static final String CONSUMER_SECRET = "XXXXXXXXXX";
public static final String CONSUMER_KEY = "XXXXXXXXXX"; //API Key
public static final String linkedIn_URL = "https://api.linkedin.com/uas/oauth/accessToken";
@Context
HttpServletRequest webRequest;
// http://howtodoinjava.com/2013/07/25/jax-rs-2-0-resteasy-3-0-2-final-security-tutorial/
@Override
public void filter(ContainerRequestContext crc) throws IOException {
OAuthConsumer consumer = new OAuthConsumer(null, CONSUMER_KEY, CONSUMER_SECRET, null);
OAuthAccessor accessor = new OAuthAccessor(consumer);
Cookie linkedInCookie = crc.getCookies().get("linkedin_oauth_" + CONSUMER_KEY);
if (linkedInCookie == null) {
crc.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("Linked In Cookie Not Found").build());
return;
}
if (linkedInCookie.getValue() == null) {
crc.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("Linked In Cookie Value Missing, possibly insecure request").build());
return;
}
String linkedInCookieValue = URLDecoder.decode(linkedInCookie.getValue(), "UTF-8");
JsonReader jsonReader = Json.createReader(new StringReader(linkedInCookieValue));
JsonObject jsonObject = jsonReader.readObject();
String signature_method = jsonObject.getString("signature_method");
JsonArray signature_order = jsonObject.getJsonArray("signature_order");
String access_token = jsonObject.getString("access_token");
String signature = jsonObject.getString("signature");
String member_id = jsonObject.getString("member_id");
String signature_version = jsonObject.getString("signature_version");
if ("1".equals(signature_version)) {
if (signature_order != null) {
StringBuilder base_string = new StringBuilder();
// build base string from values ordered by signature_order
for (JsonValue key : signature_order) {
String keyString = key.toString().replace("\"", "");
JsonString value = jsonObject.getJsonString(keyString);
if (value == null) {
crc.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("Linked In Cookie Signature missing signature parameter: " + key.toString()).build());
return;
}
base_string.append(value.getString());
}
// hex encode an HMAC-SHA1 string
if ("HMAC-SHA1".equals(signature_method)) {
// The OAuth library HMAC-SHA1 implementation is embedded in
// the library publicly inaccessible and can only calculate
// signatures based on an OAuth message and URL and is not
// general purpose
String calcSignature = calculateRFC2104HMAC(base_string.toString(), CONSUMER_SECRET);
// check if our signature matches the cookie's
if (calcSignature == null || !calcSignature.equals(signature)) {
crc.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("LinkedIn Cookie Signature validation failed").build());
return;
}
} else {
crc.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("LinkedIn Cookie Signature method not supported: " + signature_method).build());
return;
}
} else {
crc.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("LinkedIn Cookie Signature order missing").build());
return;
}
} else {
crc.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("LinkedIn Cookie unknown version").build());
return;
}
final HttpSession session = webRequest.getSession();// retrieve from DB
LinkedINPrincipal principal = (LinkedINPrincipal) session.getAttribute("LINKEDIN_PRINCIPAL");
if (principal == null || !access_token.equals(principal.getAccessToken())) {
// https://github.com/resteasy/Resteasy/blob/master/jaxrs/examples/oauth1-examples/oauth-catalina-authenticator/oauth/src/main/java/org/jboss/resteasy/examples/oauth/ConsumerResource.java
try {
OAuthMessage message = new OAuthMessage("POST", linkedIn_URL, Collections.<Map.Entry> emptyList());
message.addParameter(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.HMAC_SHA1);
message.addParameter("xoauth_oauth2_access_token", access_token);
message.addRequiredParameters(accessor);
Client lnClient = ClientBuilder.newClient();
WebTarget lnAuth = lnClient.target(message.URL);
Form form = new Form();
for (Entry<String, String> entry : message.getParameters()) {
form.param(entry.getKey(), entry.getValue());
}
Response response = lnAuth.request().post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE));
String oauthResponse = response.readEntity(String.class);
response.close();
Map<String, String> tokens = OAuth.newMap(OAuth.decodeForm(oauthResponse));
String oauth_token = tokens.get("oauth_token");
String oauth_token_secret = tokens.get("oauth_token_secret");
String oauth_expires_in = tokens.get("oauth_expires_in");
String oauth_authorization_expires_in = tokens.get("oauth_authorization_expires_in");
principal = new LinkedINPrincipal(oauth_token, oauth_token_secret, access_token);
session.setAttribute("LINKEDIN_PRINCIPAL", principal);
} catch (Exception e) {
log.error("",e);
crc.abortWith(Response.status(Response.Status.UNAUTHORIZED).entity("Linked In OAuth error").build());
return;
}
}
final LinkedINPrincipal userPrincipal = principal;
crc.setSecurityContext(new SecurityContext() {
@Override
public boolean isUserInRole(String role) {
return false;
}
@Override
public boolean isSecure() {
return false;
}
@Override
public Principal getUserPrincipal() {
return userPrincipal;
}
@Override
public String getAuthenticationScheme() {
return "LinkedIn API";
}
});
}
// http://docs.aws.amazon.com/AWSSimpleQueueService/latest/SQSDeveloperGuide/AuthJavaSampleHMACSignature.html
public static String calculateRFC2104HMAC(String data, String key) {
final String HMAC_SHA1_ALGORITHM = "HmacSHA1";
String result;
try {
// get an hmac_sha1 key from the raw key bytes
SecretKeySpec signingKey = new SecretKeySpec(key.getBytes(), HMAC_SHA1_ALGORITHM);
// get an hmac_sha1 Mac instance and initialize with the signing key
Mac mac = Mac.getInstance(HMAC_SHA1_ALGORITHM);
mac.init(signingKey);
// compute the hmac on input data bytes
byte[] rawHmac = mac.doFinal(data.getBytes());
// base64-encode the hmac
result = Base64.getEncoder().encodeToString(rawHmac);
return result;
} catch (Exception e) {
log.error("Failed to generate HMAC : " + e.getMessage());
return null;
}
}
public static class LinkedINPrincipal implements Principal {
final String name;
final String oauthToken;
final String oauthSecret;
final String accessToken;
public LinkedINPrincipal(String name, String oauthToken, String oauthSecret, String accessToken) {
this.name = name;
this.oauthToken = oauthToken;
this.oauthSecret = oauthSecret;
this.accessToken = accessToken;
}
public LinkedINPrincipal(String oauthToken, String oauthSecret, String accessToken) {
this("", oauthToken, oauthSecret, accessToken);
}
@Override
public String getName() {
return name;
}
public String getOauthToken() {
return oauthToken;
}
public String getOauthSecret() {
return oauthSecret;
}
public String getAccessToken() {
return accessToken;
}
}
}
import java.io.IOException;
import java.net.URISyntaxException;
import java.security.Principal;
import java.util.Collections;
import java.util.Map;
import javax.inject.Provider;
import javax.ws.rs.client.ClientRequestContext;
import javax.ws.rs.client.ClientRequestFilter;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.SecurityContext;
import net.oauth.OAuth;
import net.oauth.OAuthAccessor;
import net.oauth.OAuthConsumer;
import net.oauth.OAuthException;
import net.oauth.OAuthMessage;
import ContainerSecurityFilter.LinkedINPrincipal;
public class OAuthAuthenticator implements ClientRequestFilter {
private final SecurityContext sctx;
public OAuthAuthenticator(SecurityContext sctx) {
this.sctx = sctx;
}
public void filter(ClientRequestContext requestContext) throws IOException {
try {
Principal principal = sctx.getUserPrincipal();
if (principal == null || !(principal instanceof LinkedINPrincipal)) {
throw new IOException("LinkedINPrincipal not available");
}
LinkedINPrincipal lnPrincipal = (LinkedINPrincipal) principal;
OAuthConsumer consumer = new OAuthConsumer(null, ContainerSecurityFilter.CONSUMER_KEY, ContainerSecurityFilter.CONSUMER_SECRET, null);
OAuthAccessor accessor = new OAuthAccessor(consumer);
accessor.accessToken=lnPrincipal.getOauthToken();
accessor.tokenSecret=lnPrincipal.getOauthSecret();
OAuthMessage message = new OAuthMessage("GET", requestContext.getUri().toString(), Collections.<Map.Entry> emptyList());
message.addParameter(OAuth.OAUTH_SIGNATURE_METHOD, OAuth.HMAC_SHA1);
message.addRequiredParameters(accessor);
final String oauthAuthentication = message.getAuthorizationHeader(null);
MultivaluedMap<String, Object> headers = requestContext.getHeaders();
headers.add("Authorization", oauthAuthentication);
} catch (OAuthException | URISyntaxException e) {
throw new IOException(e);
}
}
}
import java.io.IOException;
import java.io.InputStream;
import java.io.StringWriter;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;
import javax.ws.rs.Consumes;
import javax.ws.rs.ProcessingException;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.core.Response;
import javax.ws.rs.ext.MessageBodyReader;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* The REST request could be configured to request the response in a JSON Format
* instead of the default XML format
*/
// @Provider
@Consumes("text/xml")
public class PersonMessageBodyReader implements MessageBodyReader<Person> {
@Override
public boolean isReadable(Class<?> type, Type genericType, Annotation[] annotations, MediaType mediaType) {
return type == Person.class;
}
@Override
public Person readFrom(Class<Person> type, Type genericType, Annotation[] annotations, MediaType mediaType, MultivaluedMap<String, String> httpHeaders, InputStream entityStream)
throws IOException, WebApplicationException {
try {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(entityStream);
if ("error".equals(doc.getDocumentElement().getLocalName())) {
throw new WebApplicationException(domToString(doc), Response.Status.INTERNAL_SERVER_ERROR);
}
XPathFactory factory = XPathFactory.newInstance();
XPath xpath = factory.newXPath();
// person is the root element
return parsePerson(doc.getDocumentElement(), xpath);
} catch (ParserConfigurationException | SAXException | TransformerException | XPathExpressionException exception) {
throw new ProcessingException("Error deserializing a Person.", exception);
}
}
public static Person parsePerson(Element personElement, XPath xpath) throws XPathExpressionException {
Person person = new Person();
person.setKey((String) xpath.evaluate("id", personElement, XPathConstants.STRING));
person.setFirstName((String) xpath.evaluate("first-name", personElement, XPathConstants.STRING));
person.setLastName((String) xpath.evaluate("last-name", personElement, XPathConstants.STRING));
person.setHeadline((String) xpath.evaluate("headline", personElement, XPathConstants.STRING));
String profileURL = (String) xpath.evaluate("public-profile-url", personElement, XPathConstants.STRING);
if (profileURL.length() > 0) {
person.setProfileURL(profileURL);
}
NodeList companies = (NodeList) xpath.evaluate("positions/position[is-current='true']/company", personElement, XPathConstants.NODESET);
for (int i = 0; i < companies.getLength(); i++) {
Company company = new Company();
company.setKey((String) xpath.evaluate("id", companies.item(i), XPathConstants.STRING));
company.setUniversalName((String) xpath.evaluate("name", companies.item(i), XPathConstants.STRING));
person.getCompanies().add(company);
}
return person;
}
public static String domToString(Document doc) throws TransformerException {
DOMSource domSource = new DOMSource(doc);
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
transformer.transform(domSource, result);
return result.toString();
}
}
import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.WebTarget;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.SecurityContext;
@ApplicationScoped
@Path("/RestService")
@Produces({ "application/json", "application/xml" })
@Consumes({ "application/json", "application/xml" })
public class RestService {
@Context
SecurityContext sctx;
private Client lnClient;
private WebTarget lnAuth;
@PostConstruct
public void init() {
//Manually register the marshaller with the client because the serialization format from LinkedIn will be different than the RestService marshalling serialization
lnClient = ClientBuilder.newClient().register(new OAuthAuthenticator(sctx)).register(PersonMessageBodyReader.class);
lnAuth = lnClient.target("https://api.linkedin.com/v1/people/~:(id,first-name,last-name,headline,public-profile-url,positions:(is-current,company))");
}
@GET
@Path("/profile")
public Person getProfile() {
Response response = lnAuth.request().get();
return response.readEntity(Person.class);
}
}
@aaronanderson
Copy link
Author

This Gist uses the OAuth 1.0 library maven dependency

<dependency>
    <groupId>net.oauth.core</groupId>
    <artifactId>oauth</artifactId>
    <version>20100527</version>
</dependency>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment