anastasis-webui: make TOTP work again
This commit is contained in:
parent
ec9aed276a
commit
5054ff6c6d
@ -14,7 +14,6 @@ import {
|
|||||||
getRandomBytes,
|
getRandomBytes,
|
||||||
hash,
|
hash,
|
||||||
HttpStatusCode,
|
HttpStatusCode,
|
||||||
j2s,
|
|
||||||
Logger,
|
Logger,
|
||||||
parsePayUri,
|
parsePayUri,
|
||||||
stringToBytes,
|
stringToBytes,
|
||||||
@ -27,8 +26,7 @@ import {
|
|||||||
import { anastasisData } from "./anastasis-data.js";
|
import { anastasisData } from "./anastasis-data.js";
|
||||||
import {
|
import {
|
||||||
EscrowConfigurationResponse,
|
EscrowConfigurationResponse,
|
||||||
IbanExternalAuthResponse,
|
RecoveryMetaResponse,
|
||||||
RecoveryMetaResponse as RecoveryMetaResponse,
|
|
||||||
TruthUploadRequest,
|
TruthUploadRequest,
|
||||||
} from "./provider-types.js";
|
} from "./provider-types.js";
|
||||||
import {
|
import {
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
"pretty": "prettier --write src",
|
"pretty": "prettier --write src",
|
||||||
"storybook": "start-storybook -p 6006"
|
"storybook": "start-storybook -p 6006"
|
||||||
},
|
},
|
||||||
|
"type": "module",
|
||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"extends": [
|
"extends": [
|
||||||
|
@ -28,7 +28,7 @@ const commitHash = cp.execSync("git rev-parse --short HEAD").toString();
|
|||||||
|
|
||||||
export default {
|
export default {
|
||||||
webpack(config, env, helpers) {
|
webpack(config, env, helpers) {
|
||||||
// add __VERSION__ to be use in the html
|
// add __VERSION__ to be used in the html
|
||||||
config.plugins.push(
|
config.plugins.push(
|
||||||
new DefinePlugin({
|
new DefinePlugin({
|
||||||
"process.env.__VERSION__": JSON.stringify(
|
"process.env.__VERSION__": JSON.stringify(
|
||||||
|
@ -23,7 +23,7 @@ import { h, VNode } from "preact";
|
|||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import langIcon from "../../assets/icons/languageicon.svg";
|
import langIcon from "../../assets/icons/languageicon.svg";
|
||||||
import { useTranslationContext } from "../../context/translation";
|
import { useTranslationContext } from "../../context/translation";
|
||||||
import { strings as messages } from "../../i18n/strings";
|
import { strings as messages } from "../../i18n/strings.js";
|
||||||
|
|
||||||
type LangsNames = {
|
type LangsNames = {
|
||||||
[P in keyof typeof messages]: string;
|
[P in keyof typeof messages]: string;
|
||||||
|
@ -78,8 +78,7 @@ export function Sidebar({ mobile }: Props): VNode {
|
|||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
{reducer.currentReducerState &&
|
{reducer.currentReducerState?.reducer_type === "backup" ? (
|
||||||
reducer.currentReducerState.backup_state ? (
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<li
|
<li
|
||||||
class={
|
class={
|
||||||
@ -191,8 +190,7 @@ export function Sidebar({ mobile }: Props): VNode {
|
|||||||
</li>
|
</li>
|
||||||
</Fragment>
|
</Fragment>
|
||||||
) : (
|
) : (
|
||||||
reducer.currentReducerState &&
|
reducer.currentReducerState?.reducer_type === "recovery" && (
|
||||||
reducer.currentReducerState?.recovery_state && (
|
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<li
|
<li
|
||||||
class={
|
class={
|
||||||
|
@ -199,7 +199,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
|
|||||||
s = await reduceAction(anastasisState.reducerState!, action, args);
|
s = await reduceAction(anastasisState.reducerState!, action, args);
|
||||||
}
|
}
|
||||||
console.log("got response from reducer", s);
|
console.log("got response from reducer", s);
|
||||||
if (s.code) {
|
if (s.reducer_type === "error") {
|
||||||
console.log("response is an error");
|
console.log("response is an error");
|
||||||
setAnastasisState({ ...anastasisState, currentError: s });
|
setAnastasisState({ ...anastasisState, currentError: s });
|
||||||
} else {
|
} else {
|
||||||
@ -223,7 +223,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
|
|||||||
} else {
|
} else {
|
||||||
s = await getBackupStartState();
|
s = await getBackupStartState();
|
||||||
}
|
}
|
||||||
if (s.code !== undefined) {
|
if (s.reducer_type === "error") {
|
||||||
setAnastasisState({
|
setAnastasisState({
|
||||||
...anastasisState,
|
...anastasisState,
|
||||||
currentError: s,
|
currentError: s,
|
||||||
@ -274,7 +274,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
|
|||||||
} else {
|
} else {
|
||||||
s = await getRecoveryStartState();
|
s = await getRecoveryStartState();
|
||||||
}
|
}
|
||||||
if (s.code !== undefined) {
|
if (s.reducer_type === "error") {
|
||||||
setAnastasisState({
|
setAnastasisState({
|
||||||
...anastasisState,
|
...anastasisState,
|
||||||
currentError: s,
|
currentError: s,
|
||||||
@ -296,8 +296,10 @@ export function useAnastasisReducer(): AnastasisReducerApi {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
reducerState.backup_state === BackupStates.ContinentSelecting ||
|
(reducerState.reducer_type === "backup" &&
|
||||||
reducerState.recovery_state === RecoveryStates.ContinentSelecting
|
reducerState.backup_state === BackupStates.ContinentSelecting) ||
|
||||||
|
(reducerState.reducer_type === "recovery" &&
|
||||||
|
reducerState.recovery_state === RecoveryStates.ContinentSelecting)
|
||||||
) {
|
) {
|
||||||
setAnastasisState({
|
setAnastasisState({
|
||||||
...anastasisState,
|
...anastasisState,
|
||||||
@ -327,7 +329,7 @@ export function useAnastasisReducer(): AnastasisReducerApi {
|
|||||||
}
|
}
|
||||||
const s = txHandle.transactionState;
|
const s = txHandle.transactionState;
|
||||||
console.log("transaction finished, new state", s);
|
console.log("transaction finished, new state", s);
|
||||||
if (s.code !== undefined) {
|
if (s.reducer_type === "error") {
|
||||||
setAnastasisState({
|
setAnastasisState({
|
||||||
...anastasisState,
|
...anastasisState,
|
||||||
currentError: txHandle.transactionState,
|
currentError: txHandle.transactionState,
|
||||||
@ -355,7 +357,7 @@ class ReducerTxImpl implements ReducerTransactionHandle {
|
|||||||
console.log("making transition in transaction", action);
|
console.log("making transition in transaction", action);
|
||||||
this.transactionState = s;
|
this.transactionState = s;
|
||||||
// Abort transaction as soon as we transition into an error state.
|
// Abort transaction as soon as we transition into an error state.
|
||||||
if (this.transactionState.code !== undefined) {
|
if (this.transactionState.reducer_type === "error") {
|
||||||
throw Error("transition resulted in error");
|
throw Error("transition resulted in error");
|
||||||
}
|
}
|
||||||
return this.transactionState;
|
return this.transactionState;
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import { UserAttributeSpec, validators } from "@gnu-taler/anastasis-core";
|
import { UserAttributeSpec, validators } from "@gnu-taler/anastasis-core";
|
||||||
import { isAfter, parse } from "date-fns";
|
import { isAfter, parse } from "date-fns";
|
||||||
import { Fragment, h, VNode } from "preact";
|
import { h, VNode } from "preact";
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { DateInput } from "../../components/fields/DateInput";
|
import { DateInput } from "../../components/fields/DateInput";
|
||||||
import { PhoneNumberInput } from "../../components/fields/NumberInput";
|
import { PhoneNumberInput } from "../../components/fields/NumberInput";
|
||||||
@ -72,7 +72,10 @@ export function AttributeEntryScreen(): VNode {
|
|||||||
|
|
||||||
const doConfirm = async () => {
|
const doConfirm = async () => {
|
||||||
await reducer.transition("enter_user_attributes", {
|
await reducer.transition("enter_user_attributes", {
|
||||||
identity_attributes: attrs,
|
identity_attributes: {
|
||||||
|
application_id: "anastasis-standalone",
|
||||||
|
...attrs,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -7,6 +7,11 @@ import { TextInput } from "../../../components/fields/TextInput";
|
|||||||
import { QR } from "../../../components/QR";
|
import { QR } from "../../../components/QR";
|
||||||
import { base32enc, computeTOTPandCheck } from "./totp";
|
import { base32enc, computeTOTPandCheck } from "./totp";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This is hard-coded in the protocol for TOTP auth.
|
||||||
|
*/
|
||||||
|
const ANASTASIS_TOTP_DIGITS = 8;
|
||||||
|
|
||||||
export function AuthMethodTotpSetup({
|
export function AuthMethodTotpSetup({
|
||||||
addAuthMethod,
|
addAuthMethod,
|
||||||
cancel,
|
cancel,
|
||||||
@ -14,20 +19,20 @@ export function AuthMethodTotpSetup({
|
|||||||
}: AuthMethodSetupProps): VNode {
|
}: AuthMethodSetupProps): VNode {
|
||||||
const [name, setName] = useState("anastasis");
|
const [name, setName] = useState("anastasis");
|
||||||
const [test, setTest] = useState("");
|
const [test, setTest] = useState("");
|
||||||
const digits = 8;
|
|
||||||
const secretKey = useMemo(() => {
|
const secretKey = useMemo(() => {
|
||||||
const array = new Uint8Array(32);
|
const array = new Uint8Array(32);
|
||||||
return window.crypto.getRandomValues(array);
|
return window.crypto.getRandomValues(array);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const secret32 = base32enc(secretKey);
|
const secret32 = base32enc(secretKey);
|
||||||
const totpURL = `otpauth://totp/${name}?digits=${digits}&secret=${secret32}`;
|
const totpURL = `otpauth://totp/${name}?digits=${ANASTASIS_TOTP_DIGITS}&secret=${secret32}`;
|
||||||
|
|
||||||
const addTotpAuth = (): void =>
|
const addTotpAuth = (): void =>
|
||||||
addAuthMethod({
|
addAuthMethod({
|
||||||
authentication_method: {
|
authentication_method: {
|
||||||
type: "totp",
|
type: "totp",
|
||||||
instructions: `Enter ${digits} digits code for "${name}"`,
|
instructions: `Enter ${ANASTASIS_TOTP_DIGITS} digits code for "${name}"`,
|
||||||
challenge: encodeCrock(stringToBytes(totpURL)),
|
challenge: encodeCrock(secretKey),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -11,7 +11,7 @@ import { AnastasisClientFrame } from "../index";
|
|||||||
import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
|
import { SolveOverviewFeedbackDisplay } from "../SolveScreen";
|
||||||
import { AuthMethodSolveProps } from "./index";
|
import { AuthMethodSolveProps } from "./index";
|
||||||
|
|
||||||
export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
|
export function AuthMethodTotpSolve(props: AuthMethodSolveProps): VNode {
|
||||||
const [answerCode, setAnswerCode] = useState("");
|
const [answerCode, setAnswerCode] = useState("");
|
||||||
|
|
||||||
const reducer = useAnastasisContext();
|
const reducer = useAnastasisContext();
|
||||||
@ -74,7 +74,7 @@ export function AuthMethodTotpSolve({ id }: AuthMethodSolveProps): VNode {
|
|||||||
async function onNext(): Promise<void> {
|
async function onNext(): Promise<void> {
|
||||||
console.log(`sending TOTP code '${answerCode}'`);
|
console.log(`sending TOTP code '${answerCode}'`);
|
||||||
return reducer?.transition("solve_challenge", {
|
return reducer?.transition("solve_challenge", {
|
||||||
pin: Number.parseInt(answerCode),
|
answer: answerCode,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function onCancel(): void {
|
function onCancel(): void {
|
||||||
|
@ -34,7 +34,7 @@ import { StartScreen } from "./StartScreen";
|
|||||||
import { TruthsPayingScreen } from "./TruthsPayingScreen";
|
import { TruthsPayingScreen } from "./TruthsPayingScreen";
|
||||||
|
|
||||||
function isBackup(reducer: AnastasisReducerApi): boolean {
|
function isBackup(reducer: AnastasisReducerApi): boolean {
|
||||||
return !!reducer.currentReducerState?.backup_state;
|
return reducer.currentReducerState?.reducer_type === "backup";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function withProcessLabel(
|
export function withProcessLabel(
|
||||||
@ -171,57 +171,96 @@ function AnastasisClientImpl(): VNode {
|
|||||||
console.log("state", reducer.currentReducerState);
|
console.log("state", reducer.currentReducerState);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
state.backup_state === BackupStates.ContinentSelecting ||
|
(state.reducer_type === "backup" &&
|
||||||
state.recovery_state === RecoveryStates.ContinentSelecting ||
|
state.backup_state === BackupStates.ContinentSelecting) ||
|
||||||
state.backup_state === BackupStates.CountrySelecting ||
|
(state.reducer_type === "recovery" &&
|
||||||
state.recovery_state === RecoveryStates.CountrySelecting
|
state.recovery_state === RecoveryStates.ContinentSelecting) ||
|
||||||
|
(state.reducer_type === "backup" &&
|
||||||
|
state.backup_state === BackupStates.CountrySelecting) ||
|
||||||
|
(state.reducer_type === "recovery" &&
|
||||||
|
state.recovery_state === RecoveryStates.CountrySelecting)
|
||||||
) {
|
) {
|
||||||
return <ContinentSelectionScreen />;
|
return <ContinentSelectionScreen />;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
state.backup_state === BackupStates.UserAttributesCollecting ||
|
(state.reducer_type === "backup" &&
|
||||||
state.recovery_state === RecoveryStates.UserAttributesCollecting
|
state.backup_state === BackupStates.UserAttributesCollecting) ||
|
||||||
|
(state.reducer_type === "recovery" &&
|
||||||
|
state.recovery_state === RecoveryStates.UserAttributesCollecting)
|
||||||
) {
|
) {
|
||||||
return <AttributeEntryScreen />;
|
return <AttributeEntryScreen />;
|
||||||
}
|
}
|
||||||
if (state.backup_state === BackupStates.AuthenticationsEditing) {
|
if (
|
||||||
|
state.reducer_type === "backup" &&
|
||||||
|
state.backup_state === BackupStates.AuthenticationsEditing
|
||||||
|
) {
|
||||||
return <AuthenticationEditorScreen />;
|
return <AuthenticationEditorScreen />;
|
||||||
}
|
}
|
||||||
if (state.backup_state === BackupStates.PoliciesReviewing) {
|
if (
|
||||||
|
state.reducer_type === "backup" &&
|
||||||
|
state.backup_state === BackupStates.PoliciesReviewing
|
||||||
|
) {
|
||||||
return <ReviewPoliciesScreen />;
|
return <ReviewPoliciesScreen />;
|
||||||
}
|
}
|
||||||
if (state.backup_state === BackupStates.SecretEditing) {
|
if (
|
||||||
|
state.reducer_type === "backup" &&
|
||||||
|
state.backup_state === BackupStates.SecretEditing
|
||||||
|
) {
|
||||||
return <SecretEditorScreen />;
|
return <SecretEditorScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.backup_state === BackupStates.BackupFinished) {
|
if (
|
||||||
|
state.reducer_type === "backup" &&
|
||||||
|
state.backup_state === BackupStates.BackupFinished
|
||||||
|
) {
|
||||||
return <BackupFinishedScreen />;
|
return <BackupFinishedScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.backup_state === BackupStates.TruthsPaying) {
|
if (
|
||||||
|
state.reducer_type === "backup" &&
|
||||||
|
state.backup_state === BackupStates.TruthsPaying
|
||||||
|
) {
|
||||||
return <TruthsPayingScreen />;
|
return <TruthsPayingScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.backup_state === BackupStates.PoliciesPaying) {
|
if (
|
||||||
|
state.reducer_type === "backup" &&
|
||||||
|
state.backup_state === BackupStates.PoliciesPaying
|
||||||
|
) {
|
||||||
return <PoliciesPayingScreen />;
|
return <PoliciesPayingScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.recovery_state === RecoveryStates.SecretSelecting) {
|
if (
|
||||||
|
state.reducer_type === "recovery" &&
|
||||||
|
state.recovery_state === RecoveryStates.SecretSelecting
|
||||||
|
) {
|
||||||
return <SecretSelectionScreen />;
|
return <SecretSelectionScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.recovery_state === RecoveryStates.ChallengeSelecting) {
|
if (
|
||||||
|
state.reducer_type === "recovery" &&
|
||||||
|
state.recovery_state === RecoveryStates.ChallengeSelecting
|
||||||
|
) {
|
||||||
return <ChallengeOverviewScreen />;
|
return <ChallengeOverviewScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.recovery_state === RecoveryStates.ChallengeSolving) {
|
if (
|
||||||
|
state.reducer_type === "recovery" &&
|
||||||
|
state.recovery_state === RecoveryStates.ChallengeSolving
|
||||||
|
) {
|
||||||
return <SolveScreen />;
|
return <SolveScreen />;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.recovery_state === RecoveryStates.RecoveryFinished) {
|
if (
|
||||||
|
state.reducer_type === "recovery" &&
|
||||||
|
state.recovery_state === RecoveryStates.RecoveryFinished
|
||||||
|
) {
|
||||||
return <RecoveryFinishedScreen />;
|
return <RecoveryFinishedScreen />;
|
||||||
}
|
}
|
||||||
if (state.recovery_state === RecoveryStates.ChallengePaying) {
|
if (
|
||||||
|
state.reducer_type === "recovery" &&
|
||||||
|
state.recovery_state === RecoveryStates.ChallengePaying
|
||||||
|
) {
|
||||||
return <ChallengePayingScreen />;
|
return <ChallengePayingScreen />;
|
||||||
}
|
}
|
||||||
console.log("unknown state", reducer.currentReducerState);
|
console.log("unknown state", reducer.currentReducerState);
|
||||||
|
@ -17,6 +17,11 @@ export function createExample<Props>(
|
|||||||
<AnastasisProvider
|
<AnastasisProvider
|
||||||
value={{
|
value={{
|
||||||
currentReducerState,
|
currentReducerState,
|
||||||
|
discoverMore: async () => {},
|
||||||
|
discoverStart: async () => {},
|
||||||
|
discoveryState: {
|
||||||
|
state: "none",
|
||||||
|
},
|
||||||
currentError: undefined,
|
currentError: undefined,
|
||||||
back: async () => {
|
back: async () => {
|
||||||
null;
|
null;
|
||||||
|
Loading…
Reference in New Issue
Block a user