Add Http Only Cookie support (#738)

* HTTP Only Cookie support

* Unit name

* Without chaning the formatting

* Without chaning the formatting

* On logoff check if UseHttpOnly is true
This commit is contained in:
Marcelo Varela 2024-04-29 09:55:58 -03:00 committed by GitHub
parent f0bf273e0b
commit 493d2f21ae
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -36,7 +36,8 @@ uses
MVCFramework.Commons,
MVCFramework.JWT,
JsonDataObjects,
MVCFramework.HMAC;
MVCFramework.HMAC,
Web.HTTPApp;
type
TMVCJWTDefaults = class sealed
@ -78,6 +79,10 @@ type
FUserNameHeaderName: string;
FPasswordHeaderName: string;
FHMACAlgorithm: String;
FUseHttpOnly: Boolean;
FTokenHttpOnlyExpires: TDateTime;
FLogoffURLSegment: string;
procedure SendLogoffRender(AContext: TWebContext);
protected
function NeedsToBeExtended(const JWTValue: TJWT): Boolean;
procedure ExtendExpirationTime(const JWTValue: TJWT);
@ -98,6 +103,15 @@ type
AClaimsToCheck: TJWTCheckableClaims = [];
ALeewaySeconds: Cardinal = 300;
AHMACAlgorithm: String = HMAC_HS512); overload; virtual;
constructor Create(AAuthenticationHandler: IMVCAuthenticationHandler;
AConfigClaims: TJWTClaimsSetup;
AUseHttpOnly: Boolean;
ALogoffURLSegment: string = '/logoff';
ASecret: string = 'D3lph1MVCFram3w0rk';
ALoginURLSegment: string = '/login';
AClaimsToCheck: TJWTCheckableClaims = [];
ALeewaySeconds: Cardinal = 300;
AHMACAlgorithm: String = HMAC_HS512); overload; virtual;
property AuthorizationHeaderName: string read FAuthorizationHeaderName;
property UserNameHeaderName: string read FUserNameHeaderName;
property PasswordHeaderName: string read FPasswordHeaderName;
@ -173,12 +187,26 @@ begin
FUserNameHeaderName := TMVCJWTDefaults.USERNAME_HEADER;
FPasswordHeaderName := TMVCJWTDefaults.PASSWORD_HEADER;
FHMACAlgorithm := AHMACAlgorithm;
FUseHttpOnly := False;
FTokenHttpOnlyExpires := Now;
end;
constructor TMVCJWTAuthenticationMiddleware.Create(AAuthenticationHandler: IMVCAuthenticationHandler; AConfigClaims: TJWTClaimsSetup; AUseHttpOnly: Boolean; ALogoffURLSegment: string;
ASecret, ALoginURLSegment: string; AClaimsToCheck: TJWTCheckableClaims; ALeewaySeconds: Cardinal; AHMACAlgorithm: String);
begin
Create(AAuthenticationHandler, AConfigClaims, ASecret, ALoginURLSegment, AClaimsToCheck, ALeewaySeconds, AHMACAlgorithm);
FUseHttpOnly := AUseHttpOnly;
FLogoffURLSegment := ALogoffURLSegment;
end;
procedure TMVCJWTAuthenticationMiddleware.ExtendExpirationTime(const JWTValue: TJWT);
begin
JWTValue.Claims.ExpirationTime := Max(JWTValue.Claims.ExpirationTime, Now) +
(JWTValue.LeewaySeconds + JWTValue.LiveValidityWindowInSeconds) * OneSecond;
if FUseHttpOnly then
begin
FTokenHttpOnlyExpires := JWTValue.Claims.ExpirationTime;
end;
end;
procedure TMVCJWTAuthenticationMiddleware.InternalRender(AJSONOb: TJDOJsonObject;
@ -186,9 +214,22 @@ procedure TMVCJWTAuthenticationMiddleware.InternalRender(AJSONOb: TJDOJsonObject
var
Encoding: TEncoding;
ContentType, JValue: string;
Cookie: TCookie;
begin
JValue := AJSONOb.ToJSON;
if FUseHttpOnly then
begin
Cookie := AContext.Response.Cookies.Add;
Cookie.Expires := FTokenHttpOnlyExpires;
Cookie.Path := '/';
Cookie.Name := 'token';
Cookie.Value := AJSONOb.S['token'];
Cookie.HttpOnly := True;
// Cookie.Secure := True;
// Cookie.SameSite := 'none';
end;
AContext.Response.RawWebResponse.ContentType := AContentType + '; charset=' + AContentEncoding;
ContentType := AContentType + '; charset=' + AContentEncoding;
@ -204,6 +245,27 @@ begin
FreeAndNil(AJSONOb)
end;
procedure TMVCJWTAuthenticationMiddleware.SendLogoffRender(AContext: TWebContext);
const
returnMessage = '{ "message": "Successful logout" }';
ContentType = 'application/json; charset=UTF-8';
AContentEncoding = 'UTF-8';
var
Encoding: TEncoding;
Cookie: TCookie;
begin
Cookie := AContext.Response.Cookies.Add;
Cookie.Name := 'token';
Cookie.Path := '/';
Encoding := TEncoding.GetEncoding(AContentEncoding);
try
AContext.Response.SetContentStream(TBytesStream.Create(TEncoding.Convert(TEncoding.Default, Encoding, TEncoding.Default.GetBytes(returnMessage))), ContentType);
finally
Encoding.Free;
end;
end;
function TMVCJWTAuthenticationMiddleware.NeedsToBeExtended(const JWTValue: TJWT): Boolean;
var
lWillExpireIn: Int64;
@ -234,6 +296,7 @@ var
AuthAccessToken: string;
AuthToken: string;
ErrorMsg: string;
cookieToken: string;
begin
// check if the resource is protected
if Assigned(FAuthenticationHandler) then
@ -309,6 +372,18 @@ begin
begin
AuthToken := AuthAccessToken.Trim;
AuthToken := Trim(TNetEncoding.URL.Decode(AuthToken));
end
else
begin
if FUseHttpOnly then
begin
cookieToken := AContext.Request.Cookie('token');
if (not cookieToken.IsEmpty) then
begin
AuthToken := cookieToken.Trim;
AuthToken := Trim(TNetEncoding.URL.Decode(AuthToken));
end;
end;
end;
end;
@ -431,6 +506,11 @@ begin
LJWTValue.Data := AContext.Request;
FSetupJWTClaims(LJWTValue);
if FUseHttpOnly then
begin
FTokenHttpOnlyExpires := LJWTValue.Claims.ExpirationTime;
end;
// these claims are mandatory and managed by the middleware
if not LJWTValue.CustomClaims['username'].IsEmpty then
raise EMVCJWTException.Create
@ -482,6 +562,14 @@ begin
finally
LRolesList.Free;
end;
end
else
begin
if SameText(AContext.Request.PathInfo, FLogoffURLSegment) and (FUseHttpOnly) then
begin
SendLogoffRender(AContext);
AHandled := True;
end;
end;
end;