@@ -61,6 +61,10 @@ export interface MicrosoftGraphSpecBuild {
6161 readonly authenticationTemplate : readonly Authentication [ ] ;
6262}
6363
64+ export interface MicrosoftGraphUrlPolicy {
65+ readonly allowUnsafeUrlOverrides ?: boolean ;
66+ }
67+
6468export type MicrosoftGraphIntegrationConfig = OpenApiIntegrationConfig & {
6569 readonly microsoftGraphPresetIds ?: readonly string [ ] ;
6670 readonly microsoftGraphCustomScopes ?: readonly string [ ] ;
@@ -180,6 +184,154 @@ const BASE_OAUTH_SCOPES = new Set(["offline_access", "openid", "profile", "email
180184const firstString = ( values : readonly unknown [ ] ) : string | undefined =>
181185 values . find ( ( value ) : value is string => typeof value === "string" && value . trim ( ) . length > 0 ) ;
182186
187+ const parseTrustedHttpsUrl = ( value : string ) : URL | null => {
188+ if ( ! URL . canParse ( value ) ) return null ;
189+ const parsed = new URL ( value ) ;
190+ if ( parsed . protocol !== "https:" || parsed . username || parsed . password || parsed . hash ) {
191+ return null ;
192+ }
193+ return parsed ;
194+ } ;
195+
196+ const allowUnsafeUrl = (
197+ value : string | undefined ,
198+ policy : MicrosoftGraphUrlPolicy | undefined ,
199+ ) : string | undefined | null => {
200+ if ( ! value ) return undefined ;
201+ if ( policy ?. allowUnsafeUrlOverrides !== true ) return null ;
202+ return parseTrustedHttpsUrl ( value ) ? value : null ;
203+ } ;
204+
205+ const normalizeMicrosoftGraphSpecUrl = (
206+ value : string ,
207+ policy ?: MicrosoftGraphUrlPolicy ,
208+ ) : string | null => {
209+ if ( value === MICROSOFT_GRAPH_OPENAPI_URL ) return value ;
210+ return allowUnsafeUrl ( value , policy ) ?? null ;
211+ } ;
212+
213+ const MICROSOFT_GRAPH_HOSTS = new Set ( [
214+ "graph.microsoft.com" ,
215+ "graph.microsoft.us" ,
216+ "dod-graph.microsoft.us" ,
217+ "microsoftgraph.chinacloudapi.cn" ,
218+ ] ) ;
219+
220+ const normalizeMicrosoftGraphBaseUrl = (
221+ value : string | undefined ,
222+ policy ?: MicrosoftGraphUrlPolicy ,
223+ ) : string | undefined | null => {
224+ const unsafe = allowUnsafeUrl ( value , policy ) ;
225+ if ( unsafe !== null ) return unsafe ;
226+ if ( ! value ) return undefined ;
227+ const parsed = parseTrustedHttpsUrl ( value ) ;
228+ if ( ! parsed || ! MICROSOFT_GRAPH_HOSTS . has ( parsed . hostname . toLowerCase ( ) ) ) return null ;
229+ if ( ! / ^ \/ (?: v 1 \. 0 | b e t a ) (?: \/ ) ? $ / . test ( parsed . pathname ) ) return null ;
230+ if ( parsed . search ) return null ;
231+ return parsed . toString ( ) . replace ( / \/ $ / , "" ) ;
232+ } ;
233+
234+ const MICROSOFT_IDENTITY_HOSTS = new Set ( [
235+ "login.microsoftonline.com" ,
236+ "login.microsoftonline.us" ,
237+ "login.partner.microsoftonline.cn" ,
238+ ] ) ;
239+
240+ const normalizeMicrosoftOAuthEndpointUrl = (
241+ value : string ,
242+ endpoint : "authorize" | "token" ,
243+ policy ?: MicrosoftGraphUrlPolicy ,
244+ ) : string | null => {
245+ const unsafe = allowUnsafeUrl ( value , policy ) ;
246+ if ( unsafe !== null ) return unsafe ?? null ;
247+ const parsed = parseTrustedHttpsUrl ( value ) ;
248+ if ( ! parsed || ! MICROSOFT_IDENTITY_HOSTS . has ( parsed . hostname . toLowerCase ( ) ) ) return null ;
249+ if ( parsed . search ) return null ;
250+ const suffix = endpoint === "authorize" ? "authorize" : "token" ;
251+ return / ^ \/ [ ^ / ] + \/ o a u t h 2 \/ v 2 \. 0 \/ (?: a u t h o r i z e | t o k e n ) $ / . test ( parsed . pathname ) &&
252+ parsed . pathname . endsWith ( `/${ suffix } ` )
253+ ? parsed . toString ( )
254+ : null ;
255+ } ;
256+
257+ const validateSelectionUrls = (
258+ selection : ReturnType < typeof normalizeSelection > ,
259+ policy ?: MicrosoftGraphUrlPolicy ,
260+ ) : Effect . Effect < ReturnType < typeof normalizeSelection > , OpenApiParseError > =>
261+ Effect . gen ( function * ( ) {
262+ const specUrl = normalizeMicrosoftGraphSpecUrl ( selection . specUrl , policy ) ;
263+ if ( ! specUrl ) {
264+ return yield * new OpenApiParseError ( {
265+ message : "Microsoft Graph specUrl must point to the trusted Microsoft Graph OpenAPI source" ,
266+ } ) ;
267+ }
268+ const baseUrl = normalizeMicrosoftGraphBaseUrl ( selection . baseUrl , policy ) ;
269+ if ( baseUrl === null ) {
270+ return yield * new OpenApiParseError ( {
271+ message : "Microsoft Graph baseUrl must point to a supported Microsoft Graph endpoint" ,
272+ } ) ;
273+ }
274+ const authorizationUrl = selection . authorizationUrl
275+ ? normalizeMicrosoftOAuthEndpointUrl ( selection . authorizationUrl , "authorize" , policy )
276+ : undefined ;
277+ if ( selection . authorizationUrl && ! authorizationUrl ) {
278+ return yield * new OpenApiParseError ( {
279+ message : "Microsoft authorizationUrl must point to a supported Microsoft identity endpoint" ,
280+ } ) ;
281+ }
282+ const tokenUrl = selection . tokenUrl
283+ ? normalizeMicrosoftOAuthEndpointUrl ( selection . tokenUrl , "token" , policy )
284+ : undefined ;
285+ if ( selection . tokenUrl && ! tokenUrl ) {
286+ return yield * new OpenApiParseError ( {
287+ message : "Microsoft tokenUrl must point to a supported Microsoft identity endpoint" ,
288+ } ) ;
289+ }
290+ const clientCredentialsTokenUrl = selection . clientCredentialsTokenUrl
291+ ? normalizeMicrosoftOAuthEndpointUrl ( selection . clientCredentialsTokenUrl , "token" , policy )
292+ : undefined ;
293+ if ( selection . clientCredentialsTokenUrl && ! clientCredentialsTokenUrl ) {
294+ return yield * new OpenApiParseError ( {
295+ message :
296+ "Microsoft clientCredentialsTokenUrl must point to a supported Microsoft identity endpoint" ,
297+ } ) ;
298+ }
299+ return {
300+ ...selection ,
301+ specUrl,
302+ ...( baseUrl ? { baseUrl } : { baseUrl : undefined } ) ,
303+ ...( authorizationUrl ? { authorizationUrl } : { authorizationUrl : undefined } ) ,
304+ ...( tokenUrl ? { tokenUrl } : { tokenUrl : undefined } ) ,
305+ ...( clientCredentialsTokenUrl
306+ ? { clientCredentialsTokenUrl }
307+ : { clientCredentialsTokenUrl : undefined } ) ,
308+ } ;
309+ } ) ;
310+
311+ const validateResolvedOAuthEndpoints = (
312+ endpoints : MicrosoftOAuthEndpoints ,
313+ policy ?: MicrosoftGraphUrlPolicy ,
314+ ) : Effect . Effect < MicrosoftOAuthEndpoints , OpenApiParseError > =>
315+ Effect . gen ( function * ( ) {
316+ const authorizationUrl = normalizeMicrosoftOAuthEndpointUrl (
317+ endpoints . authorizationUrl ,
318+ "authorize" ,
319+ policy ,
320+ ) ;
321+ const tokenUrl = normalizeMicrosoftOAuthEndpointUrl ( endpoints . tokenUrl , "token" , policy ) ;
322+ const clientCredentialsTokenUrl = normalizeMicrosoftOAuthEndpointUrl (
323+ endpoints . clientCredentialsTokenUrl ,
324+ "token" ,
325+ policy ,
326+ ) ;
327+ if ( ! authorizationUrl || ! tokenUrl || ! clientCredentialsTokenUrl ) {
328+ return yield * new OpenApiParseError ( {
329+ message : "Microsoft OAuth endpoints must point to supported Microsoft identity endpoints" ,
330+ } ) ;
331+ }
332+ return { authorizationUrl, tokenUrl, clientCredentialsTokenUrl } ;
333+ } ) ;
334+
183335const recordValues = ( value : unknown ) : readonly unknown [ ] =>
184336 isRecord ( value ) ? Object . values ( value ) : [ ] ;
185337
@@ -490,9 +642,10 @@ const streamSelectedScopes = (
490642export const buildMicrosoftGraphOpenApiSpec = (
491643 input : MicrosoftGraphSelectionInput ,
492644 httpClientLayer : Layer . Layer < HttpClient . HttpClient , never , never > ,
645+ urlPolicy ?: MicrosoftGraphUrlPolicy ,
493646) : Effect . Effect < MicrosoftGraphSpecBuild , OpenApiParseError > =>
494647 Effect . gen ( function * ( ) {
495- const selection = normalizeSelection ( input ) ;
648+ const selection = yield * validateSelectionUrls ( normalizeSelection ( input ) , urlPolicy ) ;
496649 const sourceText = yield * fetchMicrosoftGraphOpenApiSpec ( selection . specUrl ) . pipe (
497650 Effect . provide ( httpClientLayer ) ,
498651 ) ;
@@ -512,7 +665,10 @@ export const buildMicrosoftGraphOpenApiSpec = (
512665 // Head + small components (servers + securitySchemes) parse cheaply and
513666 // carry everything `resolveOAuthEndpoints` needs.
514667 const headDoc = { ...parseHead ( structure ) , components : parseSmallComponents ( structure ) } ;
515- const endpoints = resolveOAuthEndpoints ( headDoc , selection ) ;
668+ const endpoints = yield * validateResolvedOAuthEndpoints (
669+ resolveOAuthEndpoints ( headDoc , selection ) ,
670+ urlPolicy ,
671+ ) ;
516672
517673 const permissionsReference =
518674 selection . coversFullGraph === true
0 commit comments