UNPKG

@criipto/verify-expo

Version:

Accept MitID, NemID, Swedish BankID, Norwegian BankID and more logins in your Expo (React-Native) app

199 lines 9.02 kB
import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { Platform } from 'react-native'; import TextEncoding from 'text-encoding'; import * as crypto from 'expo-crypto'; import Constants, { ExecutionEnvironment } from 'expo-constants'; import { decode, encode } from 'base-64'; import jwtDecode from 'jwt-decode'; import * as WebBrowser from 'expo-web-browser'; import * as Linking from 'expo-linking'; import 'react-native-url-polyfill/auto'; import { buildAuthorizeURL, codeExchange, generatePlatformPKCE, OpenIDConfigurationManager } from '@criipto/oidc'; import CriiptoVerifyContext, { OAuth2Error, UserCancelledError } from './context'; import { createMemoryStorage } from './memory-storage'; import { SwedishBankIDTransaction, DanishMitIDTransaction } from './transaction'; import useAppState from './hooks/useAppState'; import * as CriiptoVerifyExpoModule from './CriiptoVerifyExpoModule'; if (typeof global.TextEncoder === "undefined") { global.TextEncoder = TextEncoding.TextEncoder; } if (!global.btoa) { global.btoa = encode; } if (!global.atob) { global.atob = decode; } function generatePKCE() { return generatePlatformPKCE({ getRandomValues: crypto.getRandomValues.bind(crypto), subtle: { digest: crypto.digest.bind(crypto) } }); } const CriiptoVerifyProvider = (props) => { const openIDConfigurationManager = useMemo(() => { return new OpenIDConfigurationManager(`https://${props.domain}`, props.clientID, createMemoryStorage()); }, [props.domain, props.clientID]); const [error, setError] = useState(null); const [claims, setClaims] = useState(null); const [transaction, setTransaction] = useState(null); useAppState(() => { if (!transaction) return; transaction.onForeground(); }, [transaction]); useEffect(() => { if (!transaction) return; const urlCallback = (event) => { transaction.onUrl(event.url); }; const subscription = Linking.addEventListener('url', urlCallback); return () => subscription.remove(); }, [transaction]); const login = useCallback(async (acrValues, redirectUri, params) => { const discovery = await openIDConfigurationManager.fetch(); const pkce = await generatePKCE(); const authorizeOptions = { redirect_uri: redirectUri, scope: `openid${params?.scope ? ' ' + params.scope : ''}`, response_mode: acrValues === 'urn:grn:authn:se:bankid:same-device' ? 'json' : 'query', response_type: 'code', acr_values: acrValues, code_challenge: pkce.code_challenge, code_challenge_method: pkce.code_challenge_method, prompt: 'login', login_hint: // Settings appswitch login_hint for BankID will break downstream biometrics login_hints // Needs to be fixed in Criipto Verify acrValues.startsWith('urn:grn:authn:no:bankid') ? undefined : `appswitch:${Platform.OS}${params?.login_hint ? ' ' + params.login_hint : ''}` }; const authorizeUrl = buildAuthorizeURL(discovery, authorizeOptions); authorizeUrl.searchParams.set('criipto_sdk', `@criipto/verify-expo@1.0.0`); if (acrValues === 'urn:grn:authn:se:bankid:same-device') { const authorizeResponse = await fetch(authorizeUrl); if (authorizeResponse.status >= 400) throw new Error(await authorizeResponse.text()); const authorizePayload = await authorizeResponse.json(); const transactionPromise = new Promise((resolve, reject) => { const transaction = new SwedishBankIDTransaction(redirectUri, resolve, async () => { await fetch(authorizePayload.cancelUrl); reject(new UserCancelledError()); }); setTransaction(transaction); }); await Linking.openURL(authorizePayload.launchLinks.universalLink); await transactionPromise; const completeResponse = await fetch(authorizePayload.completeUrl); if (completeResponse.status >= 400) throw new Error(await completeResponse.text()); const completePayload = await completeResponse.json(); return await handleURL(discovery, pkce, redirectUri, new URL(completePayload.location)); } if (acrValues.startsWith('urn:grn:authn:dk:mitid:')) { const isUniversalLink = redirectUri.startsWith('https://'); if (isUniversalLink && Constants.executionEnvironment === ExecutionEnvironment.StoreClient) { throw new Error('MitID with universal links will not work in Expo Go'); } const resumeUrl = redirectUri.startsWith('https://') ? redirectUri : null; if (Platform.OS === 'android' && isUniversalLink) { const url = await CriiptoVerifyExpoModule.start({ authorizeUrl: authorizeUrl.href, redirectUri: redirectUri }); if (url === null) { throw new UserCancelledError(); } return await handleURL(discovery, pkce, redirectUri, new URL(url)); } const transactionPromise = new Promise((resolve, reject) => { const transaction = new DanishMitIDTransaction(redirectUri, resumeUrl, resolve); setTransaction(transaction); }); const browserResult = WebBrowser.openAuthSessionAsync(authorizeUrl.href, redirectUri, { createTask: Platform.OS === 'android' ? false : undefined }); const url = await Promise.race([ transactionPromise, browserResult.then(result => { if (result.type === 'success') return result.url; if (result.type === 'dismiss') throw new UserCancelledError(); throw new Error('Unexpected browser result: ' + JSON.stringify(result)); }), ]); return await handleURL(discovery, pkce, redirectUri, new URL(url)); } const result = await WebBrowser.openAuthSessionAsync(authorizeUrl.href, redirectUri); if (result.type === 'success') { const url = new URL(result.url); return await handleURL(discovery, pkce, redirectUri, url); } else { if (result.type === 'dismiss') throw new UserCancelledError(); throw new Error('Unexpected browser result: ' + JSON.stringify(result)); } }, [openIDConfigurationManager, setError, setClaims]); const context = useMemo(() => { return { login: async (...args) => { return login(...args).then(result => { if ("claims" in result) { setClaims(result.claims); } if (result instanceof Error) { setError(result); } return result; }).catch(error => { setError(error); throw error; }); }, logout: async () => { setClaims(null); setError(null); }, claims, error }; }, [login, claims, error, setClaims, setError]); return (React.createElement(CriiptoVerifyContext.Provider, { value: context }, props.children)); }; export default CriiptoVerifyProvider; async function handleURL(discovery, pkce, redirectUri, url) { if (url.searchParams.get('code')) { if (!pkce.code_verifier?.length) throw new Error('pkce.code_verifier is empty'); const response = await codeExchange(discovery, { code: url.searchParams.get('code'), redirect_uri: redirectUri, code_verifier: pkce.code_verifier }); if ("error" in response) { throw new OAuth2Error(response.error, response.error_description, response.state); } else if ("id_token" in response) { return { id_token: response.id_token, claims: jwtDecode(response.id_token) }; } else { throw new Error('Unexpected code exchange response: ' + JSON.stringify(response)); } } else if (url.searchParams.get('error')) { const error = new OAuth2Error(url.searchParams.get('error'), url.searchParams.get('error_description') ?? undefined, url.searchParams.get('state') ?? undefined); throw error; } else { throw new Error('Unexpected URL response: ' + url.href); } } //# sourceMappingURL=provider.js.map