11import { Cookie , SetCookie , type SetCookieInit } from "@mjackson/headers" ;
2+ import {
3+ LinkedIn ,
4+ OAuth2RequestError ,
5+ type OAuth2Tokens ,
6+ UnexpectedErrorResponseBodyError ,
7+ UnexpectedResponseError ,
8+ generateState ,
9+ } from "arctic" ;
210import createDebug from "debug" ;
311import { Strategy } from "remix-auth/strategy" ;
412import { redirect } from "./lib/redirect.js" ;
513
14+ type URLConstructor = ConstructorParameters < typeof URL > [ 0 ] ;
15+
616const debug = createDebug ( "LinkedInStrategy" ) ;
717
818export type LinkedInScope = "openid" | "profile" | "email" ;
919
20+ export {
21+ OAuth2RequestError ,
22+ UnexpectedResponseError ,
23+ UnexpectedErrorResponseBodyError ,
24+ } ;
25+
1026/**
1127 * This type declares what configuration the strategy needs from the
1228 * developer to correctly work.
@@ -66,11 +82,19 @@ export class LinkedInStrategy<User> extends Strategy<
6682> {
6783 name = LinkedInStrategyDefaultName ;
6884
85+ protected client : LinkedIn ;
86+
6987 constructor (
7088 protected options : LinkedInStrategy . ConstructorOptions ,
7189 verify : Strategy . VerifyFunction < User , LinkedInStrategy . VerifyOptions > ,
7290 ) {
7391 super ( verify ) ;
92+
93+ this . client = new LinkedIn (
94+ options . clientId ,
95+ options . clientSecret ,
96+ options . redirectURI . toString ( ) ,
97+ ) ;
7498 }
7599
76100 private get cookieName ( ) {
@@ -100,112 +124,76 @@ export class LinkedInStrategy<User> extends Strategy<
100124 debug ( "Request URL" , request . url ) ;
101125
102126 let url = new URL ( request . url ) ;
103- let code = url . searchParams . get ( "code" ) ;
104- let state = url . searchParams . get ( "state" ) ;
127+
128+ let stateUrl = url . searchParams . get ( "state" ) ;
105129 let error = url . searchParams . get ( "error" ) ;
106130
107131 if ( error ) {
108132 let description = url . searchParams . get ( "error_description" ) ;
109133 let uri = url . searchParams . get ( "error_uri" ) ;
110- throw new Error ( `LinkedIn OAuth error: ${ error } , ${ description } , ${ uri } ` ) ;
134+ throw new OAuth2RequestError ( error , description , uri , stateUrl ) ;
111135 }
112136
113- if ( ! state ) {
137+ if ( ! stateUrl ) {
114138 debug ( "No state found in the URL, redirecting to authorization endpoint" ) ;
115139
116- // Generate a random state
117- let newState = crypto . randomUUID ( ) ;
118- debug ( "Generated State" , newState ) ;
140+ let state = generateState ( ) ;
119141
120- // Create authorization URL
121- let authorizationURL = new URL (
122- "https://www.linkedin.com/oauth/v2/authorization" ,
123- ) ;
124- authorizationURL . searchParams . set ( "client_id" , this . options . clientId ) ;
125- authorizationURL . searchParams . set (
126- "redirect_uri" ,
127- this . options . redirectURI . toString ( ) ,
128- ) ;
129- authorizationURL . searchParams . set ( "response_type" , "code" ) ;
130- authorizationURL . searchParams . set (
131- "scope" ,
132- this . getScope ( this . options . scopes ) ,
142+ debug ( "Generated State" , state ) ;
143+
144+ let url = this . client . createAuthorizationURL (
145+ state ,
146+ Array . isArray ( this . options . scopes )
147+ ? this . options . scopes
148+ : this . options . scopes
149+ ? ( this . options . scopes . split (
150+ LinkedInStrategyScopeSeparator ,
151+ ) as LinkedInScope [ ] )
152+ : ( [ "openid" , "profile" , "email" ] as LinkedInScope [ ] ) ,
133153 ) ;
134- authorizationURL . searchParams . set ( "state" , newState ) ;
135154
136- // Add any additional params
137- const params = this . authorizationParams (
138- authorizationURL . searchParams ,
155+ url . search = this . authorizationParams (
156+ url . searchParams ,
139157 request ,
140- ) ;
141- authorizationURL . search = params . toString ( ) ;
158+ ) . toString ( ) ;
142159
143- debug ( "Authorization URL" , authorizationURL . toString ( ) ) ;
160+ debug ( "Authorization URL" , url . toString ( ) ) ;
144161
145- // Set cookie with state for verification later
146162 let header = new SetCookie ( {
147163 name : this . cookieName ,
148- value : new URLSearchParams ( { state : newState } ) . toString ( ) ,
149- httpOnly : true ,
164+ value : new URLSearchParams ( { state } ) . toString ( ) ,
165+ httpOnly : true , // Prevents JavaScript from accessing the cookie
150166 maxAge : 60 * 5 , // 5 minutes
151- path : "/" ,
152- sameSite : "Lax" ,
167+ path : "/" , // Allow the cookie to be sent to any path
168+ sameSite : "Lax" , // Prevents it from being sent in cross-site requests
153169 ...this . cookieOptions ,
154170 } ) ;
155171
156- throw redirect ( authorizationURL . toString ( ) , {
172+ throw redirect ( url . toString ( ) , {
157173 headers : { "Set-Cookie" : header . toString ( ) } ,
158174 } ) ;
159175 }
160176
177+ let code = url . searchParams . get ( "code" ) ;
178+
161179 if ( ! code ) throw new ReferenceError ( "Missing code in the URL" ) ;
162180
163- // Verify state
164181 let cookie = new Cookie ( request . headers . get ( "cookie" ) ?? "" ) ;
165182 let params = new URLSearchParams ( cookie . get ( this . cookieName ) || "" ) ;
166183
167184 if ( ! params . has ( "state" ) ) {
168185 throw new ReferenceError ( "Missing state on cookie." ) ;
169186 }
170187
171- if ( params . get ( "state" ) !== state ) {
188+ if ( params . get ( "state" ) !== stateUrl ) {
172189 throw new RangeError ( "State in URL doesn't match state in cookie." ) ;
173190 }
174191
175192 debug ( "Validating authorization code" ) ;
193+ let tokens = await this . client . validateAuthorizationCode ( code ) ;
176194
177- // Exchange code for tokens
178- const formData = new URLSearchParams ( ) ;
179- formData . set ( "client_id" , this . options . clientId ) ;
180- formData . set ( "client_secret" , this . options . clientSecret ) ;
181- formData . set ( "grant_type" , "authorization_code" ) ;
182- formData . set ( "code" , code ) ;
183- formData . set ( "redirect_uri" , this . options . redirectURI . toString ( ) ) ;
184-
185- const tokenResponse = await fetch (
186- "https://www.linkedin.com/oauth/v2/accessToken" ,
187- {
188- method : "POST" ,
189- headers : { "Content-Type" : "application/x-www-form-urlencoded" } ,
190- body : formData ,
191- } ,
192- ) ;
193-
194- if ( ! tokenResponse . ok ) {
195- const error = await tokenResponse . text ( ) ;
196- throw new Error ( `Failed to get access token: ${ error } ` ) ;
197- }
198-
199- const tokens = ( await tokenResponse . json ( ) ) as {
200- access_token : string ;
201- token_type : string ;
202- expires_in : number ;
203- refresh_token ?: string ;
204- scope ?: string ;
205- } ;
206-
207- // Get user profile
208- const profile = await this . userProfile ( tokens . access_token ) ;
195+ debug ( "Fetching user profile" ) ;
196+ let profile = await this . userProfile ( tokens . accessToken ( ) ) ;
209197
210198 debug ( "Verifying the user profile" ) ;
211199 let user = await this . verify ( { request, tokens, profile } ) ;
@@ -216,6 +204,12 @@ export class LinkedInStrategy<User> extends Strategy<
216204
217205 /**
218206 * Return extra parameters to be included in the authorization request.
207+ *
208+ * Some OAuth 2.0 providers allow additional, non-standard parameters to be
209+ * included when requesting authorization. Since these parameters are not
210+ * standardized by the OAuth 2.0 specification, OAuth 2.0-based authentication
211+ * strategies can override this function in order to populate these
212+ * parameters as required by the provider.
219213 */
220214 protected authorizationParams (
221215 params : URLSearchParams ,
@@ -256,30 +250,18 @@ export class LinkedInStrategy<User> extends Strategy<
256250 }
257251
258252 /**
259- * Refresh the access token using a refresh token
253+ * Get a new OAuth2 Tokens object using the refresh token once the previous
254+ * access token has expired.
255+ * @param refreshToken The refresh token to use to get a new access token
256+ * @returns The new OAuth2 tokens object
257+ * @example
258+ * ```ts
259+ * let tokens = await strategy.refreshToken(refreshToken);
260+ * console.log(tokens.accessToken());
261+ * ```
260262 */
261- public async refreshToken ( refreshToken : string ) {
262- const formData = new URLSearchParams ( ) ;
263- formData . set ( "client_id" , this . options . clientId ) ;
264- formData . set ( "client_secret" , this . options . clientSecret ) ;
265- formData . set ( "grant_type" , "refresh_token" ) ;
266- formData . set ( "refresh_token" , refreshToken ) ;
267-
268- const response = await fetch (
269- "https://www.linkedin.com/oauth/v2/accessToken" ,
270- {
271- method : "POST" ,
272- headers : { "Content-Type" : "application/x-www-form-urlencoded" } ,
273- body : formData ,
274- } ,
275- ) ;
276-
277- if ( ! response . ok ) {
278- const error = await response . text ( ) ;
279- throw new Error ( `Failed to refresh token: ${ error } ` ) ;
280- }
281-
282- return response . json ( ) ;
263+ public refreshToken ( refreshToken : string ) {
264+ return this . client . refreshAccessToken ( refreshToken ) ;
283265 }
284266}
285267
@@ -288,13 +270,7 @@ export namespace LinkedInStrategy {
288270 /** The request that triggered the verification flow */
289271 request : Request ;
290272 /** The OAuth2 tokens retrieved from LinkedIn */
291- tokens : {
292- access_token : string ;
293- token_type : string ;
294- expires_in : number ;
295- refresh_token ?: string ;
296- scope ?: string ;
297- } ;
273+ tokens : OAuth2Tokens ;
298274 /** The LinkedIn profile */
299275 profile : LinkedInProfile ;
300276 }
@@ -319,7 +295,7 @@ export namespace LinkedInStrategy {
319295 /**
320296 * The URL of your application where LinkedIn will redirect after authentication
321297 */
322- redirectURI : URL | string ;
298+ redirectURI : URLConstructor ;
323299
324300 /**
325301 * The scopes you want to request from LinkedIn
0 commit comments