Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1887,25 +1887,26 @@ Based on the OGP metadata Converse will render a URL preview (also known as an
the ``show_images_inline``, ``embed_audio`` and ``embed_videos`` settings.


reuse_scram_keys
----------------
reuse_keys
----------
Comment on lines +1890 to +1891
Copy link
Author

@kousu kousu Jun 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I chose to repurpose reuse_scram_keys to also cover FAST because they are so similar. But there's other options

  • add a separate reuse_fast_keys option
  • make FAST always enabled, or maybe just a fast that defauls to true.

I'm not sure how is best or what your preferences are. Lemme know :)


* Default: ``true``

Most XMPP servers enable the Salted Challenge Response Authentication Mechanism
or SCRAM for short. This allows the user and the server to mutually
authenticate *without* the need to transmit the user's password in plaintext.
or SCRAM for short. Newer servers also support Fast Authentication Streamlining Tokens.
These allow the user and the server to mutually authenticate *without* the need
to transmit the user's password in plaintext.

Assuming the server does not alter the user's password or the
storage parameters, we can authenticate with the same SCRAM key multiple times.
storage parameters, we can authenticate with the same key multiple times.

This opens an opportunity: we can store the user's login credentials in the
browser without storing the sensitive plaintext password, or the
need to set up complicated third party backends, like OAuth.

Enabling this option will let Converse save a user's SCRAM keys upon successful
Enabling this option will let Converse save a user's keys upon successful
login, and next time Converse is loaded the user will be automatically logged in
with those SCRAM keys.
with those keys.


.. _`roomconfig_whitelist`:
Expand Down
8 changes: 5 additions & 3 deletions docs/source/session.rst
Original file line number Diff line number Diff line change
Expand Up @@ -196,10 +196,12 @@ We've purposefully not put this functionality in Converse.js due to the
security implications of storing plaintext passwords in localStorage.


Storing the SASL SCRAM-SHA1 hash in IndexedDB
---------------------------------------------
Storing the SASL SCRAM-SHA1 hash or FAST token in IndexedDB
-----------------------------------------------------------

Another suggestion that's been suggested is to store the SCRAM-SHA1 computed
``clientKey`` in localStorage and to use that upon page reload to log the user in again.

This has been implemented since version 10, see documentation on `reuse_scram_keys <https://conversejs.org/docs/html/configuration.html#reuse-scram-keys>`_
In more modern terms, we can store the FAST token if supported by the server.

This has been implemented since version 10, see documentation on `reuse_keys <https://conversejs.org/docs/html/configuration.html#reuse-keys>`_
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@
"pluggable.js": "^3.0.1",
"prettier": "^3.2.5",
"sizzle": "^2.3.5",
"sprintf-js": "^1.1.2"
"sprintf-js": "^1.1.2",
"strophe.js": "github:kousu/strophejs#sasl2_fast_2"
},
"resolutions": {
"autoprefixer": "10.4.5"
Expand Down
11 changes: 9 additions & 2 deletions src/headless/shared/api/user.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import _converse from '../_converse.js';
import presence_api from './presence.js';
import connection_api from '../connection/api.js';
import { replacePromise } from '../../utils/session.js';
import { attemptNonPreboundSession, setUserJID } from '../../utils/init.js';
import { attemptNonPreboundSession, setUserJID, savedLoginInfo } from '../../utils/init.js';
import { getOpenPromise } from '@converse/openpromise';
import { user_settings_api } from '../settings/user/api.js';
import { LOGOUT } from '../constants.js';
Expand Down Expand Up @@ -108,8 +108,15 @@ const api = {
// Recreate all the promises
Object.keys(_converse.promises).forEach((p) => replacePromise(_converse, p));

// Remove the session JID, otherwise the user would just be logged
// Remove the session JID/keys, otherwise the user would just be logged
// in again upon reload. See #2759
const jid = localStorage.getItem("conversejs-session-jid");
if (jid) {
savedLoginInfo(jid).then((login_info) => {
// XXX TODO: is there a cleaner call? .remove()? .clear()?
login_info.save({ fast: null, scram_keys: null });
})
}
localStorage.removeItem('conversejs-session-jid');

/**
Expand Down
4 changes: 2 additions & 2 deletions src/headless/shared/settings/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* @property {String} [assets_path='/dist']
* @property {('login'|'prebind'|'anonymous'|'external')} [authentication='login']
* @property {Boolean} [auto_login=false] - Currently only used in connection with anonymous login
* @property {Boolean} [reuse_scram_keys=true] - Save SCRAM keys after login to allow for future auto login
* @property {Boolean} [reuse_keys=true] - Save SCRAM/FAST keys after login to allow for future auto login
* @property {Boolean} [auto_reconnect=true]
* @property {Array<String>} [blacklisted_plugins]
* @property {Boolean} [clear_cache_on_logout=false]
Expand Down Expand Up @@ -50,7 +50,7 @@ export const DEFAULT_SETTINGS = {
geouri_replacement: 'https://www.openstreetmap.org/?mlat=$1&mlon=$2#map=18/$1/$2',
i18n: undefined,
jid: undefined,
reuse_scram_keys: true,
reuse_keys: true,
keepalive: true,
loglevel: 'info',
locales: [
Expand Down
8 changes: 4 additions & 4 deletions src/headless/types/shared/settings/constants.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export namespace DEFAULT_SETTINGS {
let geouri_replacement: string;
let i18n: any;
let jid: any;
let reuse_scram_keys: boolean;
let reuse_keys: boolean;
let keepalive: boolean;
let loglevel: string;
let locales: string[];
Expand Down Expand Up @@ -46,9 +46,9 @@ export type ConfigurationSettings = {
*/
auto_login?: boolean;
/**
* - Save SCRAM keys after login to allow for future auto login
* - Save keys after login to allow for future auto login
*/
reuse_scram_keys?: boolean;
reuse_keys?: boolean;
auto_reconnect?: boolean;
blacklisted_plugins?: Array<string>;
clear_cache_on_logout?: boolean;
Expand Down Expand Up @@ -77,4 +77,4 @@ export type ConfigurationSettings = {
websocket_url?: string;
whitelisted_plugins?: Array<string>;
};
//# sourceMappingURL=constants.d.ts.map
//# sourceMappingURL=constants.d.ts.map
34 changes: 26 additions & 8 deletions src/headless/utils/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,15 +348,25 @@ async function getLoginCredentialsFromBrowser() {
}
}

async function getLoginCredentialsFromSCRAMKeys() {
async function getLoginCredentialsFromStorage() {
const jid = localStorage.getItem("conversejs-session-jid");
if (!jid) return null;

await setUserJID(jid);

const login_info = await savedLoginInfo(jid);

const fast_credential = login_info.get("fast");
if (fast_credential) {
return { jid, password: fast_credential }
}

const scram_keys = login_info.get("scram_keys");
return scram_keys ? { jid, password: scram_keys } : null;
if (scram_keys) {
return { jid, password: scram_keys }
}

return null
}

/**
Expand Down Expand Up @@ -393,8 +403,13 @@ export async function attemptNonPreboundSession(credentials, automatic) {
return connect();
}

if (api.settings.get("reuse_scram_keys")) {
const credentials = await getLoginCredentialsFromSCRAMKeys();
if (api.settings.get("reuse_keys")) {
// XXX if connecting with FAST, we need to present the same user-agent and client-id as before
// This is currently implemented by storing the client ID as part of the fast keys
// and having Strophe generate it if unset.
// but it makes more sense for the client, Converse, to handle that.
// and it should probably coordinate with the OMEMO plugin.
const credentials = await getLoginCredentialsFromStorage();
if (credentials) return connect(credentials);
}

Expand Down Expand Up @@ -423,7 +438,7 @@ export async function attemptNonPreboundSession(credentials, automatic) {
* used login keys.
*/
export async function savedLoginInfo(jid) {
const id = `converse.scram-keys-${Strophe.getBareJidFromJid(jid)}`;
const id = `converse.keys-${Strophe.getBareJidFromJid(jid)}`;
if (_converse.state.login_info?.get("id") === id) {
return _converse.state.login_info;
}
Expand Down Expand Up @@ -479,20 +494,23 @@ async function connect(credentials) {

let callback;
// Save the SCRAM data if we're not already logged in with SCRAM
if (_converse.state.config.get("trusted") && jid && api.settings.get("reuse_scram_keys") && !password?.ck) {
// Store scram keys in scram storage
if (_converse.state.config.get("trusted") && jid && api.settings.get("reuse_keys") && !password?.ck) {
// Store scram/fast keys in storage
// XXX seems like we could move the *loading* of keys from storage here, since login_info contains them
const login_info = await savedLoginInfo(jid);
callback =
/**
* @param {string} status
* @param {string} message
*/
(status, message) => {
const { scram_keys } = connection;
const { scram_keys, fast } = connection;
if (scram_keys) login_info.save({ scram_keys });
if (fast?.credential?.token) login_info.save('fast', fast.credential);
connection.onConnectStatusChanged(status, message);
};
}

connection.connect(jid, password, callback);
}
}
Expand Down