Skip to content

Commit 49b9ec2

Browse files
mattermost-buildfmartingrmatthewbirtch
authored
MM-65085: Support Pre Shared Password on server connect (#9082) (#9096)
* feat: add shared server password to server setup * feat: allow editing the sever * refactor: changed password -> secret, styling and tests * e2e: draft e2e tests * chore: lint fix * feat: also send preauth secret header when using native share * fix: removed unused server database migration credentials are being stored in the keychain * i18n: added missing english translations * test(e2e): simplified connection tests * test(e2e): rework * refactor: remove setBearerToken * chore: restore migrations the way it was * chore: reverted file to original state * chore: removed unneeded test and renamed password to secret * chore: function version * chore: updated forms i18n keys * chore: remove if from test * chore: unneeded variable * fix: add missing key on object list * refactor: swift keychain access to retrieve all credentials in one call * revert: edit server screen * refactor: credentials use getGenericCredential * fix: objc code calling old method * fix: added scroll to login screen * chore: variable names * fix: avoid inline styles * fix: Improved appVersion positioning * Update app/screens/server/form.tsx * feat: show error message on 403 * Revert "feat: show error message on 403" This reverts commit f41630c. --------- (cherry picked from commit f50056f) Co-authored-by: Felipe Martin <[email protected]> Co-authored-by: Matthew Birtch <[email protected]>
1 parent e20b207 commit 49b9ec2

File tree

32 files changed

+600
-105
lines changed

32 files changed

+600
-105
lines changed

app/actions/remote/entry/login.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export async function loginEntry({serverUrl}: AfterLoginArgs): Promise<{error?:
3535
const credentials = await getServerCredentials(serverUrl);
3636
if (credentials?.token) {
3737
SecurityManager.addServer(serverUrl, clData.config, true);
38-
WebsocketManager.createClient(serverUrl, credentials.token);
38+
WebsocketManager.createClient(serverUrl, credentials.token, credentials.preauthSecret);
3939
await WebsocketManager.initializeClient(serverUrl, 'Login');
4040
SecurityManager.setActiveServer(serverUrl);
4141
}

app/actions/remote/general.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ async function getDeviceIdForPing(serverUrl: string, checkDeviceId: boolean) {
3333
}
3434

3535
// Default timeout interval for ping is 5 seconds
36-
export const doPing = async (serverUrl: string, verifyPushProxy: boolean, timeoutInterval = 5000) => {
36+
export const doPing = async (serverUrl: string, verifyPushProxy: boolean, timeoutInterval = 5000, preauthSecret?: string) => {
3737
let client: Client;
3838
try {
39-
client = await NetworkManager.createClient(serverUrl);
39+
client = await NetworkManager.createClient(serverUrl, undefined, preauthSecret);
4040
} catch (error) {
4141
return {error};
4242
}

app/actions/remote/session.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ const throwFunc = () => {
4646
const mockClient = {
4747
login: jest.fn(() => user1),
4848
setCSRFToken: jest.fn(),
49-
setBearerToken: jest.fn(),
49+
setClientCredentials: jest.fn(),
5050
getClientConfigOld: jest.fn(() => ({})),
5151
getClientLicenseOld: jest.fn(() => ({})),
5252
getSessions: jest.fn(() => [session1]),

app/actions/remote/session.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@ export const sendPasswordResetEmail = async (serverUrl: string, email: string) =
300300
}
301301
};
302302

303-
export const ssoLogin = async (serverUrl: string, serverDisplayName: string, serverIdentifier: string, bearerToken: string, csrfToken: string): Promise<LoginActionResponse> => {
303+
export const ssoLogin = async (serverUrl: string, serverDisplayName: string, serverIdentifier: string, bearerToken: string, csrfToken: string, preauthSecret?: string): Promise<LoginActionResponse> => {
304304
const database = DatabaseManager.appDatabase?.database;
305305
if (!database) {
306306
return {error: 'App database not found', failed: true};
@@ -309,7 +309,7 @@ export const ssoLogin = async (serverUrl: string, serverDisplayName: string, ser
309309
try {
310310
const client = NetworkManager.getClient(serverUrl);
311311

312-
client.setBearerToken(bearerToken);
312+
client.setClientCredentials(bearerToken, preauthSecret);
313313
client.setCSRFToken(csrfToken);
314314

315315
// Setting up active database for this SSO login flow

app/client/rest/base.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@ import ClientTracking from './tracking';
99
import type {APIClientInterface} from '@mattermost/react-native-network-client';
1010

1111
export default class ClientBase extends ClientTracking {
12-
constructor(apiClient: APIClientInterface, serverUrl: string, bearerToken?: string, csrfToken?: string) {
12+
constructor(apiClient: APIClientInterface, serverUrl: string, bearerToken?: string, csrfToken?: string, preauthSecret?: string) {
1313
super(apiClient);
1414

15-
if (bearerToken) {
16-
this.setBearerToken(bearerToken);
15+
if (bearerToken || preauthSecret) {
16+
this.setClientCredentials(bearerToken || '', preauthSecret || '');
1717
}
1818
if (csrfToken) {
1919
this.setCSRFToken(csrfToken);

app/client/rest/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export const HEADER_REQUESTED_WITH = 'X-Requested-With';
1010
export const HEADER_TOKEN = 'Token';
1111
export const HEADER_USER_AGENT = 'User-Agent';
1212
export const HEADER_X_CSRF_TOKEN = 'X-CSRF-Token';
13+
export const HEADER_X_MATTERMOST_PREAUTH_SECRET = 'X-Mattermost-Preauth-Secret';
1314
export const HEADER_X_VERSION_ID = 'X-Version-Id';
1415
export const DEFAULT_LIMIT_BEFORE = 30;
1516
export const DEFAULT_LIMIT_AFTER = 30;

app/client/rest/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ class Client extends mix(ClientBase).with(
7878
ClientPlaybooks,
7979
) {
8080
// eslint-disable-next-line no-useless-constructor
81-
constructor(apiClient: APIClientInterface, serverUrl: string, bearerToken?: string, csrfToken?: string) {
82-
super(apiClient, serverUrl, bearerToken, csrfToken);
81+
constructor(apiClient: APIClientInterface, serverUrl: string, bearerToken?: string, csrfToken?: string, preauthSecret?: string) {
82+
super(apiClient, serverUrl, bearerToken, csrfToken, preauthSecret);
8383
}
8484
}
8585

app/client/rest/tracking.test.ts

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -92,10 +92,10 @@ describe('ClientTracking', () => {
9292

9393
it('should set bearer token', () => {
9494
const token = 'testToken';
95-
client.setBearerToken(token);
95+
client.setClientCredentials(token);
9696

9797
expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} ${token}`);
98-
expect(require('@init/credentials').setServerCredentials).toHaveBeenCalledWith(apiClientMock.baseUrl, token);
98+
expect(require('@init/credentials').setServerCredentials).toHaveBeenCalledWith(apiClientMock.baseUrl, token, undefined);
9999
});
100100

101101
it('should set CSRF token', () => {
@@ -107,7 +107,7 @@ describe('ClientTracking', () => {
107107

108108
it('should get request headers', () => {
109109
client.setCSRFToken('csrfToken');
110-
client.setBearerToken('testToken');
110+
client.setClientCredentials('testToken');
111111

112112
const headers = client.getRequestHeaders('POST');
113113
expect(headers[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} testToken`);
@@ -823,5 +823,59 @@ describe('ClientTracking', () => {
823823
expect(result).toBe(100);
824824
});
825825
});
826+
827+
describe('setClientCredentials', () => {
828+
it('should set shared password header when provided', () => {
829+
client.setClientCredentials('bearer-token', 'shared-password');
830+
831+
expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} bearer-token`);
832+
expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBe('shared-password');
833+
});
834+
835+
it('should remove shared password header when undefined', () => {
836+
// First set a shared password
837+
client.setClientCredentials('bearer-token', 'shared-password');
838+
expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBe('shared-password');
839+
840+
// Then remove it by setting undefined
841+
client.setClientCredentials('bearer-token', undefined);
842+
expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBeUndefined();
843+
});
844+
845+
it('should remove shared password header when empty string', () => {
846+
// First set a shared password
847+
client.setClientCredentials('bearer-token', 'shared-password');
848+
expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBe('shared-password');
849+
850+
// Then remove it by setting empty string
851+
client.setClientCredentials('bearer-token', '');
852+
expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBeUndefined();
853+
});
854+
855+
it('should always set bearer token correctly', () => {
856+
client.setClientCredentials('test-bearer', 'shared-password');
857+
expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} test-bearer`);
858+
859+
client.setClientCredentials('new-bearer', undefined);
860+
expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} new-bearer`);
861+
});
862+
863+
it('should handle multiple header updates correctly', () => {
864+
// Set initial credentials
865+
client.setClientCredentials('bearer1', 'password1');
866+
expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} bearer1`);
867+
expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBe('password1');
868+
869+
// Update to new password
870+
client.setClientCredentials('bearer2', 'password2');
871+
expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} bearer2`);
872+
expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBe('password2');
873+
874+
// Remove password
875+
client.setClientCredentials('bearer3', undefined);
876+
expect(client.requestHeaders[ClientConstants.HEADER_AUTH]).toBe(`${ClientConstants.HEADER_BEARER} bearer3`);
877+
expect(client.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET]).toBeUndefined();
878+
});
879+
});
826880
});
827881
/* eslint-enable max-lines */

app/client/rest/tracking.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,17 @@ export default class ClientTracking {
7272
this.apiClient = apiClient;
7373
}
7474

75-
setBearerToken(bearerToken: string) {
75+
setClientCredentials(bearerToken: string, preauthSecret?: string) {
7676
this.requestHeaders[ClientConstants.HEADER_AUTH] = `${ClientConstants.HEADER_BEARER} ${bearerToken}`;
77-
setServerCredentials(this.apiClient.baseUrl, bearerToken);
77+
78+
if (preauthSecret) {
79+
this.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET] = preauthSecret;
80+
} else {
81+
// Remove shared password header when undefined
82+
delete this.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET];
83+
}
84+
85+
setServerCredentials(this.apiClient.baseUrl, bearerToken, preauthSecret);
7886
}
7987

8088
setCSRFToken(csrfToken: string) {
@@ -412,7 +420,8 @@ export default class ClientTracking {
412420

413421
const bearerToken = headers[ClientConstants.HEADER_TOKEN] || headers[ClientConstants.HEADER_TOKEN.toLowerCase()];
414422
if (bearerToken) {
415-
this.setBearerToken(bearerToken);
423+
const existingSharedPassword = this.requestHeaders[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET];
424+
this.setClientCredentials(bearerToken, existingSharedPassword);
416425
}
417426

418427
if (response.ok) {

app/client/websocket/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import {type ClientHeaders, getOrCreateWebSocketClient, type WebSocketClientInterface, WebSocketReadyState} from '@mattermost/react-native-network-client';
55
import {Platform} from 'react-native';
66

7+
import * as ClientConstants from '@client/rest/constants';
78
import {WebsocketEvents} from '@constants';
89
import DatabaseManager from '@database/manager';
910
import {getConfigValue} from '@queries/servers/system';
@@ -26,6 +27,7 @@ export default class WebSocketClient {
2627
private connectionTimeout: NodeJS.Timeout | undefined;
2728
private connectionId = '';
2829
private token: string;
30+
private preauthSecret?: string;
2931
private stop = false;
3032
private url = '';
3133
private serverUrl: string;
@@ -58,9 +60,10 @@ export default class WebSocketClient {
5860
private closeCallback?: (connectFailCount: number) => void;
5961
private connectingCallback?: () => void;
6062

61-
constructor(serverUrl: string, token: string) {
63+
constructor(serverUrl: string, token: string, preauthSecret?: string) {
6264
this.token = token;
6365
this.serverUrl = serverUrl;
66+
this.preauthSecret = preauthSecret;
6467
}
6568

6669
public async initialize(opts = {}, shouldSkipSync = false) {
@@ -134,6 +137,13 @@ export default class WebSocketClient {
134137
// iOS is using he underlying cookieJar
135138
headers.Authorization = `Bearer ${this.token}`;
136139
}
140+
141+
// Add shared password header if available
142+
if (this.preauthSecret) {
143+
headers[ClientConstants.HEADER_X_MATTERMOST_PREAUTH_SECRET] = this.preauthSecret;
144+
logDebug('WebSocket: Added shared password header for', this.serverUrl);
145+
}
146+
137147
const {client} = await getOrCreateWebSocketClient(this.url, {headers, timeoutInterval: WEBSOCKET_TIMEOUT});
138148

139149
// Check again if the client is the same, to avoid race conditions

0 commit comments

Comments
 (0)