├── .github ├── linters │ └── sun_checks.xml └── workflows │ └── main.yml ├── .gitignore ├── License.pdf ├── README.md ├── client ├── pom.xml └── src │ ├── main │ ├── java │ │ └── com │ │ │ └── linkedIn │ │ │ └── api │ │ │ ├── Constants.java │ │ │ ├── Example.java │ │ │ ├── LinkedInMarketingController.java │ │ │ └── MainController.java │ └── resources │ │ ├── application.properties │ │ ├── static │ │ └── style.css │ │ └── templates │ │ ├── index.html │ │ └── marketingtemplate.html │ └── test │ └── java │ └── com │ └── linkedIn │ └── api │ └── MainControllerTest.java └── server ├── pom.xml └── src ├── main ├── java │ └── com │ │ ├── example │ │ └── api │ │ │ ├── Constants.java │ │ │ ├── LinkedInMarketingController.java │ │ │ ├── LinkedInOAuthController.java │ │ │ └── MainApplication.java │ │ └── linkedin │ │ └── oauth │ │ ├── builder │ │ ├── AuthorizationUrlBuilder.java │ │ └── ScopeBuilder.java │ │ ├── pojo │ │ └── AccessToken.java │ │ ├── service │ │ └── LinkedInOAuthService.java │ │ └── util │ │ ├── Constants.java │ │ └── Preconditions.java └── resources │ ├── config.properties │ └── static │ └── index.html └── test └── java └── com └── example └── api └── MainApplicationTest.java /.github/linters/sun_checks.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 33 | 34 | 35 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # Workflow setup to test Client and Server components 2 | 3 | name: LinkedIn Java OAuth Sample App Validation 4 | 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | #workflow_dispatch: 14 | 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | steps: 21 | 22 | - uses: actions/checkout@v2 23 | 24 | - name: Set up JDK 11 25 | uses: actions/setup-java@v2 26 | with: 27 | java-version: '11' 28 | distribution: 'adopt' 29 | - name: Super-Linter 30 | uses: github/super-linter@v4.6.1 31 | env: 32 | GITHUB_TOKEN : ${{secrets.GITHUB_TOKEN}} 33 | VALIDATE_JAVA : true 34 | 35 | # Runs a spring boot server and test cases 36 | - name: Build and Run Tests for client & Server 37 | run: | 38 | cd client 39 | mvn clean install 40 | cd .. 41 | cd server 42 | mvn clean install 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.DS_Store 2 | target/ 3 | bin/ 4 | /.settings 5 | /.classpath 6 | /.project 7 | /.DS_Store 8 | *.settings/ 9 | *.class 10 | *.classpath 11 | *.project 12 | config.properties -------------------------------------------------------------------------------- /License.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/linkedin-developers/java-sample-application/59af675a92d1284f8ae4d6553dc1d87ae667a8d8/License.pdf -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sample Application for LinkedIn APIs 2 | 3 | > Please take a 1-minute survey to help us help you, with more Sample Apps for LinkedIn APIs. 4 | > Go to **www.slido.com** and use the code **SampleApp** to answer the survey 5 | 6 | ## Overview 7 | 8 | Sample Application is a ready-to-use code example that enables you to try out RESTful calls to LinkedIn's APIs. The application provides scalable and customizable code for your requirements as you begin API development with LinkedIn. 9 | 10 | The sample application contains the client and server component you can use to manage your requests to LinkedIn's APIs. The server creates and stores your access token and invokes APIs upon request from the client application. You can download or clone the OAuth sample application and try out these APIs. 11 | 12 | > **Note**: For a detailed demo, please visit LinkedIn's public documentation page 13 | 14 | The sample application uses the following development tools: 15 | 16 | * Spring Boot: Used as web server framework [] 17 | * LinkedIn OAuth 2.0: user authorization and API authentication 18 | * Maven: app building and management 19 | * Java: SE 7 or later versions are required for development 20 | 21 | ## Prerequisites 22 | 23 | * Ensure that you have an application registered in [LinkedIn Developer Portal](https://developer.linkedin.com/). 24 | Once you have your application, note down the Client ID and Client Secret 25 | * Add to the Authorized Redirect URLs under the **Authentication** section 26 | * Configure the application build by installing MAVEN using [Installing Apache Maven](https://maven.apache.org/install.html) 27 | 28 | ## Configure the application 29 | 30 | **Configure the client app:** 31 | 32 | 1. Navigate to the **application.properties** file. You can find this file under: **/client/src/main/resources/application.properties** 33 | 1. To edit server link or port with custom values modify the following values: 34 | 35 | > server.port = 36 | 37 | > SERVER_URL = 38 | 39 | 1. Save the changes. 40 | 41 | **Configure the server app:** 42 | 43 | 1. Navigate to the **config.properties** file. You can find this file under: **/server/src/main/resources/config.properties** 44 | 2. Edit the following properties in the file with your client credentials: 45 | 46 | > clientId = 47 | 48 | > clientSecret = 49 | 50 | > redirectUri = 51 | 52 | > scope = 53 | client_url = 54 | 55 | 3. Save the changes. 56 | 57 | ## Start the application 58 | 59 | To start the server: 60 | 61 | 1. Navigate to the server folder. 62 | 2. Open the terminal and run the following command to install dependencies: 63 | `mvn install` 64 | 3. Execute the following command to run the spring-boot server: 65 | `mvn spring-boot:run` 66 | 67 | > **Note:** The server will be running on 68 | 69 | To start the client: 70 | 71 | 1. Navigate to the client folder. 72 | 2. Open the terminal and run the following command to install dependencies: 73 | `mvn install` 74 | 3. Execute the following command to run the spring-boot server: 75 | `mvn spring-boot:run` 76 | 77 | > **Note**: The client will be running on 78 | 79 | ## List of dependencies 80 | 81 | |Component Name |License |Linked |Modified | 82 | |---------------|--------|--------|----------| 83 | |[boot:spring-boot-starter-parent:2.5.2]() |Apache 2.0 |Static |No | 84 | |[boot:spring-boot-starter-parent:2.5.2](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent/2.5.2) |Apache 2.0 |Static |No | 85 | |[org.springframework.boot:spring-boot-starter-thymeleaf:2.2.2.RELEASE](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-thymeleaf/2.2.2.RELEASE) |Apache 2.0 |Static |No | 86 | |[org.springframework.boot:spring-boot-devtools:2.6.0](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-devtools/2.6.0) |Apache 2.0 |Static |No | 87 | |[com.fasterxml.jackson.core:jackson-databind:2.13.0](https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind/2.13.0) |Apache 2.0 |Static |No | 88 | |[com.fasterxml.jackson.core:jackson-core:2.13.0](https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-core/2.13.0) |Apache 2.0 |Static |No | 89 | |[org.springframework.boot:spring-boot-starter-web:2.5.2](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web/2.5.2) |Apache 2.0 |Static |No | 90 | | [org.springframework.boot:spring-boot-starter-test:2.6.0](https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-test/2.6.0) |Apache 2.0 |Static |No | 91 | |[org.springframework:spring-core:5.3.13](https://mvnrepository.com/artifact/org.springframework/spring-core/5.3.13) |Apache 2.0 |Static |No | 92 | -------------------------------------------------------------------------------- /client/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 4.0.0 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 2.5.2 11 | 12 | 13 | 14 | com.example 15 | linkedIn-oauth-sample-app 16 | 0.0.1 17 | LinkedIn OAuth Sample App Client 18 | A sample application demonstrating how to authenticate with LinkedIn's OAuth APIs 19 | 20 | 1.8 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-thymeleaf 26 | 2.2.2.RELEASE 27 | 28 | 29 | org.springframework 30 | spring-core 31 | 5.3.13 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-web 36 | 2.5.2 37 | 38 | 39 | org.springframework 40 | spring-core 41 | 42 | 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-devtools 47 | 2.6.0 48 | runtime 49 | true 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-test 54 | 2.6.0 55 | test 56 | 57 | 58 | com.fasterxml.jackson.core 59 | jackson-databind 60 | 2.13.0 61 | 62 | 63 | com.fasterxml.jackson.core 64 | jackson-core 65 | 2.13.0 66 | 67 | 68 | 69 | 70 | 71 | org.springframework.boot 72 | spring-boot-maven-plugin 73 | 74 | 75 | 76 | 77 | src/main/resources 78 | true 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /client/src/main/java/com/linkedIn/api/Constants.java: -------------------------------------------------------------------------------- 1 | package com.linkedIn.api; 2 | 3 | /** 4 | * Constants for use 5 | */ 6 | public class Constants { 7 | public static final String TOKEN_INTROSPECTION_ENDPOINT = "tokenIntrospection"; 8 | public static final String THREE_LEGGED_TOKEN_GEN_ENDPOINT = "login"; 9 | public static final String TWO_LEGGED_TOKEN_GEN_ENDPOINT = "twoLeggedAuth"; 10 | public static final String USE_REFRESH_TOKEN_ENDPOINT = "refreshToken"; 11 | public static final String FIND_AD_ACCOUNTS_ENDPOINT = "findAdAccounts"; 12 | public static final String GET_USER_ORG_ACCESS_ENDPOINT = "getUserOrgAccess"; 13 | public static final String PROFILE_ENDPOINT = "profile"; 14 | public static final String OAUTH_PAGE = "index"; 15 | public static final String LMS_PAGE = "marketingtemplate"; 16 | public static final String GENERIC_ERROR_MESSAGE = "Error retrieving the data"; 17 | public static final String TWO_LEGGED_TOKEN_GEN_SUCCESS_MESSAGE = "2-Legged OAuth token successfully generated via client credentials."; 18 | public static final String FIND_AD_ACCOUNTS_MESSAGE = "Find Ad Accounts by Authenticated User:- "; 19 | public static final String FIND_USER_ROLES_MESSAGE = "Find Ad Account roles of Authenticated User:- "; 20 | public static final String TOKEN_EXISTS_MESSAGE = "Access Token is ready to use!"; 21 | public static final String ACTION_2_LEGGED_TOKEN_GEN = "Generating 2-legged auth access token..."; 22 | public static final String ACTION_GET_PROFILE = "Getting public profile..."; 23 | public static final String ACTION_USE_REFRESH_TOKEN = "Refreshing token..."; 24 | public static final String ACTION_TOKEN_INTROSPECTION = "Performing token introspection..."; 25 | public static final String CASE_TWO_LEGGED_TOKEN_GEN = "two_legged_auth=2+Legged+OAuth"; 26 | public static final String CASE_GET_PROFILE = "profile=Get+Profile"; 27 | public static final String CASE_USE_REFRESH_TOKEN = "refresh_token=Use+Refresh+Token"; 28 | public static final String CASE_TOKEN_INTROSPECTION = "token_introspection=Token+Introspection"; 29 | public static final String CASE_FIND_AD_ACCOUNTS = "Find_ad_account=Find+Ad+Accounts"; 30 | public static final String CASE_GET_USER_ORG_ROLES = "Get_user_org_access=Find+Org+Access"; 31 | public static final String DEFAULT_MESSAGE = "No API calls made!"; 32 | public static final String REFRESH_TOKEN_ERROR_MESSAGE = "Refresh token is empty! Generate 3L Access token again."; 33 | public static final String REFRESH_TOKEN_MESSAGE = "Generated new access token using refresh token."; 34 | } 35 | -------------------------------------------------------------------------------- /client/src/main/java/com/linkedIn/api/Example.java: -------------------------------------------------------------------------------- 1 | package com.linkedIn.api; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | /** 7 | * Create Spring Boot Application and set a default controller 8 | */ 9 | 10 | @SpringBootApplication 11 | public class Example { 12 | 13 | public Example() { } 14 | public static void main(final String[] args) { 15 | SpringApplication.run(Example.class, args); 16 | } 17 | 18 | } 19 | -------------------------------------------------------------------------------- /client/src/main/java/com/linkedIn/api/LinkedInMarketingController.java: -------------------------------------------------------------------------------- 1 | package com.linkedIn.api; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.ui.Model; 5 | import org.springframework.web.bind.annotation.GetMapping; 6 | import org.springframework.web.bind.annotation.PostMapping; 7 | import org.springframework.web.bind.annotation.RequestBody; 8 | import org.springframework.beans.factory.annotation.Value; 9 | import org.springframework.web.client.RestTemplate; 10 | import com.fasterxml.jackson.core.type.TypeReference; 11 | import com.fasterxml.jackson.databind.ObjectMapper; 12 | import java.util.Map; 13 | import java.util.logging.Logger; 14 | import java.util.logging.Level; 15 | import static com.linkedIn.api.Constants.TOKEN_INTROSPECTION_ENDPOINT; 16 | import static com.linkedIn.api.Constants.TOKEN_EXISTS_MESSAGE; 17 | import static com.linkedIn.api.Constants.LMS_PAGE; 18 | import static com.linkedIn.api.Constants.THREE_LEGGED_TOKEN_GEN_ENDPOINT; 19 | import static com.linkedIn.api.Constants.CASE_TOKEN_INTROSPECTION; 20 | import static com.linkedIn.api.Constants.GENERIC_ERROR_MESSAGE; 21 | import static com.linkedIn.api.Constants.CASE_FIND_AD_ACCOUNTS; 22 | import static com.linkedIn.api.Constants.FIND_AD_ACCOUNTS_ENDPOINT; 23 | import static com.linkedIn.api.Constants.FIND_AD_ACCOUNTS_MESSAGE; 24 | import static com.linkedIn.api.Constants.CASE_GET_USER_ORG_ROLES; 25 | import static com.linkedIn.api.Constants.GET_USER_ORG_ACCESS_ENDPOINT; 26 | import static com.linkedIn.api.Constants.DEFAULT_MESSAGE; 27 | import static com.linkedIn.api.Constants.FIND_USER_ROLES_MESSAGE; 28 | 29 | /** 30 | * LMS controller for handling the actions on the marketing page at 31 | * http://localhost:8989/marketing (Default) 32 | */ 33 | @Controller 34 | public final class LinkedInMarketingController { 35 | 36 | private RestTemplate lmsTemplate = new RestTemplate(); 37 | private Logger logger = Logger.getLogger(LinkedInMarketingController.class.getName()); 38 | 39 | 40 | @Value("${SERVER_URL}") 41 | private String SERVER_URL; 42 | 43 | /** 44 | * 45 | * Serves a html webpage with operations related to OAuth + LMS functions 46 | * 47 | * @param model Spring Boot Model 48 | * @return HTML page to render 49 | */ 50 | @GetMapping("/marketing") 51 | public String oauth(final Model model) { 52 | String action = "Start with LinkedIn's LMS APIs!"; 53 | String response = ""; 54 | String output = ""; 55 | try { 56 | response = lmsTemplate.getForObject(SERVER_URL + TOKEN_INTROSPECTION_ENDPOINT, String.class); 57 | 58 | logger.log(Level.INFO, "Validating if a token is already in session. Response from token introspection end point is: {0}", response); 59 | 60 | if (!response.toLowerCase().contains("error")) { 61 | action = TOKEN_EXISTS_MESSAGE; 62 | output = TOKEN_EXISTS_MESSAGE; 63 | } 64 | } catch (Exception e) { 65 | logger.log(Level.SEVERE, e.getMessage(), e); 66 | } 67 | model.addAttribute("auth_url", SERVER_URL + THREE_LEGGED_TOKEN_GEN_ENDPOINT); 68 | model.addAttribute("output", output); 69 | model.addAttribute("action", action); 70 | 71 | logger.log(Level.INFO, "Completed execution for rendering marketing page. The model values are output: {0},action: {1}.", new String[]{output, action}); 72 | 73 | return LMS_PAGE; 74 | } 75 | 76 | /** 77 | * Handles the post requests of Html page, calls the API endpoints of server URL. 78 | * 79 | * @param data string data from the UI component 80 | * @param model Spring Boot Model 81 | * @return HTML page to render 82 | */ 83 | @PostMapping(path = "/marketing") 84 | public String postBody(@RequestBody final String data, final Model model) { 85 | String response = ""; 86 | Object Find_Ad_Account = null; 87 | Object Get_user_org_access = null; 88 | 89 | logger.log(Level.INFO, "Handling on click of marketing page buttons. Button clicked is {0}", data); 90 | 91 | switch (data) { 92 | case CASE_TOKEN_INTROSPECTION: 93 | try { 94 | response = lmsTemplate.getForObject(SERVER_URL + TOKEN_INTROSPECTION_ENDPOINT, String.class); 95 | } catch (Exception e) { 96 | logger.log(Level.SEVERE, e.getMessage(), e); 97 | response = GENERIC_ERROR_MESSAGE; 98 | } 99 | break; 100 | 101 | case CASE_FIND_AD_ACCOUNTS: 102 | try { 103 | response = lmsTemplate.getForObject(SERVER_URL + FIND_AD_ACCOUNTS_ENDPOINT, String.class); 104 | if (response.toLowerCase().contains("error")) { 105 | response = parseJSON(response).toString(); 106 | } else { 107 | Find_Ad_Account = parseJSON(response); 108 | response = FIND_AD_ACCOUNTS_MESSAGE; 109 | } 110 | } catch (Exception e) { 111 | logger.log(Level.SEVERE, e.getMessage(), e); 112 | response = GENERIC_ERROR_MESSAGE; 113 | } 114 | break; 115 | 116 | case CASE_GET_USER_ORG_ROLES: 117 | try { 118 | response = lmsTemplate.getForObject(SERVER_URL + GET_USER_ORG_ACCESS_ENDPOINT, String.class); 119 | if (response.toLowerCase().contains("error")) { 120 | response = parseJSON(response).toString(); 121 | } else { 122 | Get_user_org_access = parseJSON(response); 123 | response = FIND_USER_ROLES_MESSAGE; 124 | } 125 | } catch (Exception e) { 126 | logger.log(Level.SEVERE, e.getMessage(), e); 127 | response = GENERIC_ERROR_MESSAGE; 128 | } 129 | break; 130 | default: 131 | response = DEFAULT_MESSAGE; 132 | } 133 | 134 | model.addAttribute("output", response); 135 | model.addAttribute("Find_ad_account", Find_Ad_Account); 136 | model.addAttribute("Get_user_org_access", Get_user_org_access); 137 | model.addAttribute("auth_url", SERVER_URL + THREE_LEGGED_TOKEN_GEN_ENDPOINT); 138 | model.addAttribute("action", "Making Server API request..."); 139 | 140 | logger.log(Level.INFO, "Completed execution on button click. The output is {0}", response); 141 | 142 | return LMS_PAGE; 143 | } 144 | 145 | /** 146 | * 147 | * @param response 148 | * @return 149 | * @throws Exception 150 | */ 151 | private Object parseJSON(final String response) throws Exception { 152 | ObjectMapper objectMapper = new ObjectMapper(); 153 | Map < String, Object > jsonMap = objectMapper.readValue(response, 154 | new TypeReference < Map < String, Object >>() { } ); 155 | if (jsonMap.containsKey("elements")) { 156 | return jsonMap.get("elements"); 157 | } else { 158 | return jsonMap.get("message"); 159 | } 160 | } 161 | 162 | } 163 | -------------------------------------------------------------------------------- /client/src/main/java/com/linkedIn/api/MainController.java: -------------------------------------------------------------------------------- 1 | package com.linkedIn.api; 2 | 3 | import org.springframework.beans.factory.annotation.Value; 4 | import org.springframework.boot.web.client.RestTemplateBuilder; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.ui.Model; 8 | import org.springframework.web.bind.annotation.GetMapping; 9 | import org.springframework.web.bind.annotation.PostMapping; 10 | import org.springframework.web.bind.annotation.RequestBody; 11 | import org.springframework.web.client.RestTemplate; 12 | 13 | import java.util.logging.Level; 14 | import java.util.logging.Logger; 15 | import static com.linkedIn.api.Constants.TOKEN_INTROSPECTION_ENDPOINT; 16 | import static com.linkedIn.api.Constants.TOKEN_EXISTS_MESSAGE; 17 | import static com.linkedIn.api.Constants.THREE_LEGGED_TOKEN_GEN_ENDPOINT; 18 | import static com.linkedIn.api.Constants.OAUTH_PAGE; 19 | import static com.linkedIn.api.Constants.CASE_TWO_LEGGED_TOKEN_GEN; 20 | import static com.linkedIn.api.Constants.ACTION_2_LEGGED_TOKEN_GEN; 21 | import static com.linkedIn.api.Constants.TWO_LEGGED_TOKEN_GEN_ENDPOINT; 22 | import static com.linkedIn.api.Constants.TWO_LEGGED_TOKEN_GEN_SUCCESS_MESSAGE; 23 | import static com.linkedIn.api.Constants.GENERIC_ERROR_MESSAGE; 24 | import static com.linkedIn.api.Constants.CASE_GET_PROFILE; 25 | import static com.linkedIn.api.Constants.ACTION_GET_PROFILE; 26 | import static com.linkedIn.api.Constants.PROFILE_ENDPOINT; 27 | import static com.linkedIn.api.Constants.CASE_USE_REFRESH_TOKEN; 28 | import static com.linkedIn.api.Constants.ACTION_USE_REFRESH_TOKEN; 29 | import static com.linkedIn.api.Constants.USE_REFRESH_TOKEN_ENDPOINT; 30 | import static com.linkedIn.api.Constants.ACTION_TOKEN_INTROSPECTION; 31 | import static com.linkedIn.api.Constants.REFRESH_TOKEN_ERROR_MESSAGE; 32 | import static com.linkedIn.api.Constants.REFRESH_TOKEN_MESSAGE; 33 | 34 | /** 35 | * Main controller called by spring-boot to handle OAuth actions at 36 | * http://localhost:8989 (Default) 37 | */ 38 | 39 | @Controller 40 | public final class MainController { 41 | 42 | @Bean 43 | public RestTemplate Rest_Template(final RestTemplateBuilder builder) { 44 | return builder.build(); 45 | } 46 | 47 | static final RestTemplate Rest_Template = new RestTemplate(); 48 | private Logger logger = Logger.getLogger(MainController.class.getName()); 49 | 50 | @Value("${SERVER_URL}") 51 | private String SERVER_URL; 52 | 53 | /** 54 | * Serves a html webpage with operations related to OAuth 55 | * 56 | * @param model Spring Boot Model 57 | * @return the html page to render 58 | */ 59 | @GetMapping("/") 60 | public String oauth(final Model model) { 61 | String action = "Start with LinkedIn's OAuth API operations..."; 62 | String response = ""; 63 | String output = ""; 64 | try { 65 | response = Rest_Template.getForObject(SERVER_URL + TOKEN_INTROSPECTION_ENDPOINT, String.class); 66 | logger.log(Level.INFO, "Validating if a token is already in session. Response from token introspection end point is: {0}", response); 67 | 68 | if (!response.toLowerCase().contains("error")) { 69 | action = TOKEN_EXISTS_MESSAGE; 70 | output = TOKEN_EXISTS_MESSAGE; 71 | } 72 | } catch (Exception e) { 73 | logger.log(Level.SEVERE, e.getMessage(), e); 74 | } 75 | 76 | model.addAttribute("auth_url", SERVER_URL + THREE_LEGGED_TOKEN_GEN_ENDPOINT); 77 | model.addAttribute("output", output); 78 | model.addAttribute("action", action); 79 | 80 | logger.log(Level.INFO, "Completed execution for rendering OAuth page. The model values are output: {0},action: {1}.", new String[]{output, action}); 81 | return OAUTH_PAGE; 82 | } 83 | 84 | /** 85 | * Handles the post requests of Html page, calls the API endpoints of server URL. 86 | * 87 | * @param data string data passed from the UI compoment 88 | * @param model Spring Boot Model 89 | * @return a page to render on UI 90 | */ 91 | @PostMapping(path = "/", produces = { "application/json", "application/xml" }, consumes = { "application/x-www-form-urlencoded" }) 92 | public String postBody(@RequestBody final String data, final Model model) { 93 | String response = ""; 94 | String action = ""; 95 | 96 | logger.log(Level.INFO, "Handling on click of marketing page buttons. Button clicked is {0}", data); 97 | 98 | if (data.equals(CASE_TWO_LEGGED_TOKEN_GEN)) { 99 | action = ACTION_2_LEGGED_TOKEN_GEN; 100 | try { 101 | Rest_Template.getForObject(SERVER_URL + TWO_LEGGED_TOKEN_GEN_ENDPOINT, String.class); 102 | response = TWO_LEGGED_TOKEN_GEN_SUCCESS_MESSAGE; 103 | } catch (Exception e) { 104 | logger.log(Level.SEVERE, e.getMessage(), e); 105 | response = GENERIC_ERROR_MESSAGE; 106 | } 107 | 108 | } else if (data.equals(CASE_GET_PROFILE)) { 109 | action = ACTION_GET_PROFILE; 110 | try { 111 | response = Rest_Template.getForObject(SERVER_URL + PROFILE_ENDPOINT, String.class); 112 | } catch (Exception e) { 113 | logger.log(Level.SEVERE, e.getMessage(), e); 114 | response = GENERIC_ERROR_MESSAGE; 115 | } 116 | 117 | } else if (data.equals(CASE_USE_REFRESH_TOKEN)) { 118 | action = ACTION_USE_REFRESH_TOKEN; 119 | try { 120 | response = Rest_Template.getForObject(SERVER_URL + USE_REFRESH_TOKEN_ENDPOINT, String.class); 121 | if (response == null) { 122 | response = REFRESH_TOKEN_ERROR_MESSAGE; 123 | } else { 124 | response = REFRESH_TOKEN_MESSAGE; 125 | } 126 | } catch (Exception e) { 127 | logger.log(Level.SEVERE, e.getMessage(), e); 128 | response = GENERIC_ERROR_MESSAGE; 129 | } 130 | } else { 131 | action = ACTION_TOKEN_INTROSPECTION; 132 | try { 133 | response = Rest_Template.getForObject(SERVER_URL + TOKEN_INTROSPECTION_ENDPOINT, String.class); 134 | } catch (Exception e) { 135 | logger.log(Level.SEVERE, e.getMessage(), e); 136 | response = GENERIC_ERROR_MESSAGE; 137 | } 138 | } 139 | 140 | model.addAttribute("output", response); 141 | model.addAttribute("auth_url", SERVER_URL + THREE_LEGGED_TOKEN_GEN_ENDPOINT); 142 | model.addAttribute("action", action); 143 | 144 | logger.log(Level.INFO, "Completed execution on button click. The output is {0}", response); 145 | return OAUTH_PAGE; 146 | } 147 | 148 | } 149 | -------------------------------------------------------------------------------- /client/src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | #change client port, default set to 8989 2 | server.port = 8989 3 | #REST based server-component URL 4 | SERVER_URL = http://localhost:8080/ 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /client/src/main/resources/static/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 40px 20px; 3 | word-wrap: break-word; 4 | font-family: -apple-system, system-ui, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Fira Sans", Ubuntu, Oxygen, "Oxygen Sans", Cantarell, "Droid Sans", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Lucida Grande", Helvetica, Arial, sans-serif; 5 | } 6 | 7 | .btn-primary { 8 | background: #0a66c2; 9 | color: white; 10 | padding: 20px; 11 | font-weight: 700; 12 | justify-content: center; 13 | } 14 | 15 | .btn-link { 16 | font-size: 14px; 17 | font-weight: 600; 18 | height: 40px; 19 | line-height: 40px; 20 | padding: 0 24px; 21 | text-align: center; 22 | border-radius: 24px; 23 | box-shadow: inset 0 0 0 1px #8f5849; 24 | color: #8f5849; 25 | } 26 | 27 | .output { 28 | border: 2px solid #8f5849; 29 | padding: 10px; 30 | max-width: 800px; 31 | margin: 10px 10px; 32 | min-height: 50px; 33 | font-weight: 600; 34 | } 35 | 36 | h1, 37 | h2, 38 | h3, 39 | h4, 40 | h5, 41 | p { 42 | display: block; 43 | margin: 20px 10px; 44 | } 45 | 46 | .header-logo { 47 | margin: 0 auto; 48 | display: block; 49 | height: 200px; 50 | width: auto; 51 | } 52 | 53 | h1 { 54 | color: #0077B5; 55 | text-align: center; 56 | } 57 | 58 | h5 { 59 | line-height: 20px; 60 | color: #70B5F9; 61 | text-align: center; 62 | } 63 | 64 | table { 65 | border-collapse: collapse; 66 | width: 100%; 67 | } 68 | 69 | td, 70 | th { 71 | border: 1px solid #ddd; 72 | padding: 8px; 73 | } 74 | 75 | tr:nth-child(even) { 76 | background-color: #f2f2f2; 77 | } 78 | 79 | tr:hover { 80 | background-color: #ddd; 81 | } 82 | 83 | th { 84 | padding-top: 12px; 85 | padding-bottom: 12px; 86 | text-align: left; 87 | background-color: #f18957; 88 | color: white; 89 | } 90 | 91 | .three { 92 | padding: 10px 25px; 93 | background-color: -internal-light-dark(rgb(239, 239, 239), rgb(59, 59, 59)); 94 | color: -internal-light-dark(black, white); 95 | border-width: 2px; 96 | border-style: outset; 97 | border-color: -internal-light-dark(rgb(118, 118, 118), rgb(133, 133, 133)); 98 | border-image: initial; 99 | } 100 | -------------------------------------------------------------------------------- /client/src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OAuth: Sample Application 5 | 6 | 7 | 8 | 9 |
10 |

OAuth Sample Application

11 |
12 |
13 |
14 |

Client Application

15 |

This client application makes calls to the server component REST Endpoints

16 |

17 |

OAuth functions to generate access token in server component

18 | 3 Legged OAuth 19 | 20 |

OAuth token calls

21 | 22 | 23 |

Use access token stored in server to make REST calls

24 | 25 |
26 |
27 |
28 |
OUTPUT: JSON response from server component
29 |

30 |
31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /client/src/main/resources/templates/marketingtemplate.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
9 |

Marketing Sample Application

10 |
11 |
12 |
13 |

Client Application

14 |

This client application makes calls to the server component REST Endpoints

15 |

16 |

OAuth functions to generate access token in server component

17 | 3 Legged OAuth 18 |

Use access token stored in server to make REST calls

19 | 20 | 21 | 22 |
23 |
24 |
25 |
OUTPUT: JSON response from server component
26 |

27 |

28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
RoleAccount
38 |
39 |
40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
Role AssigneeStateRole
52 |
53 |
54 |
55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /client/src/test/java/com/linkedIn/api/MainControllerTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Test cases for Java Sample Application. 3 | */ 4 | 5 | package com.linkedIn.api; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.web.client.RestTemplate; 14 | 15 | 16 | @SpringBootTest 17 | public class MainControllerTest { 18 | 19 | @Autowired 20 | private RestTemplate restTemplate; 21 | 22 | @Test 23 | public void contextLoads() { 24 | assertThat(restTemplate).isNotNull(); 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /server/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 4.0.0 7 | 8 | org.springframework.boot 9 | spring-boot-starter-parent 10 | 2.5.2 11 | 12 | 13 | 14 | com.example 15 | linkedIn-oauth-sample-app 16 | 0.0.1 17 | LinkedIn OAuth Sample App Server 18 | A sample application demonstrating how to authenticate with LinkedIn's OAuth APIs 19 | 20 | 1.8 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-thymeleaf 26 | 2.2.2.RELEASE 27 | 28 | 29 | org.springframework 30 | spring-core 31 | 5.3.13 32 | 33 | 34 | org.springframework.boot 35 | spring-boot-starter-web 36 | 2.5.2 37 | 38 | 39 | org.springframework 40 | spring-core 41 | 42 | 43 | 44 | 45 | org.springframework.boot 46 | spring-boot-devtools 47 | 2.6.0 48 | runtime 49 | true 50 | 51 | 52 | org.springframework.boot 53 | spring-boot-starter-test 54 | 2.6.0 55 | test 56 | 57 | 58 | org.springframework 59 | spring-core 60 | 61 | 62 | 63 | 64 | com.fasterxml.jackson.core 65 | jackson-databind 66 | 2.13.0 67 | 68 | 69 | com.fasterxml.jackson.core 70 | jackson-core 71 | 2.13.0 72 | 73 | 74 | 75 | 76 | 77 | org.springframework.boot 78 | spring-boot-maven-plugin 79 | 80 | 81 | 82 | 83 | src/main/resources 84 | true 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /server/src/main/java/com/example/api/Constants.java: -------------------------------------------------------------------------------- 1 | package com.example.api; 2 | 3 | /** 4 | * Constants file 5 | */ 6 | public class Constants { 7 | public static final String LI_FIND_AD_ACCOUNTS_FOR_USER_ENDPOINT = "https://api.linkedin.com/v2/adAccountUsersV2?q=authenticatedUser&oauth2_access_token="; 8 | public static final String LI_FIND_USER_ROLES_ENDPOINT = "https://api.linkedin.com/v2/organizationAcls?q=roleAssignee&oauth2_access_token="; 9 | public static final String LI_ME_ENDPOINT = "https://api.linkedin.com/v2/me?oauth2_access_token="; 10 | public static final String TOKEN_INTROSPECTION_ERROR_MESSAGE = "Error introspecting token, service is not initiated"; 11 | public static final String SAMPLE_APP_BASE = "java-sample-application"; 12 | public static final String SAMPLE_APP_VERSION = "version 1.0"; 13 | enum AppName { 14 | OAuth, 15 | Marketing; 16 | } 17 | public static final String USER_AGENT_OAUTH_VALUE = String.format("%s (%s, %s)", SAMPLE_APP_BASE, SAMPLE_APP_VERSION, AppName.OAuth.name()); 18 | public static final String USER_AGENT_LMS_VALUE = String.format("%s (%s, %s)", SAMPLE_APP_BASE, SAMPLE_APP_VERSION, AppName.Marketing.name()); 19 | 20 | } 21 | -------------------------------------------------------------------------------- /server/src/main/java/com/example/api/LinkedInMarketingController.java: -------------------------------------------------------------------------------- 1 | package com.example.api; 2 | 3 | import org.springframework.http.HttpMethod; 4 | import org.springframework.web.bind.annotation.RequestMapping; 5 | import org.springframework.web.bind.annotation.RestController; 6 | import org.springframework.web.client.RestTemplate; 7 | import org.springframework.web.client.HttpStatusCodeException; 8 | import org.springframework.http.HttpEntity; 9 | import org.springframework.http.HttpHeaders; 10 | 11 | import java.util.logging.Level; 12 | import java.util.logging.Logger; 13 | 14 | import static com.example.api.LinkedInOAuthController.token; 15 | import static com.example.api.Constants.LI_FIND_AD_ACCOUNTS_FOR_USER_ENDPOINT; 16 | import static com.example.api.Constants.LI_FIND_USER_ROLES_ENDPOINT; 17 | import static com.example.api.Constants.USER_AGENT_LMS_VALUE; 18 | 19 | 20 | 21 | /* 22 | * Getting Started with LinkedIn's Marketing APIs , 23 | * Documentation: https://docs.microsoft.com/en-us/linkedin/marketing/getting-started 24 | * The additional scopes required to use these functions are: 25 | * 'rw_ads, rw_organization_admin' 26 | * You can invoke these functions independently with valid access token string as a parameter. 27 | * More Docs: https://docs.microsoft.com/en-us/linkedin/marketing/integrations/ads/account-structure/create-and-manage-account-users 28 | */ 29 | 30 | @RestController 31 | public final class LinkedInMarketingController { 32 | 33 | private RestTemplate lmsTemplate = new RestTemplate(); 34 | private Logger logger = Logger.getLogger(LinkedInMarketingController.class.getName()); 35 | private HttpHeaders header = new HttpHeaders(); 36 | 37 | /** 38 | * Find Ad Accounts by Authenticated User or Verifying Ad Accounts Access 39 | * @return All Ad Accounts that an authenticated user has access to can be retrieved with the following endpoint. 40 | */ 41 | @RequestMapping("/findAdAccounts") 42 | public String Find_ad_account() { 43 | try { 44 | setUserAgentHeader(); 45 | String response = lmsTemplate.exchange(LI_FIND_AD_ACCOUNTS_FOR_USER_ENDPOINT + token, HttpMethod.GET, 46 | new HttpEntity(header), String.class).getBody(); 47 | logger.log(Level.INFO, "Find Ad Accounts for Authenticated User response is:{0}", response); 48 | return response; 49 | } catch (HttpStatusCodeException e) { 50 | logger.log(Level.SEVERE, e.getMessage(), e); 51 | return e.getResponseBodyAsString(); 52 | } 53 | 54 | } 55 | 56 | /** 57 | * Find Ad Account roles of Authenticated User 58 | * @return fetch all users associated with specified Ad Accounts 59 | */ 60 | @RequestMapping("/getUserOrgAccess") 61 | public String Get_user_org_access() { 62 | try { 63 | setUserAgentHeader(); 64 | String response = lmsTemplate.exchange(LI_FIND_USER_ROLES_ENDPOINT + token, HttpMethod.GET, new HttpEntity(header), String.class).getBody(); 65 | logger.log(Level.INFO, "Find Org Roles for Authenticated User response is:{0}", response); 66 | return response; 67 | } catch (HttpStatusCodeException e) { 68 | logger.log(Level.SEVERE, e.getMessage(), e); 69 | return e.getResponseBodyAsString(); 70 | } 71 | 72 | } 73 | 74 | /** 75 | * Fetch Ad Account by ID 76 | * @return Individual Ad Account Details 77 | */ 78 | // uncomment below to use 79 | /* 80 | @RequestMapping(value = "/fetchAdAccount") 81 | public String Fetch_ad_account(@RequestParam(name = "account", required = false) final String account, final HttpSession session) { 82 | if (account == null) { 83 | return "Please pass the account ID!"; 84 | } 85 | try { 86 | token = (String) session.getAttribute("tokenString"); 87 | String response = lmsTemplate.getForObject("https://api.linkedin.com/v2/adAccountsV2/" + account + "?oauth2_access_token=" + token, String.class); 88 | return response; 89 | } catch (HttpStatusCodeException e) { 90 | return e.getResponseBodyAsString(); 91 | } 92 | }*/ 93 | 94 | private void setUserAgentHeader() { 95 | header.set(HttpHeaders.USER_AGENT, USER_AGENT_LMS_VALUE); 96 | } 97 | 98 | } 99 | -------------------------------------------------------------------------------- /server/src/main/java/com/example/api/LinkedInOAuthController.java: -------------------------------------------------------------------------------- 1 | package com.example.api; 2 | 3 | import java.io.FileNotFoundException; 4 | import java.io.IOException; 5 | import java.io.InputStream; 6 | import java.util.Properties; 7 | import java.util.Random; 8 | import java.util.logging.Level; 9 | import java.util.logging.Logger; 10 | 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.web.client.RestTemplateBuilder; 13 | import org.springframework.context.annotation.Bean; 14 | import org.springframework.http.HttpEntity; 15 | import org.springframework.http.HttpHeaders; 16 | import org.springframework.http.HttpMethod; 17 | import org.springframework.web.bind.annotation.RequestMapping; 18 | import org.springframework.web.bind.annotation.RequestParam; 19 | import org.springframework.web.bind.annotation.RestController; 20 | import org.springframework.web.client.RestTemplate; 21 | import com.linkedin.oauth.builder.ScopeBuilder; 22 | import com.linkedin.oauth.pojo.AccessToken; 23 | import com.linkedin.oauth.service.LinkedInOAuthService; 24 | import org.springframework.web.servlet.view.RedirectView; 25 | 26 | import static com.example.api.Constants.TOKEN_INTROSPECTION_ERROR_MESSAGE; 27 | import static com.example.api.Constants.LI_ME_ENDPOINT; 28 | import static com.example.api.Constants.USER_AGENT_OAUTH_VALUE; 29 | import static com.linkedin.oauth.util.Constants.TOKEN_INTROSPECTION_URL; 30 | import static com.linkedin.oauth.util.Constants.REQUEST_TOKEN_URL; 31 | 32 | /* 33 | * Getting Started with LinkedIn's OAuth APIs , 34 | * Documentation: https://docs.microsoft.com/en-us/linkedin/shared/authentication/authentication?context=linkedin/context 35 | */ 36 | 37 | @RestController 38 | public final class LinkedInOAuthController { 39 | 40 | @Bean 41 | public RestTemplate restTemplate(final RestTemplateBuilder builder) { 42 | return builder.build(); 43 | } 44 | 45 | @Autowired 46 | private RestTemplate restTemplate; 47 | 48 | 49 | //Define all inputs in the property file 50 | private Properties prop = new Properties(); 51 | private String propFileName = "config.properties"; 52 | public static String token = null; 53 | public String refresh_token = null; 54 | public LinkedInOAuthService service; 55 | 56 | private Logger logger = Logger.getLogger(LinkedInOAuthController.class.getName()); 57 | 58 | /** 59 | * Make a Login request with LinkedIN Oauth API 60 | * 61 | * @param code optional Authorization code 62 | * @return Redirects to the client UI after successful token creation 63 | */ 64 | @RequestMapping(value = "/login") 65 | public RedirectView oauth(@RequestParam(name = "code", required = false) final String code) throws Exception { 66 | 67 | loadProperty(); 68 | 69 | // Construct the LinkedInOAuthService instance for use 70 | service = new LinkedInOAuthService.LinkedInOAuthServiceBuilder() 71 | .apiKey(prop.getProperty("clientId")) 72 | .apiSecret(prop.getProperty("clientSecret")) 73 | .defaultScope(new ScopeBuilder(prop.getProperty("scope").split(",")).build()) // replace with desired scope 74 | .callback(prop.getProperty("redirectUri")) 75 | .build(); 76 | 77 | final String secretState = "secret" + new Random().nextInt(999_999); 78 | final String authorizationUrl = service.createAuthorizationUrlBuilder() 79 | .state(secretState) 80 | .build(); 81 | 82 | RedirectView redirectView = new RedirectView(); 83 | 84 | if (code != null && !code.isEmpty()) { 85 | 86 | logger.log(Level.INFO, "Authorization code not empty, trying to generate a 3-legged OAuth token."); 87 | 88 | final AccessToken[] accessToken = { 89 | new AccessToken() 90 | }; 91 | HttpEntity request = service.getAccessToken3Legged(code); 92 | String response = restTemplate.postForObject(REQUEST_TOKEN_URL, request, String.class); 93 | accessToken[0] = service.convertJsonTokenToPojo(response); 94 | 95 | prop.setProperty("token", accessToken[0].getAccessToken()); 96 | token = accessToken[0].getAccessToken(); 97 | refresh_token = accessToken[0].getRefreshToken(); 98 | 99 | logger.log(Level.INFO, "Generated Access token and Refresh Token."); 100 | 101 | redirectView.setUrl(prop.getProperty("client_url")); 102 | } else { 103 | redirectView.setUrl(authorizationUrl); 104 | } 105 | return redirectView; 106 | } 107 | 108 | 109 | /** 110 | * Create 2 legged auth access token 111 | * 112 | * @return Redirects to the client UI after successful token creation 113 | */ 114 | @RequestMapping(value = "/twoLeggedAuth") 115 | public RedirectView two_legged_auth() throws Exception { 116 | loadProperty(); 117 | 118 | RedirectView redirectView = new RedirectView(); 119 | // Construct the LinkedInOAuthService instance for use 120 | service = new LinkedInOAuthService.LinkedInOAuthServiceBuilder() 121 | .apiKey(prop.getProperty("clientId")) 122 | .apiSecret(prop.getProperty("clientSecret")) 123 | .defaultScope(new ScopeBuilder(prop.getProperty("scope").split(",")).build()) 124 | .callback(prop.getProperty("redirectUri")) 125 | .build(); 126 | 127 | final AccessToken[] accessToken = { 128 | new AccessToken() 129 | }; 130 | 131 | HttpEntity request = service.getAccessToken2Legged(); 132 | String response = restTemplate.postForObject(REQUEST_TOKEN_URL, request, String.class); 133 | accessToken[0] = service.convertJsonTokenToPojo(response); 134 | prop.setProperty("token", accessToken[0].getAccessToken()); 135 | token = accessToken[0].getAccessToken(); 136 | 137 | logger.log(Level.INFO, "Generated Access token."); 138 | 139 | redirectView.setUrl(prop.getProperty("client_url")); 140 | return redirectView; 141 | } 142 | 143 | /** 144 | * Make a Token Introspection request with LinkedIN API 145 | * 146 | * @return check the Time to Live (TTL) and status (active/expired) for all token 147 | */ 148 | 149 | @RequestMapping(value = "/tokenIntrospection") 150 | public String token_introspection() throws Exception { 151 | if (service != null) { 152 | HttpEntity request = service.introspectToken(token); 153 | String response = restTemplate.postForObject(TOKEN_INTROSPECTION_URL, request, String.class); 154 | logger.log(Level.INFO, "Token introspected. Details are {0}", response); 155 | 156 | return response; 157 | } else { 158 | return TOKEN_INTROSPECTION_ERROR_MESSAGE; 159 | } 160 | } 161 | 162 | 163 | /** 164 | * Make a Refresh Token request with LinkedIN API 165 | * 166 | * @return get a new access token when your current access token expire 167 | */ 168 | 169 | @RequestMapping(value = "/refreshToken") 170 | public String refresh_token() throws IOException { 171 | String response = null; 172 | if (refresh_token != null) { 173 | HttpEntity request = service.getAccessTokenFromRefreshToken(refresh_token); 174 | response = restTemplate.postForObject(REQUEST_TOKEN_URL, request, String.class); 175 | logger.log(Level.INFO, "Used Refresh Token to generate a new access token successfully."); 176 | return response; 177 | } else { 178 | logger.log(Level.INFO, "Refresh Token cannot be empty. Generate 3L Access Token and Retry again."); 179 | return response; 180 | } 181 | } 182 | 183 | /** 184 | * Make a Public profile request with LinkedIN API 185 | * 186 | * @return Public profile of user 187 | */ 188 | 189 | @RequestMapping(value = "/profile") 190 | public String profile() { 191 | HttpHeaders headers = new HttpHeaders(); 192 | headers.set(HttpHeaders.USER_AGENT, USER_AGENT_OAUTH_VALUE); 193 | return restTemplate.exchange(LI_ME_ENDPOINT + token, HttpMethod.GET, new HttpEntity<>(headers), String.class).getBody(); 194 | } 195 | 196 | private void loadProperty() throws IOException { 197 | InputStream inputStream = LinkedInOAuthController.class.getClassLoader().getResourceAsStream(propFileName); 198 | if (inputStream != null) { 199 | prop.load(inputStream); 200 | } else { 201 | throw new FileNotFoundException("property file '" + propFileName + "' not found in the classpath"); 202 | } 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /server/src/main/java/com/example/api/MainApplication.java: -------------------------------------------------------------------------------- 1 | package com.example.api; 2 | import org.springframework.boot.SpringApplication; 3 | import org.springframework.boot.autoconfigure.SpringBootApplication; 4 | 5 | 6 | /* 7 | * Create Spring Boot Application and set a default controller 8 | */ 9 | 10 | @SpringBootApplication 11 | public class MainApplication { 12 | public MainApplication() { } 13 | public static void main(final String[] args) { 14 | SpringApplication.run(MainApplication.class, args); 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /server/src/main/java/com/linkedin/oauth/builder/AuthorizationUrlBuilder.java: -------------------------------------------------------------------------------- 1 | package com.linkedin.oauth.builder; 2 | 3 | import com.linkedin.oauth.service.LinkedInOAuthService; 4 | 5 | import java.io.UnsupportedEncodingException; 6 | import java.net.URLEncoder; 7 | import java.nio.charset.StandardCharsets; 8 | import java.util.Map; 9 | 10 | import static com.linkedin.oauth.util.Constants.AUTHORIZE_URL; 11 | 12 | /*** 13 | * Builder class for LinkedIn OAuth 2.0 authorization URL. 14 | */ 15 | 16 | public final class AuthorizationUrlBuilder { 17 | 18 | private String state; 19 | private Map additionalParams; 20 | private final LinkedInOAuthService oauth20Service; 21 | 22 | /** 23 | * public constructor 24 | * @param oauth20Service {@link LinkedInOAuthService} 25 | */ 26 | public AuthorizationUrlBuilder(final LinkedInOAuthService oauth20Service) { 27 | this.oauth20Service = oauth20Service; 28 | } 29 | 30 | /** 31 | * Setter for state 32 | */ 33 | public AuthorizationUrlBuilder state(final String state) { 34 | this.state = state; 35 | return this; 36 | } 37 | 38 | /** 39 | * Setter for additional params 40 | */ 41 | public AuthorizationUrlBuilder additionalParams(final Map additionalParams) { 42 | this.additionalParams = additionalParams; 43 | return this; 44 | } 45 | 46 | /** 47 | * Builds the authorization URL 48 | */ 49 | public String build() throws UnsupportedEncodingException { 50 | 51 | String authoriztaionUrl = AUTHORIZE_URL 52 | + "?response_type=code&client_id=" 53 | + oauth20Service.getApiKey() 54 | + "&redirect_uri=" 55 | + oauth20Service.getRedirectUri() 56 | + "&state=" 57 | + state 58 | + "&scope=" 59 | + URLEncoder.encode(oauth20Service.getScope(), String.valueOf(StandardCharsets.UTF_8)); 60 | return authoriztaionUrl; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /server/src/main/java/com/linkedin/oauth/builder/ScopeBuilder.java: -------------------------------------------------------------------------------- 1 | package com.linkedin.oauth.builder; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.HashSet; 6 | import java.util.Set; 7 | 8 | /** 9 | * Builder for LinkedIn OAuth 2.0 scopes. 10 | */ 11 | 12 | public final class ScopeBuilder { 13 | 14 | private final Set scopes = new HashSet<>(); 15 | 16 | /** 17 | * public constructor 18 | * @param scope that needs to be requested 19 | */ 20 | public ScopeBuilder(final String scope) { 21 | withScope(scope); 22 | } 23 | 24 | /** 25 | * public constructor 26 | * @param scopes array of scopes to be requested 27 | */ 28 | public ScopeBuilder(final String... scopes) { 29 | withScopes(scopes); 30 | } 31 | 32 | /** 33 | * public constructor 34 | * @param scopes collection of scopes to be requested 35 | */ 36 | public ScopeBuilder(final Collection scopes) { 37 | withScopes(scopes); 38 | } 39 | 40 | /** 41 | * Setter for a single scope 42 | */ 43 | public ScopeBuilder withScope(final String scope) { 44 | scopes.add(scope); 45 | return this; 46 | } 47 | 48 | /** 49 | * Setter for an array of scopes 50 | */ 51 | public ScopeBuilder withScopes(final String... scopes) { 52 | this.scopes.addAll(Arrays.asList(scopes)); 53 | return this; 54 | } 55 | 56 | /** 57 | * Setter for setting a collection of scopes 58 | */ 59 | public ScopeBuilder withScopes(final Collection scopes) { 60 | this.scopes.addAll(scopes); 61 | return this; 62 | } 63 | 64 | /** 65 | * builds all the scopes set into a single String for requesting 66 | */ 67 | public String build() { 68 | final StringBuilder scopeBuilder = new StringBuilder(); 69 | for (String scope : scopes) { 70 | scopeBuilder.append(' ').append(scope); 71 | } 72 | return scopeBuilder.substring(1); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /server/src/main/java/com/linkedin/oauth/pojo/AccessToken.java: -------------------------------------------------------------------------------- 1 | package com.linkedin.oauth.pojo; 2 | 3 | import com.fasterxml.jackson.annotation.JsonIgnoreProperties; 4 | import com.fasterxml.jackson.annotation.JsonProperty; 5 | 6 | 7 | /** 8 | * POJO to encapsulate access token from 2/3-legged LinkedIn OAuth 2.0 flow. 9 | */ 10 | 11 | @JsonIgnoreProperties(ignoreUnknown = true) 12 | public final class AccessToken { 13 | 14 | @JsonProperty(value = "access_token") 15 | private String accessToken; 16 | 17 | @JsonProperty(value = "expires_in") 18 | private String expiresIn; 19 | 20 | @JsonProperty(value = "refresh_token") 21 | private String refreshToken; 22 | 23 | @JsonProperty(value = "refresh_token_expires_in") 24 | //@Ignore 25 | private String refreshTokenExpiresIn; 26 | 27 | public AccessToken() { 28 | } 29 | 30 | public String getAccessToken() { 31 | return accessToken; 32 | } 33 | 34 | public String getExpiresIn() { 35 | return expiresIn; 36 | } 37 | 38 | public void setAccessToken(final String accessToken) { 39 | this.accessToken = accessToken; 40 | } 41 | 42 | public void setExpiresIn(final String expiresIn) { 43 | this.expiresIn = expiresIn; 44 | } 45 | 46 | public String getRefreshToken() { 47 | return refreshToken; 48 | } 49 | 50 | public void setRefreshToken(final String refreshToken) { 51 | this.refreshToken = refreshToken; 52 | } 53 | 54 | public String getRefreshTokenExpiresIn() { 55 | return refreshTokenExpiresIn; 56 | } 57 | 58 | public void setRefreshTokenExpiresIn(final String refreshTokenExpiresIn) { 59 | this.refreshTokenExpiresIn = refreshTokenExpiresIn; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /server/src/main/java/com/linkedin/oauth/service/LinkedInOAuthService.java: -------------------------------------------------------------------------------- 1 | package com.linkedin.oauth.service; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.linkedin.oauth.builder.AuthorizationUrlBuilder; 5 | import com.linkedin.oauth.builder.ScopeBuilder; 6 | import com.linkedin.oauth.pojo.AccessToken; 7 | import com.linkedin.oauth.util.Preconditions; 8 | 9 | 10 | import java.io.IOException; 11 | 12 | import org.springframework.http.HttpEntity; 13 | import org.springframework.http.HttpHeaders; 14 | import org.springframework.util.LinkedMultiValueMap; 15 | import org.springframework.util.MultiValueMap; 16 | import static com.linkedin.oauth.util.Constants.*; 17 | import static com.example.api.Constants.USER_AGENT_OAUTH_VALUE; 18 | 19 | 20 | /** 21 | * LinkedIn 3-Legged OAuth Service 22 | */ 23 | @SuppressWarnings({"AvoidStarImport"}) 24 | public final class LinkedInOAuthService { 25 | 26 | private final String redirectUri; 27 | private final String apiKey; 28 | private final String apiSecret; 29 | private final String scope; 30 | 31 | private LinkedInOAuthService(final LinkedInOAuthServiceBuilder oauthServiceBuilder) { 32 | this.redirectUri = oauthServiceBuilder.redirectUri; 33 | this.apiKey = oauthServiceBuilder.apiKey; 34 | this.apiSecret = oauthServiceBuilder.apiSecret; 35 | this.scope = oauthServiceBuilder.scope; 36 | } 37 | 38 | public String getRedirectUri() { 39 | return redirectUri; 40 | } 41 | 42 | public String getApiKey() { 43 | return apiKey; 44 | } 45 | 46 | public String getApiSecret() { 47 | return apiSecret; 48 | } 49 | 50 | public String getScope() { 51 | return scope; 52 | } 53 | 54 | /** 55 | * 56 | * @return an instance of {@link AuthorizationUrlBuilder} 57 | */ 58 | public AuthorizationUrlBuilder createAuthorizationUrlBuilder() { 59 | return new AuthorizationUrlBuilder(this); 60 | } 61 | 62 | /** 63 | * 64 | * @param code authorization code 65 | * @return response of LinkedIn's 3-legged token flow captured in a POJO {@link AccessToken} 66 | * @throws IOException 67 | */ 68 | public HttpEntity getAccessToken3Legged(final String code) throws IOException { 69 | 70 | MultiValueMap parameters = new LinkedMultiValueMap(); 71 | parameters.add(GRANT_TYPE, GrantType.AUTHORIZATION_CODE.getGrantType()); 72 | parameters.add(CODE, code); 73 | parameters.add(REDIRECT_URI, this.redirectUri); 74 | parameters.add(CLIENT_ID, this.apiKey); 75 | parameters.add(CLIENT_SECRET, this.apiSecret); 76 | HttpHeaders headers = new HttpHeaders(); 77 | headers.setContentType(org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED); 78 | headers.set(HttpHeaders.USER_AGENT, USER_AGENT_OAUTH_VALUE); 79 | HttpEntity> request = new HttpEntity>(parameters, headers); 80 | return request; 81 | 82 | } 83 | 84 | /** 85 | * 86 | * @param refreshToken the refresh token obtained from the authorization code exchange 87 | * @return response of LinkedIn's refresh token flow captured in a POJO {@link AccessToken} 88 | * @throws IOException 89 | */ 90 | public HttpEntity getAccessTokenFromRefreshToken(final String refreshToken) throws IOException { 91 | MultiValueMap parameters = new LinkedMultiValueMap(); 92 | parameters.add(GRANT_TYPE, GrantType.REFRESH_TOKEN.getGrantType()); 93 | parameters.add(REFRESH_TOKEN, refreshToken); 94 | parameters.add(CLIENT_ID, this.apiKey); 95 | parameters.add(CLIENT_SECRET, this.apiSecret); 96 | HttpHeaders headers = new HttpHeaders(); 97 | headers.setContentType(org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED); 98 | headers.set(HttpHeaders.USER_AGENT, USER_AGENT_OAUTH_VALUE); 99 | HttpEntity> request = new HttpEntity>(parameters, headers); 100 | return request; 101 | } 102 | 103 | /** 104 | * Get access token by LinkedIn's OAuth2.0 Client Credentials flow 105 | * @return JSON String response 106 | * @throws IOException 107 | */ 108 | public HttpEntity getAccessToken2Legged() throws Exception { 109 | MultiValueMap parameters = new LinkedMultiValueMap(); 110 | parameters.add(GRANT_TYPE, GrantType.CLIENT_CREDENTIALS.getGrantType()); 111 | parameters.add(CLIENT_ID, this.apiKey); 112 | parameters.add(CLIENT_SECRET, this.apiSecret); 113 | 114 | HttpHeaders headers = new HttpHeaders(); 115 | headers.setContentType(org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED); 116 | headers.set(HttpHeaders.USER_AGENT, USER_AGENT_OAUTH_VALUE); 117 | HttpEntity> request = new HttpEntity>(parameters, headers); 118 | return request; 119 | } 120 | 121 | /** 122 | * Introspect token using LinkedIn's Auth tokenIntrospect API 123 | * @param token String representation of the access token 124 | * @return JSON String response 125 | */ 126 | public HttpEntity introspectToken(final String token) throws Exception { 127 | MultiValueMap parameters = new LinkedMultiValueMap(); 128 | parameters.add(CLIENT_ID, this.apiKey); 129 | parameters.add(CLIENT_SECRET, this.apiSecret); 130 | parameters.add(TOKEN, token); 131 | 132 | HttpHeaders headers = new HttpHeaders(); 133 | headers.setContentType(org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED); 134 | headers.set(HttpHeaders.USER_AGENT, USER_AGENT_OAUTH_VALUE); 135 | HttpEntity> request = new HttpEntity>(parameters, headers); 136 | return request; 137 | } 138 | 139 | /** 140 | * Method to convert JSON String OAuth Token to POJO 141 | * @param accessToken 142 | * @return 143 | * @throws IOException 144 | */ 145 | public AccessToken convertJsonTokenToPojo(final String accessToken) throws IOException { 146 | return new ObjectMapper().readValue(accessToken, AccessToken.class); 147 | } 148 | 149 | /** 150 | * Builder class for LinkedIn's OAuth Service 151 | */ 152 | public static final class LinkedInOAuthServiceBuilder { 153 | private String redirectUri; 154 | private String apiKey; 155 | private String apiSecret; 156 | private String scope; 157 | 158 | public LinkedInOAuthServiceBuilder apiKey(final String apiKey) { 159 | Preconditions.checkEmptyString(apiKey, "Invalid Api key"); 160 | this.apiKey = apiKey; 161 | return this; 162 | } 163 | 164 | public LinkedInOAuthServiceBuilder apiSecret(final String apiSecret) { 165 | Preconditions.checkEmptyString(apiSecret, "Invalid Api secret"); 166 | this.apiSecret = apiSecret; 167 | return this; 168 | } 169 | 170 | public LinkedInOAuthServiceBuilder callback(final String callback) { 171 | this.redirectUri = callback; 172 | return this; 173 | } 174 | 175 | private LinkedInOAuthServiceBuilder setScope(final String scope) { 176 | Preconditions.checkEmptyString(scope, "Invalid OAuth scope"); 177 | this.scope = scope; 178 | return this; 179 | } 180 | 181 | public LinkedInOAuthServiceBuilder defaultScope(final ScopeBuilder scopeBuilder) { 182 | return setScope(scopeBuilder.build()); 183 | } 184 | 185 | public LinkedInOAuthServiceBuilder defaultScope(final String scope) { 186 | return setScope(scope); 187 | } 188 | 189 | public LinkedInOAuthService build() { 190 | 191 | LinkedInOAuthService baseService = new LinkedInOAuthService(this); 192 | return baseService; 193 | } 194 | } 195 | 196 | } 197 | -------------------------------------------------------------------------------- /server/src/main/java/com/linkedin/oauth/util/Constants.java: -------------------------------------------------------------------------------- 1 | package com.linkedin.oauth.util; 2 | 3 | public final class Constants { 4 | 5 | private Constants() { 6 | //private utility class constructor to forbid instantiation 7 | } 8 | 9 | public static final String AUTHORIZE_URL = "https://www.linkedin.com/oauth/v2/authorization"; 10 | 11 | public static final String REQUEST_TOKEN_URL = "https://www.linkedin.com/oauth/v2/accessToken"; 12 | 13 | public static final String TOKEN = "token"; 14 | 15 | public static final String CLIENT_SECRET = "client_secret"; 16 | 17 | public static final String CLIENT_ID = "client_id"; 18 | 19 | public static final String REFRESH_TOKEN = "refresh_token"; 20 | 21 | public static final String CODE = "code"; 22 | 23 | public static final String REDIRECT_URI = "redirect_uri"; 24 | 25 | public static final String GRANT_TYPE = "grant_type"; 26 | 27 | public static final String TOKEN_INTROSPECTION_URL = "https://www.linkedin.com/oauth/v2/introspectToken"; 28 | 29 | public static final int RESPONSE_CODE = 200; 30 | 31 | public static final int PORT = 8000; 32 | 33 | public enum GrantType { 34 | CLIENT_CREDENTIALS("client_credentials"), AUTHORIZATION_CODE("authorization_code"), REFRESH_TOKEN("refresh_token"); 35 | private String grantType; 36 | 37 | GrantType(final String grantType) { 38 | this.grantType = grantType; 39 | } 40 | 41 | public String getGrantType() { 42 | return grantType; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /server/src/main/java/com/linkedin/oauth/util/Preconditions.java: -------------------------------------------------------------------------------- 1 | package com.linkedin.oauth.util; 2 | 3 | /** 4 | * Utils for checking preconditions and invariants 5 | */ 6 | public abstract class Preconditions { 7 | 8 | private static final String DEFAULT_MESSAGE = "Received an invalid parameter"; 9 | 10 | /** 11 | * Checks that an object is not null. 12 | * 13 | * @param object any object 14 | * @param errorMsg error message 15 | * 16 | * @throws IllegalArgumentException if the object is null 17 | */ 18 | public static void checkNotNull(final Object object, final String errorMsg) { 19 | check(object != null, errorMsg); 20 | } 21 | 22 | /** 23 | * Checks that a string is not null or empty 24 | * 25 | * @param string any string 26 | * @param errorMsg error message 27 | * 28 | * @throws IllegalArgumentException if the string is null or empty 29 | */ 30 | public static void checkEmptyString(final String string, final String errorMsg) { 31 | check(hasText(string), errorMsg); 32 | } 33 | 34 | public static boolean hasText(final String str) { 35 | if (str.trim().isEmpty()) { 36 | return false; 37 | } 38 | return true; 39 | } 40 | 41 | private static void check(final boolean requirement, final String error) { 42 | if (!requirement) { 43 | throw new IllegalArgumentException(hasText(error) ? error : DEFAULT_MESSAGE); 44 | } 45 | } 46 | 47 | } 48 | -------------------------------------------------------------------------------- /server/src/main/resources/config.properties: -------------------------------------------------------------------------------- 1 | #LinkedIn OAuth Service Properties 2 | 3 | # Replace the below client ID and Secret with your developer application credentials. 4 | clientId= 5 | clientSecret= 6 | # Replace the below with the URL to direct the after successful authorization code generation(set in your developer application) 7 | redirectUri=http://localhost:8080/login 8 | #Replace with desired scopes 9 | scope=r_emailaddress,r_liteprofile,rw_ads,rw_organization_admin 10 | #REST based client-component URL 11 | client_url=http://localhost:8989/ 12 | 13 | 14 | -------------------------------------------------------------------------------- /server/src/main/resources/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | LinkedIn OAuth: Sample Application 5 | 6 | 7 | 8 |

LinkedIn OAuth Sample Application

9 |

Login with linkedIn to generate access token here

10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /server/src/test/java/com/example/api/MainApplicationTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Test cases for Java Sample Application. 3 | */ 4 | 5 | package com.example.api; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | import org.junit.jupiter.api.Test; 10 | 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.boot.test.context.SpringBootTest; 13 | import org.springframework.web.client.RestTemplate; 14 | 15 | 16 | @SpringBootTest 17 | public class MainApplicationTest { 18 | 19 | @Autowired 20 | private RestTemplate restTemplate; 21 | 22 | @Test 23 | public void contextLoads() { 24 | assertThat(restTemplate).isNotNull(); 25 | } 26 | 27 | } 28 | 29 | --------------------------------------------------------------------------------