fix(xep): When using FAST, fallback to other SASL mechanisms on failure

This commit is contained in:
PapaTutuWawa 2023-04-01 21:42:52 +02:00
parent 0033d0eb6e
commit 4a6aa79e56
3 changed files with 134 additions and 5 deletions

View File

@ -53,10 +53,15 @@ abstract class Sasl2AuthenticationNegotiator extends SaslNegotiator
/// the base64-encoded response data. /// the base64-encoded response data.
Future<String> getRawStep(String input); Future<String> getRawStep(String input);
/// Tells the negotiator that it has been selected as the SASL negotiator for SASL2.
void pickForSasl2() { void pickForSasl2() {
_pickedForSasl2 = true; _pickedForSasl2 = true;
} }
/// When SASL2 fails, should we retry (true) or just fail (false).
/// Defaults to just returning false.
bool shouldRetrySasl() => false;
@override @override
void reset() { void reset() {
_pickedForSasl2 = false; _pickedForSasl2 = false;
@ -289,6 +294,16 @@ class Sasl2Negotiator extends XmppFeatureNegotiatorBase {
await negotiator.onSasl2Failure(nonza); await negotiator.onSasl2Failure(nonza);
} }
// Check if we should retry and, if we should, reset the current
// negotiator, this negotiator, and retry.
if (_currentSaslNegotiator!.shouldRetrySasl()) {
_currentSaslNegotiator!.reset();
reset();
return const Result(
NegotiatorState.retryLater,
);
}
return Result( return Result(
SaslError.fromFailure(nonza), SaslError.fromFailure(nonza),
); );

View File

@ -30,10 +30,14 @@ class FASTToken {
); );
factory FASTToken.fromXml(XMLNode token) { factory FASTToken.fromXml(XMLNode token) {
assert(token.tag == 'token', assert(
'Token can only be deserialised from a <token /> element',); token.tag == 'token',
assert(token.xmlns == fastXmlns, 'Token can only be deserialised from a <token /> element',
'Token can only be deserialised from a <token /> element',); );
assert(
token.xmlns == fastXmlns,
'Token can only be deserialised from a <token /> element',
);
return FASTToken( return FASTToken(
token.attributes['token']! as String, token.attributes['token']! as String,
@ -80,7 +84,8 @@ class FASTSaslNegotiator extends Sasl2AuthenticationNegotiator {
@override @override
bool canInlineFeature(List<XMLNode> features) { bool canInlineFeature(List<XMLNode> features) {
return features.firstWhereOrNull( return features.firstWhereOrNull(
(child) => child.tag == 'fast' && child.xmlns == fastXmlns,) != (child) => child.tag == 'fast' && child.xmlns == fastXmlns,
) !=
null; null;
} }
@ -114,6 +119,9 @@ class FASTSaslNegotiator extends Sasl2AuthenticationNegotiator {
); );
} }
@override
bool shouldRetrySasl() => true;
@override @override
Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async { Future<List<XMLNode>> onSasl2FeaturesReceived(XMLNode sasl2Features) async {
if (fastToken != null && pickedForSasl2) { if (fastToken != null && pickedForSasl2) {

View File

@ -160,4 +160,110 @@ void main() {
expect(conn.resource, 'MU29eEZn'); expect(conn.resource, 'MU29eEZn');
expect(fakeSocket.getState(), 7); expect(fakeSocket.getState(), 7);
}); });
test('Test failed FAST authentication with a token', () async {
final fakeSocket = StubTCPSocket([
StringExpectation(
"<stream:stream xmlns='jabber:client' version='1.0' xmlns:stream='http://etherx.jabber.org/streams' to='test.server' xml:lang='en'>",
'''
<stream:stream
xmlns="jabber:client"
version="1.0"
xmlns:stream="http://etherx.jabber.org/streams"
from="test.server"
xml:lang="en">
<stream:features xmlns="http://etherx.jabber.org/streams">
<mechanisms xmlns="urn:ietf:params:xml:ns:xmpp-sasl">
<mechanism>PLAIN</mechanism>
<mechanism>HT-SHA-256-NONE</mechanism>
</mechanisms>
<authentication xmlns='urn:xmpp:sasl:2'>
<mechanism>PLAIN</mechanism>
<mechanism>HT-SHA-256-NONE</mechanism>
<mechanism>HT-SHA-256-ENDP</mechanism>
<inline>
<bind xmlns="urn:xmpp:bind:0" />
<fast xmlns="urn:xmpp:fast:0">
<mechanism>HT-SHA-256-NONE</mechanism>
<mechanism>HT-SHA-256-ENDP</mechanism>
</fast>
</inline>
</authentication>
<bind xmlns="urn:ietf:params:xml:ns:xmpp-bind">
<required/>
</bind>
</stream:features>''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='HT-SHA-256-NONE'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm</initial-response><fast xmlns='urn:xmpp:fast:0' /></authenticate>",
'''
<failure xmlns='urn:xmpp:sasl:2'>
<not-authorized xmlns='urn:ietf:params:xml:ns:xmpp-sasl'/>
</failure>
''',
),
StanzaExpectation(
"<authenticate xmlns='urn:xmpp:sasl:2' mechanism='PLAIN'><user-agent id='d4565fa7-4d72-4749-b3d3-740edbf87770'><software>moxxmpp</software><device>PapaTutuWawa's awesome device</device></user-agent><initial-response>AHBvbHlub21kaXZpc2lvbgBhYWFh</initial-response><request-token xmlns='urn:xmpp:fast:0' mechanism='HT-SHA-256-NONE' /></authenticate>",
'''
<success xmlns='urn:xmpp:sasl:2'>
<authorization-identifier>polynomdivision@test.server</authorization-identifier>
<token xmlns='urn:xmpp:fast:0'
expiry='2020-03-12T14:36:15Z'
token='ed00e36cb42449a365a306a413f51ffd5ea8' />
</success>
''',
),
StanzaExpectation(
'<iq xmlns="jabber:client" type="set" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"/></iq>',
'<iq xmlns="jabber:client" type="result" id="a"><bind xmlns="urn:ietf:params:xml:ns:xmpp-bind"><jid>polynomdivision@test.server/MU29eEZn</jid></bind></iq>',
ignoreId: true,
),
]);
final conn = XmppConnection(
TestingReconnectionPolicy(),
AlwaysConnectedConnectivityManager(),
fakeSocket,
)..setConnectionSettings(
ConnectionSettings(
jid: JID.fromString('polynomdivision@test.server'),
password: 'aaaa',
useDirectTLS: true,
),
);
await conn.registerManagers([
RosterManager(TestingRosterStateManager('', [])),
DiscoManager([]),
]);
await conn.registerFeatureNegotiators([
SaslPlainNegotiator(),
ResourceBindingNegotiator(),
FASTSaslNegotiator()
..fastToken = const FASTToken(
'WXZzciBwYmFmdmZnZiBqdmd1IGp2eXFhcmZm',
'2020-03-12T14:36:15Z',
),
Sasl2Negotiator(
userAgent: const UserAgent(
id: 'd4565fa7-4d72-4749-b3d3-740edbf87770',
software: 'moxxmpp',
device: "PapaTutuWawa's awesome device",
),
),
]);
final result1 = await conn.connect(
waitUntilLogin: true,
shouldReconnect: false,
enableReconnectOnSuccess: false,
);
expect(result1.isType<NegotiatorError>(), false);
expect(conn.resource, 'MU29eEZn');
expect(fakeSocket.getState(), 4);
final token = conn
.getNegotiatorById<FASTSaslNegotiator>(saslFASTNegotiator)!
.fastToken;
expect(token != null, true);
expect(token!.token, 'ed00e36cb42449a365a306a413f51ffd5ea8');
});
} }