2.1.6 (carbon)

FIX https://github.com/danieleteti/delphimvcframework/issues/74
Updated jsonwebtoken sample
Improved support for customclaims into the controller actions
This commit is contained in:
Daniele Teti 2017-03-10 10:37:29 +01:00
parent 3630dbe076
commit 7aa5dd1ccb
11 changed files with 185 additions and 64 deletions

View File

@ -1,3 +1,8 @@
2.1.6 (carbon)
FIX https://github.com/danieleteti/delphimvcframework/issues/74
Updated jsonwebtoken sample
Improved support for customclaims into the controller actions
2.1.4 (beryllium)
FIX https://github.com/danieleteti/delphimvcframework/issues/71

View File

@ -41,7 +41,7 @@ type
implementation
uses
System.SysUtils, System.JSON, System.Classes;
System.SysUtils, System.JSON, System.Classes, System.Generics.Collections;
{ TApp1MainController }
@ -58,12 +58,20 @@ end;
{ TAdminController }
procedure TAdminController.OnlyRole1(ctx: TWebContext);
var
lPair: TPair<String, String>;
begin
ContentType := TMVCMediaType.TEXT_PLAIN;
ResponseStream.AppendLine('Hey! Hello ' + ctx.LoggedUser.UserName +
', now you are a logged user and this is a protected content!');
ResponseStream.AppendLine('As logged user you have the following roles: ' +
sLineBreak + string.Join(sLineBreak, Context.LoggedUser.Roles.ToArray));
ResponseStream.AppendLine('You CustomClaims are: ' +
sLineBreak);
for lPair in Context.LoggedUser.CustomData do
begin
ResponseStream.AppendFormat('%s = %s' + sLineBreak, [lPair.Key, lPair.Value]);
end;
RenderResponseStream;
end;
@ -73,6 +81,7 @@ var
lJArr: TJSONArray;
lQueryParams: TStrings;
I: Integer;
lPair: TPair<String, String>;
begin
ContentType := TMVCMediaType.APPLICATION_JSON;
lJObj := TJSONObject.Create;
@ -88,6 +97,13 @@ begin
lQueryParams.ValueFromIndex[I])));
end;
lJArr := TJSONArray.Create;
lJObj.AddPair('customclaims', lJArr);
for lPair in Context.LoggedUser.CustomData do
begin
lJArr.AddElement(TJSONObject.Create(TJSONPair.Create(lPair.Key, lPair.Value)));
end;
Render(lJObj);
end;

View File

@ -11,9 +11,9 @@ type
protected
procedure OnRequest(const ControllerQualifiedClassName: string;
const ActionName: string; var AuthenticationRequired: Boolean);
procedure OnAuthentication(const UserName: string; const Password: string;
UserRoles: System.Generics.Collections.TList<System.string>;
var IsValid: Boolean; const SessionData: TSessionData);
procedure OnAuthentication(const UserName,
Password: string; UserRoles: System.Generics.Collections.TList<System.string>;
var IsValid: Boolean; const CustomData: TMVCCustomData);
procedure OnAuthorization(UserRoles
: System.Generics.Collections.TList<System.string>;
const ControllerQualifiedClassName: string; const ActionName: string;
@ -26,7 +26,7 @@ implementation
procedure TAuthenticationSample.OnAuthentication(const UserName,
Password: string; UserRoles: System.Generics.Collections.TList<System.string>;
var IsValid: Boolean; const SessionData: TSessionData);
var IsValid: Boolean; const CustomData: TMVCCustomData);
begin
IsValid := UserName.Equals(Password); // hey!, this is just a demo!!!
if IsValid then
@ -44,6 +44,8 @@ begin
UserRoles.Add('role1');
UserRoles.Add('role2');
end;
CustomData.Add('customclaim1', 'hello world');
CustomData.Add('customclaim2', 'daniele teti');
end
else
begin

View File

@ -2,8 +2,8 @@ object Form5: TForm5
Left = 0
Top = 0
Caption = 'Form5'
ClientHeight = 379
ClientWidth = 513
ClientHeight = 460
ClientWidth = 647
Color = clBtnFace
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
@ -14,18 +14,31 @@ object Form5: TForm5
PixelsPerInch = 96
TextHeight = 13
object Splitter1: TSplitter
Left = 0
Top = 309
Width = 647
Height = 3
Cursor = crVSplit
Align = alBottom
ExplicitLeft = -16
ExplicitTop = 302
ExplicitWidth = 513
end
object Splitter2: TSplitter
Left = 0
Top = 147
Width = 513
Width = 647
Height = 3
Cursor = crVSplit
Align = alTop
ExplicitWidth = 30
ExplicitLeft = -8
ExplicitTop = 302
ExplicitWidth = 513
end
object Memo1: TMemo
Left = 0
Top = 49
Width = 513
Width = 647
Height = 98
Align = alTop
Font.Charset = ANSI_CHARSET
@ -36,12 +49,13 @@ object Form5: TForm5
ParentFont = False
ReadOnly = True
TabOrder = 0
ExplicitWidth = 513
end
object Memo2: TMemo
Left = 0
Top = 150
Width = 513
Height = 229
Width = 647
Height = 159
Align = alClient
Font.Charset = ANSI_CHARSET
Font.Color = clWindowText
@ -51,14 +65,17 @@ object Form5: TForm5
ParentFont = False
ReadOnly = True
TabOrder = 1
ExplicitWidth = 513
ExplicitHeight = 229
end
object Panel1: TPanel
Left = 0
Top = 0
Width = 513
Width = 647
Height = 49
Align = alTop
TabOrder = 2
ExplicitWidth = 513
object btnGet: TButton
AlignWithMargins = True
Left = 171
@ -82,4 +99,19 @@ object Form5: TForm5
OnClick = btnLOGINClick
end
end
object Memo3: TMemo
Left = 0
Top = 312
Width = 647
Height = 148
Align = alBottom
Font.Charset = ANSI_CHARSET
Font.Color = clWindowText
Font.Height = -13
Font.Name = 'Courier New'
Font.Style = []
ParentFont = False
ReadOnly = True
TabOrder = 3
end
end

View File

@ -15,6 +15,8 @@ type
btnGet: TButton;
btnLOGIN: TButton;
Splitter1: TSplitter;
Memo3: TMemo;
Splitter2: TSplitter;
procedure btnGetClick(Sender: TObject);
procedure btnLOGINClick(Sender: TObject);
private
@ -42,6 +44,7 @@ var
lResp: IRESTResponse;
lQueryStringParams: TStringList;
begin
{ Getting JSON response }
lClient := TRESTClient.Create('localhost', 8080);
try
lClient.ReadTimeOut(0);
@ -52,10 +55,8 @@ begin
lQueryStringParams.Values['firstname'] := 'Daniele';
lQueryStringParams.Values['lastname'] := 'Teti';
lResp := lClient.doGET('/admin/role1', [], lQueryStringParams);
if lResp.HasError then
ShowMessage(lResp.Error.ExceptionMessage);
finally
lQueryStringParams.Free;
end;
@ -63,6 +64,28 @@ begin
finally
lClient.Free;
end;
{ Getting HTML response }
lClient := TRESTClient.Create('localhost', 8080);
try
lClient.ReadTimeOut(0);
if not FJWT.IsEmpty then
lClient.RequestHeaders.Values['Authentication'] := 'bearer ' + FJWT;
lQueryStringParams := TStringList.Create;
try
lQueryStringParams.Values['firstname'] := 'Daniele';
lQueryStringParams.Values['lastname'] := 'Teti';
lResp := lClient.Accept('text/html').doGET('/admin/role1', [], lQueryStringParams);
if lResp.HasError then
ShowMessage(lResp.Error.ExceptionMessage);
finally
lQueryStringParams.Free;
end;
Memo3.Lines.Text := lResp.BodyAsString;
finally
lClient.Free;
end;
end;
procedure TForm5.btnLOGINClick(Sender: TObject);

View File

@ -36,7 +36,7 @@ uses
{$ELSE}
, Data.DBXJSON
{$ENDIF}
, MVCFramework.Patches
, MVCFramework.Patches, MVCFramework
;
type
@ -182,6 +182,7 @@ type
TJWTCustomClaims = class(TJWTDictionaryObject)
property Items; default;
function AsCustomData: TMVCCustomData;
end;
TJWT = class
@ -341,6 +342,13 @@ begin
Items[Index] := IntToStr(DateTimeToUnix(Value, False));
end;
{ TJWTCustomClaims }
function TJWTCustomClaims.AsCustomData: TMVCCustomData;
begin
Result := TMVCCustomData.Create(FClaims);
end;
{ TJWT }
function TJWT.CheckExpirationTime(Payload: TJSONObject;

View File

@ -48,7 +48,7 @@ type
implementation
uses
System.StrUtils, MVCFramework.Commons;
System.StrUtils, MVCFramework.Commons, System.Classes;
{ TCORSMiddleware }
@ -74,17 +74,14 @@ end;
procedure TCORSMiddleware.OnBeforeRouting(Context: TWebContext;
var Handled: Boolean);
var
lCustomHeaders: TStrings;
begin
Context.Response.RawWebResponse.CustomHeaders.Values
['Access-Control-Allow-Origin'] := FAllowedOriginURL;
Context.Response.RawWebResponse.CustomHeaders.Values
['Access-Control-Allow-Methods'] :=
'POST, GET, OPTIONS, PUT, DELETE';
Context.Response.RawWebResponse.CustomHeaders.Values
['Access-Control-Allow-Headers'] := 'Content-Type, Accept';
Context.Response.RawWebResponse.CustomHeaders.Values
['Access-Control-Allow-Credentials'] := FAllowsCredentials;
lCustomHeaders := Context.Response.RawWebResponse.CustomHeaders;
lCustomHeaders.Values['Access-Control-Allow-Origin'] := FAllowedOriginURL;
lCustomHeaders.Values['Access-Control-Allow-Methods'] := 'POST, GET, OPTIONS, PUT, DELETE';
lCustomHeaders.Values['Access-Control-Allow-Headers'] := 'Content-Type, Accept, jwtusername, jwtpassword, authentication';
lCustomHeaders.Values['Access-Control-Allow-Credentials'] := FAllowsCredentials;
if Context.Request.HTTPMethod = httpOPTIONS then
begin
Context.Response.StatusCode := 200;

View File

@ -49,6 +49,7 @@ type
private
FClaimsToChecks: TJWTCheckableClaims;
FSetupJWTClaims: TJWTClaimsSetup;
FLoginURLSegment: string;
protected
FSecret: string;
@ -62,6 +63,7 @@ type
constructor Create(AMVCAuthenticationHandler: IMVCAuthenticationHandler;
aConfigClaims: TJWTClaimsSetup;
aSecret: string = 'D3lph1MVCFram3w0rk';
aLoginURLSegment: string = '/login';
aClaimsToCheck: TJWTCheckableClaims = [
TJWTCheckableClaim.ExpirationTime,
TJWTCheckableClaim.NotBefore,
@ -73,31 +75,47 @@ implementation
uses
MVCFramework.Session
{$IFDEF SYSTEMJSON}
{$IFDEF SYSTEMJSON}
, System.JSON
{$ELSE}
{$ELSE}
, Data.DBXJSON
{$ENDIF}
{$IFDEF WEBAPACHEHTTP}
{$ENDIF}
{$IFDEF WEBAPACHEHTTP}
, Web.ApacheHTTP
{$ENDIF}
{$IFDEF SYSTEMNETENCODING}
{$ENDIF}
{$IFDEF SYSTEMNETENCODING}
, System.NetEncoding
{$ELSE}
{$ELSE}
, Soap.EncdDecd
{$ENDIF};
{$ENDIF};
{ TMVCSalutationMiddleware }
constructor TMVCJwtAuthenticationMiddleware.Create(AMVCAuthenticationHandler
: IMVCAuthenticationHandler;
constructor TMVCJwtAuthenticationMiddleware.Create(AMVCAuthenticationHandler: IMVCAuthenticationHandler;
aConfigClaims: TJWTClaimsSetup;
aSecret: string;
aClaimsToCheck: TJWTCheckableClaims);
aSecret: string = 'D3lph1MVCFram3w0rk';
aLoginURLSegment: string = '/login';
aClaimsToCheck: TJWTCheckableClaims = [
TJWTCheckableClaim.ExpirationTime,
TJWTCheckableClaim.NotBefore,
TJWTCheckableClaim.IssuedAt
]);
begin
inherited Create;
FMVCAuthenticationHandler := AMVCAuthenticationHandler;
FSecret := aSecret;
FLoginURLSegment := aLoginURLSegment;
FClaimsToChecks := aClaimsToCheck;
FSetupJWTClaims := aConfigClaims;
end;
@ -172,6 +190,7 @@ begin
AControllerQualifiedClassName, AActionName, lIsAuthorized);
if lIsAuthorized then
begin
Context.LoggedUser.CustomData := lJWT.CustomClaims.AsCustomData;
Handled := False;
end
else
@ -193,11 +212,12 @@ var
lUserName: string;
lPassword: string;
lRoles: TList<String>;
lSessionData: TSessionData;
lCustomData: TMVCCustomData;
lIsValid: Boolean;
lJWT: TJWT;
lPair: TPair<String, String>;
begin
if SameText(Context.Request.PathInfo, '/login') and (Context.Request.HTTPMethod = httpPOST) then
if (Context.Request.HTTPMethod = httpPOST) and SameText(Context.Request.PathInfo, FLoginURLSegment) then
begin
lUserName := Context.Request.Headers['jwtusername'];
lPassword := Context.Request.Headers['jwtpassword'];
@ -212,33 +232,40 @@ begin
// check the authorization for the requested resource
lRoles := TList<string>.Create;
try
lSessionData := TSessionData.Create;
lCustomData := TMVCCustomData.Create;
try
FMVCAuthenticationHandler.OnAuthentication(lUserName, lPassword,
lRoles, lIsValid, lSessionData);
lRoles, lIsValid, lCustomData);
if lIsValid then
begin
lJWT := TJWT.Create(FSecret);
try
// let's user config claims and custom claims
// CustomData becomes custom claims
for lPair in lCustomData do
begin
lJWT.CustomClaims[lPair.Key] := lPair.Value;
end;
// let's user config additional claims and custom claims
FSetupJWTClaims(lJWT);
// these claims are mandatory and managed by the middleware
if not lJWT.CustomClaims['username'].IsEmpty then
raise EMVCJWTException.Create
('Custom claim "username" is reserved and cannot be modified in the JWT setup');
('Custom claim "username" is reserved and cannot be modified in the JWT setup nor in CustomData');
if not lJWT.CustomClaims['roles'].IsEmpty then
raise EMVCJWTException.Create
('Custom claim "roles" is reserved and cannot be modified in the JWT setup');
('Custom claim "roles" is reserved and cannot be modified in the JWT setup nor in CustomData');
lJWT.CustomClaims['username'] := lUserName;
lJWT.CustomClaims['roles'] := String.Join(',', lRoles.ToArray);
/// / setup the current logged user from the JWT
Context.LoggedUser.Roles.AddRange(lRoles);
Context.LoggedUser.UserName := lJWT.CustomClaims['username'];
Context.LoggedUser.LoggedSince := lJWT.Claims.IssuedAt;
Context.LoggedUser.Realm := lJWT.Claims.Subject;
// Context.LoggedUser.Roles.AddRange(lRoles);
// Context.LoggedUser.UserName := lJWT.CustomClaims['username'];
// Context.LoggedUser.LoggedSince := lJWT.Claims.IssuedAt;
// Context.LoggedUser.Realm := lJWT.Claims.Subject;
// Context.LoggedUser.CustomData :=
/// ////////////////////////////////////////////////
InternalRender(TJSONObject.Create(TJSONPair.Create('token', lJWT.GetToken)),
@ -255,7 +282,7 @@ begin
Handled := True;
end;
finally
lSessionData.Free;
lCustomData.Free;
end;
finally
lRoles.Free;

View File

@ -69,7 +69,8 @@ uses
type
TDMVCSerializationType = TSerializationType;
TSessionData = TDictionary<string, string>;
TMVCCustomData = TDictionary<string, string>;
TSessionData = TMVCCustomData;
// RTTI ATTRIBUTES
@ -262,10 +263,12 @@ type
FUserName: string;
FLoggedSince: TDateTime;
FRealm: string;
FCustomData: TMVCCustomData;
procedure SetUserName(const Value: string);
procedure SetLoggedSince(const Value: TDateTime);
function GetIsValidLoggedUser: Boolean;
procedure SetRealm(const Value: string);
procedure SetCustomData(const Value: TMVCCustomData);
public
procedure SaveToSession(AWebSession: TWebSession);
@ -276,6 +279,7 @@ type
property LoggedSince: TDateTime read FLoggedSince write SetLoggedSince;
property IsValid: Boolean read GetIsValidLoggedUser;
property Realm: string read FRealm write SetRealm;
property CustomData: TMVCCustomData read FCustomData write SetCustomData;
constructor Create; virtual;
destructor Destroy; override;
end;
@ -3040,6 +3044,7 @@ end;
constructor TUser.Create;
begin
inherited;
FCustomData := nil;
FRoles := TList<string>.Create;
Clear;
end;
@ -3047,6 +3052,7 @@ end;
destructor TUser.Destroy;
begin
FRoles.Free;
FreeAndNil(FCustomData);
inherited;
end;
@ -3092,6 +3098,11 @@ begin
ISODateTimeToString(FLoggedSince) + '$$' + FRealm + '$$' + LRoles;
end;
procedure TUser.SetCustomData(const Value: TMVCCustomData);
begin
FCustomData := Value;
end;
procedure TUser.SetLoggedSince(const Value: TDateTime);
begin
if FLoggedSince = 0 then

View File

@ -1,2 +1,2 @@
const
DMVCFRAMEWORK_VERSION = '2.1.5 (boron)';
DMVCFRAMEWORK_VERSION = '2.1.6 (carbon)';

View File

@ -215,16 +215,7 @@
<Overwrite>true</Overwrite>
</Platform>
</DeployFile>
<DeployClass Name="DependencyModule">
<Platform Name="Win32">
<Operation>0</Operation>
<Extensions>.dll;.bpl</Extensions>
</Platform>
<Platform Name="OSX32">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSDeviceResourceRules"/>
<DeployClass Name="ProjectOSXResource">
<Platform Name="OSX32">
<RemoteDir>Contents\Resources</RemoteDir>
@ -564,7 +555,16 @@
<Operation>1</Operation>
</Platform>
</DeployClass>
<DeployClass Name="ProjectiOSDeviceResourceRules"/>
<DeployClass Name="DependencyModule">
<Platform Name="Win32">
<Operation>0</Operation>
<Extensions>.dll;.bpl</Extensions>
</Platform>
<Platform Name="OSX32">
<Operation>1</Operation>
<Extensions>.dylib</Extensions>
</Platform>
</DeployClass>
<ProjectRoot Platform="iOSDevice64" Name="$(PROJECTNAME).app"/>
<ProjectRoot Platform="Win64" Name="$(PROJECTNAME)"/>
<ProjectRoot Platform="iOSDevice32" Name="$(PROJECTNAME).app"/>