feat: show pre-auth secret error on field on server create (#9102)

* feat: show potential pre-auth secret error on server create

* chroe: address comments

* chore: updated message

* feat: read response header to check error source

* fix: i18n

* chore: rename pre-auth to just auth
This commit is contained in:
Felipe Martin 2025-09-17 08:04:28 +02:00 committed by GitHub
parent 0835f6e6aa
commit 432cfb08fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 76 additions and 29 deletions

View file

@ -15,6 +15,7 @@ import {logDebug} from '@utils/log';
import {forceLogoutIfNecessary} from './session';
import type {Client} from '@client/rest';
import type ClientError from '@client/rest/error';
import type {ClientResponse} from '@mattermost/react-native-network-client';
async function getDeviceIdForPing(serverUrl: string, checkDeviceId: boolean) {
@ -66,12 +67,21 @@ export const doPing = async (serverUrl: string, verifyPushProxy: boolean, timeou
}
if (!response.ok) {
logDebug('Server ping returned not ok response', response);
NetworkManager.invalidateClient(serverUrl);
if (response.code === 403 && response.headers?.['x-reject-reason'] === 'pre-auth') {
return {error: {intl: pingError}, isPreauthError: true};
}
return {error: {intl: pingError}};
}
} catch (error) {
logDebug('Server ping threw an exception', getFullErrorMessage(error));
// Check if this is a 403 with pre-auth header
const errorObj = error as ClientError;
if (errorObj.status_code === 403) {
if (errorObj.headers?.['x-reject-reason'] === 'pre-auth') {
return {error: {intl: pingError}, isPreauthError: true};
}
}
NetworkManager.invalidateClient(serverUrl);
return {error: {intl: pingError}};
}

View file

@ -9,6 +9,7 @@ export default class ClientError extends Error {
server_error_id?: string;
status_code?: number;
details?: unknown;
headers?: Record<string, string>;
constructor(baseUrl: string, data: ClientErrorProps) {
super(data.message + ': ' + cleanUrlForLogging(baseUrl, data.url));
@ -18,6 +19,7 @@ export default class ClientError extends Error {
this.server_error_id = data.server_error_id;
this.status_code = data.status_code;
this.details = data.details;
this.headers = data.headers;
// Ensure message is treated as a property of this class when object spreading. Without this,
// copying the object by using `{...error}` would not include the message.

View file

@ -387,6 +387,7 @@ export default class ClientTracking {
try {
response = await request!(url, this.buildRequestOptions(options));
} catch (error) {
const response_error = error as ClientError;
const status_code = isErrorWithStatusCode(error) ? error.status_code : undefined;
throw new ClientError(this.apiClient.baseUrl, {
message: 'Received invalid response from the server.',
@ -395,7 +396,8 @@ export default class ClientTracking {
defaultMessage: 'Received invalid response from the server.',
}),
url,
details: error,
details: response_error,
headers: response_error.headers ? response_error.headers : undefined,
status_code,
});
} finally {
@ -433,6 +435,7 @@ export default class ClientTracking {
server_error_id: response.data?.id as string,
status_code: response.code,
url,
headers,
});
};
}

View file

@ -1,7 +1,7 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import React, {type RefObject, useCallback, useRef, useState} from 'react';
import React, {type RefObject, useCallback, useRef} from 'react';
import {defineMessages, useIntl} from 'react-intl';
import {Keyboard, Pressable, View} from 'react-native';
@ -28,6 +28,9 @@ type Props = {
handleUrlTextChanged: (text: string) => void;
keyboardAwareRef: RefObject<KeyboardAwareScrollView>;
preauthSecret?: string;
preauthSecretError?: string;
setShowAdvancedOptions: (show: boolean) => void;
showAdvancedOptions: boolean;
theme: Theme;
url?: string;
urlError?: string;
@ -93,11 +96,15 @@ const messages = defineMessages({
},
preauthSecret: {
id: 'mobile.components.select_server_view.sharedSecret',
defaultMessage: 'Pre-authentication secret',
defaultMessage: 'Authentication secret',
},
preauthSecretHelp: {
id: 'mobile.components.select_server_view.sharedSecretHelp',
defaultMessage: 'The pre-authentication secret shared by the administrator',
defaultMessage: 'The authentication secret shared by the administrator',
},
preauthSecretInvalid: {
id: 'mobile.server.preauth_secret.invalid',
defaultMessage: 'Authentication secret is invalid. Try again or contact your admin.',
},
});
@ -114,6 +121,9 @@ const ServerForm = ({
handleUrlTextChanged,
keyboardAwareRef,
preauthSecret = '',
preauthSecretError,
setShowAdvancedOptions,
showAdvancedOptions,
theme,
url = '',
urlError,
@ -124,8 +134,6 @@ const ServerForm = ({
const urlRef = useRef<FloatingTextInputRef>(null);
const styles = getStyleSheet(theme);
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
useAvoidKeyboard(keyboardAwareRef);
const onConnect = useCallback(() => {
@ -138,12 +146,12 @@ const ServerForm = ({
}, []);
const onDisplayNameSubmit = useCallback(() => {
if (showAdvancedOptions) {
if (showAdvancedOptions || preauthSecretError) {
preauthSecretRef.current?.focus();
} else {
onConnect();
}
}, [showAdvancedOptions, onConnect]);
}, [showAdvancedOptions, preauthSecretError, onConnect]);
const toggleAdvancedOptions = useCallback(() => {
setShowAdvancedOptions(!showAdvancedOptions);
@ -190,7 +198,7 @@ const ServerForm = ({
onChangeText={handleDisplayNameTextChanged}
onSubmitEditing={onDisplayNameSubmit}
ref={displayNameRef}
returnKeyType={showAdvancedOptions ? 'next' : 'done'}
returnKeyType={showAdvancedOptions || preauthSecretError ? 'next' : 'done'}
spellCheck={false}
testID='server_form.server_display_name.input'
theme={theme}
@ -231,6 +239,7 @@ const ServerForm = ({
autoCorrect={false}
autoCapitalize={'none'}
enablesReturnKeyAutomatically={true}
error={preauthSecretError}
label={formatMessage(messages.preauthSecret)}
onChangeText={handlePreauthSecretTextChanged}
onSubmitEditing={onConnect}

View file

@ -94,6 +94,8 @@ const Server = ({
const [url, setUrl] = useState<string>('');
const [displayNameError, setDisplayNameError] = useState<string | undefined>();
const [urlError, setUrlError] = useState<string | undefined>();
const [preauthSecretError, setPreauthSecretError] = useState<string | undefined>();
const [showAdvancedOptions, setShowAdvancedOptions] = useState<boolean>(false);
const styles = getStyleSheet(theme);
const {formatMessage} = intl;
const disableServerUrl = Boolean(managedConfig?.allowOtherServers === 'false' && managedConfig?.serverUrl);
@ -146,12 +148,12 @@ const Server = ({
}, [managedConfig?.allowOtherServers, managedConfig?.serverUrl, managedConfig?.serverName, defaultServerUrl]);
useEffect(() => {
if (url && displayName) {
if (url && displayName && !urlError && !preauthSecretError) {
setButtonDisabled(false);
} else {
setButtonDisabled(true);
}
}, [url, displayName]);
}, [url, displayName, urlError, preauthSecretError]);
useEffect(() => {
const listener = {
@ -277,7 +279,15 @@ const Server = ({
const handlePreauthSecretTextChanged = useCallback((text: string) => {
setPreauthSecret(text);
}, []);
// Clear any connection errors when preauth secret is modified
if (urlError) {
setUrlError(undefined);
}
if (preauthSecretError) {
setPreauthSecretError(undefined);
}
}, [urlError, preauthSecretError]);
const isServerUrlValid = (serverUrl?: string) => {
const testUrl = sanitizeUrl(serverUrl ?? url);
@ -300,34 +310,42 @@ const Server = ({
cancelPing = undefined;
};
const ping = await getServerUrlAfterRedirect(pingUrl, !retryWithHttp, preauthSecret.trim() || undefined);
if (!ping.url) {
const headRequest = await getServerUrlAfterRedirect(pingUrl, !retryWithHttp, preauthSecret.trim() || undefined);
if (!headRequest.url) {
cancelPing();
if (retryWithHttp) {
const nurl = pingUrl.replace('https:', 'http:');
pingServer(nurl, false);
} else {
setUrlError(getErrorMessage(ping.error, intl));
setUrlError(getErrorMessage(headRequest.error, intl));
setButtonDisabled(true);
setConnecting(false);
}
return;
}
const result = await doPing(ping.url, true, managedConfig?.timeout ? parseInt(managedConfig?.timeout, 10) : undefined, preauthSecret.trim() || undefined);
const result = await doPing(headRequest.url, true, managedConfig?.timeout ? parseInt(managedConfig?.timeout, 10) : undefined, preauthSecret.trim() || undefined);
if (canceled) {
return;
}
if (result.error) {
setUrlError(getErrorMessage(result.error, intl));
if (result.isPreauthError) {
setPreauthSecretError(intl.formatMessage({
id: 'mobile.server.preauth_secret.invalid',
defaultMessage: 'Authentication secret is invalid. Try again or contact your admin.',
}));
setShowAdvancedOptions(true);
} else {
setUrlError(getErrorMessage(result.error, intl));
}
setButtonDisabled(true);
setConnecting(false);
return;
}
canReceiveNotifications(ping.url, result.canReceiveNotifications as string, intl);
const data = await fetchConfigAndLicense(ping.url, true);
canReceiveNotifications(headRequest.url, result.canReceiveNotifications as string, intl);
const data = await fetchConfigAndLicense(headRequest.url, true);
if (data.error) {
setButtonDisabled(true);
setUrlError(getErrorMessage(data.error, intl));
@ -345,7 +363,7 @@ const Server = ({
}
if (data.config.MobileJailbreakProtection === 'true') {
const isJailbroken = await SecurityManager.isDeviceJailbroken(ping.url, data.config.SiteName);
const isJailbroken = await SecurityManager.isDeviceJailbroken(headRequest.url, data.config.SiteName);
if (isJailbroken) {
setConnecting(false);
return;
@ -353,7 +371,7 @@ const Server = ({
}
if (data.config.MobileEnableBiometrics === 'true') {
const biometricsResult = await SecurityManager.authenticateWithBiometrics(ping.url, data.config.SiteName);
const biometricsResult = await SecurityManager.authenticateWithBiometrics(headRequest.url, data.config.SiteName);
if (!biometricsResult) {
setConnecting(false);
return;
@ -361,7 +379,7 @@ const Server = ({
}
const server = await getServerByIdentifier(data.config.DiagnosticId);
const credentials = await getServerCredentials(ping.url);
const credentials = await getServerCredentials(headRequest.url);
setConnecting(false);
if (server && server.lastActiveAt > 0 && credentials?.token) {
@ -373,7 +391,7 @@ const Server = ({
return;
}
displayLogin(ping.url, data.config!, data.license!);
displayLogin(headRequest.url, data.config!, data.license!);
};
return (
@ -417,6 +435,9 @@ const Server = ({
handleUrlTextChanged={handleUrlTextChanged}
keyboardAwareRef={keyboardAwareRef}
preauthSecret={preauthSecret}
preauthSecretError={preauthSecretError}
setShowAdvancedOptions={setShowAdvancedOptions}
showAdvancedOptions={showAdvancedOptions}
theme={theme}
url={url}
urlError={urlError}

View file

@ -632,8 +632,8 @@
"mobile.components.select_server_view.msg_description": "A server is your team's communication hub accessed using a unique URL",
"mobile.components.select_server_view.msg_welcome": "Welcome",
"mobile.components.select_server_view.proceed": "Proceed",
"mobile.components.select_server_view.sharedSecret": "Pre-authentication secret",
"mobile.components.select_server_view.sharedSecretHelp": "The pre-authentication secret shared by the administrator",
"mobile.components.select_server_view.sharedSecret": "Authentication secret",
"mobile.components.select_server_view.sharedSecretHelp": "The authentication secret shared by the administrator",
"mobile.create_channel": "Create",
"mobile.create_channel.title": "New channel",
"mobile.create_direct_message.max_limit_reached": "Group messages are limited to {maxCount} members",
@ -849,6 +849,7 @@
"mobile.server_url.deeplink.emm.denied": "This app is controlled by an EMM and the DeepLink server url does not match the EMM allowed server",
"mobile.server_url.empty": "Please enter a valid server URL",
"mobile.server_url.invalid_format": "URL must start with http:// or https://",
"mobile.server.preauth_secret.invalid": "Authentication secret is invalid. Try again or contact your admin.",
"mobile.session_expired_days": "Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}}.",
"mobile.session_expired_days_hrs": "Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {daysCount, number} {daysCount, plural, one {day} other {days}} and {hoursCount, number} {hoursCount, plural, one {hour} other {hours}}.",
"mobile.session_expired_hrs": "Please log in to continue receiving notifications. Sessions for {siteName} are configured to expire every {hoursCount, number} {hoursCount, plural, one {hour} other {hours}}.",

View file

@ -68,8 +68,8 @@ describe('Server Login - Preauth Secret Connection', () => {
// * Verify preauth secret field is now visible with correct text and styling
await expect(preauthSecretInput).toBeVisible();
await expect(preauthSecretHelp).toBeVisible();
await expect(element(by.text('Pre-authentication secret'))).toBeVisible();
await expect(preauthSecretHelp).toHaveText('The pre-authentication secret shared by the administrator');
await expect(element(by.text('Authentication secret'))).toBeVisible();
await expect(preauthSecretHelp).toHaveText('The authentication secret shared by the administrator');
// # Toggle advanced options again to hide
await ServerScreen.toggleAdvancedOptions();

View file

@ -27,5 +27,6 @@ interface ClientErrorProps {
url: string;
server_error_id?: string;
status_code?: number;
headers?: Record<string, string>;
message: string;
}