Commit 6a8f477c by Mac Stephens

Update to 0.9.4.7 - Secure WebSockets and API access; add SMS device verification

parent 7fed875b
...@@ -11,6 +11,7 @@ emiMobileServer/*.log ...@@ -11,6 +11,7 @@ emiMobileServer/*.log
emiMobileServer/*.txt emiMobileServer/*.txt
emiMobileServer/bin/logs/ emiMobileServer/bin/logs/
emiMobileServer/bin/static/ emiMobileServer/bin/static/
emiMobileServer/bin/emiMobileServer.ini
emiMobileServer/Source/__history emiMobileServer/Source/__history
emiMobileServer/Source/__recovery/ emiMobileServer/Source/__recovery/
......
...@@ -4,6 +4,7 @@ interface ...@@ -4,6 +4,7 @@ interface
uses uses
XData.Service.Common, XData.Service.Common,
XData.Security.Attributes,
Aurelius.Mapping.Attributes, Aurelius.Mapping.Attributes,
System.JSON, System.JSON,
System.Generics.Collections, System.Generics.Collections,
...@@ -15,6 +16,7 @@ const ...@@ -15,6 +16,7 @@ const
type type
[ServiceContract, Model(API_MODEL)] [ServiceContract, Model(API_MODEL)]
[Authorize]
IApiService = interface(IInvokable) IApiService = interface(IInvokable)
['{4FCB7FAF-44E5-49D6-9C0F-EE44BFB33313}'] ['{4FCB7FAF-44E5-49D6-9C0F-EE44BFB33313}']
[HttpGet] function GetBadgeCounts: TJSONObject; [HttpGet] function GetBadgeCounts: TJSONObject;
......
...@@ -46,11 +46,8 @@ type ...@@ -46,11 +46,8 @@ type
function GetAgencieslist(): TAgenciesList; function GetAgencieslist(): TAgenciesList;
function GetAgencyConfiglist: TAgencyConfigList; function GetAgencyConfiglist: TAgencyConfigList;
function BeginRegistration(const PhoneNumber: string): TJSONObject; function BeginRegistration(const PhoneNumber: string): TJSONObject;
function VerifyRegistrationCode(const PhoneNumber, VerificationCode, function VerifyRegistrationCode(const PhoneNumber, VerificationCode, VerificationToken: string): TJSONObject;
VerificationToken: string): TJSONObject; function CompleteRegistration(const PhoneNumber, CredentialId, AttestationObject, ClientDataJSON, ChallengeToken: string): TJSONObject;
function CompleteRegistration(const PhoneNumber, CredentialId,
AttestationObject, ClientDataJSON,
ChallengeToken: string): TJSONObject;
function BeginAuthentication(const CredentialId: string): TJSONObject; function BeginAuthentication(const CredentialId: string): TJSONObject;
function GetDeviceUser(const CredentialId: string): TJSONObject; function GetDeviceUser(const CredentialId: string): TJSONObject;
function LoginAutomatic(const CredentialId, ChallengeToken, function LoginAutomatic(const CredentialId, ChallengeToken,
...@@ -82,6 +79,21 @@ uses ...@@ -82,6 +79,21 @@ uses
Webauthn.Crypto, Webauthn.Crypto,
Webauthn.Cbor; Webauthn.Cbor;
const
VERIFICATION_RESEND_SECONDS = 60;
MAX_VERIFICATION_ATTEMPTS = 5;
type
TVerificationAttempt = record
Attempts: Integer;
ExpiresAt: TDateTime;
end;
var
VerificationLock: TObject;
VerificationSentAt: TDictionary<string, TDateTime>;
VerificationAttempts: TDictionary<string, TVerificationAttempt>;
{ TAuthService } { TAuthService }
...@@ -215,9 +227,6 @@ begin ...@@ -215,9 +227,6 @@ begin
(Cardinal(bytes[2]) shl 8) or (Cardinal(bytes[2]) shl 8) or
Cardinal(bytes[3]); Cardinal(bytes[3]);
Result := FormatFloat('000000', value mod 1000000); Result := FormatFloat('000000', value mod 1000000);
// ADD THIS DELETE BEFORE DEPLOY
Logger.Log(3, '---TAuthService.GenerateVerificationCode initiated with Verification Code: ' + Result);
Logger.Log(3, '---TAuthService.GenerateVerificationCode End');
end; end;
function TAuthService.CreateVerificationToken(const PhoneNumber, function TAuthService.CreateVerificationToken(const PhoneNumber,
...@@ -314,6 +323,10 @@ function TAuthService.BeginRegistration(const PhoneNumber: string): TJSONObject; ...@@ -314,6 +323,10 @@ function TAuthService.BeginRegistration(const PhoneNumber: string): TJSONObject;
var var
code, normalizedPhone, verificationToken, deviceName: string; code, normalizedPhone, verificationToken, deviceName: string;
deviceId: Integer; deviceId: Integer;
secondsSinceLastSend: Integer;
secondsUntilRetry: Integer;
sentAt: TDateTime;
attempt: TVerificationAttempt;
q: TUniQuery; q: TUniQuery;
begin begin
Logger.Log(3, '---TAuthService.BeginRegistration initiated with phone number: ' + PhoneNumber); Logger.Log(3, '---TAuthService.BeginRegistration initiated with phone number: ' + PhoneNumber);
...@@ -363,6 +376,31 @@ begin ...@@ -363,6 +376,31 @@ begin
q.Free; q.Free;
end; end;
secondsUntilRetry := 0;
TMonitor.Enter(VerificationLock);
try
if VerificationSentAt.TryGetValue(normalizedPhone, sentAt) then
begin
secondsSinceLastSend := SecondsBetween(Now, sentAt);
if secondsSinceLastSend < VERIFICATION_RESEND_SECONDS then
secondsUntilRetry := VERIFICATION_RESEND_SECONDS - secondsSinceLastSend;
end;
if secondsUntilRetry = 0 then
VerificationSentAt.AddOrSetValue(normalizedPhone, Now);
finally
TMonitor.Exit(VerificationLock);
end;
if secondsUntilRetry > 0 then
begin
Result.AddPair('status', 'error');
Result.AddPair('message', Format(
'Please wait %d seconds before requesting another verification code.',
[secondsUntilRetry]));
Exit;
end;
code := GenerateVerificationCode; code := GenerateVerificationCode;
verificationToken := CreateVerificationToken(normalizedPhone, code); verificationToken := CreateVerificationToken(normalizedPhone, code);
...@@ -371,6 +409,12 @@ begin ...@@ -371,6 +409,12 @@ begin
except except
on E: Exception do on E: Exception do
begin begin
TMonitor.Enter(VerificationLock);
try
VerificationSentAt.Remove(normalizedPhone);
finally
TMonitor.Exit(VerificationLock);
end;
Logger.Log(2, '--Error sending registration verification code: ' + E.Message); Logger.Log(2, '--Error sending registration verification code: ' + E.Message);
Result.AddPair('status', 'error'); Result.AddPair('status', 'error');
Result.AddPair('message', 'The verification code could not be sent. Please try again.'); Result.AddPair('message', 'The verification code could not be sent. Please try again.');
...@@ -378,10 +422,18 @@ begin ...@@ -378,10 +422,18 @@ begin
end; end;
end; end;
// ADD THIS DELETE BEFORE DEPLOY attempt.Attempts := 0;
attempt.ExpiresAt := IncMinute(Now, 5);
TMonitor.Enter(VerificationLock);
try
VerificationAttempts.AddOrSetValue(normalizedPhone, attempt);
finally
TMonitor.Exit(VerificationLock);
end;
Logger.Log(3, Format( Logger.Log(3, Format(
'Verification code set to %s for phone number: %s with device id %d and device name: %s', 'Verification code sent for device id %d and device name: %s',
[code, normalizedPhone, deviceId, deviceName])); [deviceId, deviceName]));
Result.AddPair('status', 'ok'); Result.AddPair('status', 'ok');
Result.AddPair('message', 'Verification code sent.'); Result.AddPair('message', 'Verification code sent.');
Result.AddPair('verificationToken', verificationToken); Result.AddPair('verificationToken', verificationToken);
...@@ -392,13 +444,11 @@ function TAuthService.VerifyRegistrationCode(const PhoneNumber, ...@@ -392,13 +444,11 @@ function TAuthService.VerifyRegistrationCode(const PhoneNumber,
VerificationCode, VerificationToken: string): TJSONObject; VerificationCode, VerificationToken: string): TJSONObject;
var var
normalizedPhone, token, challengeB64: string; normalizedPhone, token, challengeB64: string;
attempt: TVerificationAttempt;
verificationValid: Boolean;
tooManyAttempts: Boolean;
q: TUniQuery; q: TUniQuery;
begin begin
// ADD THIS DELETE BEFORE DEPLOY
Logger.Log(3, Format(
'---TAuthService.VerifyRegistrationCode initiated with phone number: %s and verification code: %s',
[PhoneNumber, VerificationCode]));
Result := TJSONObject.Create; Result := TJSONObject.Create;
TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result); TXDataOperationContext.Current.Handler.ManagedObjects.Add(Result);
...@@ -433,13 +483,50 @@ begin ...@@ -433,13 +483,50 @@ begin
q.Free; q.Free;
end; end;
if (Length(Trim(VerificationCode)) <> 6) or verificationValid := False;
not VerifyVerificationToken(normalizedPhone, Trim(VerificationCode), tooManyAttempts := False;
VerificationToken) then TMonitor.Enter(VerificationLock);
try
if VerificationAttempts.TryGetValue(normalizedPhone, attempt) and
(attempt.ExpiresAt < Now) then
VerificationAttempts.Remove(normalizedPhone);
if VerificationAttempts.TryGetValue(normalizedPhone, attempt) and
(attempt.Attempts >= MAX_VERIFICATION_ATTEMPTS) then
tooManyAttempts := True
else
begin
verificationValid := (Length(Trim(VerificationCode)) = 6) and
VerifyVerificationToken(normalizedPhone, Trim(VerificationCode),
VerificationToken);
if verificationValid then
VerificationAttempts.Remove(normalizedPhone)
else
begin
if not VerificationAttempts.TryGetValue(normalizedPhone, attempt) then
begin
attempt.Attempts := 0;
attempt.ExpiresAt := IncMinute(Now, 5);
end;
Inc(attempt.Attempts);
VerificationAttempts.AddOrSetValue(normalizedPhone, attempt);
tooManyAttempts := attempt.Attempts >= MAX_VERIFICATION_ATTEMPTS;
end;
end;
finally
TMonitor.Exit(VerificationLock);
end;
if not verificationValid then
begin begin
Logger.Log(2, '--Verification failed for phone number: ' + normalizedPhone); Logger.Log(2, '--Verification failed for phone number: ' + normalizedPhone);
Result.AddPair('status', 'error'); Result.AddPair('status', 'error');
Result.AddPair('message', 'Invalid or expired verification code.'); if tooManyAttempts then
Result.AddPair('message',
'Too many incorrect attempts. Request a new verification code.')
else
Result.AddPair('message', 'Invalid or expired verification code.');
Exit; Exit;
end; end;
...@@ -1697,6 +1784,14 @@ begin ...@@ -1697,6 +1784,14 @@ begin
end; end;
initialization initialization
VerificationLock := TObject.Create;
VerificationSentAt := TDictionary<string, TDateTime>.Create;
VerificationAttempts := TDictionary<string, TVerificationAttempt>.Create;
RegisterServiceType(TAuthService); RegisterServiceType(TAuthService);
finalization
VerificationAttempts.Free;
VerificationSentAt.Free;
VerificationLock.Free;
end. end.
...@@ -45,7 +45,7 @@ implementation ...@@ -45,7 +45,7 @@ implementation
uses uses
System.JSON, System.SysUtils, System.IOUtils, System.StrUtils, System.JSON, System.SysUtils, System.IOUtils, System.StrUtils,
Common.Logging; Common.Logging, Common.Ini;
procedure LoadServerConfig; procedure LoadServerConfig;
...@@ -65,6 +65,9 @@ var ...@@ -65,6 +65,9 @@ var
jsonObj: TJSONObject; jsonObj: TJSONObject;
begin begin
Logger.Log(1, '--LoadServerConfig - start'); Logger.Log(1, '--LoadServerConfig - start');
serverConfig.twilioAccountSid := IniEntries.TwilioAccountSid;
serverConfig.twilioAuthToken := IniEntries.TwilioAuthToken;
serverConfig.twilioFromNumber := IniEntries.TwilioFromNumber;
configFile := TPath.ChangeExtension(ParamStr(0), '.json'); configFile := TPath.ChangeExtension(ParamStr(0), '.json');
Logger.Log(1, '-- Config file: ' + configFile); Logger.Log(1, '-- Config file: ' + configFile);
...@@ -93,9 +96,6 @@ begin ...@@ -93,9 +96,6 @@ begin
s := GetStr(jsonObj, 'reportsFolder'); if s <> '' then serverConfig.reportsFolder := s; s := GetStr(jsonObj, 'reportsFolder'); if s <> '' then serverConfig.reportsFolder := s;
s := GetStr(jsonObj, 'rpId'); if s <> '' then serverConfig.rpId := s; s := GetStr(jsonObj, 'rpId'); if s <> '' then serverConfig.rpId := s;
s := GetStr(jsonObj, 'rpName'); if s <> '' then serverConfig.rpName := s; s := GetStr(jsonObj, 'rpName'); if s <> '' then serverConfig.rpName := s;
serverConfig.twilioAccountSid := GetStr(jsonObj, 'twilioAccountSid');
serverConfig.twilioAuthToken := GetStr(jsonObj, 'twilioAuthToken');
serverConfig.twilioFromNumber := GetStr(jsonObj, 'twilioFromNumber');
serverConfig.auditEnabled := jsonObj.GetValue<Boolean>('auditEnabled', serverConfig.auditEnabled); serverConfig.auditEnabled := jsonObj.GetValue<Boolean>('auditEnabled', serverConfig.auditEnabled);
finally finally
jsonObj.Free; jsonObj.Free;
...@@ -104,8 +104,8 @@ begin ...@@ -104,8 +104,8 @@ begin
Logger.Log(1, ''); Logger.Log(1, '');
Logger.Log(1, '--- Server Config Values ---'); Logger.Log(1, '--- Server Config Values ---');
Logger.Log(1, '-- url: ' + serverConfig.url + IfThen(serverConfig.url = defaultServerUrl, ' [default]', ' [from config]')); Logger.Log(1, '-- url: ' + serverConfig.url + IfThen(serverConfig.url = defaultServerUrl, ' [default]', ' [from config]'));
Logger.Log(1, '-- adminPassword: ' + serverConfig.adminPassword + IfThen(serverConfig.adminPassword = 'whatisthisusedfor', ' [default]', ' [from config]')); Logger.Log(1, '-- adminPassword: ' + IfThen(serverConfig.adminPassword <> '', '[configured]', '[not set]'));
Logger.Log(1, '-- jwtTokenSecret: ' + serverConfig.jwtTokenSecret + IfThen(serverConfig.jwtTokenSecret = 'super_secret0123super_secret4567', ' [default]', ' [from config]')); Logger.Log(1, '-- jwtTokenSecret: ' + IfThen(serverConfig.jwtTokenSecret <> '', '[configured]', '[not set]'));
Logger.Log(1, '-- webAppFolder: ' + serverConfig.webAppFolder + IfThen(serverConfig.webAppFolder = 'static', ' [default]', ' [from config]')); Logger.Log(1, '-- webAppFolder: ' + serverConfig.webAppFolder + IfThen(serverConfig.webAppFolder = 'static', ' [default]', ' [from config]'));
Logger.Log(1, '-- rpId: ' + serverConfig.rpId + IfThen(serverConfig.rpId = 'wcemimobile.em-sys.net', ' [default]', ' [from config]')); Logger.Log(1, '-- rpId: ' + serverConfig.rpId + IfThen(serverConfig.rpId = 'wcemimobile.em-sys.net', ' [default]', ' [from config]'));
Logger.Log(1, '-- auditEnabled: ' + BoolToStr(serverConfig.auditEnabled, True)); Logger.Log(1, '-- auditEnabled: ' + BoolToStr(serverConfig.auditEnabled, True));
......
...@@ -18,6 +18,14 @@ type ...@@ -18,6 +18,14 @@ type
FWebClientVersion: string; FWebClientVersion: string;
FWebClientVersionFromIni: Boolean; FWebClientVersionFromIni: Boolean;
// [Twilio]
FTwilioAccountSid: string;
FTwilioAccountSidFromIni: Boolean;
FTwilioAuthToken: string;
FTwilioAuthTokenFromIni: Boolean;
FTwilioFromNumber: string;
FTwilioFromNumberFromIni: Boolean;
// [Database] // [Database]
FDatabaseServer: string; FDatabaseServer: string;
FDatabaseServerFromIni: Boolean; FDatabaseServerFromIni: Boolean;
...@@ -44,6 +52,14 @@ type ...@@ -44,6 +52,14 @@ type
property WebClientVersion: string read FWebClientVersion; property WebClientVersion: string read FWebClientVersion;
property WebClientVersionFromIni: Boolean read FWebClientVersionFromIni; property WebClientVersionFromIni: Boolean read FWebClientVersionFromIni;
// [Twilio]
property TwilioAccountSid: string read FTwilioAccountSid;
property TwilioAccountSidFromIni: Boolean read FTwilioAccountSidFromIni;
property TwilioAuthToken: string read FTwilioAuthToken;
property TwilioAuthTokenFromIni: Boolean read FTwilioAuthTokenFromIni;
property TwilioFromNumber: string read FTwilioFromNumber;
property TwilioFromNumberFromIni: Boolean read FTwilioFromNumberFromIni;
// [Database] // [Database]
property DatabaseServer: string read FDatabaseServer; property DatabaseServer: string read FDatabaseServer;
property DatabaseServerFromIni: Boolean read FDatabaseServerFromIni; property DatabaseServerFromIni: Boolean read FDatabaseServerFromIni;
...@@ -93,6 +109,16 @@ begin ...@@ -93,6 +109,16 @@ begin
FWebClientVersion := iniFile.ReadString('Settings', 'webClientVersion', ''); FWebClientVersion := iniFile.ReadString('Settings', 'webClientVersion', '');
FWebClientVersionFromIni := iniFile.ValueExists('Settings', 'webClientVersion'); FWebClientVersionFromIni := iniFile.ValueExists('Settings', 'webClientVersion');
// [Twilio]
FTwilioAccountSid := iniFile.ReadString('Twilio', 'AccountSid', '');
FTwilioAccountSidFromIni := iniFile.ValueExists('Twilio', 'AccountSid');
FTwilioAuthToken := iniFile.ReadString('Twilio', 'AuthToken', '');
FTwilioAuthTokenFromIni := iniFile.ValueExists('Twilio', 'AuthToken');
FTwilioFromNumber := iniFile.ReadString('Twilio', 'FromNumber', '');
FTwilioFromNumberFromIni := iniFile.ValueExists('Twilio', 'FromNumber');
// [Database] // [Database]
FDatabaseServer := iniFile.ReadString('Database', 'Server', ''); FDatabaseServer := iniFile.ReadString('Database', 'Server', '');
FDatabaseServerFromIni := iniFile.ValueExists('Database', 'Server'); FDatabaseServerFromIni := iniFile.ValueExists('Database', 'Server');
......
...@@ -29,6 +29,7 @@ type ...@@ -29,6 +29,7 @@ type
private private
FConnectionId: string; FConnectionId: string;
FUserId: string; FUserId: string;
FCredentialId: string;
FClientVersion: string; FClientVersion: string;
FConnectedAt: TDateTime; FConnectedAt: TDateTime;
FLastSeenAt: TDateTime; FLastSeenAt: TDateTime;
...@@ -38,6 +39,7 @@ type ...@@ -38,6 +39,7 @@ type
public public
property ConnectionId: string read FConnectionId write FConnectionId; property ConnectionId: string read FConnectionId write FConnectionId;
property UserId: string read FUserId write FUserId; property UserId: string read FUserId write FUserId;
property CredentialId: string read FCredentialId write FCredentialId;
property ClientVersion: string read FClientVersion write FClientVersion; property ClientVersion: string read FClientVersion write FClientVersion;
property ConnectedAt: TDateTime read FConnectedAt write FConnectedAt; property ConnectedAt: TDateTime read FConnectedAt write FConnectedAt;
property LastSeenAt: TDateTime read FLastSeenAt write FLastSeenAt; property LastSeenAt: TDateTime read FLastSeenAt write FLastSeenAt;
...@@ -59,11 +61,17 @@ type ...@@ -59,11 +61,17 @@ type
FStarted: Integer; FStarted: Integer;
FOnClientsChanged: TClientsChangedEvent; FOnClientsChanged: TClientsChangedEvent;
function GetQueryParameter(AQuery, AName: string): string;
function IsDeviceActive(const ACredentialId: string): Boolean;
function AuthenticateConnection(AConnection: TTMSFNCWebSocketServerConnection;
out AUserId, ACredentialId, AReason: string): Boolean;
function TrySendTextToClient(const AConnectionId, AMessage: string; function TrySendTextToClient(const AConnectionId, AMessage: string;
ALogFailure: Boolean = True): Boolean; ALogFailure: Boolean = True): Boolean;
function TryCloseClient(const AConnectionId: string): Boolean; function TryCloseClient(const AConnectionId: string): Boolean;
procedure HeartbeatSweep; procedure HeartbeatSweep;
procedure NotifyClientsChanged; procedure NotifyClientsChanged;
procedure AllowConnection(Sender: TObject;
AConnection: TTMSFNCWebSocketServerConnection; var AAllow: Boolean);
procedure HandshakeResponseSent(Sender: TObject; AConnection: TTMSFNCWebSocketServerConnection); procedure HandshakeResponseSent(Sender: TObject; AConnection: TTMSFNCWebSocketServerConnection);
procedure MessageReceived(Sender: TObject; AConnection: TTMSFNCWebSocketConnection; const AMessage: string); procedure MessageReceived(Sender: TObject; AConnection: TTMSFNCWebSocketConnection; const AMessage: string);
procedure ClientDisconnected(Sender: TObject; AConnection: TTMSFNCWebSocketConnection); procedure ClientDisconnected(Sender: TObject; AConnection: TTMSFNCWebSocketConnection);
...@@ -85,12 +93,37 @@ implementation ...@@ -85,12 +93,37 @@ implementation
uses uses
System.DateUtils, System.DateUtils,
System.NetEncoding,
IdStackConsts, IdStackConsts,
DBAccess,
Uni,
Bcl.JOSE.Core.JWT,
Bcl.JOSE.Core.Builder,
Common.Config,
Common.Ini,
Common.Logging; Common.Logging;
type type
TWebSocketServerConnectionAccess = class(TTMSFNCWebSocketServerConnection); TWebSocketServerConnectionAccess = class(TTMSFNCWebSocketServerConnection);
function TWebSocketManager.GetQueryParameter(AQuery, AName: string): string;
var
queryPart: string;
nameValue: TArray<string>;
begin
Result := '';
for queryPart in AQuery.Split(['&']) do
begin
nameValue := queryPart.Split(['='], 2);
if (Length(nameValue) = 2) and
SameText(TNetEncoding.URL.Decode(nameValue[0]), AName) then
begin
Result := TNetEncoding.URL.Decode(nameValue[1]);
Exit;
end;
end;
end;
constructor TWebSocketManager.Create; constructor TWebSocketManager.Create;
begin begin
inherited Create; inherited Create;
...@@ -102,6 +135,7 @@ begin ...@@ -102,6 +135,7 @@ begin
FServer := TTMSFNCWebSocketServer.Create; FServer := TTMSFNCWebSocketServer.Create;
FServer.Port := WEBSOCKET_PORT; FServer.Port := WEBSOCKET_PORT;
FServer.UseSSL := False; FServer.UseSSL := False;
FServer.OnAllow := AllowConnection;
FServer.OnHandshakeResponseSent := HandshakeResponseSent; FServer.OnHandshakeResponseSent := HandshakeResponseSent;
FServer.OnMessageReceived := MessageReceived; FServer.OnMessageReceived := MessageReceived;
FServer.OnDisconnect := ClientDisconnected; FServer.OnDisconnect := ClientDisconnected;
...@@ -124,6 +158,113 @@ begin ...@@ -124,6 +158,113 @@ begin
FHeartbeatThread.Start; FHeartbeatThread.Start;
end; end;
function TWebSocketManager.IsDeviceActive(const ACredentialId: string): Boolean;
var
connection: TUniConnection;
query: TUniQuery;
begin
Result := False;
connection := TUniConnection.Create(nil);
try
connection.ProviderName := 'PostgreSQL';
connection.Server := IniEntries.DatabaseServer;
connection.Port := IniEntries.DatabasePort;
connection.Database := IniEntries.DatabaseName;
connection.Username := IniEntries.DatabaseUsername;
connection.Password := IniEntries.DatabasePassword;
connection.LoginPrompt := False;
connection.Connect;
query := TUniQuery.Create(nil);
try
query.Connection := connection;
query.SQL.Text :=
'SELECT id FROM lems.device_registrations ' +
'WHERE credential_id = :CID AND status = ''active''';
query.ParamByName('CID').AsString := ACredentialId;
query.Open;
Result := not query.IsEmpty;
finally
query.Free;
end;
finally
connection.Free;
end;
end;
function TWebSocketManager.AuthenticateConnection(
AConnection: TTMSFNCWebSocketServerConnection;
out AUserId, ACredentialId, AReason: string): Boolean;
var
token: string;
jwt: TJWT;
begin
Result := False;
AUserId := '';
ACredentialId := '';
AReason := '';
token := GetQueryParameter(AConnection.HandshakeRequest.QueryParams, 'token');
if token = '' then
begin
AReason := 'missing token';
Exit;
end;
jwt := nil;
try
try
jwt := TJOSE.Verify(ServerConfig.jwtTokenSecret, token);
except
AReason := 'invalid token';
Exit;
end;
if not Assigned(jwt) or not jwt.Verified then
begin
AReason := 'invalid token';
Exit;
end;
if (jwt.Claims.Expiration = 0) or (jwt.Claims.Expiration <= Now) then
begin
AReason := 'expired token';
Exit;
end;
if jwt.Claims.JSON.GetValue<Boolean>('device_management_only', False) then
begin
AReason := 'device manager token';
Exit;
end;
AUserId := Trim(jwt.Claims.JSON.GetValue<string>('user_name', ''));
ACredentialId := Trim(jwt.Claims.JSON.GetValue<string>('credential_id', ''));
if AUserId = '' then
begin
AReason := 'missing username';
Exit;
end;
if ACredentialId = '' then
begin
AReason := 'missing credential id';
Exit;
end;
if not IsDeviceActive(ACredentialId) then
begin
AReason := 'device is not active';
Exit;
end;
Result := True;
finally
jwt.Free;
end;
end;
destructor TWebSocketManager.Destroy; destructor TWebSocketManager.Destroy;
begin begin
Stop; Stop;
...@@ -345,6 +486,41 @@ begin ...@@ -345,6 +486,41 @@ begin
FOnClientsChanged; FOnClientsChanged;
end; end;
procedure TWebSocketManager.AllowConnection(Sender: TObject;
AConnection: TTMSFNCWebSocketServerConnection; var AAllow: Boolean);
var
client: TConnectedClient;
userId: string;
credentialId: string;
reason: string;
begin
AAllow := False;
client := nil;
try
if not AuthenticateConnection(AConnection, userId, credentialId, reason) then
begin
Logger.Log(2, Format('WebSocket connection rejected from %s: %s',
[AConnection.PeerIP, reason]));
Exit;
end;
client := TConnectedClient.Create;
client.UserId := userId;
client.CredentialId := credentialId;
AConnection.OwnsUserData := True;
AConnection.UserData := client;
client := nil;
AAllow := True;
except
on E: Exception do
Logger.Log(1, Format('WebSocket authentication failed from %s: %s',
[AConnection.PeerIP, E.Message]));
end;
client.Free;
end;
procedure TWebSocketManager.HandshakeResponseSent(Sender: TObject; AConnection: TTMSFNCWebSocketServerConnection); procedure TWebSocketManager.HandshakeResponseSent(Sender: TObject; AConnection: TTMSFNCWebSocketServerConnection);
var var
client: TConnectedClient; client: TConnectedClient;
...@@ -352,10 +528,13 @@ var ...@@ -352,10 +528,13 @@ var
begin begin
CreateGUID(guid); CreateGUID(guid);
client := TConnectedClient(AConnection.UserData);
if not Assigned(client) then
Exit;
TWebSocketServerConnectionAccess(AConnection).Connection.Socket.Binding.SetSockOpt( TWebSocketServerConnectionAccess(AConnection).Connection.Socket.Binding.SetSockOpt(
Id_SOL_SOCKET, Id_SO_SNDTIMEO, WEBSOCKET_SEND_TIMEOUT_MS); Id_SOL_SOCKET, Id_SO_SNDTIMEO, WEBSOCKET_SEND_TIMEOUT_MS);
client := TConnectedClient.Create;
client.ConnectionId := GUIDToString(guid); client.ConnectionId := GUIDToString(guid);
client.ConnectedAt := Now; client.ConnectedAt := Now;
client.LastSeenAt := client.ConnectedAt; client.LastSeenAt := client.ConnectedAt;
...@@ -366,14 +545,12 @@ begin ...@@ -366,14 +545,12 @@ begin
TMonitor.Enter(FClientsLock); TMonitor.Enter(FClientsLock);
try try
FClients.Add(client); FClients.Add(client);
AConnection.OwnsUserData := False;
finally finally
TMonitor.Exit(FClientsLock); TMonitor.Exit(FClientsLock);
end; end;
AConnection.OwnsUserData := False; Logger.Log(1, 'WebSocket client connected: ' + client.ConnectionId + ' - ' + client.UserId);
AConnection.UserData := client;
Logger.Log(1, 'WebSocket client connected: ' + client.ConnectionId);
NotifyClientsChanged; NotifyClientsChanged;
end; end;
...@@ -381,7 +558,6 @@ procedure TWebSocketManager.MessageReceived(Sender: TObject; AConnection: TTMSFN ...@@ -381,7 +558,6 @@ procedure TWebSocketManager.MessageReceived(Sender: TObject; AConnection: TTMSFN
var var
json: TJSONValue; json: TJSONValue;
messageType: string; messageType: string;
userId: string;
clientVersion: string; clientVersion: string;
connectionId: string; connectionId: string;
client: TConnectedClient; client: TConnectedClient;
...@@ -399,10 +575,6 @@ begin ...@@ -399,10 +575,6 @@ begin
(not SameText(messageType, 'heartbeat')) then (not SameText(messageType, 'heartbeat')) then
Exit; Exit;
if SameText(messageType, 'identify') and
(not json.TryGetValue<string>('userId', userId)) then
Exit;
clientVersion := ''; clientVersion := '';
if SameText(messageType, 'identify') then if SameText(messageType, 'identify') then
json.TryGetValue<string>('clientVersion', clientVersion); json.TryGetValue<string>('clientVersion', clientVersion);
...@@ -421,10 +593,7 @@ begin ...@@ -421,10 +593,7 @@ begin
connectionId := client.ConnectionId; connectionId := client.ConnectionId;
if SameText(messageType, 'identify') then if SameText(messageType, 'identify') then
begin client.ClientVersion := clientVersion
client.UserId := userId;
client.ClientVersion := clientVersion;
end
else else
client.HeartbeatSeen := True; client.HeartbeatSeen := True;
finally finally
...@@ -435,7 +604,7 @@ begin ...@@ -435,7 +604,7 @@ begin
begin begin
Logger.Log(1, Format( Logger.Log(1, Format(
'WebSocket client identified: %s - %s - version %s', 'WebSocket client identified: %s - %s - version %s',
[connectionId, userId, clientVersion])); [connectionId, client.UserId, clientVersion]));
NotifyClientsChanged; NotifyClientsChanged;
end end
else else
......
[Settings] [Settings]
LogFileNum=123 LogFileNum=130
webClientVersion=0.9.4.6 webClientVersion=0.9.4.7
[Twilio]
AccountSid=
AuthToken=
FromNumber=
[Database] [Database]
--Server=192.168.91.129 --Server=192.168.91.129
......
...@@ -2,8 +2,5 @@ ...@@ -2,8 +2,5 @@
"url": "http://localhost:2001/emsys/emiMobile/", "url": "http://localhost:2001/emsys/emiMobile/",
"jwtTokenSecret": "super_secret0123super_secret4567", "jwtTokenSecret": "super_secret0123super_secret4567",
"adminPassword": "whatisthisusedfor?", "adminPassword": "whatisthisusedfor?",
"webAppFolder": "static", "webAppFolder": "static"
"twilioAccountSid": "",
"twilioAuthToken": "",
"twilioFromNumber": ""
} }
...@@ -108,9 +108,9 @@ ...@@ -108,9 +108,9 @@
<VerInfo_MajorVer>0</VerInfo_MajorVer> <VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_MinorVer>9</VerInfo_MinorVer> <VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_Release>4</VerInfo_Release> <VerInfo_Release>4</VerInfo_Release>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.9.4.6;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.2.0;Comments=</VerInfo_Keys> <VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.9.4.7;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.2.0;Comments=</VerInfo_Keys>
<DCC_UnitSearchPath>C:\RADTOOLS\FastMM4;$(DCC_UnitSearchPath)</DCC_UnitSearchPath> <DCC_UnitSearchPath>C:\RADTOOLS\FastMM4;$(DCC_UnitSearchPath)</DCC_UnitSearchPath>
<VerInfo_Build>6</VerInfo_Build> <VerInfo_Build>7</VerInfo_Build>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(Cfg_1_Win64)'!=''"> <PropertyGroup Condition="'$(Cfg_1_Win64)'!=''">
<AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode> <AppDPIAwarenessMode>PerMonitorV2</AppDPIAwarenessMode>
......
AccountSid=AC717f06b3c10bbecff42487426e34fdb3
AuthToken=a0dec128c1b5a3231909ba81da8947b6
FromNumber=+18775346578
\ No newline at end of file
...@@ -21,7 +21,7 @@ type ...@@ -21,7 +21,7 @@ type
procedure VerifyVersion(SuccessProc: TSuccessProc); procedure VerifyVersion(SuccessProc: TSuccessProc);
public public
property WsUrl: string read FWsUrl; property WsUrl: string read FWsUrl;
const clientVersion = '0.9.4.6'; const clientVersion = '0.9.4.7';
procedure InitApp(SuccessProc: TSuccessProc; procedure InitApp(SuccessProc: TSuccessProc;
UnauthorizedAccessProc: TUnauthorizedAccessProc); UnauthorizedAccessProc: TUnauthorizedAccessProc);
end; end;
......
...@@ -44,7 +44,6 @@ object FViewDeviceRegistration: TFViewDeviceRegistration ...@@ -44,7 +44,6 @@ object FViewDeviceRegistration: TFViewDeviceRegistration
Height = 21 Height = 21
ElementID = 'view.devicereg.edtverificationcode' ElementID = 'view.devicereg.edtverificationcode'
HeightPercent = 100.000000000000000000 HeightPercent = 100.000000000000000000
TabOrder = 0
TextHint = 'Six-digit code' TextHint = 'Six-digit code'
WidthPercent = 100.000000000000000000 WidthPercent = 100.000000000000000000
end end
......
...@@ -173,7 +173,7 @@ begin ...@@ -173,7 +173,7 @@ begin
end; end;
for i := 1 to Length(verificationCode) do for i := 1 to Length(verificationCode) do
if not CharInSet(verificationCode[i], ['0'..'9']) then if (verificationCode[i] < '0') or (verificationCode[i] > '9') then
begin begin
ShowNotification('Please enter the six-digit verification code.'); ShowNotification('Please enter the six-digit verification code.');
Exit; Exit;
......
...@@ -91,12 +91,12 @@ ...@@ -91,12 +91,12 @@
<DCC_RemoteDebug>false</DCC_RemoteDebug> <DCC_RemoteDebug>false</DCC_RemoteDebug>
<VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo> <VerInfo_IncludeVerInfo>true</VerInfo_IncludeVerInfo>
<VerInfo_Locale>1033</VerInfo_Locale> <VerInfo_Locale>1033</VerInfo_Locale>
<VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.9.4.6;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.3.0;Comments=;LastCompiledTime=2018/08/27 15:18:29</VerInfo_Keys> <VerInfo_Keys>CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=0.9.4.7;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=0.9.3.0;Comments=;LastCompiledTime=2018/08/27 15:18:29</VerInfo_Keys>
<AppDPIAwarenessMode>PerMonitor</AppDPIAwarenessMode> <AppDPIAwarenessMode>PerMonitor</AppDPIAwarenessMode>
<VerInfo_MinorVer>9</VerInfo_MinorVer> <VerInfo_MinorVer>9</VerInfo_MinorVer>
<VerInfo_MajorVer>0</VerInfo_MajorVer> <VerInfo_MajorVer>0</VerInfo_MajorVer>
<VerInfo_Release>4</VerInfo_Release> <VerInfo_Release>4</VerInfo_Release>
<VerInfo_Build>6</VerInfo_Build> <VerInfo_Build>7</VerInfo_Build>
<TMSWebSingleInstance>1</TMSWebSingleInstance> <TMSWebSingleInstance>1</TMSWebSingleInstance>
<TMSWebBrowser>1</TMSWebBrowser> <TMSWebBrowser>1</TMSWebBrowser>
<TMSWebOutputPath>..\emiMobileServer\bin\static</TMSWebOutputPath> <TMSWebOutputPath>..\emiMobileServer\bin\static</TMSWebOutputPath>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment