@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
JavaScript
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