Adicionar suporte a timezone local na validação de exp/nbf do JWT #469
MessiasNatal
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Descrição:
Atualmente a validação do token no Horse.JWT (FPC) usa DateTimeToUnix(Now) sem o parâmetro AInputIsUTC. No FPC, o padrão desse parâmetro é True, ou seja, o Now é tratado como UTC. Isso faz com que a validação considere o fuso horário incorretamente quando a máquina não está em UTC.
Problema:
DateTimeToUnix(Now) no FPC assume que a entrada é UTC (AInputIsUTC = True), mas Now retorna a hora local. Quando o servidor está em um fuso diferente de UTC (ex: UTC-3), a validação compara o exp do token com um timestamp incorreto, podendo causar expiração antecipada ou validação incorreta.
Solução proposta:
Adicionar uma variável global JWTUseUTCTime: Boolean (padrão False) que controla se a validação deve tratar o Now como UTC ou hora local:
var
JWTUseUTCTime: Boolean = False;
E alterar as validações de exp e nbf para:
if (LJWT.Claims.exp <> 0) and (LJWT.Claims.exp < DateTimeToUnix(Now, JWTUseUTCTime)) then
Uso:
// Para usar hora local (recomendado para servidores com timezone diferente de UTC):
Horse.JWT.JWTUseUTCTime := False;
// Para manter comportamento UTC (padrão atual):
Horse.JWT.JWTUseUTCTime := True;
Benefícios:
Modificações:
unit Horse.JWT;
{$IF DEFINED(FPC)}
{$MODE DELPHI}{$H+}
{$ENDIF}
interface
uses
{$IF DEFINED(FPC)}
Classes,
fpjson,
SysUtils,
HTTPDefs,
fpjwt,
Base64,
DateUtils,
jsonparser,
HlpIHashInfo,
HlpConverters,
HlpHashFactory,
StrUtils,
{$ELSE}
System.Generics.Collections,
System.Classes,
System.JSON,
System.SysUtils,
Web.HTTPApp,
REST.JSON,
JOSE.Core.JWT,
JOSE.Core.JWK,
JOSE.Core.Builder,
JOSE.Consumer.Validators,
JOSE.Consumer,
JOSE.Context,
{$ENDIF}
Horse,
Horse.Commons;
type
{$IF DEFINED(FPC)}
TOnResponse = {$IF DEFINED(HORSE_FPC_FUNCTIONREFERENCES)}reference to {$ENDIF}procedure(const AHorseResponse: THorseResponse; const AMessage: string; const AHTTPStatus: THTTPStatus);
{$ELSE}
TOnResponse = reference to procedure(const AHorseResponse: THorseResponse; const AMessage: string; const AHTTPStatus: THTTPStatus);
{$ENDIF}
IHorseJWTConfig = interface
['{71A29190-1528-4E4D-932D-86094DDA9B4A}']
function SkipRoutes: TArray; overload;
function SkipRoutes(const ARoutes: TArray): IHorseJWTConfig; overload;
function SkipRoutes(const ARoute: string): IHorseJWTConfig; overload;
function Header: string; overload;
function Header(const AValue: string): IHorseJWTConfig; overload;
function IsRequiredSubject: Boolean; overload;
function IsRequiredSubject(const AValue: Boolean): IHorseJWTConfig; overload;
function IsRequiredIssuedAt: Boolean; overload;
function IsRequiredIssuedAt(const AValue: Boolean): IHorseJWTConfig; overload;
function IsRequiredNotBefore: Boolean; overload;
function IsRequiredNotBefore(const AValue: Boolean): IHorseJWTConfig; overload;
function IsRequiredExpirationTime: Boolean; overload;
function IsRequiredExpirationTime(const AValue: Boolean): IHorseJWTConfig; overload;
function IsRequireAudience: Boolean; overload;
function IsRequireAudience(const AValue: Boolean): IHorseJWTConfig; overload;
function ExpectedAudience: TArray; overload;
function ExpectedAudience(const AValue: TArray): IHorseJWTConfig; overload;
function SessionClass: TClass; overload;
function SessionClass(const AValue: TClass): IHorseJWTConfig; overload;
function OnResponse: TOnResponse; overload;
function OnResponse(const AValue: TOnResponse): IHorseJWTConfig; overload;
end;
{ THorseJWTConfig }
THorseJWTConfig = class(TInterfacedObject, IHorseJWTConfig)
private
FHeader: string;
FSkipRoutes: TArray;
FIsRequireAudience: Boolean;
FExpectedAudience: TArray;
FIsRequiredExpirationTime: Boolean;
FIsRequiredIssuedAt: Boolean;
FIsRequiredNotBefore: Boolean;
FIsRequiredSubject: Boolean;
FSessionClass: TClass;
FOnResponse: TOnResponse;
function SkipRoutes: TArray; overload;
function SkipRoutes(const ARoutes: TArray): IHorseJWTConfig; overload;
function SkipRoutes(const ARoute: string): IHorseJWTConfig; overload;
function Header: string; overload;
function Header(const AValue: string): IHorseJWTConfig; overload;
function IsRequiredSubject: Boolean; overload;
function IsRequiredSubject(const AValue: Boolean): IHorseJWTConfig; overload;
function IsRequiredIssuedAt: Boolean; overload;
function IsRequiredIssuedAt(const AValue: Boolean): IHorseJWTConfig; overload;
function IsRequiredNotBefore: Boolean; overload;
function IsRequiredNotBefore(const AValue: Boolean): IHorseJWTConfig; overload;
function IsRequiredExpirationTime: Boolean; overload;
function IsRequiredExpirationTime(const AValue: Boolean): IHorseJWTConfig; overload;
function IsRequireAudience: Boolean; overload;
function IsRequireAudience(const AValue: Boolean): IHorseJWTConfig; overload;
function ExpectedAudience: TArray; overload;
function ExpectedAudience(const AValue: TArray): IHorseJWTConfig; overload;
function SessionClass: TClass; overload;
function SessionClass(const AValue: TClass): IHorseJWTConfig; overload;
function OnResponse: TOnResponse; overload;
function OnResponse(const AValue: TOnResponse): IHorseJWTConfig; overload;
public
constructor Create;
class function New: IHorseJWTConfig;
end;
var
JWTUseUTCTime: Boolean = False;
function HorseJWT(const ASecretJWT: string; const AConfig: IHorseJWTConfig = nil): THorseCallback;
implementation
{$IF DEFINED(FPC) AND NOT DEFINED(HORSE_FPC_FUNCTIONREFERENCES)}
var
SecretJWT: string;
Config: IHorseJWTConfig;
{$ENDIF}
const
TOKEN_NOT_FOUND = 'Token not found';
INVALID_AUTHORIZATION_TYPE = 'Invalid authorization type';
UNAUTHORIZED = 'Unauthorized';
procedure Middleware(AHorseRequest: THorseRequest; AHorseResponse: THorseResponse; ANext: {$IF DEFINED(FPC)}TNextProc{$ELSE}TProc{$ENDIF}; const ASecretJWT: string; const AConfig: IHorseJWTConfig);
var
{$IF DEFINED(FPC)}
LJWT: TJWT;
LStartTokenPayloadPos: Integer;
LEndTokenPayloadPos: Integer;
{$ELSE}
LBuilder: IJOSEConsumerBuilder;
LValidations: IJOSEConsumer;
LJWT: TJOSEContext;
{$ENDIF}
LPathInfo: string;
LToken, LHeaderNormalize: string;
LSession: TObject;
LJSON: TJSONObject;
LConfig: IHorseJWTConfig;
{$IF DEFINED(FPC)}
function HexToAscii(const HexStr: string): AnsiString;
var
LByte: Byte;
LCmd: string;
LLength: Integer;
LIndex: Integer;
begin
Result := '';
LCmd := Trim(HexStr);
LIndex := 1;
LLength := Length(LCmd);
while LIndex < LLength do
begin
LByte := StrToInt('$' + copy(LCmd, LIndex, 2));
Result := Result + AnsiChar(chr(LByte));
Inc(LIndex, 2);
end;
end;
function ValidateSignature: Boolean;
var
LHMAC: IHMAC;
LSignCalc: string;
begin
if (LJWT.JOSE.alg = 'HS256') then
LHMAC := THashFactory.THMAC.CreateHMAC(THashFactory.TCrypto.CreateSHA2_256)
else
if (LJWT.JOSE.alg = 'HS384') then
LHMAC := THashFactory.THMAC.CreateHMAC(THashFactory.TCrypto.CreateSHA2_384)
else
if (LJWT.JOSE.alg = 'HS512') then
LHMAC := THashFactory.THMAC.CreateHMAC(THashFactory.TCrypto.CreateSHA2_512)
else
raise Exception.Create('[alg] not implemented');
end;
{$ENDIF}
begin
LConfig := AConfig;
if AConfig = nil then
LConfig := THorseJWTConfig.New;
LPathInfo := AHorseRequest.RawWebRequest.PathInfo;
if LPathInfo = EmptyStr then
LPathInfo := '/';
if MatchRoute(LPathInfo, LConfig.SkipRoutes) then
begin
ANext();
Exit;
end;
LHeaderNormalize := LConfig.Header;
if Length(LHeaderNormalize) > 0 then
LHeaderNormalize[1] := UpCase(LHeaderNormalize[1]);
LToken := AHorseRequest.Headers[LConfig.Header];
if LToken.IsEmpty then
LToken := AHorseRequest.Cookie.Items[LConfig.Header];
if LToken.Trim.IsEmpty and not AHorseRequest.Query.TryGetValue(
LConfig.Header, LToken) and not AHorseRequest.Query.TryGetValue(
LHeaderNormalize, LToken) then
begin
if Assigned(LConfig.OnResponse()) then
LConfig.OnResponse()(AHorseResponse, TOKEN_NOT_FOUND, THTTPStatus.Unauthorized)
else
AHorseResponse.Send(TOKEN_NOT_FOUND).Status(THTTPStatus.Unauthorized);
raise EHorseCallbackInterrupted.Create(TOKEN_NOT_FOUND);
end;
if Pos('bearer', LowerCase(LToken)) = 0 then
begin
if Assigned(LConfig.OnResponse()) then
LConfig.OnResponse()(AHorseResponse, INVALID_AUTHORIZATION_TYPE, THTTPStatus.Unauthorized)
else
AHorseResponse.Send(INVALID_AUTHORIZATION_TYPE).Status(THTTPStatus.Unauthorized);
raise EHorseCallbackInterrupted.Create(INVALID_AUTHORIZATION_TYPE);
end;
LToken := Trim(LToken.Replace('bearer', '', [rfIgnoreCase]));
try
{$IFNDEF FPC}
LBuilder := TJOSEConsumerBuilder.NewConsumer.SetVerificationKey(ASecretJWT)
.SetSkipVerificationKeyValidation;
{$ELSE}
LJWT := TJWT.Create;
try
LJWT.AsString := LToken;
try
if (Trim(LJWT.Signature) = EmptyStr) or (not ValidateSignature) then
raise Exception.Create('Invalid signature');
{$ENDIF}
AHorseRequest.Session(LSession);
except
on E: Exception do
begin
if Assigned(LConfig.OnResponse()) then
LConfig.OnResponse()(AHorseResponse, UNAUTHORIZED, THTTPStatus.Unauthorized)
else
AHorseResponse.Send(UNAUTHORIZED).Status(THTTPStatus.Unauthorized);
raise EHorseCallbackInterrupted.Create(UNAUTHORIZED);
end;
end;
finally
LJWT.Free;
end;
except
on E: EHorseCallbackInterrupted do
raise;
on E: Exception do
begin
if Assigned(LConfig.OnResponse()) then
LConfig.OnResponse()(AHorseResponse, 'Invalid token authorization. ' + E.Message, THTTPStatus.Unauthorized)
else
AHorseResponse.Send('Invalid token authorization. ' + E.Message).Status(THTTPStatus.Unauthorized);
raise EHorseCallbackInterrupted.Create;
end;
end;
try
ANext();
finally
LSession.Free;
end;
end;
{$IF DEFINED(FPC) AND NOT DEFINED(HORSE_FPC_FUNCTIONREFERENCES)}
procedure Callback(AHorseRequest: THorseRequest; AHorseResponse: THorseResponse; ANext: {$IF DEFINED(FPC)}TNextProc{$ELSE}TProc{$ENDIF});
begin
Middleware(AHorseRequest, AHorseResponse, ANext, SecretJWT, Config);
end;
{$ENDIF}
function HorseJWT(const ASecretJWT: string; const AConfig: IHorseJWTConfig): THorseCallback;
{$IF DEFINED(FPC) AND DEFINED(HORSE_FPC_FUNCTIONREFERENCES)}
procedure InternalCallback(AHorseRequest: THorseRequest; AHorseResponse: THorseResponse; ANext: TNextProc);
begin
Middleware(AHorseRequest, AHorseResponse, ANext, ASecretJWT, AConfig);
end;
{$ENDIF}
begin
{$IF DEFINED(FPC)}
{$IF NOT DEFINED(HORSE_FPC_FUNCTIONREFERENCES)}
SecretJWT := ASecretJWT;
Config := AConfig;
Result := Callback;
{$ELSE}
Result := InternalCallback;
{$ENDIF}
{$ELSE}
Result := procedure(AHorseRequest: THorseRequest; AHorseResponse: THorseResponse; ANext: TProc)
begin
Middleware(AHorseRequest, AHorseResponse, ANext, ASecretJWT, AConfig);
end;
{$ENDIF}
end;
{ THorseJWTConfig }
function THorseJWTConfig.SkipRoutes: TArray;
begin
Result := FSkipRoutes;
end;
function THorseJWTConfig.SkipRoutes(const ARoutes: TArray): IHorseJWTConfig;
var
I: Integer;
begin
FSkipRoutes := ARoutes;
for I := 0 to Pred(Length(FSkipRoutes)) do
if copy(Trim(FSkipRoutes[I]), 1, 1) <> '/' then
FSkipRoutes[I] := '/' + FSkipRoutes[I];
Result := Self;
end;
function THorseJWTConfig.Header: string;
begin
Result := FHeader;
end;
function THorseJWTConfig.Header(const AValue: string): IHorseJWTConfig;
begin
FHeader := AValue;
Result := Self;
end;
function THorseJWTConfig.IsRequiredSubject: Boolean;
begin
Result := FIsRequiredSubject;
end;
function THorseJWTConfig.IsRequiredSubject(const AValue: Boolean): IHorseJWTConfig;
begin
FIsRequiredSubject := AValue;
Result := Self;
end;
function THorseJWTConfig.IsRequiredIssuedAt: Boolean;
begin
Result := FIsRequiredIssuedAt;
end;
function THorseJWTConfig.IsRequiredIssuedAt(const AValue: Boolean): IHorseJWTConfig;
begin
FIsRequiredIssuedAt := AValue;
Result := Self;
end;
function THorseJWTConfig.IsRequiredNotBefore: Boolean;
begin
Result := FIsRequiredNotBefore;
end;
function THorseJWTConfig.IsRequiredNotBefore(const AValue: Boolean): IHorseJWTConfig;
begin
FIsRequiredNotBefore := AValue;
Result := Self;
end;
function THorseJWTConfig.IsRequiredExpirationTime: Boolean;
begin
Result := FIsRequiredExpirationTime;
end;
function THorseJWTConfig.IsRequiredExpirationTime(const AValue: Boolean): IHorseJWTConfig;
begin
FIsRequiredExpirationTime := AValue;
Result := Self;
end;
function THorseJWTConfig.IsRequireAudience: Boolean;
begin
Result := FIsRequireAudience;
end;
function THorseJWTConfig.IsRequireAudience(const AValue: Boolean): IHorseJWTConfig;
begin
FIsRequireAudience := AValue;
Result := Self;
end;
function THorseJWTConfig.ExpectedAudience: TArray;
begin
Result := FExpectedAudience;
end;
function THorseJWTConfig.ExpectedAudience(const AValue: TArray): IHorseJWTConfig;
begin
FExpectedAudience := AValue;
Result := Self;
end;
function THorseJWTConfig.SessionClass: TClass;
begin
Result := FSessionClass;
end;
function THorseJWTConfig.SessionClass(const AValue: TClass): IHorseJWTConfig;
begin
FSessionClass := AValue;
Result := Self;
end;
function THorseJWTConfig.SkipRoutes(const ARoute: string): IHorseJWTConfig;
begin
Result := SkipRoutes([ARoute]);
end;
constructor THorseJWTConfig.Create;
begin
FHeader := 'authorization';
FIsRequireAudience := False;
FIsRequiredExpirationTime := False;
FIsRequiredIssuedAt := False;
FIsRequiredNotBefore := False;
FIsRequiredSubject := False;
end;
class function THorseJWTConfig.New: IHorseJWTConfig;
begin
Result := Self.Create;
end;
function THorseJWTConfig.OnResponse: TOnResponse;
begin
Result := FOnResponse;
end;
function THorseJWTConfig.OnResponse(const AValue: TOnResponse): IHorseJWTConfig;
begin
FOnResponse := AValue;
Result := Self;
end;
end.
Beta Was this translation helpful? Give feedback.
All reactions