Last active
March 10, 2018 15:05
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} | |
} | |
} | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This Gist uses the OAuth 1.0 library maven dependency