Ory Kratos settings flow not successfully changing password

451 Views Asked by At

I have created my own UI for the Ory Kratos settings flow. The problem I am having is that after submitting the flow, although the returned messages say that my settings have been updated successfully, my new password is not reflected and I haven't successfully recovered my account. Here is a snippet of the code I am using to render the UI with Vue 3.

// SettingsVue.vue
<template>
  <div id="settings">
    <HomeTopbar :views="[]" />

    <div class="settings-wrapper">
      <h1 class="poppins fs_3 fw_6">Recover your password</h1>
      <OryFlow v-if="settingsFlow"
        :flow="settingsFlow"
        title="Login"
        form-id="settings-form"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';

/* ory */
import { injectStrict } from '@/utils';
import { $ory } from '@/plugins/ory';
import { makeHandleGetFlowError } from '@/utils/flows';

/* types */
import type { SelfServiceSettingsFlow } from '@ory/kratos-client';

/* composables */
import { useRoute, useRouter } from 'vue-router';

/* components */
import OryFlow from '@/components/flows/OryFlow.vue';
import HomeTopbar from '@/components/ui/HomeTopbar.vue';

const ory = injectStrict($ory);
const route = useRoute();
const router = useRouter();
const settingsFlow = ref<SelfServiceSettingsFlow | undefined>();
const handleGetFlowError = makeHandleGetFlowError(router);

// check if we have a flow param
const { flow } = route.query;

const initializeSelfServiceSettingsFlowForBrowsers = () =>
  ory
    .initializeSelfServiceSettingsFlowForBrowsers()
    .then((response) => {
      settingsFlow.value = response.data;
      router.replace({
        query: {
          flow: response.data.id,
        },
      });
    })
    .catch(handleGetFlowError);

if (typeof flow !== 'string') {
  // if there's no flow in our route,
  // we need to initialize our login flow
  initializeSelfServiceSettingsFlowForBrowsers();
} else {
  ory
    .getSelfServiceSettingsFlow(flow)
    .then((response) => {
      settingsFlow.value = response.data;
    })
    .catch(handleGetFlowError);
}
</script>

<style scoped lang="scss">
#settings {
  width: 100%;
  height: 100%;

  .settings-wrapper {
    margin: var(--space-5) auto 0 auto;
    padding: 0 var(--space-3);
    margin-top: var(--space-4);
    width: 300px;
  }
}
</style>

#OryFlow.vue
<template>
  <div class="ory-flow">
    <form :id="formId" :action="flow.ui.action" :method="flow.ui.method">
      <OryUiNode v-for="node in flow.ui.nodes"
        :key="getNodeId(node)"
        :id="getNodeId(node)"
        :node="node"
        class="ui-node"
      />
    </form>
    <div class="messages" v-if="flow.ui.messages">
      <OryUiMessage v-for="message in flow.ui.messages" :message="message" />
    </div>
  </div>
</template>

<script setup lang="ts">
import type {
  SelfServiceLoginFlow,
  SelfServiceRegistrationFlow,
  SelfServiceRecoveryFlow,
  SelfServiceSettingsFlow,
  SelfServiceVerificationFlow,
} from '@ory/kratos-client';
import OryUiNode from './OryUiNode.vue';
import OryUiMessage from './OryUiMessage.vue';
import { getNodeId } from '@ory/integrations/ui';

defineProps<{
  flow:
    | SelfServiceLoginFlow
    | SelfServiceRegistrationFlow
    | SelfServiceRecoveryFlow
    | SelfServiceSettingsFlow
    | SelfServiceVerificationFlow;
  formId?: string;
}>();
</script>

<style scoped lang="scss">
.ui-node + .ui-node {
  margin-top: var(--space-2);
}

.message {
  margin-top: var(--space-2);
}
</style>
1

There are 1 best solutions below

0
On

So the solution to this was actually pretty straight-forward and obvious after I reviewed the source code of the selfservice ui for React created and maintained by the Ory team.

If you submit all of the UI node groups together in the same form, it will only execute on the default group and one other group. In my case, it was disregarding the password group after successfully handling the profile group. The solution I followed was essentially the same as that provided in the repository I mention in the previous paragraph. Implement a filtering mechanism which will allow you to use two separate forms and thus separate those values at the time the form is submitted. Bear in mind you must always submit the default group as it includes your CSRF token.

Here is the updated code which solved my issue:

First, add support for an :only prop which will filter your nodes:

// OryFlow.vue now with support for filtering via 'only' prop
<template>
  <div class="ory-flow">
    <form :id="formId"
      :action="flow.ui.action"
      :method="flow.ui.method"
    >
      <OryUiNode v-for="node in nodes"
        :id="getNodeId(node)"
        :key="getNodeId(node)"
        :node="node"
        class="ui-node"
      />
    </form>
    <div v-if="flow.ui.messages"
      class="messages"
    >
      <OryUiMessage v-for="message in flow.ui.messages"
        :key="message.id"
        :message="message"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import type {
  SelfServiceLoginFlow,
  SelfServiceRegistrationFlow,
  SelfServiceRecoveryFlow,
  SelfServiceSettingsFlow,
  SelfServiceVerificationFlow,
} from '@ory/kratos-client';
import OryUiNode from './OryUiNode.vue';
import OryUiMessage from './OryUiMessage.vue';
import { getNodeId } from '@ory/integrations/ui';
import { computed, toRefs } from 'vue';

const props = defineProps<{
  flow:
    | SelfServiceLoginFlow
    | SelfServiceRegistrationFlow
    | SelfServiceRecoveryFlow
    | SelfServiceSettingsFlow
    | SelfServiceVerificationFlow;
  formId?: string;
  only?: string | string[];
}>();

const { flow, only } = toRefs(props);

const nodes = computed(() => {
  if (!only?.value) {
    return flow.value.ui.nodes;
  }

  const onlyArr: string[] = Array.isArray(only.value) ? only.value : [only.value];
  const onlyMap = onlyArr.reduce((acc, curr: string) => {
    acc[curr] = true;
    return acc;
  }, {} as Record<string, boolean>);

  return flow.value.ui.nodes.filter((node) => onlyMap[node.group]);
});
</script>

Next, utilize this new prop to filter only node groups password and default in one of your forms. You can use the same method to filter for profile and default in another form.

// SettingsView.vue
<template>
  <div id="settings">
    <HomeTopbar :views="[]" />

    <div class="settings-wrapper">
      <h1 class="poppins fs_3 fw_6">Recover your password</h1>
      <!-- add the only prop here -->
      <OryFlow v-if="settingsFlow"
        :flow="settingsFlow"
        title="Login"
        form-id="settings-form"
        :only="[ 'password', 'default' ]"
      />
    </div>
  </div>
</template>