I'm trying to send response back to servers requesting digest access authentication
....
FResponseHeader.Text := FResponseText;// received header.
FResponseHeader.ProcessHeaders;
....
WriteLn(FResponseHeader.WWWAuthenticate); //WWW-Authenticate: Digest realm="xxxx.com", nonce="fq1uvocyzvr17e6a5syproazd5phwdvhvlc5", stale=false, algorithm=MD5, qop="auth"
LIdAuthentication := TIdDigestAuthentication.Create;
try
LIdAuthentication.Username := FUser;
LIdAuthentication.Password := FPass;
LIdAuthentication.Uri := FURI;
LIdAuthentication.Method := GetMsgTypeString(FResponseHeader.RequestMethods);
LIdAuthentication.Params.Values['Authorization'] := FResponseHeader.WWWAuthenticate;
LIdAuthentication.AuthParams.AddValue('Digest', FResponseHeader.WWWAuthenticate);
for I := 0 to LIdAuthentication.Steps do
LIdAuthentication.Next;
Result := LIdAuthentication.Authentication;
finally
LIdAuthentication.Free;
end;
I got 401 from the server.
What is the correct way to create the Authorization Header ?
TIdDigestAuthentication (and other TIdAuthentication-derived classes) are intended to be used with TIdHTTP, not standalone.
If you are using TIdHTTP to communicate with a server, you do not need to manage Digest authentication manually at all. If the server requests Digest in its WWW-Authenticate header, and if IdAuthenticationDigest (or IdAllAuthentications) is in your uses clause, then TIdHTTP will automatically send a Digest response for you. The only thing you have to concern yourself with doing is:
set the TIdHTTP.Request.Username and TIdHTTP.Request.Password properties for the initial authentication attempt.
set a TIdHTTP.OnAuthorization event handler to handle the possibility of the server rejecting the current Username/Password so you can supply new values for retry, optionally after prompting the user.
optionally set a TIdHTTP.OnSelectProxyAuthorization event handler to choose which authentication scheme to use if multiple schemes are requested by the server, and/or if you want to control which scheme takes priority over others.
For example:
uses
..., IdHTTP, IdAuthenticationDigest;
...
IdHTTP1.OnAuthorization := AuthRequested;
IdHTTP1.Request.Username := ...; // initial username
IdHTTP1.Request.Password := ...; // initial password
IdHTTP1.Get(...);
...
procedure TMyClass.AuthRequested(Sender: TObject; Authentication: TIdAuthentication; var Handled: Boolean);
begin
if (new credentials are available) then
begin
Authentication.Username := ...; // new username
Authentication.Password := ...; // new password
Handled := True;
end else
Handled := False;
end;
That being said, if you want to use TIdDigestAuthentication standalone, then you should use it similarly to how TIdHTTP uses it, eg:
LIdAuthentication := TIdDigestAuthentication.Create;
try
LIdAuthentication.SetRequest(FGetMsgTypeString(FResponseHeader.RequestMethods), FURI);
LIdAuthentication.Username := FUser;
LIdAuthentication.Password := FPass;
LIdAuthentication.Params.Values['Authorization'] := LIdAuthentication.Authentication;
LIdAuthentication.AuthParams := FResponseHeader.WWWAuthenticate; // assuming WWWAuthenticate is a TIdHeaderList...
repeat
case LIdAuthentication.Next of
wnAskTheProgram:
begin
// set LIdAuthentication.Username and LIdAuthentication.Password to new credentials to retry...
end;
wnDoRequest:
begin
// send new request with LIdAuthentication.Authentication in the 'Authorization' header...
Result := LIdAuthentication.Authentication;
Exit;
end;
wnFail:
begin
// error handling ...
Result := '';
Exit;
end;
end;
until False;
finally
LIdAuthentication.Free;
end;
Related
This is a follow up question to Using INDY 10 SMTP with Office365 and Cannot use secure SMTP connection to Office365 with Delphi 2010 and Indy 10.5.5.
I have the following routine, incorporating points made in both threads (specifically, I set IdSMTP.AuthType := satSASL, provide the most common SASL mechanisms for TIdSMTP to choose from and I use Explicit TLS and sslvTLSv1):
procedure TEmailAlertSuite.AddSslHandler(const Username, Password: string);
var
SASLAnonymous: TIdSASLAnonymous;
SASLDigest: TIdSASLDigest;
SASLLogin: TIdSASLLogin;
SASLOTP: TIdSASLOTP;
SASLMD5: TIdSASLCRAMMD5;
SASLPlain: TIdSASLPlain;
SASLSHA1: TIdSASLCRAMSHA1;
SASLSkey: TIdSASLSKey;
UserPassProvider: TIdUserPassProvider;
begin
FSmtp.IOHandler := TIdSSLIOHandlerSocketOpenSSL.Create(FSmtp);
TIdSSLIOHandlerSocketOpenSSL(FSmtp.IOHandler).SSLOptions.Method := sslvTLSv1;
FSmtp.UseTLS := utUseExplicitTLS;
// When we explicitly assign the port, we just get connection timeouts...
// FSmtp.Port := 587;
// This needs to be true, so that TIdSMTP can decide which SASL to use,
// from the ones provided below.
// (https://stackoverflow.com/questions/17734414/using-indy-10-smtp-with-office365)
FSmtp.UseEhlo := True;
FSmtp.Username := Username;
FSmtp.Password := Password;
FSmtp.AuthType := satSASL;
UserPassProvider := TIdUserPassProvider.Create;
UserPassProvider.Username := Username;
UserPassProvider.Password := Password;
// SASL mechanisms are declared and added in descending order of security
// (as far as that can be known -- not all units give a value for FSecurityLevel).
SASLOTP := TIdSASLOTP.Create(FSmtp);
SASLOTP.UserPassProvider := UserPassProvider;
FSmtp.SASLMechanisms.Add.SASL := SASLOTP;
SASLSkey := TIdSASLSKey.Create(FSmtp);
SASLSkey.UserPassProvider := UserPassProvider;
FSmtp.SASLMechanisms.Add.SASL := SASLSKey;
SASLSHA1 := TIdSASLCRAMSHA1.Create(FSmtp);
SASLSHA1.UserPassProvider := UserPassProvider;
FSmtp.SASLMechanisms.Add.SASL := SASLSHA1;
SASLMD5 := TIdSASLCRAMMD5.Create(FSmtp);
SASLMD5.UserPassProvider := UserPassProvider;
FSmtp.SASLMechanisms.Add.SASL := SASLMD5;
SASLLogin := TIdSASLLogin.Create(FSmtp);
SASLLogin.UserPassProvider := UserPassProvider;
FSmtp.SASLMechanisms.Add.SASL := SASLLogin;
SASLDigest := TIdSASLDigest.Create(FSmtp);
SASLDigest.UserPassProvider := UserPassProvider;
FSmtp.SASLMechanisms.Add.SASL := SASLDigest;
SASLPlain := TIdSASLPlain.Create(FSmtp);
SASLPlain.UserPassProvider := UserPassProvider;
FSmtp.SASLMechanisms.Add.SASL := SASLPlain;
// (least secure)
SASLAnonymous := TIdSASLAnonymous.Create(FSmtp);
FSmtp.SASLMechanisms.Add.SASL := SASLAnonymous;
FSmtp.ValidateAuthLoginCapability := False;
end;
I set FSmtp.ValidateAuthLoginCapability := False; and FSmtp.UseEhlo := True;, so that TIdSMTP would decide for itself which SASL mechanism to use.
Notice, too, that I had to comment out the explicit assignment of the port. This is because we would get a socket timeout, when assigning it. This is despite following the advice in Send email using indy component delphi xe2 SSL negotiation faild, and making the assignment after setting UseTLS to utUseExplicitTLS.
When I run this on my O365 tenant, I get the following log output, which shows the capabilities of the tenant and an exception:
FSmtp.Capabilities:
SIZE 157286400
PIPELINING
DSN
ENHANCEDSTATUSCODES
STARTTLS
8BITMIME
BINARYMIME
CHUNKING
SMTPUTF8
2020-06-02 10:12:46.232 - Doesn't support AUTH or the specified SASL handlers!!
Notice there is no announced value for AUTH.
I'm not sure what to try next. At this point, I expected TIdSMTP to be able to determine the correct mechanism to use and to successfully connect and send email. Any thoughts?
Thanks!
I'm connecting to a web service using basic authentication using the following code:
var
RIO: THTTPRIO;
begin
RIO := THTTPRIO.Create(nil);
EndPoint := GetWebServicePort(True, '', RIO);
RIO.HTTPWebNode.UserName := 'xxxx';
RIO.HTTPWebNode.Password := 'yyyy';
...
end;
If the username and password are correct, everything works fine. However, if they are not correct, a Windows dialog pops up requesting the correct credentials. Instead of the dialog I need to catch the error.
How do I stop the dialog popping up? I've searched and found a couple of results (Link 1, Link 2), but neither seems offer a real solution.
To catch the error, you can use a HTTP client library, for example Indy TIdHTTP, to run a HTTP GET (or HEAD) request on the web service address first, and catch the exception which is thrown when user / password are wrong.
uses
... IdHTTP ...;
...
var
HTTP: TIdHTTP;
ValidCredentials := False;
...
HTTP.Request.Username := username;
HTTP.Request.Password := password;
HTTP.Request.BasicAuthentication := True;
try
HTTP.Head(url);
ValidCredentials := HTTP.ResponseCode = 200;
except
on ... (some Indy exception) do
begin
// signal that username / password are incorrect
...
end;
end;
if ValidCredentials then
begin
// invoke Web Service ...
I try to test my webservice with the TIdHTTP (Indy 10.6.0 and Delphi XE5) by this code:
GIdDefaultTextEncoding := encUTF8;
HTTP.IOHandler.DefStringEncoding := IndyTextEncoding_UTF8;
Http.Request.UserName := AUser;
Http.Request.Password := APass;
Http.Request.Accept := 'text/javascript';
Http.Request.ContentType := 'application/json';
Http.Request.ContentEncoding := 'utf-8';
Http.Request.URL := 'https://sameService';
Http.MaxAuthRetries := 1;
Http.Request.BasicAuthentication := True;
TIdSSLIOHandlerSocketOpenSSL(HTTP.IOHandler).SSLOptions.Method := sslvSSLv3;
HTTP.HandleRedirects := True;
"AUser" and "APass" in UTF-8. When "APass" have same Russian chars I can't login.
By "HTTP Analyze" I see:
...
Authorization: Basic cDh1c2VyOj8/Pz8/PzEyMw==
Decode from Base 64 (base64decode.org) we can see:
p8user:??????123
Why DefStringEncoding not work ?
TIdHTTP's authentication system has no concept of TIdIOHandler or its DefStringEncoding property.
Internally, TIdBasicAuthentication uses TIdEncoderMIME.Encode(), but without specifying any encoding. TIdEncoder.Encode() defaults to 8bit encoding, and thus is not affected by GIdDefaultTextEncoding.
If you need to send a UTF-8 encoded password with BASIC authentication, you will have to encode the UTF-8 data manually and store the resulting octets into a string, then the 8bit encoder can process the octets as-is, eg:
Http.Request.Password := BytesToStringRaw(IndyTextEncoding_UTF8.GetBytes(APass));
On the other hand, Indy's DIGEST authentication, for instance, uses TIdHashMessageDigest5.HashStringAsHex(), and TIdHash.HashString() does not default to any specific encoding, it depends on GIdDefaultTextEncoding.
So, you have to be careful about how you encode passwords, based on which authentications you use. To account for the discrepency, what you could try is not encode TIdHTTP.Request.Password itself, but instead encode the password inside the TIdHTTP.OnAuthorization event instead when BASIC authentication is being used, eg:
Http.Request.Password := APass;
...
procedure TMyForm.HttpAuthorization(Sender: TObject;
Authentication: TIdAuthentication; var Handled: Boolean);
begin
if Authentication is TIdBasicAuthentication then
begin
Authentication.Password := BytesToStringRaw(IndyTextEncoding_UTF8.GetBytes(TheDesiredPasswordHere));
Handled := True;
end;
end;
UPDATE:
Internally, TIdBasicAuthentication uses TIdEncoderMIME.Encode(), but without specifying any encoding.
That last part is no longer true. TIdBasicAuthentication was updated in 2016 to now pass an encoding to TIdEncoderMIME.Encode(). When an HTTP server asks for BASIC authentication, TIdBasicAuthentication now checks if the server's WWW-Authenticate header includes one of the following attributes: charset, accept-charset, encoding, or enc (in that order). If one is found, the specified charset is passed to Encode(), otherwise ISO-8859-1 is used (there is a TODO in the code to use UTF-8 if the username or password contain any characters that do not exist in ISO-8859-1).
If you want to ensure that UTF-8 is used in BASIC authentication, you are better off setting Request.BasicAuthentication to False and using the Request.CustomHeaders to supply your own Authorization header, eg:
Http.Request.BasicAuthentication := False;
Http.Request.CustomHeaders.Values['Authorization'] := 'Basic ' + TIdEncoderMIME.EncodeString(AUser + ':' + APass, IndyTextEncoding_UTF8);
Alternatively, you might be able to just get away with updating the protected TIdBasicAuthentication.FCharset member inside of the TIdHTTP.OnAuthorization event (which is fired after the server's WWW-Authenticate header has been parsed), eg:
Http.Request.Password := APass;
...
type
TIdBasicAuthenticationAccess = class(TIdBasicAuthentication)
end;
procedure TMyForm.HttpAuthorization(Sender: TObject;
Authentication: TIdAuthentication; var Handled: Boolean);
begin
if Authentication is TIdBasicAuthentication then
begin
TIdBasicAuthenticationAccess(Authentication).FCharset := 'utf-8';
Authentication.Password := TheDesiredPasswordHere;
Handled := True;
end;
end;
I'm using Indy TIdHTTP along with TIdCookieManager. I would like to check the current cookies for the request I'm about to send and identify the likelyhood that it will be valid (I know I can't be 100% sure the server will accept my request). If there are no cookies, or if they're expired, I will want to login first and acquire new cookies. Otherwise, just send the request.
How would I go about doing such a check? I believe I have to check the cookie manager before I send a request, but don't know what to check.
Try something like this:
function CheckCookies(Cookies: TIdCookieManager; const TargetURL: String): Boolean;
var
URL: TIdURI;
Headers: TIdHeaderList;
begin
Result := False;
URL := TIdURI.Create(TargetURL);
try
Headers := TIdHeaderList.Create(QuoteHTTP);
try
Cookies.GenerateClientCookies(URL, False, Headers);
Result := Headers.Count > 0;
finally
Headers.Free;
end;
finally
URL.Free;
end;
end;
.
if not CheckCookies(IdHTTP1.CookieManager, 'http://www.someurl.com/') then
begin
// login and get new cookies ...
end;
Like already stated in the comments you cannot perform an actual acceptance check on the client, only the server can do that.
However you can filter out expired or invalid cookies:
function filterInvalidCookies(cookies: TIdCookies; targetURL: TIdURI): Boolean;
var
c: Integer;
begin
Result := False;
c := 0;
while (cookies.Count > c) do
if (not cookies[c].IsExpired and cookies[c].IsAllowed(targetURL, False) and
(cookies[c].CookieName <> '')) then
begin
Result := True;
Inc(c);
end
else
cookies.Delete(c);
end;
The function removes invalid cookies and returns False if there are no valid ones left. Call it before a request like this:
if (Assigned(con.CookieManager)) then
filterInvalidCookies(con.CookieManager.CookieCollection,
TIdURI.Create('http://www.someurl.com/'));
where con is an TIdHTTP object.
You can do additional, maybe target page specific checks of course.
I know there's alot of Indy threads but I can't get one to match my case.
I have been given a URL with a username and password form. this then actions to a URL/reports.php on which there are multiple hyperlinks.
Each of these links will direct to a page with URL variables e.g. reports.php?report=variablename where a download will immediately start.
My thinking so far:
procedure TForm1.PostData(Sender: TObject);
var
paramList:TStringList;
url,text:string;
// IdHTTP1: TIdHTTP;
IdSSLIOHandlerSocket1: TIdSSLIOHandlerSocket;
idLogFile1 : TidLogFile;
begin
idLogFile1 := TidLogFile.Create(nil);
with idLogFile1 do
begin
idLogFile1.Filename := 'C:\HTTPSlogfile.txt';
idLogFile1.active := True;
end;
IdHTTP1 := TIdHTTP.Create(nil);
IdSSLIOHandlerSocket1 := TIdSSLIOHandlerSocket.Create(nil);
IdSSLIOHandlerSocket1.SSLOptions.Method := sslvSSLv23;
IdHTTP1.IOHandler := IdSSLIOHandlerSocket1;
IdHTTP1.HandleRedirects := true;
IdHTTP1.ReadTimeout := 5000;
IdHTTP1.Intercept := idLogFile1;
paramList:=TStringList.create;
paramList.Clear;
paramList.Add('loguser=testuser');
paramList.Add('logpass=duke7aunt');
paramList.Add('logclub=8005');
url := 'https://www.dfcdata.co.uk/integration/reports.php?report=live';
try
IdHTTP1.Post(url,paramList);
except
on E:Exception do
begin
showMessage('failed to post to: '+url);
ShowMessage('Exception message = '+E.Message);
end;
end;
end;
procedure TForm1.Button1Click(Sender: TObject);
var
reportType : String;
begin
PostData(Self);
reportType := 'live';
GetUrlToFile('',reportType+'.csv');
end;
procedure TForm1.GetUrlToFile(AURL, AFile : String);
var
Output : TMemoryStream;
success : Boolean;
begin
success := True;
Output := TMemoryStream.Create;
try
try
IdHTTP1.Get(AURL, Output);
IdHTTP1.Disconnect;
except
on E : Exception do
begin
ShowMessage('Get failed to GET from '+IdHTTP1.GetNamePath +'. Exception message = '+E.Message);
success := False;
end;
end;
if success = True then
begin
showMessage('Filed saved');
Output.SaveToFile(AFile);
end;
finally
Output.Free;
end;
end;
On each try I get "IOHandler is not valid" error. Obviously I'm not posting correctly to the initial page but can anyone advise me on what I'm missing? Also can I simply then hit the download URL after login or will I have to use cookies?
Thanks
There are several bugs in your code:
1) PostData() is requesting an HTTPS URL, but it is not assigning an SSL-enabled IOHandler to the TIdHTTP.IOHandler property. You need to do so.
2) Button1Click() is passing a URL to GetUrlToFile() that does not specify any protocol, so TIdHTTP will end up treating that URL as relative to its existing URL, and thus try to download from https://www.testurl.com/test/testurl.com/test/reports.phpinstead of https://testurl.com/test/reports.php. If you want to request a relative URL, don't include the hostname (or even the path in this case, since you are sending multiple requests to the same path, just different documents).
3) you are leaking the TIdHTTP object.
Issue 1) has now been resolved in another post:
Delphi 5 Indy/ics SSL workaround?
However I would greatly appreciate help on the rest, as follows.
Would I need to make a GET call with the same IdHTTP object and additional URL variable? or should I create a new IdHTTP object?
Would I need to record the session using cookies or can all of this be done with the same call?
Is the GET call above actually what I need to save a csv to file? I may also choose to handle it directly as the data will need importing anyway.
Currently the code gets the error: EIdHTTPProtocolException