Implementing LinkedIn OAuth 2.0 in Java

Overview

LinkedIn OAuth 2.0 allows your Java application to authenticate users and access their LinkedIn data securely. This guide covers the complete implementation of LinkedIn OAuth integration.

Prerequisites

1. LinkedIn Developer Setup

  • Create a LinkedIn Developer account
  • Register your application at LinkedIn Developer Portal
  • Obtain Client ID and Client Secret
  • Configure redirect URIs

2. Dependencies

Add dependencies to your pom.xml:

<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.15.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.2.1</version>
</dependency>
<dependency>
<groupId>com.sun.net.httpserver</groupId>
<artifactId>http</artifactId>
<version>20070405</version>
</dependency>

Core Implementation

1. Configuration Class

public class LinkedInConfig {
public static final String CLIENT_ID = "your_client_id";
public static final String CLIENT_SECRET = "your_client_secret";
public static final String REDIRECT_URI = "http://localhost:8080/callback";
// LinkedIn OAuth endpoints
public static final String AUTHORIZATION_URL = 
"https://www.linkedin.com/oauth/v2/authorization";
public static final String ACCESS_TOKEN_URL = 
"https://www.linkedin.com/oauth/v2/accessToken";
public static final String PROFILE_API_URL = 
"https://api.linkedin.com/v2/me";
public static final String EMAIL_API_URL = 
"https://api.linkedin.com/v2/emailAddress?q=members&projection=(elements*(handle~))";
// Required scopes
public static final String SCOPES = "r_liteprofile r_emailaddress";
public static String getAuthorizationUrl(String state) {
return String.format("%s?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&state=%s",
AUTHORIZATION_URL,
CLIENT_ID,
URLEncoder.encode(REDIRECT_URI, StandardCharsets.UTF_8),
SCOPES,
state);
}
}

2. OAuth Service Class

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.hc.client5.http.classic.methods.HttpGet;
import org.apache.hc.client5.http.classic.methods.HttpPost;
import org.apache.hc.client5.http.entity.UrlEncodedFormEntity;
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
import org.apache.hc.client5.http.impl.classic.CloseableHttpResponse;
import org.apache.hc.client5.http.impl.classic.HttpClients;
import org.apache.hc.core5.http.HttpHeaders;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.io.entity.EntityUtils;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import java.util.*;
public class LinkedInOAuthService {
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* Exchange authorization code for access token
*/
public static String getAccessToken(String authorizationCode) throws Exception {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpPost httpPost = new HttpPost(LinkedInConfig.ACCESS_TOKEN_URL);
// Set request parameters
List<NameValuePair> params = new ArrayList<>();
params.add(new BasicNameValuePair("grant_type", "authorization_code"));
params.add(new BasicNameValuePair("code", authorizationCode));
params.add(new BasicNameValuePair("redirect_uri", LinkedInConfig.REDIRECT_URI));
params.add(new BasicNameValuePair("client_id", LinkedInConfig.CLIENT_ID));
params.add(new BasicNameValuePair("client_secret", LinkedInConfig.CLIENT_SECRET));
httpPost.setEntity(new UrlEncodedFormEntity(params));
httpPost.setHeader(HttpHeaders.CONTENT_TYPE, "application/x-www-form-urlencoded");
try (CloseableHttpResponse response = httpClient.execute(httpPost)) {
String responseBody = EntityUtils.toString(response.getEntity());
JsonNode jsonNode = objectMapper.readTree(responseBody);
if (response.getCode() == 200) {
return jsonNode.get("access_token").asText();
} else {
throw new RuntimeException("Failed to get access token: " + 
jsonNode.get("error_description").asText());
}
}
}
}
/**
* Get LinkedIn user profile
*/
public static LinkedInProfile getProfile(String accessToken) throws Exception {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet(LinkedInConfig.PROFILE_API_URL);
httpGet.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
String responseBody = EntityUtils.toString(response.getEntity());
JsonNode jsonNode = objectMapper.readTree(responseBody);
if (response.getCode() == 200) {
return parseProfile(jsonNode);
} else {
throw new RuntimeException("Failed to get profile: " + responseBody);
}
}
}
}
/**
* Get user's email address
*/
public static String getEmailAddress(String accessToken) throws Exception {
try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
HttpGet httpGet = new HttpGet(LinkedInConfig.EMAIL_API_URL);
httpGet.setHeader(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken);
try (CloseableHttpResponse response = httpClient.execute(httpGet)) {
String responseBody = EntityUtils.toString(response.getEntity());
JsonNode jsonNode = objectMapper.readTree(responseBody);
if (response.getCode() == 200) {
JsonNode elements = jsonNode.get("elements");
if (elements.isArray() && elements.size() > 0) {
JsonNode emailElement = elements.get(0);
return emailElement.get("handle~").get("emailAddress").asText();
}
}
throw new RuntimeException("Failed to get email address: " + responseBody);
}
}
}
private static LinkedInProfile parseProfile(JsonNode jsonNode) {
LinkedInProfile profile = new LinkedInProfile();
profile.setId(jsonNode.get("id").asText());
if (jsonNode.has("localizedFirstName")) {
profile.setFirstName(jsonNode.get("localizedFirstName").asText());
}
if (jsonNode.has("localizedLastName")) {
profile.setLastName(jsonNode.get("localizedLastName").asText());
}
// Handle profile picture
if (jsonNode.has("profilePicture") && 
jsonNode.get("profilePicture").has("displayImage~")) {
JsonNode pictureNode = jsonNode.get("profilePicture").get("displayImage~");
if (pictureNode.has("elements") && pictureNode.get("elements").isArray()) {
JsonNode elements = pictureNode.get("elements");
for (JsonNode element : elements) {
if (element.has("identifiers") && element.get("identifiers").isArray()) {
JsonNode identifiers = element.get("identifiers");
if (identifiers.size() > 0) {
profile.setProfilePicture(identifiers.get(0).get("identifier").asText());
break;
}
}
}
}
}
return profile;
}
}

3. Data Models

public class LinkedInProfile {
private String id;
private String firstName;
private String lastName;
private String email;
private String profilePicture;
// Constructors
public LinkedInProfile() {}
public LinkedInProfile(String id, String firstName, String lastName, String email) {
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
// Getters and setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getProfilePicture() { return profilePicture; }
public void setProfilePicture(String profilePicture) { this.profilePicture = profilePicture; }
public String getFullName() {
return firstName + " " + lastName;
}
@Override
public String toString() {
return String.format("LinkedInProfile{id='%s', name='%s %s', email='%s'}",
id, firstName, lastName, email);
}
}

4. Callback Handler with Embedded HTTP Server

import com.sun.net.httpserver.HttpServer;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpExchange;
import java.io.IOException;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.util.HashMap;
import java.util.Map;
public class LinkedInOAuthCallbackHandler {
private HttpServer server;
private String authorizationCode;
private String state;
private String error;
public LinkedInOAuthCallbackHandler() throws IOException {
this.server = HttpServer.create(new InetSocketAddress(8080), 0);
this.server.createContext("/callback", new CallbackHandler());
}
public void start() {
server.start();
System.out.println("Callback server started on port 8080");
}
public void stop() {
server.stop(0);
System.out.println("Callback server stopped");
}
public String getAuthorizationCode() throws InterruptedException {
// Wait for callback
synchronized (this) {
while (authorizationCode == null && error == null) {
wait(5000); // Wait 5 seconds
}
}
if (error != null) {
throw new RuntimeException("OAuth error: " + error);
}
return authorizationCode;
}
private class CallbackHandler implements HttpHandler {
@Override
public void handle(HttpExchange exchange) throws IOException {
String response;
try {
Map<String, String> params = queryToMap(exchange.getRequestURI().getQuery());
if (params.containsKey("code")) {
authorizationCode = params.get("code");
state = params.get("state");
response = "<html><body><h1>Authentication Successful!</h1><p>You can close this window.</p></body></html>";
} else if (params.containsKey("error")) {
error = params.get("error") + ": " + params.get("error_description");
response = "<html><body><h1>Authentication Failed</h1><p>" + error + "</p></body></html>";
} else {
response = "<html><body><h1>Invalid Request</h1></body></html>";
}
exchange.sendResponseHeaders(200, response.length());
try (OutputStream os = exchange.getResponseBody()) {
os.write(response.getBytes());
}
// Notify waiting thread
synchronized (LinkedInOAuthCallbackHandler.this) {
LinkedInOAuthCallbackHandler.this.notifyAll();
}
} catch (Exception e) {
exchange.sendResponseHeaders(500, 0);
exchange.close();
}
}
private Map<String, String> queryToMap(String query) {
Map<String, String> result = new HashMap<>();
if (query == null) return result;
for (String param : query.split("&")) {
String[] pair = param.split("=");
if (pair.length > 1) {
result.put(pair[0], pair[1]);
} else {
result.put(pair[0], "");
}
}
return result;
}
}
}

5. Main Application Class

import java.util.UUID;
public class LinkedInOAuthExample {
public static void main(String[] args) {
try {
// Generate state parameter for security
String state = UUID.randomUUID().toString();
// Start callback server
LinkedInOAuthCallbackHandler callbackHandler = new LinkedInOAuthCallbackHandler();
callbackHandler.start();
// Display authorization URL to user
String authUrl = LinkedInConfig.getAuthorizationUrl(state);
System.out.println("Please open the following URL in your browser:");
System.out.println(authUrl);
System.out.println("\nWaiting for authorization...");
// Wait for authorization code
String authorizationCode = callbackHandler.getAuthorizationCode();
System.out.println("Authorization code received: " + authorizationCode);
// Exchange code for access token
String accessToken = LinkedInOAuthService.getAccessToken(authorizationCode);
System.out.println("Access token: " + accessToken);
// Get user profile
LinkedInProfile profile = LinkedInOAuthService.getProfile(accessToken);
System.out.println("Profile: " + profile.getFullName());
// Get email address
String email = LinkedInOAuthService.getEmailAddress(accessToken);
profile.setEmail(email);
System.out.println("Email: " + email);
// Display complete profile information
System.out.println("\n=== LinkedIn Profile Information ===");
System.out.println("ID: " + profile.getId());
System.out.println("Name: " + profile.getFullName());
System.out.println("Email: " + profile.getEmail());
if (profile.getProfilePicture() != null) {
System.out.println("Profile Picture: " + profile.getProfilePicture());
}
// Stop callback server
callbackHandler.stop();
} catch (Exception e) {
System.err.println("Error during OAuth flow: " + e.getMessage());
e.printStackTrace();
}
}
}

Spring Boot Implementation

1. Spring Boot Controller

@RestController
@RequestMapping("/auth/linkedin")
public class LinkedInOAuthController {
@Value("${linkedin.client.id}")
private String clientId;
@Value("${linkedin.client.secret}")
private String clientSecret;
@GetMapping("/login")
public ResponseEntity<String> login(HttpSession session) {
String state = UUID.randomUUID().toString();
session.setAttribute("oauth_state", state);
String authUrl = String.format(
"https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=%s&redirect_uri=%s&scope=%s&state=%s",
clientId,
URLEncoder.encode("http://localhost:8080/auth/linkedin/callback", StandardCharsets.UTF_8),
"r_liteprofile r_emailaddress",
state
);
return ResponseEntity.ok(authUrl);
}
@GetMapping("/callback")
public ResponseEntity<LinkedInProfile> callback(
@RequestParam String code,
@RequestParam String state,
HttpSession session) {
// Verify state parameter
String sessionState = (String) session.getAttribute("oauth_state");
if (!state.equals(sessionState)) {
return ResponseEntity.status(401).build();
}
try {
String accessToken = LinkedInOAuthService.getAccessToken(code);
LinkedInProfile profile = LinkedInOAuthService.getProfile(accessToken);
String email = LinkedInOAuthService.getEmailAddress(accessToken);
profile.setEmail(email);
return ResponseEntity.ok(profile);
} catch (Exception e) {
return ResponseEntity.status(500).build();
}
}
}

Security Considerations

  1. State Parameter: Always use and validate the state parameter to prevent CSRF attacks
  2. HTTPS: Always use HTTPS in production
  3. Token Storage: Store access tokens securely (encrypted)
  4. Token Expiry: Handle token expiration and refresh tokens
  5. Error Handling: Implement proper error handling for OAuth failures

Best Practices

  1. Request Minimal Scopes: Only request the scopes you actually need
  2. Handle Consent: Properly handle cases where users deny consent
  3. User Session Management: Create proper user sessions after successful authentication
  4. Logging: Log authentication events for security monitoring
  5. Rate Limiting: Implement rate limiting on authentication endpoints

This implementation provides a complete LinkedIn OAuth 2.0 integration in Java, handling the entire authentication flow from user authorization to profile data retrieval.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper