@criipto/verify-expo
Version:
Accept MitID, NemID, Swedish BankID, Norwegian BankID and more logins in your Expo (React-Native) app
205 lines • 9.27 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,
preferEphemeralSession: params?.preferEphemeralSession ?? false,
});
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, {
preferEphemeralSession: params?.preferEphemeralSession ?? false,
});
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