1919import { Data , Effect , Option , Predicate , Schema } from "effect" ;
2020import * as oauth from "oauth4webapi" ;
2121
22- import type { SubjectTokenType } from "./oauth-client" ;
22+ import type { SubjectTokenType , TokenEndpointAuthMethod } from "./oauth-client" ;
2323
2424// ---------------------------------------------------------------------------
2525// Errors
@@ -842,16 +842,18 @@ const hostnameForTelemetry = (url: string): string => URL.parse(url)?.hostname ?
842842// oauth4webapi adapter helpers
843843// ---------------------------------------------------------------------------
844844
845- export type ClientAuthMethod = "body" | "basic" ;
845+ export type ClientAuthMethod = TokenEndpointAuthMethod ;
846846
847847/**
848848 * The token-endpoint client-auth transport used when a caller doesn't specify
849849 * one. `"body"` is `client_secret_post` (the secret in the form body) — the
850850 * method our DCR registers (`token_endpoint_auth_method: client_secret_post`)
851851 * and the one every confidential client in the v2 model uses. EXPLICIT and
852852 * documented rather than a hidden inline `?? "body"`: callers that need
853- * `client_secret_basic` pass `clientAuth: "basic"`. For PUBLIC clients (no
854- * secret) the method is irrelevant — `pickClientAuth` returns `None()`.
853+ * `client_secret_basic` pass `clientAuth: "basic"`. Providers that reject the
854+ * RFC form encoding can explicitly pass `clientAuth: "basic_raw"`. For PUBLIC
855+ * clients (no secret) the method is irrelevant — `pickClientAuth` returns
856+ * `None()`.
855857 */
856858export const DEFAULT_CLIENT_AUTH_METHOD : ClientAuthMethod = "body" ;
857859
@@ -914,15 +916,29 @@ const oauth4webapiRequestOptions = (
914916// (public PKCE — `None()`, RFC 7636). This is not a silent guess: `loadClient`
915917// persists a non-empty secret for confidential clients and null/"" for public
916918// ones, so an absent secret here unambiguously means "public client". The
917- // `method` only chooses HOW a present secret is sent (post vs basic).
919+ // `method` only chooses HOW a present secret is sent (post vs either Basic
920+ // credential encoding).
921+ const base64BasicCredentials = ( clientId : string , clientSecret : string ) : string => {
922+ const bytes = new TextEncoder ( ) . encode ( `${ clientId } :${ clientSecret } ` ) ;
923+ let binary = "" ;
924+ for ( const byte of bytes ) binary += String . fromCharCode ( byte ) ;
925+ return globalThis . btoa ( binary ) ;
926+ } ;
927+
928+ const rawClientSecretBasic =
929+ ( clientSecret : string ) : oauth . ClientAuth =>
930+ ( _authorizationServer , client , _body , headers ) => {
931+ headers . set ( "authorization" , `Basic ${ base64BasicCredentials ( client . client_id , clientSecret ) } ` ) ;
932+ } ;
933+
918934const pickClientAuth = (
919935 clientSecret : string | null | undefined ,
920936 method : ClientAuthMethod ,
921937) : oauth . ClientAuth => {
922938 if ( ! clientSecret ) return oauth . None ( ) ;
923- return method === "basic"
924- ? oauth . ClientSecretBasic ( clientSecret )
925- : oauth . ClientSecretPost ( clientSecret ) ;
939+ if ( method === "basic" ) return oauth . ClientSecretBasic ( clientSecret ) ;
940+ if ( method === "basic_raw" ) return rawClientSecretBasic ( clientSecret ) ;
941+ return oauth . ClientSecretPost ( clientSecret ) ;
926942} ;
927943
928944const normalizedTokenScope = (
@@ -1122,13 +1138,6 @@ export type ExchangeAuthorizationCodeInput = {
11221138 readonly fetch ?: typeof globalThis . fetch ;
11231139} ;
11241140
1125- const base64BasicCredentials = ( clientId : string , clientSecret : string ) : string => {
1126- const bytes = new TextEncoder ( ) . encode ( `${ clientId } :${ clientSecret } ` ) ;
1127- let binary = "" ;
1128- for ( const byte of bytes ) binary += String . fromCharCode ( byte ) ;
1129- return globalThis . btoa ( binary ) ;
1130- } ;
1131-
11321141const jsonTokenEndpointRequest = async ( input : {
11331142 readonly tokenUrl : string ;
11341143 readonly clientId : string ;
@@ -1149,21 +1158,24 @@ const jsonTokenEndpointRequest = async (input: {
11491158 accept : "application/json" ,
11501159 "content-type" : "application/json" ,
11511160 } ) ;
1152- const confidential = Boolean ( input . clientSecret ) ;
1153- if ( confidential && input . clientAuth === "basic" ) {
1154- headers . set (
1155- "authorization" ,
1156- `Basic ${ base64BasicCredentials ( input . clientId , input . clientSecret ?? "" ) } ` ,
1161+ const clientSecret = input . clientSecret ?? "" ;
1162+ const confidential = clientSecret . length > 0 ;
1163+ if ( confidential && input . clientAuth !== "body" ) {
1164+ await pickClientAuth ( clientSecret , input . clientAuth ) (
1165+ asFromTokenUrl ( tokenUrl , input . endpointUrlPolicy ) ,
1166+ { client_id : input . clientId } ,
1167+ new URLSearchParams ( ) ,
1168+ headers ,
11571169 ) ;
11581170 }
11591171 const body = {
11601172 grant_type : input . grantType ,
11611173 ...input . parameters ,
1162- ...( confidential && input . clientAuth === "basic "
1174+ ...( confidential && input . clientAuth !== "body "
11631175 ? { }
11641176 : {
11651177 client_id : input . clientId ,
1166- ...( confidential ? { client_secret : input . clientSecret ?? "" } : { } ) ,
1178+ ...( confidential ? { client_secret : clientSecret } : { } ) ,
11671179 } ) ,
11681180 } ;
11691181 // oxlint-disable-next-line executor/no-raw-fetch -- boundary: provider token exchange is the SDK's HTTP boundary and preserves its injected fetch seam
0 commit comments