@@ -191,6 +191,20 @@ export const createPkceCodeChallenge = (verifier: string): Promise<string> =>
191191 * and redeemed by `oauth.complete`. */
192192export const createOAuthState = ( ) : string => oauth . generateRandomState ( ) ;
193193
194+ /** LinkedIn's standard confidential web flow rejects token requests that
195+ * include PKCE material. Its separate native endpoint supports PKCE, so keep
196+ * the exception tied to the documented web authorization endpoint and only
197+ * apply it when the client has a secret. */
198+ export const shouldUsePkce = ( authorizationUrl : string , clientSecret ?: string | null ) : boolean => {
199+ if ( ! clientSecret ) return true ;
200+ if ( ! URL . canParse ( authorizationUrl ) ) return true ;
201+ const url = new URL ( authorizationUrl ) ;
202+ return ! (
203+ url . origin . toLowerCase ( ) === "https://www.linkedin.com" &&
204+ url . pathname === "/oauth/v2/authorization"
205+ ) ;
206+ } ;
207+
194208// ---------------------------------------------------------------------------
195209// Authorization URL builder
196210// ---------------------------------------------------------------------------
@@ -202,7 +216,7 @@ export type BuildAuthorizationUrlInput = {
202216 readonly scopes : readonly string [ ] ;
203217 readonly state : string ;
204218 /** Pre-computed base64url S256 challenge (from `createPkceCodeChallenge`). */
205- readonly codeChallenge : string ;
219+ readonly codeChallenge ? : string ;
206220 /** Separator between scopes. RFC 6749 says space; some providers use comma. */
207221 readonly scopeSeparator ?: string ;
208222 /** RFC 8707 Resource Indicator. MCP Authorization 2025-06-18 §"Resource
@@ -235,8 +249,13 @@ export const buildAuthorizationUrl = (input: BuildAuthorizationUrlInput): string
235249 url . searchParams . set ( "scope" , input . scopes . join ( separator ) ) ;
236250 }
237251 url . searchParams . set ( "state" , input . state ) ;
238- url . searchParams . set ( "code_challenge_method" , "S256" ) ;
239- url . searchParams . set ( "code_challenge" , input . codeChallenge ) ;
252+ if ( input . codeChallenge ) {
253+ url . searchParams . set ( "code_challenge_method" , "S256" ) ;
254+ url . searchParams . set ( "code_challenge" , input . codeChallenge ) ;
255+ } else {
256+ url . searchParams . delete ( "code_challenge_method" ) ;
257+ url . searchParams . delete ( "code_challenge" ) ;
258+ }
240259 if ( input . resource ) {
241260 url . searchParams . set ( "resource" , input . resource ) ;
242261 }
@@ -1213,7 +1232,7 @@ export type ExchangeAuthorizationCodeInput = {
12131232 readonly clientId : string ;
12141233 readonly clientSecret ?: string | null ;
12151234 readonly redirectUrl : string ;
1216- readonly codeVerifier : string ;
1235+ readonly codeVerifier ? : string ;
12171236 readonly code : string ;
12181237 readonly clientAuth ?: ClientAuthMethod ;
12191238 /** Encoding required by the provider's token endpoint. OAuth defaults to
@@ -1300,8 +1319,10 @@ export const exchangeAuthorizationCode = (
13001319 const params = new URLSearchParams ( {
13011320 code : input . code ,
13021321 redirect_uri : input . redirectUrl ,
1303- code_verifier : input . codeVerifier ,
13041322 } ) ;
1323+ if ( input . codeVerifier ) {
1324+ params . set ( "code_verifier" , input . codeVerifier ) ;
1325+ }
13051326 if ( input . resource ) {
13061327 params . set ( "resource" , input . resource ) ;
13071328 }
0 commit comments