Merge pull request #227 from joaoduarte19/jwt_improvements

JWT middleware improvements
This commit is contained in:
Daniele Teti 2019-06-24 12:47:26 +02:00 committed by GitHub
commit 0cbcbd51f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 82 additions and 37 deletions

View File

@ -10,6 +10,7 @@ uses
Web.WebReq,
Web.WebBroker,
IdHTTPWebBrokerBridge,
IdContext,
WebModuleUnit1 in 'WebModuleUnit1.pas' {WebModule1: TWebModule} ,
AppControllerU in 'AppControllerU.pas',
MVCFramework.Middleware.JWT in '..\..\sources\MVCFramework.Middleware.JWT.pas',
@ -17,6 +18,12 @@ uses
{$R *.res}
type
TWebBrokerBridgeAuthEvent = class
public
class procedure ServerParserAuthentication(AContext: TIdContext; const AAuthType, AAuthData: String; var VUsername,
VPassword: String; var VHandled: Boolean);
end;
procedure RunServer(APort: Integer);
var
@ -25,6 +32,7 @@ begin
Writeln(Format('Starting HTTP Server or port %d', [APort]));
LServer := TIdHTTPWebBrokerBridge.Create(nil);
try
LServer.OnParseAuthentication := TWebBrokerBridgeAuthEvent.ServerParserAuthentication;
LServer.DefaultPort := APort;
LServer.Active := True;
Writeln('Press RETURN to stop the server');
@ -35,6 +43,15 @@ begin
end;
end;
{ TWebBrokerBridgeAuthEvent }
class procedure TWebBrokerBridgeAuthEvent.ServerParserAuthentication(AContext: TIdContext; const AAuthType, AAuthData: String;
var VUsername, VPassword: String; var VHandled: Boolean);
begin
if SameText(AAuthType, 'bearer') then
VHandled := True;
end;
begin
ReportMemoryLeaksOnShutdown := True;
try

View File

@ -58,7 +58,12 @@ begin
TAuthenticationSample.Create,
lClaimsSetup,
'mys3cr37',
'/login'
'/login',
[TJWTCheckableClaim.ExpirationTime, TJWTCheckableClaim.NotBefore, TJWTCheckableClaim.IssuedAt],
300,
'Authorization',
'username',
'password'
));
end;

View File

@ -48,7 +48,7 @@ object Form5: TForm5
Font.Style = []
ParentFont = False
ReadOnly = True
TabOrder = 0
TabOrder = 1
end
object Memo2: TMemo
Left = 0
@ -63,7 +63,7 @@ object Form5: TForm5
Font.Style = []
ParentFont = False
ReadOnly = True
TabOrder = 1
TabOrder = 2
end
object Panel1: TPanel
Left = 0
@ -71,7 +71,7 @@ object Form5: TForm5
Width = 647
Height = 49
Align = alTop
TabOrder = 2
TabOrder = 0
object btnGet: TButton
AlignWithMargins = True
Left = 171
@ -80,8 +80,9 @@ object Form5: TForm5
Height = 41
Align = alLeft
Caption = 'Get a protected resource'
TabOrder = 0
TabOrder = 1
OnClick = btnGetClick
ExplicitTop = 2
end
object btnLOGIN: TButton
AlignWithMargins = True
@ -91,7 +92,7 @@ object Form5: TForm5
Height = 41
Align = alLeft
Caption = 'Login'
TabOrder = 1
TabOrder = 0
OnClick = btnLOGINClick
end
end

View File

@ -49,9 +49,10 @@ begin
{ Getting JSON response }
lClient := TRESTClient.Create('localhost', 8080);
try
lClient.UseBasicAuthentication := False;
lClient.ReadTimeOut(0);
if not FJWT.IsEmpty then
lClient.RequestHeaders.Values['Authentication'] := 'bearer ' + FJWT;
lClient.RequestHeaders.Values['Authorization'] := 'bearer ' + FJWT;
lQueryStringParams := TStringList.Create;
try
lQueryStringParams.Values['firstname'] := 'Daniele';
@ -70,9 +71,12 @@ begin
{ Getting HTML response }
lClient := TRESTClient.Create('localhost', 8080);
try
// when the JWT authorization header is named "Authorization", the basic authorization must be disabled
lClient.UseBasicAuthentication := False;
lClient.ReadTimeOut(0);
if not FJWT.IsEmpty then
lClient.RequestHeaders.Values['Authentication'] := 'bearer ' + FJWT;
lClient.RequestHeaders.Values['Authorization'] := 'bearer ' + FJWT;
lQueryStringParams := TStringList.Create;
try
lQueryStringParams.Values['firstname'] := 'Daniele';
@ -100,8 +104,8 @@ begin
try
lClient.ReadTimeOut(0);
lClient
.Header('jwtusername', 'user1')
.Header('jwtpassword', 'user1');
.Header('username', 'user1')
.Header('password', 'user1');
lRest := lClient.doPOST('/login', []);
if lRest.HasError then
begin

View File

@ -38,6 +38,21 @@ uses
JsonDataObjects;
type
TMVCJWTDefaults = class sealed
public const
/// <summary>
/// Default authorization header name
/// </summary>
AUTHORIZATION_HEADER = 'Authentication';
/// <summary>
/// Default username header name
/// </summary>
USERNAME_HEADER = 'jwtusername';
/// <summary>
/// Default password header name
/// </summary>
PASSWORD_HEADER = 'jwtpassword';
end;
TJWTClaimsSetup = reference to procedure(const JWT: TJWT);
@ -49,6 +64,9 @@ type
FSecret: string;
FLeewaySeconds: Cardinal;
FLoginURLSegment: string;
FAuthorizationHeaderName: string;
FUserNameHeaderName: string;
FPasswordHeaderName: string;
protected
function NeedsToBeExtended(const JWTValue: TJWT): Boolean;
procedure ExtendExpirationTime(const JWTValue: TJWT);
@ -65,10 +83,16 @@ type
procedure OnAfterControllerAction(AContext: TWebContext; const AActionName: string; const AHandled: Boolean);
public
constructor Create(AAuthenticationHandler: IMVCAuthenticationHandler; AConfigClaims: TJWTClaimsSetup;
ASecret: string = 'D3lph1MVCFram3w0rk'; ALoginURLSegment: string = '/login';
AClaimsToCheck: TJWTCheckableClaims = [TJWTCheckableClaim.ExpirationTime, TJWTCheckableClaim.NotBefore,
TJWTCheckableClaim.IssuedAt]; ALeewaySeconds: Cardinal = 300); virtual;
constructor Create(
AAuthenticationHandler: IMVCAuthenticationHandler;
AConfigClaims: TJWTClaimsSetup;
ASecret: string = 'D3lph1MVCFram3w0rk';
ALoginURLSegment: string = '/login';
AClaimsToCheck: TJWTCheckableClaims = [TJWTCheckableClaim.ExpirationTime, TJWTCheckableClaim.NotBefore, TJWTCheckableClaim.IssuedAt];
ALeewaySeconds: Cardinal = 300;
AAuthorizationHeaderName: string = TMVCJWTDefaults.AUTHORIZATION_HEADER;
AUserNameHeaderName: string = TMVCJWTDefaults.USERNAME_HEADER;
APasswordHeaderName: string = TMVCJWTDefaults.PASSWORD_HEADER); virtual;
end;
implementation
@ -81,9 +105,14 @@ uses
{ TMVCJWTAuthenticationMiddleware }
constructor TMVCJWTAuthenticationMiddleware.Create(AAuthenticationHandler: IMVCAuthenticationHandler;
AConfigClaims: TJWTClaimsSetup; ASecret: string = 'D3lph1MVCFram3w0rk'; ALoginURLSegment: string = '/login';
AClaimsToCheck: TJWTCheckableClaims = [TJWTCheckableClaim.ExpirationTime, TJWTCheckableClaim.NotBefore,
TJWTCheckableClaim.IssuedAt]; ALeewaySeconds: Cardinal = 300);
AConfigClaims: TJWTClaimsSetup;
ASecret: string = 'D3lph1MVCFram3w0rk';
ALoginURLSegment: string = '/login';
AClaimsToCheck: TJWTCheckableClaims = [TJWTCheckableClaim.ExpirationTime, TJWTCheckableClaim.NotBefore, TJWTCheckableClaim.IssuedAt];
ALeewaySeconds: Cardinal = 300;
AAuthorizationHeaderName: string = TMVCJWTDefaults.AUTHORIZATION_HEADER;
AUserNameHeaderName: string = TMVCJWTDefaults.USERNAME_HEADER;
APasswordHeaderName: string = TMVCJWTDefaults.PASSWORD_HEADER);
begin
inherited Create;
FAuthenticationHandler := AAuthenticationHandler;
@ -92,6 +121,9 @@ begin
FSecret := ASecret;
FLoginURLSegment := ALoginURLSegment;
FLeewaySeconds := ALeewaySeconds;
FAuthorizationHeaderName := AAuthorizationHeaderName;
FUserNameHeaderName := AUserNameHeaderName;
FPasswordHeaderName := APasswordHeaderName;
end;
procedure TMVCJWTAuthenticationMiddleware.ExtendExpirationTime(const JWTValue: TJWT);
@ -129,12 +161,6 @@ var
begin
lWillExpireIn := SecondsBetween(Now, JWTValue.Claims.ExpirationTime);
Result := lWillExpireIn <= JWTValue.LiveValidityWindowInSeconds;
// Log.Debug('--------------------------', 'EXPIRE');
// Log.DebugFmt('Now : %s', [TimeToStr(Now)], 'EXPIRE');
// Log.DebugFmt('ExpirationTime : %s', [TimeToStr(JWTValue.Claims.ExpirationTime)], 'EXPIRE');
// Log.DebugFmt('WillExpireIn : %d', [lWillExpireIn], 'EXPIRE');
// Log.DebugFmt('LVW : %d', [JWTValue.LiveValidityWindowInSeconds], 'EXPIRE');
// Log.DebugFmt('NeedsToBeExtened: %s', [BoolToStr(Result, True)], 'EXPIRE');
end;
procedure TMVCJWTAuthenticationMiddleware.OnAfterControllerAction(AContext: TWebContext; const AActionName: string;
@ -167,10 +193,10 @@ begin
JWTValue := TJWT.Create(FSecret, FLeewaySeconds);
try
JWTValue.RegClaimsToChecks := Self.FClaimsToChecks;
AuthHeader := AContext.Request.Headers['Authentication'];
AuthHeader := AContext.Request.Headers[FAuthorizationHeaderName];
if AuthHeader.IsEmpty then
begin
RenderError(HTTP_STATUS.Unauthorized, 'Authentication Required', AContext);
RenderError(HTTP_STATUS.Unauthorized, 'Authorization Required', AContext);
AHandled := True;
Exit;
end;
@ -183,14 +209,6 @@ begin
AuthToken := Trim(TNetEncoding.URL.Decode(AuthToken));
end;
// check the jwt
// if not JWTValue.IsValidToken(AuthToken, ErrorMsg) then
// begin
// RenderError(HTTP_STATUS.Unauthorized, ErrorMsg, AContext);
// AHandled := True;
// end
// else
if not JWTValue.LoadToken(AuthToken, ErrorMsg) then
begin
RenderError(HTTP_STATUS.Unauthorized, ErrorMsg, AContext);
@ -200,7 +218,7 @@ begin
if JWTValue.CustomClaims['username'].IsEmpty then
begin
RenderError(HTTP_STATUS.Unauthorized, 'Invalid Token, Authentication Required', AContext);
RenderError(HTTP_STATUS.Unauthorized, 'Invalid Token, Authorization Required', AContext);
AHandled := True;
end
else
@ -222,7 +240,7 @@ begin
if NeedsToBeExtended(JWTValue) then
begin
ExtendExpirationTime(JWTValue);
AContext.Response.SetCustomHeader('Authentication', 'bearer ' + JWTValue.GetToken);
AContext.Response.SetCustomHeader(FAuthorizationHeaderName, 'bearer ' + JWTValue.GetToken);
end;
end;
AHandled := False
@ -251,8 +269,8 @@ var
begin
if SameText(AContext.Request.PathInfo, FLoginURLSegment) and (AContext.Request.HTTPMethod = httpPOST) then
begin
UserName := TNetEncoding.URL.Decode(AContext.Request.Headers['jwtusername']);
Password := TNetEncoding.URL.Decode(AContext.Request.Headers['jwtpassword']);
UserName := TNetEncoding.URL.Decode(AContext.Request.Headers[FUserNameHeaderName]);
Password := TNetEncoding.URL.Decode(AContext.Request.Headers[FPasswordHeaderName]);
if (UserName.IsEmpty) or (Password.IsEmpty) then
begin
RenderError(HTTP_STATUS.Unauthorized, 'Username and password Required', AContext);