1import { useCallback, useMemo, useEffect, useState } from 'react'; 2 3import { AuthRequest } from './AuthRequest'; 4import { AuthRequestConfig, AuthRequestPromptOptions } from './AuthRequest.types'; 5import { AuthSessionResult } from './AuthSession.types'; 6import { DiscoveryDocument, IssuerOrDiscovery, resolveDiscoveryAsync } from './Discovery'; 7 8/** 9 * Fetch the discovery document from an OpenID Connect issuer. 10 * 11 * @param issuerOrDiscovery 12 */ 13export function useAutoDiscovery(issuerOrDiscovery: IssuerOrDiscovery): DiscoveryDocument | null { 14 const [discovery, setDiscovery] = useState<DiscoveryDocument | null>(null); 15 16 useEffect(() => { 17 let isAllowed = true; 18 resolveDiscoveryAsync(issuerOrDiscovery).then(discovery => { 19 if (isAllowed) { 20 setDiscovery(discovery); 21 } 22 }); 23 24 return () => { 25 isAllowed = false; 26 }; 27 }, [issuerOrDiscovery]); 28 29 return discovery; 30} 31 32export function useLoadedAuthRequest( 33 config: AuthRequestConfig, 34 discovery: DiscoveryDocument | null, 35 AuthRequestInstance: typeof AuthRequest 36): AuthRequest | null { 37 const [request, setRequest] = useState<AuthRequest | null>(null); 38 const scopeString = useMemo(() => config.scopes?.join(','), [config.scopes]); 39 const extraParamsString = useMemo(() => JSON.stringify(config.extraParams || {}), [ 40 config.extraParams, 41 ]); 42 useEffect( 43 () => { 44 let isMounted = true; 45 46 if (discovery) { 47 const request = new AuthRequestInstance(config); 48 request.makeAuthUrlAsync(discovery).then(() => { 49 if (isMounted) { 50 // @ts-ignore 51 setRequest(request); 52 } 53 }); 54 } 55 return () => { 56 isMounted = false; 57 }; 58 }, 59 // eslint-disable-next-line react-hooks/exhaustive-deps 60 [ 61 discovery?.authorizationEndpoint, 62 config.clientId, 63 config.redirectUri, 64 config.responseType, 65 config.prompt, 66 config.clientSecret, 67 config.codeChallenge, 68 config.state, 69 config.usePKCE, 70 scopeString, 71 extraParamsString, 72 ] 73 ); 74 return request; 75} 76 77type PromptMethod = (options?: AuthRequestPromptOptions) => Promise<AuthSessionResult>; 78 79export function useAuthRequestResult( 80 request: AuthRequest | null, 81 discovery: DiscoveryDocument | null, 82 customOptions: AuthRequestPromptOptions = {} 83): [AuthSessionResult | null, PromptMethod] { 84 const [result, setResult] = useState<AuthSessionResult | null>(null); 85 86 const promptAsync = useCallback( 87 async ({ windowFeatures = {}, ...options }: AuthRequestPromptOptions = {}) => { 88 if (!discovery || !request) { 89 throw new Error('Cannot prompt to authenticate until the request has finished loading.'); 90 } 91 const inputOptions = { 92 ...customOptions, 93 ...options, 94 windowFeatures: { 95 ...(customOptions.windowFeatures ?? {}), 96 ...windowFeatures, 97 }, 98 }; 99 const result = await request?.promptAsync(discovery, inputOptions); 100 setResult(result); 101 return result; 102 }, 103 // eslint-disable-next-line react-hooks/exhaustive-deps 104 [request?.url, discovery?.authorizationEndpoint] 105 ); 106 107 return [result, promptAsync]; 108} 109 110/** 111 * Load an authorization request. 112 * Returns a loaded request, a response, and a prompt method. 113 * When the prompt method completes then the response will be fulfilled. 114 * 115 * @param config 116 * @param discovery 117 */ 118export function useAuthRequest( 119 config: AuthRequestConfig, 120 discovery: DiscoveryDocument | null 121): [ 122 AuthRequest | null, 123 AuthSessionResult | null, 124 (options?: AuthRequestPromptOptions) => Promise<AuthSessionResult> 125] { 126 const request = useLoadedAuthRequest(config, discovery, AuthRequest); 127 const [result, promptAsync] = useAuthRequestResult(request, discovery); 128 return [request, result, promptAsync]; 129} 130