Merge pull request #243 from viniciussanchez/master

#241 - RFC 6750
This commit is contained in:
Daniele Teti 2019-07-24 19:43:42 +02:00 committed by GitHub
commit 9b59fd6e44
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 141 additions and 119 deletions

View File

@ -40,36 +40,44 @@ resourcestring
sDMVCDPR =
'program %0:s;' + sLineBreak +
sLineBreak +
' {$APPTYPE CONSOLE}' + sLineBreak +
'' + sLineBreak +
'{$APPTYPE CONSOLE}' + sLineBreak +
sLineBreak +
'uses' + sLineBreak +
' System.SysUtils,' + sLineBreak +
' MVCFramework.Logger,' + sLineBreak +
' MVCFramework.Commons,' + sLineBreak +
' MVCFramework.REPLCommandsHandlerU,' + sLineBreak +
' Web.ReqMulti, {If you have problem with this unit, see https://quality.embarcadero.com/browse/RSP-17216}' + sLineBreak +
' Web.ReqMulti, //If you have problem with this unit, see https://quality.embarcadero.com/browse/RSP-17216' + sLineBreak +
' Web.WebReq,' + sLineBreak +
' Web.WebBroker,' + sLineBreak +
' IdContext,' + sLineBreak +
' IdHTTPWebBrokerBridge;' + sLineBreak +
'' + sLineBreak +
'{$R *.res}' + sLineBreak + sLineBreak +
sLineBreak +
'{$R *.res}' + sLineBreak +
sLineBreak +
'type' + sLineBreak +
' TDMVCParseAuthentication = class' + sLineBreak +
' public' + sLineBreak +
' class procedure OnParseAuthentication(AContext: TIdContext; const AAuthType, AAuthData: String; var VUsername,' + sLineBreak +
' VPassword: String; var VHandled: Boolean);' + sLineBreak +
' end;' + sLineBreak +
sLineBreak +
'procedure RunServer(APort: Integer);' + sLineBreak +
'var' + sLineBreak +
' lServer: TIdHTTPWebBrokerBridge;' + sLineBreak +
' lCustomHandler: TMVCCustomREPLCommandsHandler;' + sLineBreak +
' lCmd: string;' + sLineBreak +
' LServer: TIdHTTPWebBrokerBridge;' + sLineBreak +
' LCustomHandler: TMVCCustomREPLCommandsHandler;' + sLineBreak +
' LCmd: string;' + sLineBreak +
'begin' + sLineBreak +
' Writeln(''** DMVCFramework Server ** build '' + DMVCFRAMEWORK_VERSION);' + sLineBreak +
' LCmd := ''start'';' + sLineBreak +
' if ParamCount >= 1 then' + sLineBreak +
' lCmd := ParamStr(1)' + sLineBreak +
' else' + sLineBreak +
' lCmd := ''start'';' + sLineBreak +
'' + sLineBreak +
' lCustomHandler := function(const Value: String; const Server: TIdHTTPWebBrokerBridge; out Handled: Boolean): THandleCommandResult' + sLineBreak +
' LCmd := ParamStr(1);' + sLineBreak +
sLineBreak +
' LCustomHandler := function(const Value: String; const Server: TIdHTTPWebBrokerBridge; out Handled: Boolean): THandleCommandResult' + sLineBreak +
' begin' + sLineBreak +
' Handled := False;' + sLineBreak +
' Result := THandleCommandResult.Unknown;' + sLineBreak +
'' + sLineBreak +
sLineBreak +
' // Write here your custom command for the REPL using the following form...' + sLineBreak +
' // ***' + sLineBreak +
' // Handled := False;' + sLineBreak +
@ -86,28 +94,29 @@ resourcestring
' // Handled := True;' + sLineBreak +
' // end;' + sLineBreak +
' end;' + sLineBreak +
'' + sLineBreak +
sLineBreak +
' LServer := TIdHTTPWebBrokerBridge.Create(nil);' + sLineBreak +
' try' + sLineBreak +
' LServer.OnParseAuthentication := TDMVCParseAuthentication.OnParseAuthentication;' + sLineBreak +
' LServer.DefaultPort := APort;' + sLineBreak +
'' + sLineBreak +
sLineBreak +
' { more info about MaxConnections' + sLineBreak +
' http://www.indyproject.org/docsite/html/frames.html?frmname=topic&frmfile=TIdCustomTCPServer_MaxConnections.html }' + sLineBreak +
' LServer.MaxConnections := 0;' + sLineBreak +
'' + sLineBreak +
sLineBreak +
' { more info about ListenQueue' + sLineBreak +
' http://www.indyproject.org/docsite/html/frames.html?frmname=topic&frmfile=TIdCustomTCPServer_ListenQueue.html }' + sLineBreak +
' LServer.ListenQueue := 200;' + sLineBreak +
'' + sLineBreak +
sLineBreak +
' WriteLn(''Write "quit" or "exit" to shutdown the server'');' + sLineBreak +
' repeat' + sLineBreak +
' if lCmd.IsEmpty then' + sLineBreak +
' if LCmd.IsEmpty then' + sLineBreak +
' begin' + sLineBreak +
' Write(''-> '');' + sLineBreak +
' ReadLn(lCmd)' + sLineBreak +
' ReadLn(LCmd)' + sLineBreak +
' end;' + sLineBreak +
' try' + sLineBreak +
' case HandleCommand(lCmd.ToLower, LServer, lCustomHandler) of' + sLineBreak +
' case HandleCommand(LCmd.ToLower, LServer, LCustomHandler) of' + sLineBreak +
' THandleCommandResult.Continue:' + sLineBreak +
' begin' + sLineBreak +
' Continue;' + sLineBreak +
@ -118,18 +127,23 @@ resourcestring
' end;' + sLineBreak +
' THandleCommandResult.Unknown:' + sLineBreak +
' begin' + sLineBreak +
' REPLEmit(''Unknown command: '' + lCmd);' + sLineBreak +
' REPLEmit(''Unknown command: '' + LCmd);' + sLineBreak +
' end;' + sLineBreak +
' end;' + sLineBreak +
' finally' + sLineBreak +
' lCmd := '''';' + sLineBreak +
' LCmd := '''';' + sLineBreak +
' end;' + sLineBreak +
' until false;' + sLineBreak +
' until False;' + sLineBreak +
'' + sLineBreak +
' finally' + sLineBreak +
' LServer.Free;' + sLineBreak +
' end;' + sLineBreak +
'end;' + sLineBreak +
sLineBreak +
'class procedure TDMVCParseAuthentication.OnParseAuthentication(AContext: TIdContext; const AAuthType, AAuthData: String;' + sLineBreak +
' var VUsername, VPassword: String; var VHandled: Boolean);' + sLineBreak +
'begin' + sLineBreak +
' VHandled := SameText(LowerCase(AAuthType), ''bearer'');' + sLineBreak +
'end;' + sLineBreak +
sLineBreak +
'begin' + sLineBreak +

View File

@ -324,9 +324,13 @@ function TJWTDictionaryObject.GetItemAsDateTime(const Index: string): TDateTime;
var
lIntValue: Int64;
begin
if not TryStrToInt64(Items[index], lIntValue) then
raise Exception.Create('Item cannot be converted as Unix Epoch');
Result := UnixToDateTime(lIntValue, False);
Result := -693594;
if Trim(Items[index]) <> EmptyStr then
begin
if not TryStrToInt64(Items[index], lIntValue) then
raise Exception.Create('Item cannot be converted as Unix Epoch');
Result := UnixToDateTime(lIntValue, False);
end;
end;
function TJWTDictionaryObject.Keys: TArray<string>;

View File

@ -41,9 +41,9 @@ type
TMVCJWTDefaults = class sealed
public const
/// <summary>
/// Default authorization header name
/// Default authorization header name (RFC 6750)
/// </summary>
AUTHORIZATION_HEADER = 'Authentication';
AUTHORIZATION_HEADER = 'Authorization';
/// <summary>
/// Default username header name
/// </summary>
@ -70,26 +70,25 @@ type
protected
function NeedsToBeExtended(const JWTValue: TJWT): Boolean;
procedure ExtendExpirationTime(const JWTValue: TJWT);
procedure InternalRender(AJSONOb: TJDOJsonObject; AContentType: string; AContentEncoding: string;
AContext: TWebContext; AInstanceOwner: Boolean = True);
procedure RenderError(const AHTTPStatusCode: UInt16; const AErrorMessage: string;
const AContext: TWebContext; const AErrorClassName: string = ''; const AErrorNumber: Integer = 0);
procedure InternalRender(AJSONOb: TJDOJsonObject; AContentType: string; AContentEncoding: string; AContext: TWebContext;
AInstanceOwner: Boolean = True);
procedure RenderError(const AHTTPStatusCode: UInt16; const AErrorMessage: string; const AContext: TWebContext;
const AErrorClassName: string = ''; const AErrorNumber: Integer = 0);
procedure OnBeforeRouting(AContext: TWebContext; var AHandled: Boolean);
procedure OnBeforeControllerAction(AContext: TWebContext; const AControllerQualifiedClassName: string;
const AActionName: string; var AHandled: Boolean);
procedure OnAfterControllerAction(AContext: TWebContext; const AActionName: string; const AHandled: Boolean);
public
/// <remarks>
/// The AAuthorizationHeaderName, AUserNameHeaderName, and APasswordHeaderName parameters do not follow
/// the IETF national convention - RFC 6750;
/// </remarks>
constructor Create(
AAuthenticationHandler: IMVCAuthenticationHandler;
AConfigClaims: TJWTClaimsSetup;
ASecret: string = 'D3lph1MVCFram3w0rk';
ALoginURLSegment: string = '/login';
AClaimsToCheck: TJWTCheckableClaims = [TJWTCheckableClaim.ExpirationTime, TJWTCheckableClaim.NotBefore,
TJWTCheckableClaim.IssuedAt];
AClaimsToCheck: TJWTCheckableClaims = [];
ALeewaySeconds: Cardinal = 300;
AAuthorizationHeaderName: string = TMVCJWTDefaults.AUTHORIZATION_HEADER;
AUserNameHeaderName: string = TMVCJWTDefaults.USERNAME_HEADER;
@ -110,8 +109,7 @@ constructor TMVCJWTAuthenticationMiddleware.Create(AAuthenticationHandler: IMVCA
AConfigClaims: TJWTClaimsSetup;
ASecret: string = 'D3lph1MVCFram3w0rk';
ALoginURLSegment: string = '/login';
AClaimsToCheck: TJWTCheckableClaims = [TJWTCheckableClaim.ExpirationTime, TJWTCheckableClaim.NotBefore,
TJWTCheckableClaim.IssuedAt];
AClaimsToCheck: TJWTCheckableClaims = [];
ALeewaySeconds: Cardinal = 300;
AAuthorizationHeaderName: string = TMVCJWTDefaults.AUTHORIZATION_HEADER;
AUserNameHeaderName: string = TMVCJWTDefaults.USERNAME_HEADER;
@ -233,8 +231,7 @@ begin
AContext.LoggedUser.LoggedSince := JWTValue.Claims.IssuedAt;
AContext.LoggedUser.CustomData := JWTValue.CustomClaims.AsCustomData;
FAuthenticationHandler.OnAuthorization(AContext, AContext.LoggedUser.Roles, AControllerQualifiedClassName,
AActionName, IsAuthorized);
FAuthenticationHandler.OnAuthorization(AContext, AContext.LoggedUser.Roles, AControllerQualifiedClassName, AActionName, IsAuthorized);
if IsAuthorized then
begin
@ -261,92 +258,107 @@ end;
procedure TMVCJWTAuthenticationMiddleware.OnBeforeRouting(AContext: TWebContext; var AHandled: Boolean);
var
UserName: string;
Password: string;
RolesList: TList<string>;
SessionData: TSessionData;
IsValid: Boolean;
JWTValue: TJWT;
lCustomPair: TPair<string, string>;
LObj: TJDOJsonObject;
LUsername, LPassword, LBasicAuthenticationEncode: string;
LBasicAuthenticationDecode: TStringList;
LRolesList: TList<string>;
LSessionData: TSessionData;
LIsValid: Boolean;
LJWTValue: TJWT;
LCustomPair: TPair<string, string>;
LJsonObject: TJDOJsonObject;
begin
if SameText(AContext.Request.PathInfo, FLoginURLSegment) and (AContext.Request.HTTPMethod = httpPOST) then
if SameText(AContext.Request.PathInfo, FLoginURLSegment) then
begin
UserName := TNetEncoding.URL.Decode(AContext.Request.Headers[FUserNameHeaderName]);
Password := TNetEncoding.URL.Decode(AContext.Request.Headers[FPasswordHeaderName]);
if (UserName.IsEmpty) or (Password.IsEmpty) then
LBasicAuthenticationEncode := AContext.Request.Headers[FAuthorizationHeaderName];
if LBasicAuthenticationEncode.IsEmpty then
begin
RenderError(HTTP_STATUS.Unauthorized, 'Username and password Required', AContext);
AHandled := True;
Exit;
LUsername := TNetEncoding.URL.Decode(AContext.Request.Headers[FUserNameHeaderName]);
LPassword := TNetEncoding.URL.Decode(AContext.Request.Headers[FPasswordHeaderName]);
if (LUsername.IsEmpty) or (LPassword.IsEmpty) then
begin
RenderError(HTTP_STATUS.Unauthorized, 'Username and password Required', AContext);
AHandled := True;
Exit;
end;
end
else
begin
if not LBasicAuthenticationEncode.StartsWith('basic', True) then
begin
RenderError(HTTP_STATUS.Unauthorized, 'Invalid authorization type', AContext);
AHandled := True;
Exit;
end;
LBasicAuthenticationDecode := TStringList.Create;
try
LBasicAuthenticationDecode.Delimiter := ':';
LBasicAuthenticationDecode.DelimitedText := TBase64Encoding.Base64.Decode(LBasicAuthenticationEncode.Replace('basic ', '', [rfIgnoreCase]));
LUsername := LBasicAuthenticationDecode.Strings[0];
LPassword := LBasicAuthenticationDecode.Strings[1];
finally
LBasicAuthenticationDecode.Free;
end;
end;
// check the authorization for the requested resource
RolesList := TList<string>.Create;
LRolesList := TList<string>.Create;
try
SessionData := TSessionData.Create;
LSessionData := TSessionData.Create;
try
try
FAuthenticationHandler.OnAuthentication(AContext, UserName, Password, RolesList, IsValid, SessionData);
if IsValid then
FAuthenticationHandler.OnAuthentication(AContext, LUsername, LPassword, LRolesList, LIsValid, LSessionData);
if LIsValid then
begin
JWTValue := TJWT.Create(FSecret, FLeewaySeconds);
LJWTValue := TJWT.Create(FSecret, FLeewaySeconds);
try
// let's user config claims and custom claims
if not Assigned(FSetupJWTClaims) then
raise EMVCJWTException.Create('SetupJWTClaims not set');
FSetupJWTClaims(JWTValue);
FSetupJWTClaims(LJWTValue);
// these claims are mandatory and managed by the middleware
if not JWTValue.CustomClaims['username'].IsEmpty then
raise EMVCJWTException.Create
('Custom claim "username" is reserved and cannot be modified in the JWT setup');
if not LJWTValue.CustomClaims['username'].IsEmpty then
raise EMVCJWTException.Create('Custom claim "username" is reserved and cannot be modified in the JWT setup');
if not JWTValue.CustomClaims['roles'].IsEmpty then
raise EMVCJWTException.Create
('Custom claim "roles" is reserved and cannot be modified in the JWT setup');
if not LJWTValue.CustomClaims['roles'].IsEmpty then
raise EMVCJWTException.Create('Custom claim "roles" is reserved and cannot be modified in the JWT setup');
JWTValue.CustomClaims['username'] := UserName;
JWTValue.CustomClaims['roles'] := string.Join(',', RolesList.ToArray);
LJWTValue.CustomClaims['username'] := LUsername;
LJWTValue.CustomClaims['roles'] := string.Join(',', LRolesList.ToArray);
if JWTValue.LiveValidityWindowInSeconds > 0 then
begin
if NeedsToBeExtended(JWTValue) then
begin
ExtendExpirationTime(JWTValue);
end;
end;
if LJWTValue.LiveValidityWindowInSeconds > 0 then
if NeedsToBeExtended(LJWTValue) then
ExtendExpirationTime(LJWTValue);
// setup the current logged user from the JWT
AContext.LoggedUser.Roles.AddRange(RolesList);
AContext.LoggedUser.UserName := JWTValue.CustomClaims['username'];
AContext.LoggedUser.LoggedSince := JWTValue.Claims.IssuedAt;
AContext.LoggedUser.Realm := JWTValue.Claims.Subject;
AContext.LoggedUser.Roles.AddRange(LRolesList);
AContext.LoggedUser.UserName := LJWTValue.CustomClaims['username'];
AContext.LoggedUser.LoggedSince := LJWTValue.Claims.IssuedAt;
AContext.LoggedUser.Realm := LJWTValue.Claims.Subject;
if SessionData.Count > 0 then
if LSessionData.Count > 0 then
begin
AContext.LoggedUser.CustomData := TMVCCustomData.Create;
for lCustomPair in SessionData do
for LCustomPair in LSessionData do
begin
AContext.LoggedUser.CustomData.AddOrSetValue(lCustomPair.Key, lCustomPair.Value);
if not JWTValue.CustomClaims.Items[lCustomPair.Key].IsEmpty then
raise EMVCJWTException.CreateFmt('JWT Error: "%s" is a reserved key name', [lCustomPair.Key]);
JWTValue.CustomClaims.Items[lCustomPair.Key] := lCustomPair.Value;
AContext.LoggedUser.CustomData.AddOrSetValue(LCustomPair.Key, LCustomPair.Value);
if not LJWTValue.CustomClaims.Items[LCustomPair.Key].IsEmpty then
raise EMVCJWTException.CreateFmt('JWT Error: "%s" is a reserved key name', [LCustomPair.Key]);
LJWTValue.CustomClaims.Items[LCustomPair.Key] := LCustomPair.Value;
end;
end;
LObj := TJDOJsonObject.Create;
LJsonObject := TJDOJsonObject.Create;
try
LObj.S['token'] := JWTValue.GetToken;
InternalRender(LObj, TMVCMediaType.APPLICATION_JSON, TMVCConstants.DEFAULT_CONTENT_CHARSET,
AContext, False);
LJsonObject.S['token'] := LJWTValue.GetToken;
InternalRender(LJsonObject, TMVCMediaType.APPLICATION_JSON, TMVCConstants.DEFAULT_CONTENT_CHARSET, AContext, False);
finally
LObj.Free;
LJsonObject.Free;
end;
AHandled := True;
finally
JWTValue.Free;
LJWTValue.Free;
end;
end
else
@ -367,10 +379,10 @@ begin
end;
end;
finally
SessionData.Free;
LSessionData.Free;
end;
finally
RolesList.Free;
LRolesList.Free;
end;
end;
end;
@ -378,37 +390,29 @@ end;
procedure TMVCJWTAuthenticationMiddleware.RenderError(const AHTTPStatusCode: UInt16; const AErrorMessage: string;
const AContext: TWebContext; const AErrorClassName: string; const AErrorNumber: Integer);
var
lJObj: TJDOJsonObject;
lStatus: string;
LJObj: TJDOJsonObject;
LStatus: string;
begin
AContext.Response.StatusCode := AHTTPStatusCode;
AContext.Response.ReasonString := AErrorMessage;
lStatus := 'error';
LStatus := 'error';
if (AHTTPStatusCode div 100) = 2 then
lStatus := 'ok';
LStatus := 'ok';
lJObj := TJDOJsonObject.Create;
lJObj.S['status'] := lStatus;
lJObj.I['statuscode'] := AHTTPStatusCode;
lJObj.S['message'] := AErrorMessage;
if AErrorClassName = '' then
begin
lJObj.Values['classname'] := nil
end
else
begin
lJObj.S['classname'] := AErrorClassName;
end;
LJObj := TJDOJsonObject.Create;
LJObj.S['status'] := LStatus;
LJObj.I['statuscode'] := AHTTPStatusCode;
LJObj.S['message'] := AErrorMessage;
LJObj.Values['classname'] := nil;
if AErrorClassName <> '' then
LJObj.S['classname'] := AErrorClassName;
if AErrorNumber <> 0 then
begin
lJObj.I['errornumber'] := AErrorNumber;
end;
LJObj.I['errornumber'] := AErrorNumber;
InternalRender(lJObj, TMVCConstants.DEFAULT_CONTENT_TYPE, TMVCConstants.DEFAULT_CONTENT_CHARSET, AContext);
InternalRender(LJObj, TMVCConstants.DEFAULT_CONTENT_TYPE, TMVCConstants.DEFAULT_CONTENT_CHARSET, AContext);
end;
end.