🎯 Learning Path:

  1. Complete REL-ID Complete Activation & Login Flow Codelab
  2. Complete REL-ID Additional Device Activation Flow Codelab
  3. Complete REL-ID Forgot Password Flow Codelab
  4. You are here β†’ Password Expiry Flow Implementation

Welcome to the REL-ID Password Expiry codelab! This tutorial builds upon your existing MFA implementation to add secure expired password update capabilities using REL-ID SDK's updatePassword API.

What You'll Build

In this codelab, you'll enhance your existing MFA application with:

What You'll Learn

By completing this codelab, you'll master:

  1. Password Expiry Detection: Identifying when password has expired and routing to update flow
  2. UpdatePassword API Integration: Implementing updatePassword(current, new, 4) with proper handling
  3. Password Policy Extraction: Parsing RELID_PASSWORD_POLICY from challenge data
  4. Password Reuse Handling: Detecting and recovering from password reuse errors (statusCode 164)
  5. Three-Field Validation: Validating current, new, and confirm passwords with proper error messages
  6. Production Security Patterns: Implement secure password expiry with comprehensive error handling

Prerequisites

Before starting this codelab, ensure you have:

Get the Code from GitHub

The code to get started can be found in a GitHub repository.

You can clone the repository using the following command:

git clone https://github.com/uniken-public/codelab-react-native.git

Navigate to the relid-MFA-password-expiry folder in the repository you cloned earlier

Codelab Architecture Overview

This codelab extends your MFA application with three core password expiry components:

  1. UpdateExpiryPasswordScreen: Three-field password form with policy display and validation
  2. UpdatePassword API Integration: Service layer implementation following established SDK patterns
  3. getPassword Event Routing Enhancement: SDKEventProvider routing for challengeMode 4 detection

Before implementing password expiry functionality, let's understand the key SDK events and APIs that power the expired password update workflow.

Password Expiry Event Flow

The password expiry process follows this event-driven pattern:

Login with Expired Password with challengeMode=0(RDNA_CHALLENGE_OP_VERIFY) β†’ Server Detects Expiry (statusCode 118) β†’
SDK Triggers getPassword Event with challengeMode=4(RDNA_OP_UPDATE_ON_EXPIRY) β†’ UpdateExpiryPasswordScreen Displays β†’
User Updates Password β†’ updatePassword(current, new, 4) API β†’ onUserLoggedIn Event β†’ Dashboard

Password Expiry Trigger Mechanism

When a user's password expires, the login flow changes:

Step

Event

Description

1. User Login

VerifyPasswordScreen with challengeMode = 0

User enters credentials for standard login

2. Password Expired

Server returns statusCode = 118

Server detects password has expired

3. SDK Re-triggers

getPassword event with challengeMode = 4

SDK automatically requests password update

4. User Shows Screen

UpdateExpiryPasswordScreen displays

Show UpdateExpiryPasswordScreen with current, new, and confirm password fields

5. User Update Password

updatePassword API

User must provide current and new password

Challenge Mode 4 - RDNA_OP_UPDATE_ON_EXPIRY

Challenge Mode 4 is specifically for expired password updates:

Challenge Mode

Purpose

User Action Required

Screen

challengeMode = 0

Verify existing password

Enter password to login

VerifyPasswordScreen

challengeMode = 1

Set new password

Create password during activation

SetPasswordScreen

challengeMode = 4

Update expired password

Provide current + new password

UpdateExpiryPasswordScreen

Core Password Expiry Event Types

The REL-ID SDK triggers these main events during password expiry flow:

Event Type

Description

User Action Required

getPassword (challengeMode=4)

Password expiry detected, update required

User provides current and new passwords

onUserLoggedIn

Automatic login after successful password update

System navigates to dashboard automatically

Password Policy Extraction

Password expiry flow uses the same default policy key as password creation.:

Flow

Policy Key

Description

Password Creation (challengeMode=1)

RELID_PASSWORD_POLICY

Policy for new password creation

Password Expiry (challengeMode=4)

RELID_PASSWORD_POLICY

Policy for expired password update

Password Reuse Detection

The server maintains password history and detects reuse:

Status Code

Meaning

Action

statusCode = 118

Password has expired

Initial trigger for password update

statusCode = 164

Password reuse detected

Clear fields and prompt for different password

UpdatePassword API Pattern

Add these TypeScript definitions to understand the updatePassword API structure:

// src/uniken/services/rdnaService.ts (password expiry addition)

/**
 * Updates password when expired (Password Expiry Flow)
 * @param currentPassword The user's current password
 * @param newPassword The new password to set
 * @param challengeMode Challenge mode (should be 4 for RDNA_OP_UPDATE_ON_EXPIRY)
 * @returns Promise<RDNASyncResponse> that resolves with sync response structure
 */
async updatePassword(
  currentPassword: string,
  newPassword: string,
  challengeMode: number = 4
): Promise<RDNASyncResponse> {
  return new Promise((resolve, reject) => {
    RdnaClient.updatePassword(currentPassword, newPassword, challengeMode, response => {
      const result: RDNASyncResponse = response;
      if (result.error && result.error.longErrorCode === 0) {
        resolve(result);
      } else {
        reject(result);
      }
    });
  });
}

Let's implement the updatePassword API in your service layer following established REL-ID SDK patterns.

Enhance rdnaService.ts with UpdatePassword

Add the updatePassword method to your existing service implementation:

// src/uniken/services/rdnaService.ts (addition to existing class)

/**
 * Updates password when expired (Password Expiry Flow)
 *
 * This method is specifically used for updating expired passwords during the MFA flow.
 * When a password is expired during login (challengeMode=0), the SDK automatically
 * re-triggers getPassword() with challengeMode=4 (RDNA_OP_UPDATE_ON_EXPIRY).
 * The app should then call this method with both current and new passwords.
 * Uses sync response pattern similar to setPassword() method.
 *
 * @see https://developer.uniken.com/docs/password-expiry
 *
 * Workflow:
 * 1. User logs in with expired password (challengeMode=0)
 * 2. Server detects expiry (statusCode=118)
 * 3. SDK triggers getPassword with challengeMode=4
 * 4. App displays UpdateExpiryPasswordScreen
 * 5. User provides current and new passwords
 * 6. App calls updatePassword(current, new, 4)
 * 7. SDK validates and updates password
 * 8. SDK logs user in automatically (onUserLoggedIn event)
 *
 * Response Validation Logic:
 * 1. Check error.longErrorCode: 0 = success, > 0 = error
 * 2. On success, triggers onUserLoggedIn event immediately
 * 3. On failure, may trigger getPassword again with error status
 * 4. StatusCode 164 = Password reuse error (used in last N passwords)
 * 5. Async events will be handled by event listeners
 *
 * @param currentPassword The user's current password
 * @param newPassword The new password to set
 * @param challengeMode Challenge mode (should be 4 for RDNA_OP_UPDATE_ON_EXPIRY)
 * @returns Promise<RDNASyncResponse> that resolves with sync response structure
 */
async updatePassword(
  currentPassword: string,
  newPassword: string,
  challengeMode: number = 4
): Promise<RDNASyncResponse> {
  return new Promise((resolve, reject) => {
    console.log('RdnaService - Updating expired password with challengeMode:', challengeMode);

    RdnaClient.updatePassword(currentPassword, newPassword, challengeMode, response => {
      console.log('RdnaService - UpdatePassword sync callback received');
      const result: RDNASyncResponse = response;

      if (result.error && result.error.longErrorCode === 0) {
        console.log('RdnaService - UpdatePassword sync response success, waiting for onUserLoggedIn event');
        resolve(result);
      } else {
        console.error('RdnaService - UpdatePassword sync response error:', result);
        reject(result);
      }
    });
  });
}

Service Pattern Consistency

Notice how this implementation follows the exact pattern established by other service methods:

Pattern Element

Implementation Detail

Promise Wrapper

Wraps native sync SDK callback in Promise for async/await usage

Error Checking

Validates longErrorCode === 0 for success

Logging Strategy

Comprehensive console logging for debugging (without exposing passwords)

Error Handling

Proper reject/resolve based on sync response

Challenge Mode

Defaults to 4 (RDNA_OP_UPDATE_ON_EXPIRY) but accepts as parameter

Now let's enhance your SDKEventProvider to detect and route challengeMode 4 to the UpdateExpiryPasswordScreen.

Add Challenge Mode 4 Detection

Update your existing handleGetPassword callback in SDKEventProvider:

// src/uniken/providers/SDKEventProvider.tsx (enhancement to existing handler)

/**
 * Event handler for get password requests
 */
const handleGetPassword = useCallback((data: RDNAGetPasswordData) => {
  console.log('SDKEventProvider - Get password event received, status:', data.challengeResponse.status.statusCode);
  console.log('SDKEventProvider - UserID:', data.userID, 'ChallengeMode:', data.challengeMode, 'AttemptsLeft:', data.attemptsLeft);

  // Navigate to appropriate screen based on challengeMode
  if (data.challengeMode === 0) {
    // challengeMode = 0: Verify existing password
    NavigationService.navigateOrUpdate('VerifyPasswordScreen', {
      eventData: data,
      title: 'Verify Password',
      subtitle: `Enter your password to continue`,
      userID: data.userID,
      challengeMode: data.challengeMode,
      attemptsLeft: data.attemptsLeft,
      responseData: data,
    });
  } else if (data.challengeMode === 4) {
    // challengeMode = 4: Update expired password (RDNA_OP_UPDATE_ON_EXPIRY)
    // Extract status message from response (e.g., "Password has expired. Please contact the admin.")
    const statusMessage = data.challengeResponse?.status?.statusMessage ||
                         'Your password has expired. Please update it to continue.';

    NavigationService.navigateOrUpdate('UpdateExpiryPasswordScreen', {
      eventData: data,
      title: 'Update Expired Password',
      subtitle: statusMessage,
      responseData: data,
    });
  } else {
    // challengeMode = 1: Set new password
    NavigationService.navigateOrUpdate('SetPasswordScreen', {
      eventData: data,
      title: 'Set Password',
      subtitle: `Create a secure password for user: ${data.userID}`,
      responseData: data,
    });
  }
}, []);

Challenge Mode Routing Logic

The enhanced routing logic handles three password scenarios:

Challenge Mode

Screen

Purpose

challengeMode = 0

VerifyPasswordScreen

Verify existing password for login

challengeMode = 1

SetPasswordScreen

Set new password during activation

challengeMode = 4

UpdateExpiryPasswordScreen

Update expired password

Status Message Extraction

Extract the server's status message for better user experience:

// Extract dynamic status message from server response
const statusMessage = data.challengeResponse?.status?.statusMessage ||
                     'Your password has expired. Please update it to continue.';

Status Code

Typical Status Message

118

"Password has expired. Please contact the admin."

164

"Please enter a new password as your entered password has been used by you previously. You are not allowed to use last N passwords."

Now let's create the UpdateExpiryPasswordScreen component with three password fields, comprehensive validation, and keyboard management.

Create the Screen Component

Create a new file for the password expiry screen:

// src/tutorial/screens/mfa/UpdateExpiryPasswordScreen.tsx

import React, { useState, useEffect, useRef } from 'react';
import {
  View,
  Text,
  TextInput,
  StyleSheet,
  StatusBar,
  ScrollView,
  Alert,
  SafeAreaView,
  KeyboardAvoidingView,
  Platform,
} from 'react-native';
import { useRoute } from '@react-navigation/native';
import type { RouteProp } from '@react-navigation/native';
import { RDNAEventUtils, RDNASyncUtils } from '../../../uniken/types/rdnaEvents';
import { parseAndGeneratePolicyMessage } from '../../../uniken/utils';
import type { RDNAGetPasswordData, RDNASyncResponse } from '../../../uniken/types/rdnaEvents';
import rdnaService from '../../../uniken/services/rdnaService';
import { CloseButton, Button, Input, StatusBanner } from '../components';
import type { RootStackParamList } from '../../navigation/AppNavigator';

type UpdateExpiryPasswordScreenRouteProp = RouteProp<RootStackParamList, 'UpdateExpiryPasswordScreen'>;

/**
 * Update Expiry Password Screen Component
 */
const UpdateExpiryPasswordScreen: React.FC = () => {
  const route = useRoute<UpdateExpiryPasswordScreenRouteProp>();

  const {
    eventData,
    title,
    subtitle,
    responseData,
  } = route.params;

  const [currentPassword, setCurrentPassword] = useState<string>('');
  const [newPassword, setNewPassword] = useState<string>('');
  const [confirmPassword, setConfirmPassword] = useState<string>('');
  const [error, setError] = useState<string>('');
  const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
  const [challengeMode, setChallengeMode] = useState<number>(4);
  const [userName, setUserName] = useState<string>('');
  const [passwordPolicyMessage, setPasswordPolicyMessage] = useState<string>('');

  const currentPasswordRef = useRef<TextInput>(null);
  const newPasswordRef = useRef<TextInput>(null);
  const confirmPasswordRef = useRef<TextInput>(null);

  /**
   * Handle close button - direct resetAuthState call
   */
  const handleClose = async () => {
    try {
      console.log('UpdateExpiryPasswordScreen - Calling resetAuthState');
      await rdnaService.resetAuthState();
      console.log('UpdateExpiryPasswordScreen - ResetAuthState successful');
    } catch (error) {
      console.error('UpdateExpiryPasswordScreen - ResetAuthState error:', error);
    }
  };

  /**
   * Handle response data from route params
   */
  useEffect(() => {
    if (responseData) {
      console.log('UpdateExpiryPasswordScreen - Processing response data from route params:', responseData);

      // Extract challenge data
      setUserName(responseData.userID || '');
      setChallengeMode(responseData.challengeMode || 4);

      // Extract and process password policy from RELID_PASSWORD_POLICY
      const policyJsonString = RDNAEventUtils.getChallengeValue(responseData, 'RELID_PASSWORD_POLICY');
      if (policyJsonString) {
        const policyMessage = parseAndGeneratePolicyMessage(policyJsonString);
        setPasswordPolicyMessage(policyMessage);
        console.log('UpdateExpiryPasswordScreen - Password policy extracted:', policyMessage);
      }

      console.log('UpdateExpiryPasswordScreen - Processed password data:', {
        userID: responseData.userID,
        challengeMode: responseData.challengeMode,
        passwordPolicy: policyJsonString ? 'Found' : 'Not found',
      });

      // Check for API errors first
      if (RDNAEventUtils.hasApiError(responseData)) {
        const errorMessage = RDNAEventUtils.getErrorMessage(responseData);
        console.log('UpdateExpiryPasswordScreen - API error:', errorMessage);
        setError(errorMessage);
        // Clear password fields on error
        setCurrentPassword('');
        setNewPassword('');
        setConfirmPassword('');
        return;
      }

      // Check for status errors (including password reuse errors like statusCode 164)
      if (RDNAEventUtils.hasStatusError(responseData)) {
        const errorMessage = RDNAEventUtils.getErrorMessage(responseData);
        console.log('UpdateExpiryPasswordScreen - Status error:', errorMessage);
        setError(errorMessage);
        // Clear password fields on error (e.g., password reuse)
        setCurrentPassword('');
        setNewPassword('');
        setConfirmPassword('');
        return;
      }
    }
  }, [responseData]);

  // ... (Continue with remaining handler functions)
};

export default UpdateExpiryPasswordScreen;

Key Implementation Features

Feature

Implementation Detail

Three Password Fields

Current, new, and confirm password with refs for focus management

Keyboard Management

KeyboardAvoidingView with platform-specific behavior (iOS: padding, Android: height)

Policy Extraction

Extracts from RELID_PASSWORD_POLICY

Error Handling

Automatic field clearing on API and status errors

Loading States

Proper isSubmitting state management

Let's implement comprehensive password validation for the three-field form.

Add Input Change Handlers

Implement handlers to clear errors when user types:

// src/tutorial/screens/mfa/UpdateExpiryPasswordScreen.tsx (additions)

/**
 * Handle input changes
 */
const handleCurrentPasswordChange = (text: string) => {
  setCurrentPassword(text);
  if (error) {
    setError('');
  }
};

const handleNewPasswordChange = (text: string) => {
  setNewPassword(text);
  if (error) {
    setError('');
  }
};

const handleConfirmPasswordChange = (text: string) => {
  setConfirmPassword(text);
  if (error) {
    setError('');
  }
};

/**
 * Reset form inputs
 */
const resetInputs = () => {
  setCurrentPassword('');
  setNewPassword('');
  setConfirmPassword('');
  if (currentPasswordRef.current) {
    currentPasswordRef.current.focus();
  }
};

Implement Update Password Logic

Add the main validation and update logic:

// src/tutorial/screens/mfa/UpdateExpiryPasswordScreen.tsx (additions)

/**
 * Handle password update submission
 */
const handleUpdatePassword = async () => {
  if (isSubmitting) return;

  const trimmedCurrentPassword = currentPassword.trim();
  const trimmedNewPassword = newPassword.trim();
  const trimmedConfirmPassword = confirmPassword.trim();

  // Basic validation
  if (!trimmedCurrentPassword) {
    setError('Please enter your current password');
    if (currentPasswordRef.current) {
      currentPasswordRef.current.focus();
    }
    return;
  }

  if (!trimmedNewPassword) {
    setError('Please enter a new password');
    if (newPasswordRef.current) {
      newPasswordRef.current.focus();
    }
    return;
  }

  if (!trimmedConfirmPassword) {
    setError('Please confirm your new password');
    if (confirmPasswordRef.current) {
      confirmPasswordRef.current.focus();
    }
    return;
  }

  // Check password match
  if (trimmedNewPassword !== trimmedConfirmPassword) {
    setError('New password and confirm password do not match');
    Alert.alert(
      'Password Mismatch',
      'New password and confirm password do not match',
      [{ text: 'OK', onPress: () => {
        setNewPassword('');
        setConfirmPassword('');
        if (newPasswordRef.current) {
          newPasswordRef.current.focus();
        }
      }}]
    );
    return;
  }

  // Check if new password is same as current password
  if (trimmedCurrentPassword === trimmedNewPassword) {
    setError('New password must be different from current password');
    Alert.alert(
      'Invalid New Password',
      'Your new password must be different from your current password',
      [{ text: 'OK', onPress: () => {
        setNewPassword('');
        setConfirmPassword('');
        if (newPasswordRef.current) {
          newPasswordRef.current.focus();
        }
      }}]
    );
    return;
  }

  setIsSubmitting(true);
  setError('');

  try {
    console.log('UpdateExpiryPasswordScreen - Updating password with challengeMode:', challengeMode);

    const syncResponse: RDNASyncResponse = await rdnaService.updatePassword(
      trimmedCurrentPassword,
      trimmedNewPassword,
      challengeMode
    );

    console.log('UpdateExpiryPasswordScreen - UpdatePassword sync response successful, waiting for async events');
    console.log('UpdateExpiryPasswordScreen - Sync response received:', {
      longErrorCode: syncResponse.error?.longErrorCode,
      shortErrorCode: syncResponse.error?.shortErrorCode,
      errorString: syncResponse.error?.errorString
    });

    // Success - wait for onUserLoggedIn event
    // Event handlers in SDKEventProvider will handle the navigation

  } catch (error) {
    // This catch block handles sync response errors (rejected promises)
    console.error('UpdateExpiryPasswordScreen - UpdatePassword sync error:', error);

    // Cast the error back to RDNASyncResponse
    const result: RDNASyncResponse = error as RDNASyncResponse;
    const errorMessage = RDNASyncUtils.getErrorMessage(result);

    setError(errorMessage);
    resetInputs();
  } finally {
    setIsSubmitting(false);
  }
};

/**
 * Check if form is valid
 */
const isFormValid = (): boolean => {
  return (
    currentPassword.trim().length > 0 &&
    newPassword.trim().length > 0 &&
    confirmPassword.trim().length > 0 &&
    !error
  );
};

Validation Rules Summary

Validation Rule

Error Message

Action

Current password empty

"Please enter your current password"

Focus current password field

New password empty

"Please enter a new password"

Focus new password field

Confirm password empty

"Please confirm your new password"

Focus confirm password field

Passwords don't match

"New password and confirm password do not match"

Clear new and confirm fields

New = Current password

"New password must be different from current password"

Clear new and confirm fields

Now let's build the complete UI with password policy requirements display and keyboard management.

Add the Render Method with KeyboardAvoidingView

Complete the component with the full UI including keyboard management:

// src/tutorial/screens/mfa/UpdateExpiryPasswordScreen.tsx (UI rendering)

return (
  <SafeAreaView style={styles.safeArea}>
    <StatusBar barStyle="dark-content" backgroundColor="#f8f9fa" />
    <KeyboardAvoidingView
      style={styles.keyboardAvoidingView}
      behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
      keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
    >
      <ScrollView
        style={styles.container}
        contentContainerStyle={styles.scrollContent}
        keyboardShouldPersistTaps="handled"
      >
        {/* Close Button */}
        <CloseButton
          onPress={handleClose}
          disabled={isSubmitting}
        />

        <View style={styles.content}>
        <Text style={styles.title}>{title}</Text>
        <Text style={styles.subtitle}>{subtitle}</Text>

        {/* User Information */}
        {userName && (
          <View style={styles.userContainer}>
            <Text style={styles.welcomeText}>Welcome</Text>
            <Text style={styles.userNameText}>{userName}</Text>
          </View>
        )}

        {/* Password Policy Display */}
        {passwordPolicyMessage && (
          <View style={styles.policyContainer}>
            <Text style={styles.policyTitle}>Password Requirements</Text>
            <Text style={styles.policyText}>{passwordPolicyMessage}</Text>
          </View>
        )}

        {/* Error Display */}
        {error && (
          <StatusBanner
            type="error"
            message={error}
          />
        )}

        {/* Current Password Input */}
        <Input
          ref={currentPasswordRef}
          label="Current Password"
          value={currentPassword}
          onChangeText={handleCurrentPasswordChange}
          placeholder="Enter current password"
          secureTextEntry={true}
          returnKeyType="next"
          onSubmitEditing={() => newPasswordRef.current?.focus()}
          editable={!isSubmitting}
          autoFocus={true}
          containerStyle={styles.inputContainer}
        />

        {/* New Password Input */}
        <Input
          ref={newPasswordRef}
          label="New Password"
          value={newPassword}
          onChangeText={handleNewPasswordChange}
          placeholder="Enter new password"
          secureTextEntry={true}
          returnKeyType="next"
          onSubmitEditing={() => confirmPasswordRef.current?.focus()}
          editable={!isSubmitting}
          containerStyle={styles.inputContainer}
        />

        {/* Confirm New Password Input */}
        <Input
          ref={confirmPasswordRef}
          label="Confirm New Password"
          value={confirmPassword}
          onChangeText={handleConfirmPasswordChange}
          placeholder="Confirm new password"
          secureTextEntry={true}
          returnKeyType="done"
          onSubmitEditing={handleUpdatePassword}
          editable={!isSubmitting}
          containerStyle={styles.inputContainer}
        />

        {/* Submit Button */}
        <Button
          title={isSubmitting ? 'Updating Password...' : 'Update Password'}
          onPress={handleUpdatePassword}
          loading={isSubmitting}
          disabled={!isFormValid()}
        />

        {/* Help Text */}
        <View style={styles.helpContainer}>
          <Text style={styles.helpText}>
            Update your password. Your new password must be different from your current password.
          </Text>
        </View>
      </View>
      </ScrollView>
    </KeyboardAvoidingView>
  </SafeAreaView>
);

UI Component Breakdown

Component

Purpose

Key Props

KeyboardAvoidingView

Manages keyboard visibility for input fields

Platform-specific behavior, keyboardVerticalOffset

ScrollView

Scrollable container with keyboard handling

keyboardShouldPersistTaps="handled"

CloseButton

Allow user to cancel and reset auth

onPress={handleClose}

Policy Container

Display password requirements

Shows parsed RELID_PASSWORD_POLICY

Three Input Fields

Current, new, confirm passwords

Each with ref prop for keyboard navigation (currentPasswordRef, newPasswordRef, confirmPasswordRef)

StatusBanner

Display errors (including statusCode 164)

Conditional rendering

Submit Button

Trigger password update

Disabled until form valid

Keyboard Management Explanation

The KeyboardAvoidingView ensures input fields remain visible when keyboard appears:

Platform

Behavior

Description

iOS

padding

Adds padding to shift content above keyboard

Android

height

Adjusts view height to accommodate keyboard

The keyboardShouldPersistTaps="handled" prop allows users to tap buttons even when keyboard is visible, providing seamless interaction.

Keyboard Navigation with Refs

Each Input component has a ref prop attached for smooth keyboard navigation:

Input Field

Ref

Next Field Focus

Current Password

currentPasswordRef

Focuses newPasswordRef when "Next" pressed

New Password

newPasswordRef

Focuses confirmPasswordRef when "Next" pressed

Confirm Password

confirmPasswordRef

Calls handleUpdatePassword when "Done" pressed

This creates a seamless user experience where pressing the keyboard's "Next" button automatically advances to the next field, and "Done" on the final field submits the form.

Add Styling

Complete the component with consistent styling including keyboard management styles:

// src/tutorial/screens/mfa/UpdateExpiryPasswordScreen.tsx (styles)

const styles = StyleSheet.create({
  safeArea: {
    flex: 1,
    backgroundColor: '#f8f9fa',
  },
  keyboardAvoidingView: {
    flex: 1,
  },
  container: {
    flex: 1,
  },
  scrollContent: {
    flexGrow: 1,
  },
  content: {
    flex: 1,
    padding: 20,
    paddingTop: 80, // Add space for close button
  },
  title: {
    fontSize: 28,
    fontWeight: 'bold',
    color: '#2c3e50',
    textAlign: 'center',
    marginBottom: 8,
  },
  subtitle: {
    fontSize: 16,
    color: '#7f8c8d',
    textAlign: 'center',
    marginBottom: 30,
  },
  userContainer: {
    alignItems: 'center',
    marginBottom: 20,
  },
  welcomeText: {
    fontSize: 18,
    color: '#2c3e50',
    marginBottom: 4,
  },
  userNameText: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#3498db',
    marginBottom: 20,
  },
  inputContainer: {
    marginBottom: 20,
  },
  helpContainer: {
    backgroundColor: '#e8f4f8',
    borderRadius: 8,
    padding: 16,
    marginTop: 20,
  },
  helpText: {
    fontSize: 14,
    color: '#2c3e50',
    textAlign: 'center',
    lineHeight: 20,
  },
  policyContainer: {
    backgroundColor: '#f0f8ff',
    borderLeftColor: '#3498db',
    borderLeftWidth: 4,
    borderRadius: 8,
    padding: 16,
    marginBottom: 20,
  },
  policyTitle: {
    fontSize: 16,
    fontWeight: 'bold',
    color: '#2c3e50',
    marginBottom: 8,
  },
  policyText: {
    fontSize: 14,
    color: '#2c3e50',
    lineHeight: 20,
  },
});

The following images showcase screens from the sample application:

Expiry Update Password Screen

Expiry Update Password Screen

Let's register the UpdateExpiryPasswordScreen in your navigation configuration.

Update AppNavigator Type Definitions

Add the screen parameters to your navigation types:

// src/tutorial/navigation/AppNavigator.tsx (type additions)

// Update Expiry Password Screen Parameters
interface UpdateExpiryPasswordScreenParams {
  eventData: RDNAGetPasswordData;
  title: string;
  subtitle: string;
  responseData?: RDNAGetPasswordData; // Direct response data
}

export type RootStackParamList = {
  // ... other routes
  UpdateExpiryPasswordScreen: UpdateExpiryPasswordScreenParams;
};

Register the Screen in Stack Navigator

Add the screen to your navigation stack:

// src/tutorial/navigation/AppNavigator.tsx (screen registration)

import { UpdateExpiryPasswordScreen } from '../screens/mfa';

// In your Stack.Navigator:
<Stack.Screen
  name="UpdateExpiryPasswordScreen"
  component={UpdateExpiryPasswordScreen}
  options={{
    title: 'Update Expired Password',
    headerShown: false,
  }}
/>

Export from MFA Index

Add the screen to your MFA screens export:

// src/tutorial/screens/mfa/index.ts

export { default as CheckUserScreen } from './CheckUserScreen';
export { default as ActivationCodeScreen } from './ActivationCodeScreen';
export { default as UserLDAConsentScreen } from './UserLDAConsentScreen';
export { default as SetPasswordScreen } from './SetPasswordScreen';
export { default as VerifyPasswordScreen } from './VerifyPasswordScreen';
export { default as UpdateExpiryPasswordScreen } from './UpdateExpiryPasswordScreen';
export { default as VerifyAuthScreen } from './VerifyAuthScreen';
export { default as DashboardScreen } from './DashboardScreen';

Navigation Flow Verification

Verify your navigation flow is complete:

Step

Navigation Event

Screen

1. User Login

getPassword (challengeMode=0)

VerifyPasswordScreen

2. Password Expired

getPassword (challengeMode=4)

UpdateExpiryPasswordScreen

3. Password Updated

onUserLoggedIn

DashboardScreen

Now let's test the complete password expiry implementation with various scenarios.

Test Scenario 1: Standard Password Expiry Flow

Follow these steps to test standard password expiry:

  1. Login with expired password
    • Use VerifyPasswordScreen (challengeMode = 0)
    • Enter credentials for user with expired password
  2. Verify automatic navigation
    • SDK should detect expiry (statusCode 118)
    • SDK triggers getPassword with challengeMode = 4
    • App navigates to UpdateExpiryPasswordScreen
  3. Check password policy display
    • Verify "Password Requirements" section appears
    • Confirm default policy is extracted from RELID_PASSWORD_POLICY
    • Check policy message is user-friendly
  4. Update password
    • Enter current password
    • Enter new password (meeting policy requirements)
    • Enter confirm password (matching new password)
    • Tap "Update Password"
  5. Verify automatic login
    • SDK should trigger onUserLoggedIn event
    • App should navigate to Dashboard automatically

Test Scenario 2: Password Reuse Detection

Test password reuse error handling:

  1. Navigate to UpdateExpiryPasswordScreen (following Scenario 1 steps 1-3)
  2. Enter recently used password
    • Current password: [user's current password]
    • New password: [password used in last N passwords]
    • Confirm password: [same as new password]
    • Tap "Update Password"
  3. Verify reuse detection
    • SDK returns statusCode 164
    • SDK re-triggers getPassword with challengeMode = 4
    • Error message displayed: "Please enter a new password as your entered password has been used by you previously..."
  4. Verify automatic field clearing
    • All three password fields should clear automatically
    • User can retry with different password
    • Error message remains visible
  5. Retry with valid password
    • Enter current password again
    • Enter new password (not in history)
    • Enter confirm password
    • Verify successful update and login

Test Scenario 3: Validation Errors

Test all validation rules:

Test Case

Expected Error

Expected Behavior

Empty current password

"Please enter your current password"

Focus current password field

Empty new password

"Please enter a new password"

Focus new password field

Empty confirm password

"Please confirm your new password"

Focus confirm password field

Passwords don't match

"New password and confirm password do not match"

Alert + clear new/confirm fields

New = Current password

"New password must be different from current password"

Alert + clear new/confirm fields

Test Scenario 4: Password Policy Violations

Test password policy enforcement:

  1. Navigate to UpdateExpiryPasswordScreen
  2. Check displayed policy requirements
    • Note minimum length, character requirements
    • Note any special restrictions
  3. Enter policy-violating password
    • Example: Too short, missing uppercase, etc.
    • Tap "Update Password"
  4. Verify server-side validation
    • Server should return policy violation error
    • SDK re-triggers getPassword with error
    • Fields should clear automatically
    • Error message should display policy violation

Debugging Tips

If you encounter issues, check these areas:

Issue

Possible Cause

Solution

Policy not displaying

Usung default policy key β€˜RELID_PASSWORD_POLICY'

Update extraction key to RELID_PASSWORD_POLICY

Fields not clearing

Missing field clear logic in error handling

Add setCurrentPassword(''), setNewPassword(''), setConfirmPassword('')

Navigation not working

challengeMode 4 not routed in SDKEventProvider

Add if (data.challengeMode === 4) routing

API not called

Form validation failing

Check isFormValid() logic

Keyboard covering inputs

Missing KeyboardAvoidingView

Wrap ScrollView with KeyboardAvoidingView and platform-specific behavior

Scroll not working with keyboard

Missing keyboardShouldPersistTaps

Add keyboardShouldPersistTaps="handled" to ScrollView

Before deploying password expiry functionality to production, review these important considerations.

Security Best Practices

Practice

Implementation

Importance

Never log passwords

Remove all console.log statements that might expose passwords

Critical

Password history

Respect server-configured history limits

High

Policy enforcement

Always display and enforce RELID_PASSWORD_POLICY

High

Error handling

Clear fields on all errors to prevent data exposure

High

User Experience Optimization

Enhance user experience with these patterns:

// 1. Clear, specific error messages
if (trimmedNewPassword === trimmedCurrentPassword) {
  setError('New password must be different from current password');
  // Show alert with actionable guidance
  Alert.alert('Invalid New Password', 'Your new password must be different from your current password');
}

// 2. Automatic field clearing on errors
if (RDNAEventUtils.hasStatusError(responseData)) {
  setError(errorMessage);
  setCurrentPassword('');
  setNewPassword('');
  setConfirmPassword('');
}

// 3. Keyboard navigation
<Input
  returnKeyType="next"
  onSubmitEditing={() => newPasswordRef.current?.focus()}
/>

// 4. Keyboard management for better input visibility
<KeyboardAvoidingView
  style={styles.keyboardAvoidingView}
  behavior={Platform.OS === 'ios' ? 'padding' : 'height'}
  keyboardVerticalOffset={Platform.OS === 'ios' ? 0 : 20}
>
  <ScrollView
    style={styles.container}
    contentContainerStyle={styles.scrollContent}
    keyboardShouldPersistTaps="handled"
  >
    {/* Input fields */}
  </ScrollView>
</KeyboardAvoidingView>

Performance Considerations

Consideration

Implementation

Field refs

Use useRef for efficient focus management

Memoization

Consider useMemo for password policy parsing if expensive

Loading states

Show clear loading indicators during API calls

Error recovery

Implement retry logic with proper state cleanup

Testing Checklist

Before production deployment, verify:

What You've Accomplished

Congratulations! You've successfully implemented REL-ID Password Expiry functionality in your React Native application.

You now have:

βœ… Password Expiry Detection: Automatic detection and routing of challengeMode 4

βœ… UpdatePassword API: Full integration with proper error handling

βœ… Three-Field Validation: Current, new, and confirm password validation

βœ… Password Policy Display: Extraction and display of RELID_PASSWORD_POLICY

βœ… Password Reuse Handling: StatusCode 164 detection with automatic field clearing

βœ… Production-Ready: Secure, user-friendly password expiry flow

Additional Resources

Thank you for completing the REL-ID Password Expiry Flow Codelab!

You're now equipped to build secure, production-ready password expiry workflows that provide excellent user experience while maintaining strong security standards.