Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@ Also user can manually register topic-alias pair using PUBLISH topic:'some', ta:
- [`mqtt.Client#endAsync()`](#end-async)
- [`mqtt.Client#removeOutgoingMessage()`](#removeOutgoingMessage)
- [`mqtt.Client#reconnect()`](#reconnect)
- [`mqtt.Client#reauthenticate()`](#client-reauthenticate)
- [`mqtt.Client#handleMessage()`](#handleMessage)
- [`mqtt.Client#connected`](#connected)
- [`mqtt.Client#reconnecting`](#reconnecting)
Expand Down Expand Up @@ -599,6 +600,17 @@ and connections
- `packet` received packet, as defined in
[mqtt-packet](https://github.com/mcollina/mqtt-packet)

#### Event `'reauth'`

`function (packet) {}`

Emitted when an MQTT 5 re-authentication completes successfully.

- `packet` the AUTH packet received from the broker.

Triggered after calling [`client.reauthenticate()`](#client-reauthenticate).


Comment thread
robertsLando marked this conversation as resolved.
Outdated
---

<a name="client-connect"></a>
Expand Down Expand Up @@ -742,6 +754,30 @@ Connect again using the same options as connect()

---

<a name="client-reauthenticate"></a>

### mqtt.Client#reauthenticate(reauthOptions, [callback])

Start an MQTT 5 re-authentication exchange.

- `reauthOptions`:
- `authenticationData` (`Buffer`)
- `reasonString` (`string`, optional)
- `userProperties` (`object`, optional)

- `callback` - `function (err, packet)`
- called when the AUTH exchange completes or fails

Errors:
- client is not connected
- MQTT version is not 5
- `authenticationData` is missing
- `authenticationMethod` is missing from the initial CONNECT

Emits `'reauth'` on successful completion.

---

<a name="handleMessage"></a>

### mqtt.Client#handleMessage(packet, callback)
Expand Down
106 changes: 106 additions & 0 deletions src/lib/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ export interface MqttClientEventCallbacks {
reconnect: VoidCallback
offline: VoidCallback
outgoingEmpty: VoidCallback
reauth: OnPacketCallback
}

/**
Expand Down Expand Up @@ -521,6 +522,8 @@ export default class MqttClient extends TypedEventEmitter<MqttClientEventCallbac

private connackPacket: IConnackPacket

private _reauthCallback: PacketCallback
Comment thread
robertsLando marked this conversation as resolved.
Outdated

public static defaultId() {
return `mqttjs_${Math.random().toString(16).substr(2, 8)}`
}
Expand Down Expand Up @@ -1678,6 +1681,105 @@ export default class MqttClient extends TypedEventEmitter<MqttClientEventCallbac
return this
}

/**
* reauthenticate - MQTT 5.0 Re-authentication
* * @param {Object} reauthOptions - Re-authentication properties
Comment thread
robertsLando marked this conversation as resolved.
Outdated
* @param {Buffer} [reauthOptions.authenticationData] - Binary data for auth exchange
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[Doc] JSDoc says authenticationData is optional, the code says it's required.

 * @param {Buffer} [reauthOptions.authenticationData] - Binary data for auth exchange

The square brackets in JSDoc denote an optional parameter, but reauthenticate immediately rejects calls without it (return fail('authenticationData is required')) and the README correctly lists it as required. Drop the brackets in both reauthenticate and reauthenticateAsync JSDoc blocks.

* @param {string} [reauthOptions.reasonString] - Human-readable reason for re-auth
* @param {Object} [reauthOptions.userProperties] - Custom user properties
* @param {PacketCallback} [callback] - Fired when the AUTH exchange completes or fails
* @returns {MqttClient} - Returns the client instance
*/
public reauthenticate(
reauthOptions: Pick<
NonNullable<IAuthPacket['properties']>,
'authenticationData' | 'reasonString' | 'userProperties'
>,
callback?: PacketCallback,
): MqttClient {
let error: Error | null = null

if (this._reauthCallback) {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[High] The interrupt fires before the new request is validated.

if (this._reauthCallback) {
    this._handleReauth(new Error('reauthenticate: interrupted by new reauthentication request'))
}

if (this.options.protocolVersion !== 5) return fail(...)
else if (!this.connected) return fail(...)
else if (!reauthOptions.authenticationData) return fail(...)

A second call with invalid arguments (wrong protocol version, missing data, etc.) will still cancel a perfectly valid in-flight re-auth before its own validation runs. The first caller is rejected with interrupted even though the second call is rejected immediately and never sends a new AUTH packet — net result: a healthy re-auth gets killed by a bogus call.

Move the validation above the interrupt branch so that interruption only happens when the new request will actually go on the wire.

this._handleReauthCompleted(
new Error(
'reauthenticate: interrupted by new reauthentication request',
),
)
}

if (this.options.protocolVersion !== 5) {
error = new Error(
'reauthenticate: this feature works only with mqtt-v5',
)
} else if (!this.connected) {
error = new Error('reauthenticate: client is not connected')
} else if (!reauthOptions.authenticationData) {
error = new Error('reauthenticate: authenticationData is required')
}

const method = this.options.properties?.authenticationMethod

if (!error && !method) {
error = new Error(
'reauthenticate: authenticationMethod is required from initial CONNECT',
)
}

if (error) {
if (callback) {
callback(error)
} else {
this.emit('error', error)
}
return this
}
Comment thread
robertsLando marked this conversation as resolved.
Outdated

if (
reauthOptions.authenticationData &&
Buffer.isBuffer(this.options.properties.authenticationData) &&
reauthOptions.authenticationData.equals(
this.options.properties.authenticationData,
)
) {
this.log(
'reauthenticate: sending same authenticationData as initial connection',
)
}
Comment thread
robertsLando marked this conversation as resolved.
Outdated

const authPacket: IAuthPacket = {
cmd: 'auth',
reasonCode: 0x19, // Re-authentication (MQTT 5.0 Spec)
properties: {
...reauthOptions,
authenticationMethod: method,
},
}

this._reauthCallback = callback
this._sendPacket(authPacket)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

[Medium] _sendPacket is fire-and-forget — callback can hang forever.

Two related issues here:

  1. No error path on send. _sendPacket(packet, cb) exists for a reason — if the underlying stream write fails, the user callback is never invoked and _reauthCallback stays set until _cleanUp runs (i.e. until the client is destroyed). Use:
    this._sendPacket(authPacket, (err) => { if (err) this._handleReauth(err) })
  2. No timeout. A silent broker leaves _reauthCallback pending indefinitely. Other request/response paths in this client (subscribe, unsubscribe, publish-QoS>0) have inflight tracking and reconnection-aware retries; re-auth has neither. Add a timeout (configurable, default ~10–30s) and clear it in _handleReauth.

return this
}

/**
* _handleReauthCompleted
* Internal method to finalize the re-authentication process.
* Clears the pending reauth callback and signals completion via callback or event.
* @param {Error | null} err - The error if the re-authentication failed
* @param {IAuthPacket} [packet] - The AUTH packet received from the broker
* @api private
*/
public _handleReauthCompleted(err: Error | null, packet?: IAuthPacket) {
Comment thread
robertsLando marked this conversation as resolved.
Outdated
if (this._reauthCallback) {
const cb = this._reauthCallback
this._reauthCallback = null
cb(err, packet)
}

if (!err && packet) {
this.emit('reauth', packet)
}
}
Comment thread
robertsLando marked this conversation as resolved.

/**
* PRIVATE METHODS
* =====================
Expand Down Expand Up @@ -1864,6 +1966,10 @@ export default class MqttClient extends TypedEventEmitter<MqttClientEventCallbac
})
}

if (this._reauthCallback) {
this._handleReauthCompleted(new Error('client disconnected'))
}

if (!this.disconnecting && !this.reconnecting) {
this.log(
'_cleanUp :: client not disconnecting/reconnecting. Clearing and resetting reconnect.',
Expand Down
18 changes: 16 additions & 2 deletions src/lib/handlers/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,40 @@ const handleAuth: PacketHandler = (
return
}

if (rc === 0) {
client._handleReauthCompleted(null, packet)
return
}
Comment thread
robertsLando marked this conversation as resolved.
Outdated

client.handleAuth(
packet,
(err: ErrorWithReasonCode, packet2: IAuthPacket) => {
if (err) {
client.emit('error', err)
if (client.connected) client._handleReauthCompleted(err, packet)
else client.emit('error', err)
return
}

if (rc === 24) {
client.reconnecting = false
client['_sendPacket'](packet2)
} else {
console.log('### DEBUGGING rc: ', rc, ' ####')
Comment thread
robertsLando marked this conversation as resolved.
Outdated
const error = new ErrorWithReasonCode(
`Connection refused: ${ReasonCodes[rc]}`,
rc,
)
client.emit('error', error)
if (client.connected) {
client._handleReauthCompleted(error, packet)
} else {
client.emit('error', error)
}
}
},
)
console.log(
'DEBUG 3: Finished calling handleAuth (but did the callback run?)',
)
Comment thread
robertsLando marked this conversation as resolved.
Outdated
}

export default handleAuth
Loading
Loading