When integrating LinkedIn as an Identity Provider in Keycloak, you may quickly discover a limitation: by default, LinkedIn only provides basic user information such as email, first name, and last name.
However, in many real-world applications, you may also want to retrieve and store additional profile details, including:
Vanity name (public LinkedIn profile ID)
High-resolution profile picture
Professional headline (user's job title or tagline)
To achieve this, you'll need to create a custom Identity Provider Mapper SPI (Service Provider Interface) in Keycloak. This extension allows you to fetch additional data from LinkedIn's API and map it into the Keycloak user model during the authentication process.
Step 1: Set Up Your LinkedIn App Permissions
Before you begin implementing the custom SPI, make sure your LinkedIn developer app has the correct permissions. You'll need to request access to the following
These scopes allow your Keycloak instance to retrieve detailed user information such as their profile picture and professional headline.
Configuring LinkedIn as an Identity Provider in Keycloak (Tested on Keycloak 26.2.5)
After you've created a LinkedIn App and obtained the required credentials, follow these steps to configure LinkedIn in your example Keycloak realm:
- Navigate to your Keycloak admin console.
Go to:
Identity Providers β Select LinkedIn.Fill in the following fields:
- Client ID: (from your LinkedIn app)
- Client Secret: (from your LinkedIn app)
- In the Scopes field, add the following permissions:
openid profile email r_basicprofile
These scopes are necessary to:
- Authenticate the user (openid)
- Retrieve profile information (profile, r_basicprofile)
- Fetch the userβs verified email address (email)
- Enable the following options:
- Store Tokens (This is required for the SPI to extract the LinkedIn access token and make additional API calls.)
- Trust Email (Assumes the email provided by LinkedIn is verified.)
Click Save.
To use this LinkedIn SPI extension in your Keycloak setup:
Download the JAR file
Either build it yourself using Maven, or download a prebuilt version from the GitHub Releases page.Copy the JAR to Keycloak's provider directory
cp linkedin-profile-mapper-1.0.0.jar /opt/keycloak/providers/
- Rebuild Keycloak to recognize the new provider
/opt/keycloak/bin/kc.sh build
- Restart Keycloak
/opt/keycloak/bin/kc.sh start
Source Code & Releases
You can find the full source code and latest releases at:
https://github.com/maradwan/keycloak-linkedin-profile-mapper
Then you need to add Identity Provider Mapper
Then you need to add attributes for vanityName, profilePicture, and headline in the User profile that is in the Realm settings of your example realm
Create an example-client in Keycloak
In your Keycloak Admin Console:
Go to your realm (e.g., example), then navigate to Clients β click Create client.
Enter example-client as the Client ID, choose OpenID Connect, and click Next.
Set Root URL to http://localhost:3000, and Valid Redirect URI to http://localhost:3000/callback, then click Save.
Go to the Credentials tab β copy the Client Secret.
Under Settings, enable Standard Flow, optionally enable Direct Access Grants.
Use these details in your app:
Test the Integration (Optional FastAPI Example)
You can test the LinkedIn login flow and see the enriched tokens by using a simple FastAPI application:
Install the required packages:
pip install fastapi uvicorn requests
# app.py
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
import requests
app = FastAPI()
# === CONFIG ===
KEYCLOAK_BASE_URL = "https://idp.example.com"
REALM = "example"
CLIENT_ID = "example-client"
CLIENT_SECRET = "XXXXXXXXX" # Only needed for confidential clients
REDIRECT_URI = "http://localhost:3000/callback" # Your app's redirect URI
# === ROUTES ===
@app.get("/login")
def login():
return RedirectResponse(
f"{KEYCLOAK_BASE_URL}/realms/{REALM}/protocol/openid-connect/auth"
f"?client_id={CLIENT_ID}"
f"&redirect_uri={REDIRECT_URI}"
f"&response_type=code"
f"&scope=openid"
)
@app.get("/callback")
def callback(request: Request):
code = request.query_params.get("code")
if not code:
return {"error": "No authorization code provided"}
# Exchange code with Keycloak (not LinkedIn)
token_response = requests.post(
f"{KEYCLOAK_BASE_URL}/realms/{REALM}/protocol/openid-connect/token",
data={
"grant_type": "authorization_code",
"code": code,
"redirect_uri": REDIRECT_URI,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
},
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
if token_response.status_code != 200:
return {"error": "Token exchange failed", "details": token_response.json()}
token_data = token_response.json()
# Decode the token to inspect (optional)
id_token = token_data.get("id_token")
access_token = token_data.get("access_token")
return {
"access_token": access_token,
"id_token": id_token,
"message": "Use this token to call secured APIs or decode for LinkedIn data",
}
Run the App
uvicorn app:app --reload --port 3000
Top comments (0)