delphimvcframework/sources/MVCFramework.Middleware.Authentication.pas

494 lines
14 KiB
ObjectPascal
Raw Normal View History

// ***************************************************************************
//
// Delphi MVC Framework
//
// Copyright (c) 2010-2017 Daniele Teti and the DMVCFramework Team
//
// https://github.com/danieleteti/delphimvcframework
//
// ***************************************************************************
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// *************************************************************************** }
2015-12-22 12:38:17 +01:00
2015-04-01 17:01:23 +02:00
unit MVCFramework.Middleware.Authentication;
interface
{$I dmvcframework.inc}
2015-04-01 17:01:23 +02:00
uses
MVCFramework, MVCFramework.Logger,
System.Generics.Collections, MVCFramework.Commons;
2015-04-01 17:01:23 +02:00
type
TMVCBasicAuthenticationMiddleware = class(TInterfacedObject, IMVCMiddleware)
strict private
FMVCAuthenticationHandler: IMVCAuthenticationHandler;
protected
FRealm: string;
procedure OnBeforeRouting(Context: TWebContext; var Handled: Boolean);
procedure OnAfterControllerAction(Context: TWebContext;
const AActionName: string; const Handled: Boolean);
2015-04-01 17:01:23 +02:00
procedure OnBeforeControllerAction(Context: TWebContext;
const AControllerQualifiedClassName: string; const AActionName: string;
var Handled: Boolean);
2015-04-01 17:01:23 +02:00
public
constructor Create(AMVCAuthenticationHandler: IMVCAuthenticationHandler;
Realm: string = 'DelphiMVCFramework REALM'); virtual;
end;
TMVCCustomAuthenticationMiddleware = class(TInterfacedObject, IMVCMiddleware)
strict private
FMVCAuthenticationHandler: IMVCAuthenticationHandler;
private
FLoginUrl: string;
procedure SendResponse(const Context: TWebContext; var Handled: Boolean;
HTTPStatus: Word = HTTP_STATUS.Unauthorized);
protected
procedure OnBeforeRouting(Context: TWebContext; var Handled: Boolean);
procedure OnAfterControllerAction(Context: TWebContext;
const AActionName: string; const Handled: Boolean);
procedure OnBeforeControllerAction(Context: TWebContext;
const AControllerQualifiedClassName: string; const AActionName: string;
var Handled: Boolean);
2016-09-18 19:19:23 +02:00
procedure DoLogin(Context: TWebContext; var Handled: Boolean);
procedure DoLogout(Context: TWebContext; var Handled: Boolean);
public
constructor Create(
AMVCAuthenticationHandler: IMVCAuthenticationHandler;
2016-09-18 19:19:23 +02:00
aLoginUrl: string = '/system/users/logged'
); virtual;
end;
2015-04-01 17:01:23 +02:00
implementation
uses
2017-02-22 19:50:37 +01:00
System.SysUtils
, MVCFramework.Session
, ObjectsMappers
, System.StrUtils
, System.Classes
, MVCFramework.Patches
, MVCFramework.TypesAliases
{$IFDEF SYSTEMNETENCODING}
, System.NetEncoding
{$ELSE}
, Soap.EncdDecd
2017-02-22 19:50:37 +01:00
{$ENDIF};
2015-04-01 17:01:23 +02:00
{
401 Unauthorized response should be used for missing or bad authentication, and a
403 Forbidden response should be used afterwards, when the user is authenticated
but isn<EFBFBD>t authorized to perform the requested operation on the given resource.
}
const
CONTENT_HTML_FORMAT = '<html><body><h1>%s</h1><p>%s</p></body></html>';
2015-04-01 17:01:23 +02:00
CONTENT_401_NOT_AUTHORIZED = '401: Not authorized';
CONTENT_403_FORBIDDEN = '403: Forbidden';
function Base64DecodeString(const Value: string): string; inline;
begin
{$IFDEF SYSTEMNETENCODING}
Result := TNetEncoding.Base64.Decode(Value);
{$ELSE}
Result := DecodeString(Value);
{$ENDIF}
end;
{ TMVCSalutationMiddleware }
2015-04-01 17:01:23 +02:00
constructor TMVCBasicAuthenticationMiddleware.Create(AMVCAuthenticationHandler
: IMVCAuthenticationHandler; Realm: string);
begin
inherited Create;
FMVCAuthenticationHandler := AMVCAuthenticationHandler;
FRealm := Realm;
end;
procedure TMVCBasicAuthenticationMiddleware.OnAfterControllerAction
(Context: TWebContext; const AActionName: string; const Handled: Boolean);
2015-04-01 17:01:23 +02:00
begin
// do nothing
2015-04-01 17:01:23 +02:00
end;
procedure TMVCBasicAuthenticationMiddleware.OnBeforeControllerAction
(Context: TWebContext; const AControllerQualifiedClassName,
AActionName: string; var Handled: Boolean);
2015-04-01 17:01:23 +02:00
var
LAuth: string;
LPieces: TArray<string>;
LRoles: TList<string>;
LIsValid: Boolean;
LIsAuthorized: Boolean;
LAuthRequired: Boolean;
LSessionData: TSessionData;
LPair: TPair<string, string>;
2015-04-01 17:01:23 +02:00
procedure SendWWWAuthenticate;
begin
Context.LoggedUser.Clear;
if Context.Request.ClientPreferHTML then
begin
Context.Response.ContentType := 'text/html';
Context.Response.RawWebResponse.Content :=
Format(CONTENT_HTML_FORMAT, [CONTENT_401_NOT_AUTHORIZED,
Context.Config[TMVCConfigKey.ServerName]]);
2015-04-01 17:01:23 +02:00
end
else
begin
Context.Response.ContentType := 'text/plain';
Context.Response.RawWebResponse.Content := CONTENT_401_NOT_AUTHORIZED + sLineBreak +
Context.Config[TMVCConfigKey.ServerName];
2015-04-01 17:01:23 +02:00
end;
Context.Response.StatusCode := 401;
Context.Response.SetCustomHeader('WWW-Authenticate',
'Basic realm=' + QuotedStr(FRealm));
2015-04-01 17:01:23 +02:00
Handled := true;
end;
procedure Send403Forbidden;
begin
Context.LoggedUser.Clear;
if Context.Request.ClientPreferHTML then
begin
Context.Response.ContentType := 'text/html';
Context.Response.RawWebResponse.Content :=
Format(CONTENT_HTML_FORMAT, [CONTENT_403_FORBIDDEN,
Context.Config[TMVCConfigKey.ServerName]]);
2015-04-01 17:01:23 +02:00
end
else
begin
Context.Response.ContentType := 'text/plain';
Context.Response.RawWebResponse.Content := CONTENT_403_FORBIDDEN + sLineBreak + Context.Config
[TMVCConfigKey.ServerName];
2015-04-01 17:01:23 +02:00
end;
Context.Response.StatusCode := 403;
Handled := true;
end;
begin
// check if the resource is protected
FMVCAuthenticationHandler.OnRequest(AControllerQualifiedClassName,
AActionName, LAuthRequired);
2015-04-01 17:01:23 +02:00
if not LAuthRequired then
begin
Handled := False;
Exit;
end;
Context.LoggedUser.LoadFromSession(Context.Session);
LIsValid := Context.LoggedUser.IsValid;
if not LIsValid then
2015-04-01 17:01:23 +02:00
begin
// We NEED authentication
2015-04-01 17:01:23 +02:00
LAuth := Context.Request.Headers['Authorization'];
LAuth := Base64DecodeString(LAuth.Remove(0, 'Basic'.Length).Trim);
2015-04-01 17:01:23 +02:00
LPieces := LAuth.Split([':']);
if LAuth.IsEmpty or (Length(LPieces) <> 2) then
begin
SendWWWAuthenticate;
Exit;
end;
// now, we have username and password.
// check the authorization for the requested resource
LRoles := TList<string>.Create;
try
LSessionData := TSessionData.Create;
try
FMVCAuthenticationHandler.OnAuthentication(LPieces[0], LPieces[1],
LRoles, LIsValid, LSessionData);
if LIsValid then
begin
Context.LoggedUser.Roles.AddRange(LRoles);
Context.LoggedUser.UserName := LPieces[0];
Context.LoggedUser.LoggedSince := Now;
Context.LoggedUser.Realm := FRealm;
Context.LoggedUser.SaveToSession(Context.Session);
// save sessiondata to the actual session
for LPair in LSessionData do
begin
Context.Session[LPair.Key] := LPair.Value;
end;
end;
finally
LSessionData.Free;
2015-04-01 17:01:23 +02:00
end;
finally
LRoles.Free;
end;
end;
// authorization
LIsAuthorized := False;
if LIsValid then
begin
FMVCAuthenticationHandler.OnAuthorization(Context.LoggedUser.Roles,
AControllerQualifiedClassName, AActionName, LIsAuthorized)
end;
if LIsAuthorized then
Handled := False
else
begin
if LIsValid then
Send403Forbidden
else
SendWWWAuthenticate;
end;
end;
procedure TMVCBasicAuthenticationMiddleware.OnBeforeRouting
(Context: TWebContext; var Handled: Boolean);
2015-04-01 17:01:23 +02:00
begin
// do nothing
2015-04-01 17:01:23 +02:00
end;
{ TMVCFormAuthenticationMiddleware }
constructor TMVCCustomAuthenticationMiddleware.Create(
AMVCAuthenticationHandler: IMVCAuthenticationHandler;
2016-09-18 19:19:23 +02:00
aLoginUrl: string = '/system/users/logged');
begin
inherited Create;
FMVCAuthenticationHandler := AMVCAuthenticationHandler;
FLoginUrl := aLoginUrl.ToLower;
end;
2016-09-18 19:19:23 +02:00
procedure TMVCCustomAuthenticationMiddleware.DoLogin(Context: TWebContext;
var Handled: Boolean);
var
lJObj: TJSONObject;
lUserName: string;
lPassword: string;
LRoles: TList<string>;
LPair: TPair<string, string>;
LSessionData: TSessionData;
LIsValid: Boolean;
begin
Context.SessionStop(False);
Context.LoggedUser.Clear;
if not Context.Request.ThereIsRequestBody then
begin
Handled := true;
Context.Response.StatusCode := HTTP_STATUS.BadRequest;
Context.Response.ContentType := TMVCMediaType.APPLICATION_JSON;
Context.Response.RawWebResponse.Content :=
'{"status":"KO", "message":"username and password are mandatory in the body request as json object"}';
Exit;
end;
2017-03-20 19:08:01 +01:00
lJObj := TJSONObject.ParseJSONValue(Context.Request.Body) as TJSONObject;
2016-09-18 19:19:23 +02:00
if not Assigned(lJObj) then
begin
Handled := true;
SendResponse(Context, Handled, HTTP_STATUS.BadRequest);
Exit;
end;
lUserName := Mapper.GetStringDef(lJObj, 'username', '');
lPassword := Mapper.GetStringDef(lJObj, 'password', '');
if lUserName.IsEmpty or lPassword.IsEmpty then
begin
Handled := true;
SendResponse(Context, Handled);
Exit;
end;
// now, we have username and password.
// check the authorization for the requested resource
LRoles := TList<string>.Create;
try
LSessionData := TSessionData.Create;
try
LIsValid := False;
FMVCAuthenticationHandler.OnAuthentication(lUserName, lPassword,
LRoles, LIsValid, LSessionData);
if not LIsValid then
begin
SendResponse(Context, Handled);
Exit;
end;
// create the session
Context.LoggedUser.Roles.AddRange(LRoles);
Context.LoggedUser.UserName := lUserName;
Context.LoggedUser.LoggedSince := Now;
Context.LoggedUser.Realm := 'custom';
Context.LoggedUser.SaveToSession(Context.Session);
// save sessiondata to the actual session
for LPair in LSessionData do
begin
Context.Session[LPair.Key] := LPair.Value;
end;
Context.Response.StatusCode := HTTP_STATUS.OK;
Context.Response.CustomHeaders.Values['X-LOGOUT-URL'] := FLoginUrl;
Context.Response.CustomHeaders.Values['X-LOGOUT-METHOD'] := 'DELETE';
Context.Response.ContentType := TMVCMediaType.APPLICATION_JSON;
Context.Response.RawWebResponse.Content := '{"status":"OK"}';
Handled := true;
finally
LSessionData.Free;
end;
finally
LRoles.Free;
end;
end;
procedure TMVCCustomAuthenticationMiddleware.DoLogout(Context: TWebContext;
var Handled: Boolean);
begin
Context.SessionStop(False);
SendResponse(Context, Handled, HTTP_STATUS.OK);
end;
procedure TMVCCustomAuthenticationMiddleware.OnAfterControllerAction(
Context: TWebContext; const AActionName: string; const Handled: Boolean);
begin
// do nothing
end;
procedure TMVCCustomAuthenticationMiddleware.SendResponse(const Context: TWebContext;
var Handled: Boolean; HTTPStatus: Word);
2016-09-18 19:19:23 +02:00
var
lIsPositive: Boolean;
lMsg: string;
begin
Context.LoggedUser.Clear;
Context.Response.CustomHeaders.Values['X-LOGIN-URL'] := FLoginUrl;
Context.Response.CustomHeaders.Values['X-LOGIN-METHOD'] := 'POST';
Context.Response.StatusCode := HTTPStatus;
if Context.Request.ClientPreferHTML then
begin
Context.Response.ContentType := 'text/html';
Context.Response.RawWebResponse.Content :=
Format(CONTENT_HTML_FORMAT, [IntToStr(HTTPStatus),
Context.Config[TMVCConfigKey.ServerName]]);
end
else
begin
2016-09-18 19:19:23 +02:00
lIsPositive := (HTTPStatus div 100) = 2;
lMsg := ifthen(lIsPositive, 'OK', 'KO');
Context.Response.ContentType := 'application/json';
Context.Response.RawWebResponse.Content :=
'{"status":"' + lMsg + '", "message":"' + IntToStr(HTTPStatus) + '"}';
end;
Handled := true;
end;
procedure TMVCCustomAuthenticationMiddleware.OnBeforeControllerAction(
Context: TWebContext; const AControllerQualifiedClassName,
AActionName: string; var Handled: Boolean);
var
LIsValid: Boolean;
LIsAuthorized: Boolean;
LAuthRequired: Boolean;
begin
// check if the resource is protected
FMVCAuthenticationHandler.OnRequest(AControllerQualifiedClassName,
AActionName, LAuthRequired);
if not LAuthRequired then
begin
Handled := False;
Exit;
end;
Context.LoggedUser.LoadFromSession(Context.Session);
LIsValid := Context.LoggedUser.IsValid;
if not LIsValid then
begin
Context.SessionStop(False);
SendResponse(Context, Handled);
Exit;
end;
// authorization
LIsAuthorized := False;
FMVCAuthenticationHandler.OnAuthorization(Context.LoggedUser.Roles,
AControllerQualifiedClassName, AActionName, LIsAuthorized);
if LIsAuthorized then
Handled := False
else
begin
if LIsValid then
SendResponse(Context, Handled, HTTP_STATUS.Forbidden)
else
SendResponse(Context, Handled, HTTP_STATUS.Unauthorized);
end;
end;
procedure TMVCCustomAuthenticationMiddleware.OnBeforeRouting(Context: TWebContext;
var Handled: Boolean);
begin
2016-09-18 19:19:23 +02:00
if (Context.Request.PathInfo.ToLower = FLoginUrl) then
begin
2016-09-18 19:19:23 +02:00
Handled := False;
if (Context.Request.HTTPMethod = httpPOST)
and (Context.Request.ContentType.StartsWith(TMVCMediaType.APPLICATION_JSON))
then
begin
2016-09-18 19:19:23 +02:00
DoLogin(Context, Handled);
end;
2016-09-18 19:19:23 +02:00
if Context.Request.HTTPMethod = httpDELETE then
begin
2016-09-18 19:19:23 +02:00
DoLogout(Context, Handled);
end;
end;
{
Context.LoggedUser.LoadFromSession(Context.Session);
if not Context.LoggedUser.IsValid then
begin
Send401Unauthorized;
Exit;
end;
// authorization
LIsAuthorized := False;
if LIsValid then
begin
FMVCAuthenticationHandler.OnAuthorization(Context.LoggedUser.Roles,
AControllerQualifiedClassName, AActionName, LIsAuthorized)
end;
if LIsAuthorized then
Handled := False
else
begin
if LIsValid then
Send403Forbidden
else
SendWWWAuthenticate;
end;
end;
}
end;
2015-04-01 17:01:23 +02:00
end.