Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: otp expires after use #1594

Closed
wants to merge 10 commits into from
Original file line number Diff line number Diff line change
Expand Up @@ -118,8 +118,7 @@ class EnrollVerbHandler extends AbstractVerbHandler {
}
if (!atConnection.getMetaData().isAuthenticated) {
var otp = enrollParams.otp;
if (otp == null ||
(await OtpVerbHandler.cache.get(otp.toString()) == null)) {
if (!await OtpVerbHandler.isValidOtp(otp)) {
throw AtEnrollmentException(
'invalid otp. Cannot process enroll request');
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,22 @@ class OtpVerbHandler extends AbstractVerbHandler {
await cache.set(response.data!, response.data!);
break;
case 'validate':
String? otp = verbParams['otp'];
if (otp != null && (await cache.get(otp)) == otp) {
response.data = 'valid';
} else {
response.data = 'invalid';
}
response.data =
await isValidOtp(verbParams['otp']) ? 'valid' : 'invalid';
break;
}
}

static Future<bool> isValidOtp(String? otp) async {
if (otp != null && (await cache.get(otp)) == otp) {
// if an opt is validated, remove that otp from the cache
await cache.invalidate(otp);
return true;
} else {
return false;
}
}

/// This function generates a UUID and converts it into a 6-character alpha-numeric string.
///
/// The process involves converting the UUID to a hashcode, then transforming the hashcode
Expand Down Expand Up @@ -88,7 +94,7 @@ class OtpVerbHandler extends AbstractVerbHandler {
int randomAscii;
do {
randomAscii = minAscii + Random().nextInt((maxAscii - minAscii) + 1);
// 79 is the ASCII value of "O". If randamAscii is 79, generate again.
// 79 is the ASCII value of "O". If randomAscii is 79, generate again.
} while (randomAscii == 79);
return String.fromCharCode(randomAscii);
}
Expand Down
22 changes: 22 additions & 0 deletions packages/at_secondary_server/test/otp_verb_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,28 @@ void main() {
expect(response.data, 'valid');
});

test('verify otp:validate invalidates an OTP after it is used',
() async {
Response response = Response();
HashMap<String, String?> verbParams =
getVerbParam(VerbSyntax.otp, 'otp:get');
inboundConnection.getMetaData().isAuthenticated = true;

OtpVerbHandler otpVerbHandler = OtpVerbHandler(secondaryKeyStore);
await otpVerbHandler.processVerb(response, verbParams, inboundConnection);
String? otp = response.data;
// attempt #1 should return a valid response
verbParams =
getVerbParam(VerbSyntax.otp, 'otp:validate:$otp');
await otpVerbHandler.processVerb(response, verbParams, inboundConnection);
expect(response.data, 'valid');
// attempt #2 should return a invalid response
verbParams =
getVerbParam(VerbSyntax.otp, 'otp:validate:$otp');
await otpVerbHandler.processVerb(response, verbParams, inboundConnection);
expect(response.data, 'invalid');
});

test('A test to verify otp:validate returns invalid when OTP is expired',
() async {
Response response = Response();
Expand Down
101 changes: 61 additions & 40 deletions tests/at_functional_test/test/all_verbs_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -105,54 +105,75 @@ void main() async {

//THE FOLLOWING TESTS ONLY WORK WHEN IN TESTING MODE
test('config verb test set-reset-print operation', () async {
//the below block of code sets the commit log compaction freq to 4
await socket_writer(
socketFirstAtsign!, 'config:set:commitLogCompactionFrequencyMins=4');
var response = await read();
print('config verb response $response');
expect(response, contains('data:ok'));

//this resets the commit log compaction freq previously set to 4 to default value
await socket_writer(
socketFirstAtsign!, 'config:reset:commitLogCompactionFrequencyMins');
//await Future.delayed(Duration(seconds: 2));
response = await read();
print('config verb response $response');
expect(response, contains('data:ok'));

// this ensures that the reset actually works and the current value is 18 as per the config
// at tools/build_virtual_environment/ve_base/contents/atsign/secondary/base/config/config.yaml
await socket_writer(
socketFirstAtsign!, 'config:print:commitLogCompactionFrequencyMins');
//await Future.delayed(Duration(seconds: 2));
response = (await read()).trim();
print('config verb response [$response]');
// TODO gkc 20230219 It's two values because we used to set to 30 but now we're setting to 18.
// TODO gkc 20230219 We can remove the 'or' once build_virtual_environment is merged to trunk
expect((response == 'data:18' || response == 'data:30'), true);
//the below block of code sets the commit log compaction freq to 4
await socket_writer(
socketFirstAtsign!, 'config:set:commitLogCompactionFrequencyMins=4');
var response = await read();
print('config verb response $response');
expect(response, contains('data:ok'));

//this resets the commit log compaction freq previously set to 4 to default value
await socket_writer(
socketFirstAtsign!, 'config:reset:commitLogCompactionFrequencyMins');
//await Future.delayed(Duration(seconds: 2));
response = await read();
print('config verb response $response');
expect(response, contains('data:ok'));

// this ensures that the reset actually works and the current value is 18 as per the config
// at tools/build_virtual_environment/ve_base/contents/atsign/secondary/base/config/config.yaml
await socket_writer(
socketFirstAtsign!, 'config:print:commitLogCompactionFrequencyMins');
//await Future.delayed(Duration(seconds: 2));
response = (await read()).trim();
print('config verb response [$response]');
// TODO gkc 20230219 It's two values because we used to set to 30 but now we're setting to 18.
// TODO gkc 20230219 We can remove the 'or' once build_virtual_environment is merged to trunk
expect((response == 'data:18' || response == 'data:30'), true);
});

test('config verb test set-print', () async {
//the block of code below sets max notification retries to 25
await socket_writer(
socketFirstAtsign!, 'config:set:maxNotificationRetries=25');
var response = await read();
print('config verb response $response');
expect(response, contains('data:ok'));

//the block of code below verifies that the max notification retries is set to 25
await socket_writer(
socketFirstAtsign!, 'config:print:maxNotificationRetries');
await Future.delayed(Duration(seconds: 2));
response = await read();
print('config verb response $response');
expect(response, contains('data:25'));
//the block of code below sets max notification retries to 25
await socket_writer(
socketFirstAtsign!, 'config:set:maxNotificationRetries=25');
var response = await read();
print('config verb response $response');
expect(response, contains('data:ok'));

//the block of code below verifies that the max notification retries is set to 25
await socket_writer(
socketFirstAtsign!, 'config:print:maxNotificationRetries');
await Future.delayed(Duration(seconds: 2));
response = await read();
print('config verb response $response');
expect(response, contains('data:25'));
});

test('fetch and validate otp', () async {
await socket_writer(socketFirstAtsign!, 'otp:get');
String otp = (await read()).replaceFirst('data:', '');
otp = otp.replaceFirst('\n', '');
expect(otp.length, 6);
// validate otp
await socket_writer(socketFirstAtsign!, 'otp:validate:$otp');
expect(await read(), 'data:valid\n');
});

test('ensure otp is invalidated after use', () async {
await socket_writer(socketFirstAtsign!, 'otp:get');
String otp = (await read()).replaceFirst('data:', '');
otp = otp.replaceFirst('\n', '');
// validate otp attempt #1 should be valid
await socket_writer(socketFirstAtsign!, 'otp:validate:$otp');
expect(await read(), 'data:valid\n');
// validate otp attempt #2 should be invalid
await socket_writer(socketFirstAtsign!, 'otp:validate:$otp');
expect(await read(), 'data:invalid\n');
});

tearDown(() {
//Closing the socket connection
clear();
socketFirstAtsign!.destroy();
});
}
}
16 changes: 16 additions & 0 deletions tests/at_functional_test/test/enroll_verb_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,18 @@ void main() {
otp = otp.replaceAll('data:', '').trim();
});

Future<String> getNewOtp() async {
socketConnection2 =
await secure_socket_connection(firstAtsignServer, firstAtsignPort);
socket_listener(socketConnection2!);
await prepare(socketConnection2!, firstAtsign);
await socket_writer(socketConnection2!, 'otp:get');
String otp = (await read()).replaceFirst('data:', '');
otp = otp.replaceFirst('\n', '');
await socketConnection2?.close();
return otp;
}

test(
'A test to verify exception is thrown when request exceed the configured limit',
() async {
Expand Down Expand Up @@ -805,11 +817,13 @@ void main() {
jsonDecode((await read()).replaceAll('data:', ''));
expect(enrollmentResponse['status'], 'pending');
expect(enrollmentResponse['enrollmentId'], isNotNull);
otp = await getNewOtp();
enrollRequest =
'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n';
await socket_writer(unAuthenticatedConnection, enrollRequest);
enrollmentResponse = await read()
..replaceAll('error:', '');
print(enrollmentResponse);
expect(
enrollmentResponse.contains(
'Enrollment requests have exceeded the limit within the specified time frame'),
Expand All @@ -832,6 +846,7 @@ void main() {
jsonDecode((await read()).replaceAll('data:', ''));
expect(enrollmentResponse['status'], 'pending');
expect(enrollmentResponse['enrollmentId'], isNotNull);
otp = await getNewOtp();
enrollRequest =
'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n';
await socket_writer(unAuthenticatedConnection, enrollRequest);
Expand All @@ -844,6 +859,7 @@ void main() {
SecureSocket secondUnAuthenticatedConnection2 =
await secure_socket_connection(firstAtsignServer, firstAtsignPort);
socket_listener(secondUnAuthenticatedConnection2);
otp = await getNewOtp();
enrollRequest =
'enroll:request:{"appName":"wavi","deviceName":"pixel","namespaces":{"wavi":"rw"},"otp":"$otp","apkamPublicKey":"${pkamPublicKeyMap[firstAtsign]!}"}\n';
await socket_writer(secondUnAuthenticatedConnection2, enrollRequest);
Expand Down