@@ -123,41 +123,84 @@ function parseMicroformats(doc: htmlParser.HTMLElement, baseUrl: string, id: str
123123 return { name, logo } ;
124124}
125125
126- // https://indieauth.spec.indieweb.org/#client-information-discovery
127- // "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id,
128- // and if there is an [h-app] with a url property matching the client_id URL,
129- // then it should use the name and icon and display them on the authorization prompt."
130- // (But we don't display any icon for now)
131- // https://indieauth.spec.indieweb.org/#redirect-url
132- // "The client SHOULD publish one or more <link> tags or Link HTTP headers with a rel attribute
133- // of redirect_uri at the client_id URL.
134- // Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST
135- // look for an exact match of the given redirect_uri in the request against the list of
136- // redirect_uris discovered after resolving any relative URLs."
137126async function discoverClientInformation ( logger : Logger , httpRequestService : HttpRequestService , id : string ) : Promise < ClientInformation > {
138127 try {
139128 const res = await httpRequestService . send ( id ) ;
129+
140130 const redirectUris : string [ ] = [ ] ;
131+ let name = id ;
132+ let logo : string | null = null ;
141133
134+ // https://indieauth.spec.indieweb.org/#redirect-url
135+ // "The client SHOULD publish one or more <link> tags or Link HTTP headers with a rel attribute
136+ // of redirect_uri at the client_id URL.
137+ // Authorization endpoints verifying that a redirect_uri is allowed for use by a client MUST
138+ // look for an exact match of the given redirect_uri in the request against the list of
139+ // redirect_uris discovered after resolving any relative URLs."
142140 const linkHeader = res . headers . get ( 'link' ) ;
143141 if ( linkHeader ) {
144142 redirectUris . push ( ...httpLinkHeader . parse ( linkHeader ) . get ( 'rel' , 'redirect_uri' ) . map ( r => r . uri ) ) ;
145143 }
146144
147- const text = await res . text ( ) ;
148- const doc = htmlParser . parse ( `<div>${ text } </div>` ) ;
145+ if ( res . headers . get ( 'content-type' ) ?. includes ( 'application/json' ) ) {
146+ // Client discovery via JSON document (11 July 2024 spec)
147+ // https://indieauth.spec.indieweb.org/#client-metadata
148+ // "Clients SHOULD have a JSON [RFC7159] document at their client_id URL containing
149+ // client metadata defined in [RFC7591], the minimum properties for an IndieAuth
150+ // client defined below."
151+
152+ const json = await res . json ( ) as {
153+ client_id : string ;
154+ client_name ?: string ;
155+ client_uri : string ;
156+ logo_uri ?: string ;
157+ redirect_uris ?: string [ ] ;
158+ } ;
159+
160+ // https://indieauth.spec.indieweb.org/#client-metadata-li-1
161+ // "The authorization server MUST verify that the client_id in the document matches the
162+ // client_id of the URL where the document was retrieved."
163+ if ( json . client_id !== id ) {
164+ throw new AuthorizationError ( 'client_id in the document does not match the client_id URL' , 'invalid_request' ) ;
165+ }
149166
150- redirectUris . push ( ...[ ...doc . querySelectorAll ( 'link[rel=redirect_uri][href]' ) ] . map ( el => el . attributes . href ) ) ;
167+ // https://indieauth.spec.indieweb.org/#client-metadata-li-1
168+ // "The client_uri MUST be a prefix of the client_id."
169+ if ( ! json . client_uri || ! id . startsWith ( json . client_uri ) ) {
170+ throw new AuthorizationError ( 'client_uri is not a prefix of client_id' , 'invalid_request' ) ;
171+ }
151172
152- let name = id ;
153- let logo : string | null = null ;
154- if ( text ) {
155- const microformats = parseMicroformats ( doc , res . url , id ) ;
156- if ( typeof microformats . name === 'string' ) {
157- name = microformats . name ;
173+ if ( typeof json . client_name === 'string' ) {
174+ name = json . client_name ;
158175 }
159- if ( typeof microformats . logo === 'string' ) {
160- logo = microformats . logo ;
176+
177+ if ( typeof json . logo_uri === 'string' ) {
178+ // Since uri can be relative, resolve it against the document URL
179+ logo = new URL ( json . logo_uri , res . url ) . toString ( ) ;
180+ }
181+
182+ if ( Array . isArray ( json . redirect_uris ) ) {
183+ redirectUris . push ( ...json . redirect_uris . filter ( ( uri ) : uri is string => typeof uri === 'string' ) ) ;
184+ }
185+ } else {
186+ // Client discovery via HTML microformats (12 February 2022 spec)
187+ // https://indieauth.spec.indieweb.org/20220212/#client-information-discovery
188+ // "Authorization servers SHOULD support parsing the [h-app] Microformat from the client_id,
189+ // and if there is an [h-app] with a url property matching the client_id URL,
190+ // then it should use the name and icon and display them on the authorization prompt."
191+ const text = await res . text ( ) ;
192+ const doc = htmlParser . parse ( `<div>${ text } </div>` ) ;
193+
194+ redirectUris . push ( ...[ ...doc . querySelectorAll ( 'link[rel=redirect_uri][href]' ) ] . map ( el => el . attributes . href ) ) ;
195+
196+ if ( text ) {
197+ const microformats = parseMicroformats ( doc , res . url , id ) ;
198+ if ( typeof microformats . name === 'string' ) {
199+ name = microformats . name ;
200+ }
201+ if ( typeof microformats . logo === 'string' ) {
202+ logo = microformats . logo ;
203+ }
161204 }
162205 }
163206
@@ -172,6 +215,8 @@ async function discoverClientInformation(logger: Logger, httpRequestService: Htt
172215 logger . error ( 'Error while fetching client information' , { err } ) ;
173216 if ( err instanceof StatusError ) {
174217 throw new AuthorizationError ( 'Failed to fetch client information' , 'invalid_request' ) ;
218+ } else if ( err instanceof AuthorizationError ) {
219+ throw err ;
175220 } else {
176221 throw new AuthorizationError ( 'Failed to parse client information' , 'server_error' ) ;
177222 }
0 commit comments