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// @needsAudit 9/** 10 * Given an OpenID Connect issuer URL, this will fetch and return the [`DiscoveryDocument`](#discoverydocument) 11 * (a collection of URLs) from the resource provider. 12 * 13 * @param issuerOrDiscovery URL using the `https` scheme with no query or fragment component that the OP asserts as its Issuer Identifier. 14 * @return Returns `null` until the [`DiscoveryDocument`](#discoverydocument) has been fetched from the provided issuer URL. 15 * 16 * @example 17 * ```ts 18 * const discovery = useAutoDiscovery('https://example.com/auth'); 19 * ``` 20 */ 21export function useAutoDiscovery(issuerOrDiscovery: IssuerOrDiscovery): DiscoveryDocument | null { 22 const [discovery, setDiscovery] = useState<DiscoveryDocument | null>(null); 23 24 useEffect(() => { 25 let isAllowed = true; 26 resolveDiscoveryAsync(issuerOrDiscovery).then((discovery) => { 27 if (isAllowed) { 28 setDiscovery(discovery); 29 } 30 }); 31 32 return () => { 33 isAllowed = false; 34 }; 35 }, [issuerOrDiscovery]); 36 37 return discovery; 38} 39 40export function useLoadedAuthRequest( 41 config: AuthRequestConfig, 42 discovery: DiscoveryDocument | null, 43 AuthRequestInstance: typeof AuthRequest 44): AuthRequest | null { 45 const [request, setRequest] = useState<AuthRequest | null>(null); 46 const scopeString = useMemo(() => config.scopes?.join(','), [config.scopes]); 47 const extraParamsString = useMemo( 48 () => JSON.stringify(config.extraParams || {}), 49 [config.extraParams] 50 ); 51 useEffect(() => { 52 let isMounted = true; 53 54 if (discovery) { 55 const request = new AuthRequestInstance(config); 56 request.makeAuthUrlAsync(discovery).then(() => { 57 if (isMounted) { 58 setRequest(request); 59 } 60 }); 61 } 62 return () => { 63 isMounted = false; 64 }; 65 }, [ 66 discovery?.authorizationEndpoint, 67 config.clientId, 68 config.redirectUri, 69 config.responseType, 70 config.prompt, 71 config.clientSecret, 72 config.codeChallenge, 73 config.state, 74 config.usePKCE, 75 scopeString, 76 extraParamsString, 77 ]); 78 return request; 79} 80 81type PromptMethod = (options?: AuthRequestPromptOptions) => Promise<AuthSessionResult>; 82 83export function useAuthRequestResult( 84 request: AuthRequest | null, 85 discovery: DiscoveryDocument | null, 86 customOptions: AuthRequestPromptOptions = {} 87): [AuthSessionResult | null, PromptMethod] { 88 const [result, setResult] = useState<AuthSessionResult | null>(null); 89 90 const promptAsync = useCallback( 91 async ({ windowFeatures = {}, ...options }: AuthRequestPromptOptions = {}) => { 92 if (!discovery || !request) { 93 throw new Error('Cannot prompt to authenticate until the request has finished loading.'); 94 } 95 const inputOptions = { 96 ...customOptions, 97 ...options, 98 windowFeatures: { 99 ...(customOptions.windowFeatures ?? {}), 100 ...windowFeatures, 101 }, 102 }; 103 const result = await request?.promptAsync(discovery, inputOptions); 104 setResult(result); 105 return result; 106 }, 107 [request?.url, discovery?.authorizationEndpoint] 108 ); 109 110 return [result, promptAsync]; 111} 112 113// @needsAudit 114/** 115 * Load an authorization request for a code. When the prompt method completes then the response will be fulfilled. 116 * 117 * > In order to close the popup window on web, you need to invoke `WebBrowser.maybeCompleteAuthSession()`. 118 * > See the [Identity example](/guides/authentication#identityserver-4) for more info. 119 * 120 * If an Implicit grant flow was used, you can pass the `response.params` to `TokenResponse.fromQueryParams()` 121 * to get a `TokenResponse` instance which you can use to easily refresh the token. 122 * 123 * @param config A valid [`AuthRequestConfig`](#authrequestconfig) that specifies what provider to use. 124 * @param discovery A loaded [`DiscoveryDocument`](#discoverydocument) with endpoints used for authenticating. 125 * Only `authorizationEndpoint` is required for requesting an authorization code. 126 * 127 * @return Returns a loaded request, a response, and a prompt method in a single array in the following order: 128 * - `request` - An instance of [`AuthRequest`](#authrequest) that can be used to prompt the user for authorization. 129 * This will be `null` until the auth request has finished loading. 130 * - `response` - This is `null` until `promptAsync` has been invoked. Once fulfilled it will return information about the authorization. 131 * - `promptAsync` - When invoked, a web browser will open up and prompt the user for authentication. 132 * Accepts an [`AuthRequestPromptOptions`](#authrequestpromptoptions) object with options about how the prompt will execute. 133 * 134 * @example 135 * ```ts 136 * const [request, response, promptAsync] = useAuthRequest({ ... }, { ... }); 137 * ``` 138 */ 139export function useAuthRequest( 140 config: AuthRequestConfig, 141 discovery: DiscoveryDocument | null 142): [ 143 AuthRequest | null, 144 AuthSessionResult | null, 145 (options?: AuthRequestPromptOptions) => Promise<AuthSessionResult>, 146] { 147 const request = useLoadedAuthRequest(config, discovery, AuthRequest); 148 const [result, promptAsync] = useAuthRequestResult(request, discovery); 149 return [request, result, promptAsync]; 150} 151