Skip to content

Commit d673af9

Browse files
docs: update README with comprehensive LinkedIn strategy documentation
- Implement robust callback route for LinkedIn authentication - Add comprehensive error handling and parameter validation - Improve session creation and user authentication flow - Update token retrieval methods to handle potential undefined tokens - Enhance redirect logic with detailed error scenarios
1 parent bcd73b6 commit d673af9

File tree

1 file changed

+205
-73
lines changed

1 file changed

+205
-73
lines changed

README.md

Lines changed: 205 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# LinkedinStrategy
1+
# Remix Auth LinkedIn Strategy
22

3-
The Linkedin strategy is used to authenticate users against a Linkedin account. It extends the [OAuth2Strategy](https://github.com/sergiodxa/remix-auth-oauth2).
3+
The LinkedIn strategy is used for [Remix Auth](https://github.com/sergiodxa/remix-auth) authentication using OAuth 2.0 and OpenID Connect protocols with LinkedIn.
44

55
## Supported runtimes
66

@@ -9,105 +9,237 @@ The Linkedin strategy is used to authenticate users against a Linkedin account.
99
| Node.js ||
1010
| Cloudflare ||
1111

12+
## Installation
13+
14+
```bash
15+
npm install remix-auth-linkedin
16+
```
1217

1318
## Usage
1419

1520
### Create an OAuth application
1621

17-
First you need to create a new application in the [Linkedin's developers page](https://developer.linkedin.com/). Then I encourage you to read [this documentation page](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2), it explains how to configure your app and gives you useful information on the auth flow.
18-
The app is mandatory in order to obtain a `clientID` and `client secret` to use with the Linkedin's API.
22+
First, create a new application in the [LinkedIn Developers portal](https://developer.linkedin.com/). Read the [LinkedIn documentation](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2) for details on configuring your app and understanding the authentication flow.
1923

2024
### Create the strategy instance
2125

2226
```ts
23-
// linkedin.server.ts
24-
import { createCookieSessionStorage } from 'remix';
25-
import { Authenticator } from 'remix-auth';
26-
import { LinkedinStrategy } from "remix-auth-linkedin";
27-
28-
// Personalize this options for your usage.
29-
const cookieOptions = {
30-
path: '/',
31-
httpOnly: true,
32-
sameSite: 'lax' as const,
33-
maxAge: 24 * 60 * 60 * 1000 * 30,
34-
secrets: ['THISSHOULDBESECRET_AND_NOT_SHARED'],
35-
secure: process.env.NODE_ENV !== 'development',
36-
};
37-
38-
const sessionStorage = createCookieSessionStorage({
39-
cookie: cookieOptions,
40-
});
41-
42-
export const authenticator = new Authenticator<string>(sessionStorage, {
43-
throwOnError: true,
44-
});
45-
46-
const linkedinStrategy = new LinkedinStrategy(
47-
{
48-
clientID: "YOUR_CLIENT_ID",
27+
import { Authenticator } from "remix-auth";
28+
import { LinkedInStrategy } from "remix-auth-linkedin";
29+
30+
// Create an instance of the authenticator
31+
const authenticator = new Authenticator<User>();
32+
33+
// Register the LinkedIn strategy
34+
authenticator.use(
35+
new LinkedInStrategy(
36+
{
37+
clientId: "YOUR_CLIENT_ID",
4938
clientSecret: "YOUR_CLIENT_SECRET",
50-
// LinkedIn is expecting a full URL here, not a relative path
51-
// see: https://learn.microsoft.com/en-us/linkedin/shared/authentication/authorization-code-flow?tabs=HTTPS1#step-1-configure-your-application
52-
callbackURL: "https://example.com/auth/linkedin/callback";
53-
},
54-
async ({accessToken, refreshToken, extraParams, profile, context}) => {
55-
/*
56-
profile:
57-
type LinkedinProfile = {
58-
id: string;
59-
displayName: string;
60-
name: {
61-
givenName: string;
62-
familyName: string;
63-
};
64-
emails: Array<{ value: string }>;
65-
photos: Array<{ value: string }>;
66-
_json: LiteProfileData & EmailData;
67-
} & OAuth2Profile;
68-
*/
69-
70-
// Get the user data from your DB or API using the tokens and profile
71-
return User.findOrCreate({ email: profile.emails[0].value });
72-
}
39+
// LinkedIn requires a full URL, not a relative path
40+
redirectURI: "https://example.com/auth/linkedin/callback",
41+
// Optional: customize scopes
42+
scopes: ["openid", "profile", "email"],
43+
},
44+
async ({ profile, tokens, request }) => {
45+
// Find or create a user in your database
46+
return {
47+
id: profile.id,
48+
email: profile.emails[0].value,
49+
name: profile.displayName,
50+
accessToken: tokens.accessToken(),
51+
refreshToken: tokens.refreshToken ? tokens.refreshToken() : null,
52+
};
53+
}
54+
)
7355
);
74-
75-
authenticator.use(linkedinStrategy, 'linkedin');
7656
```
7757

7858
### Setup your routes
7959

8060
```tsx
8161
// app/routes/login.tsx
62+
import { Form } from "react-router";
63+
8264
export default function Login() {
83-
return (
84-
<Form action="/auth/linkedin" method="post">
85-
<button>Login with Linkedin</button>
86-
</Form>
87-
)
65+
return (
66+
<Form action="/auth/linkedin" method="post">
67+
<button>Login with LinkedIn</button>
68+
</Form>
69+
);
8870
}
8971
```
9072

9173
```tsx
9274
// app/routes/auth/linkedin.tsx
93-
import { ActionFunction, LoaderFunction } from 'remix'
94-
import { authenticator } from '~/linkedin.server'
75+
import type { ActionFunctionArgs } from "react-router";
76+
import { authenticator } from "~/services/auth.server";
77+
78+
export function loader() {
79+
return { message: "This route is not meant to be visited directly." };
80+
}
9581

96-
export let loader: LoaderFunction = () => redirect('/login')
97-
export let action: ActionFunction = ({ request }) => {
98-
return authenticator.authenticate('linkedin', request)
82+
export async function action({ request }: ActionFunctionArgs) {
83+
return await authenticator.authenticate("linkedin", request);
9984
}
10085
```
10186

87+
### Handling the callback
88+
89+
The callback route handles the OAuth flow completion when LinkedIn redirects back to your application:
90+
10291
```tsx
10392
// app/routes/auth/linkedin/callback.tsx
104-
import { ActionFunction, LoaderFunction } from 'remix'
105-
import { authenticator } from '~/linkedin.server'
106-
107-
export let loader: LoaderFunction = ({ request }) => {
108-
return authenticator.authenticate('linkedin', request, {
109-
successRedirect: '/dashboard',
110-
failureRedirect: '/login',
111-
})
93+
import type { LoaderFunctionArgs } from "react-router";
94+
import { authenticator } from "~/services/auth.server";
95+
import { redirect } from "react-router";
96+
import { sessionStorage } from "~/services/session.server";
97+
98+
export async function loader({ request }: LoaderFunctionArgs) {
99+
// Check for required parameters (optional but recommended)
100+
const url = new URL(request.url);
101+
const code = url.searchParams.get('code');
102+
const state = url.searchParams.get('state');
103+
104+
if (!code || !state) {
105+
return redirect('/login?error=missing_params');
106+
}
107+
108+
try {
109+
// Authenticate the user
110+
const user = await authenticator.authenticate("linkedin", request);
111+
112+
// Create session and store the authenticated user
113+
// Reference implementation: https://github.com/sergiodxa/sergiodxa.com/blob/main/app/routes/auth.%24provider.callback.ts
114+
const session = await sessionStorage.getSession(
115+
request.headers.get("cookie")
116+
);
117+
118+
session.set("user", user);
119+
120+
// Prepare headers for the response
121+
const headers = new Headers();
122+
123+
// Add the session cookie to headers
124+
headers.append(
125+
"Set-Cookie",
126+
await sessionStorage.commitSession(session)
127+
);
128+
129+
// Optional: Add a cookie to clear the auth state
130+
// If you're using the stateless approach with commitSession, you don't need this
131+
// headers.append("Set-Cookie", await auth.clear(request));
132+
133+
// Redirect to dashboard with the session cookie
134+
return redirect('/dashboard', { headers });
135+
} catch (error) {
136+
// Handle authentication errors
137+
console.error('Authentication failed', error);
138+
return redirect('/login?error=auth_failed');
139+
}
112140
}
113141
```
142+
143+
144+
145+
## Configuration Options
146+
147+
`LinkedInStrategy` accepts the following configuration options:
148+
149+
```ts
150+
interface LinkedInStrategy.ConstructorOptions {
151+
/**
152+
* The client ID for your LinkedIn application
153+
*/
154+
clientId: string;
155+
156+
/**
157+
* The client secret for your LinkedIn application
158+
*/
159+
clientSecret: string;
160+
161+
/**
162+
* The URL LinkedIn will redirect to after authentication
163+
*/
164+
redirectURI: URL | string;
165+
166+
/**
167+
* The scopes to request from LinkedIn
168+
* @default ["openid", "profile", "email"]
169+
*/
170+
scopes?: LinkedInScope[] | string;
171+
172+
/**
173+
* The name and options for the cookie used to store state
174+
* @default "linkedin"
175+
*/
176+
cookie?: string | (Omit<SetCookieInit, "value"> & { name: string });
177+
}
178+
```
179+
180+
## User Profile
181+
182+
After successful authentication, you'll receive user data in this format:
183+
184+
```ts
185+
interface LinkedInProfile {
186+
id: string;
187+
displayName: string;
188+
name: {
189+
givenName: string;
190+
familyName: string;
191+
};
192+
emails: Array<{ value: string }>;
193+
photos: Array<{ value: string }>;
194+
_json: {
195+
sub: string;
196+
name: string;
197+
given_name: string;
198+
family_name: string;
199+
picture: string;
200+
locale: string;
201+
email: string;
202+
email_verified: boolean;
203+
};
204+
}
205+
```
206+
207+
## Refresh Token
208+
209+
If you need to refresh an access token, you can use the `refreshToken` method:
210+
211+
```ts
212+
const strategy = new LinkedInStrategy(options, verify);
213+
const tokens = await strategy.refreshToken(refreshToken);
214+
```
215+
216+
The most common approach is to store the refresh token in the user data and then update it after refreshing:
217+
218+
```ts
219+
authenticator.use(
220+
new LinkedInStrategy(
221+
options,
222+
async ({ tokens, profile, request }) => {
223+
let user = await findOrCreateUser(profile);
224+
return {
225+
...user,
226+
accessToken: tokens.accessToken(),
227+
refreshToken: tokens.refreshToken ? tokens.refreshToken() : null,
228+
};
229+
}
230+
)
231+
);
232+
233+
// Later in your code you can use it to get new tokens
234+
const tokens = await strategy.refreshToken(user.refreshToken);
235+
```
236+
237+
## Version Compatibility
238+
239+
- Version 3.x is compatible with Remix Auth v4.x
240+
- Version 2.x is compatible with Remix Auth v3.x
241+
- Version 1.x is compatible with Remix Auth v1.x and v2.x
242+
243+
## License
244+
245+
MIT

0 commit comments

Comments
 (0)