Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for XOAUTH2 SASL authentication #192

Open
rlebeau opened this issue Feb 8, 2018 · 67 comments · May be fixed by #479
Open

Add support for XOAUTH2 SASL authentication #192

rlebeau opened this issue Feb 8, 2018 · 67 comments · May be fixed by #479
Assignees
Labels
Element: SASL Issues related to SASL handling, TIdSASL and descendants, etc Status: In Progress Issue is being worked on Type: Enhancement Issue is proposing a new feature/enhancement

Comments

@rlebeau
Copy link
Member

rlebeau commented Feb 8, 2018

Outlook/Hotmail/Live, Gmail, and possibly others, support XOAUTH2 authentication over SASL for POP3, SMTP and IMAP. Indy should implement a TIdSASL component to support this. This way, users do not need to create application-specific passwords when 2-step verification is enabled in their accounts.

https://developers.google.com/gmail/imap/xoauth2-protocol

https://msdn.microsoft.com/en-us/library/dn440163.aspx

@rlebeau rlebeau changed the title Add support for XOUTH2 SASL authentication Add support for XOAUTH2 SASL authentication Feb 8, 2018
@rlebeau rlebeau self-assigned this May 24, 2018
@geoffsmith82
Copy link

Hi,
I have a demo of this at
https://github.com/geoffsmith82/GmailAuthSMTP

@BretBordwell
Copy link

Remy,

Now that Microsoft has removed all but OAuth2 authentication for IMAP, this is now on our front burner. Any resolution in sight?

@rlebeau rlebeau added Type: Enhancement Issue is proposing a new feature/enhancement Element: SASL Issues related to SASL handling, TIdSASL and descendants, etc labels May 18, 2022
@rlebeau
Copy link
Member Author

rlebeau commented Jun 1, 2022

I do have some new TIdSASL classes in the works for OAUTHBEARER, OAUTH10A and XOAUTH2 that I started awhile back ago, but they have not been finished or tested yet (for instance, response handling is not implemented yet), so they are not ready for release. The SASL classes linked above are similar to what I have, but a bit less fleshed out than what I have. But you can try them in the meantime and see if they work.

@rlebeau
Copy link
Member Author

rlebeau commented Jun 1, 2022

I have now pushed a new sasl-oauth branch into Indy's repo. It includes a new IdSASLOAuth.pas unit, and updates various client components to include their Port number when authenticating using OAuth.

@BretBordwell
Copy link

Thanks Remy,

The above sample by geoffsmith82 works. Confusing for me, but I've been able to implement it into our app. Now debugging.

Just wanted to say thanks.

For anyone that needs help, the following Google link pointed me in the right direction:
https://developers.google.com/identity/protocols/oauth2

@rlebeau
Copy link
Member Author

rlebeau commented Jun 9, 2022

The above sample by geoffsmith82 works

OK, but do things also work when using the new SASL classes I checked in, rather than using geoff's SASL classes?

@BretBordwell
Copy link

OK, but do things also work when using the new SASL classes I checked in, rather than using geoff's SASL classes?

Thank you Remy. I gave it a fair shot yesterday, but had compile issues. Differences in procedure calls, declaration of uses in implementation when it belonged in interface (IdGlobals for example), etc.
I'm using Alexandria (280), with the project files available for 270, etc (eg:IndyCore270). I'm fairly experienced, but just didn't have the time to work out all the differences and get past this learning curve. Some of this could be path issues, where this branch was trying to load default dcu and dcp packages as released with Delphi. All on me and my inexperience.

I would have preferred a subclass for now vs the implementation in the base classes and this is where geoffsmith82's implementation works. Sorry, I can not say if it works or not. I may give it another shot soon. Thanks again, really.

@rlebeau
Copy link
Member Author

rlebeau commented Jul 19, 2022

I gave it a fair shot yesterday, but had compile issues. Differences in procedure calls, declaration of uses in implementation when it belonged in interface (IdGlobals for example), etc.

Can you elaborate on the errors?

I'm using Alexandria (280), with the project files available for 270, etc (eg:IndyCore270)

FYI, package files for Alexandria (280) have now been checked in.

I would have preferred a subclass for now vs the implementation in the base classes and this is where geoffsmith82's implementation works.

What do you mean? The new SASL classes are subclasses.

@LongDelphiHalfLife
Copy link

Hi Remy
I have your branch implemented in my app but it doesnt have any code to generate the token. In TryStartAuthenticate you GetPassword or call the GetAccessToken event. Im considering recreating Geoffs TEnhancedOAuth2Authenticator as a TIdUserPassProvider to return the token via GetPassword, or leaving that blank and using the GetAccessToekn event.

What do you recommend? Have a missed the token generator in your code?

function TIdSASLOAuth2Base.TryStartAuthenticate(const AHost: string; const APort: TIdPort; const AProtocolName : string; var VInitialResponse: String): Boolean;
var
LToken: String;
begin
LToken := GetPassword;
if (LToken = '') and Assigned(FOnGetAccessToken) then begin
FOnGetAccessToken(Self, LToken);
end;
VInitialResponse := DoStartAuthenticate(AHost, APort, LToken);
Result := True;
end;

@rlebeau
Copy link
Member Author

rlebeau commented Aug 9, 2022

I have your branch implemented in my app but it doesnt have any code to generate the token.

Correct, it does not. The user is responsible for obtaining the necessary token first, such as via HTTP to whatever OAuth provider they are working with (Google, Microsoft, etc), and then assign the token to the SASL component.

Have a missed the token generator in your code?

No. Outside of submitting the OAuth token over SASL, I do not have any other OAuth code implemented at this time.

@LongDelphiHalfLife
Copy link

Thanks Remy, IIm getting my own tokens now and passing them in with OnGetAccessToken. Geoffs class above was useful to get started but didn't cover the OAuth requirements i had.

@hairy77
Copy link

hairy77 commented Aug 16, 2022

Hi Remy I have your branch implemented in my app but it doesnt have any code to generate the token. In TryStartAuthenticate you GetPassword or call the GetAccessToken event. Im considering recreating Geoffs TEnhancedOAuth2Authenticator as a TIdUserPassProvider to return the token via GetPassword, or leaving that blank and using the GetAccessToekn event.

What do you recommend? Have a missed the token generator in your code?

function TIdSASLOAuth2Base.TryStartAuthenticate(const AHost: string; const APort: TIdPort; const AProtocolName : string; var VInitialResponse: String): Boolean; var LToken: String; begin LToken := GetPassword; if (LToken = '') and Assigned(FOnGetAccessToken) then begin FOnGetAccessToken(Self, LToken); end; VInitialResponse := DoStartAuthenticate(AHost, APort, LToken); Result := True; end;

FOnGetAccessToken

Hi

Thanks for this. I have downloaded Remy's branch, and can see the IdSASLOAuth.pas unit installed. However there is no component installed for TIdSASLOAuth2Base component in my component suite. (I completely uninstalled Indy from Delphi and deleted all references before installing this so I'm fairly sureI have the latest install).

It this not an actual design time component? Can you please advise if I need to try and use this another way? And are you able to confirm please if you actually were able to get POP3 working with Microsoft oAuth?

Thanks

Adam

@rlebeau
Copy link
Member Author

rlebeau commented Aug 16, 2022

Thanks for this. I have downloaded Remy's branch, and can see the IdSASLOAuth.pas unit installed. However there is no component installed for TIdSASLOAuth2Base component in my component suite. (I completely uninstalled Indy from Delphi and deleted all references before installing this so I'm fairly sure I have the latest install).

I hadn't yet updated the IdRegister.pas file in the Lib/Protocols folder to register the new SASL components in the IDE palette. I have now updated that file, so you should be able to pull the latest branch, recompile Indy, and see the new components. Although, I still need to update the DCR file to add palette icons for them.

FYI, TIdSASLOAuth2Base is just a base class, it is not meant to be used directly, so you will not see it on the palette. Use the derived classes instead: TIdSASLOAuth2Bearer, TIdSASLOAuth10A, and TIdSASLXOAuth2.

It this not an actual design time component?

It is now.

Can you please advise if I need to try and use this another way?

You can alternatively create the components in code at runtime, like any other component.

And are you able to confirm please if you actually were able to get POP3 working with Microsoft oAuth?

At this time, I haven't tested any of this new code myself.

@hairy77
Copy link

hairy77 commented Aug 16, 2022

Thanks Remy,

I've updated to the latest branch and indeed have access to the 3 new components. I have added these to a test project and added them to the POP3 SASLMechanisms, but when testing POP3 on Office365 still get the error "Doesn't support AUTH or the specified SASL handlers".

I'm assuming there's still work to be done before Indy is compatible with Microsoft's changes?

@LongDelphiHalfLife
Copy link

"I've updated to the latest branch and indeed have access to the 3 new components. I have added these to a test project and added them to the POP3 SASLMechanisms, but when testing POP3 on Office365 still get the error "Doesn't support AUTH or the specified SASL handlers"."

I have it working for MS IMAP and POP. If you see above I had to write a class to get an OAuth token from the MS service then pass it into the Indy component which uses that token to authorize the IMAP or POP connection. Works for google too but you need a class that can get a token from the google service (similar but just different parameters passed in)

@rlebeau
Copy link
Member Author

rlebeau commented Aug 22, 2022

I've updated to the latest branch and indeed have access to the 3 new components. I have added these to a test project and added them to the POP3 SASLMechanisms, but when testing POP3 on Office365 still get the error "Doesn't support AUTH or the specified SASL handlers".

That error message means that the POP3 server's CAPA response did not include a SASL line specifying any of the SASL components in the SASLMechanisms.

I'm assuming there's still work to be done before Indy is compatible with Microsoft's changes?

Microsoft uses the XOAUTH2 SASL, which is covered by the TIdSASLXOAuth2 component. All you need to do is assign the desired username to the Username property, and either assign the access token to the Password property or return the token from an OnGetAccessToken event handler.

Unfortunately, Microsoft's SASL documentation only demonstrates IMAP querying the server for OAuth2 support via a CAPABILITY command, it does not demonstrate a similar query for POP3 or SMTP, or maybe Microsoft didn't implement that yet? I don't know.

@hairy77
Copy link

hairy77 commented Aug 22, 2022

"I've updated to the latest branch and indeed have access to the 3 new components. I have added these to a test project and added them to the POP3 SASLMechanisms, but when testing POP3 on Office365 still get the error "Doesn't support AUTH or the specified SASL handlers"."

I have it working for MS IMAP and POP. If you see above I had to write a class to get an OAuth token from the MS service then pass it into the Indy component which uses that token to authorize the IMAP or POP connection. Works for google too but you need a class that can get a token from the google service (similar but just different parameters passed in)

Thanks for your replies LongDelphiHalfLife and Remy

I must be doing something wrong as I'm trying to follow what LongDelphiHalfLife has done, but the TidSASLXOAuth2's OnGetAccessToken event never fires. I get a "Doesn't support AUTH or the specified SASL Handlers!!" error raised when I perform the POP3's Connect command.

I've double check I have assigned the TidSASLXOAuth2 component to the SASLMechanisms (moved it from Available to Assigned) so I'm at a loss why it's not executing the onGetToken event or why it doesn't believe that the component isn't supported.

It's very encouraging to hear that LDHL has this actually working with Microsoft for POP3 - gives me hope that there's a solution with Indy that I may be able to implement before the Microsoft deadline - I just don't know why it's failing for me.

@rlebeau
Copy link
Member Author

rlebeau commented Aug 22, 2022

the TidSASLXOAuth2's OnGetAccessToken event never fires. I get a "Doesn't support AUTH or the specified SASL Handlers!!" error raised when I perform the POP3's Connect command.

As I explained in my previous reply, that error means that TIdPOP3 is not able to discover XOAuth2 listed in Microsoft's reply to the CAPA command when TIdPOP3 is trying to login, even though XOAuth2 is really supported by the server.

When logging in to a server, TIdPOP3 sends a CAPA command to discover which authentications the server supports, and then if TIdPOP3.AuthType is patSASL then TIdPOP3.Login() matches that list against TIdPOP3.SASLMechanisms to find a common set of authentications that both parties support, and then it attempts each of those in order until one succeeds or they all fail.

Please check this for yourself. You can either:

  • look at the contents of the TIdPOP3.Capabilities property after calling TIdPOP3.Connect() (to avoid TIdPOP3.Connect() raising the exception, you can set TIdPOP3.AutoLogin to false, and then call TIdPOP3.Login() afterwards)

  • assign a TIdLog... component to the TIdPOP3.Intercept property to capture the raw POP3 commands/responses.

The CAPA response should look something like this:

C: CAPA

S: +OK
TOP
UIDL
SASL PLAIN XOAUTH2 // <-- HERE
USER
.

Are you seeing that entry when logging in to your Microsoft server?

I've ... moved it from Available to Assigned

What does that mean?

so I'm at a loss why it's not executing the onGetToken event

Because it is not attempting to login with XOAuth2.

or why it doesn't believe that the component isn't supported.

Because it thinks Microsoft doesn't support XOAuth2.

It's very encouraging to hear that LDHL has this actually working with Microsoft for POP3

I haven't looked at LDHL's implementation, but I suspect it is simply ignoring the CAPA response and attempting XOAuth2 unconditionally. TIdPOP3 does not do that.

@hairy77
Copy link

hairy77 commented Aug 23, 2022

Good Morning Remy,

Thanks for your reply (and your patience!). I'm not fully familiar with CAPA/XOAuth2 protocols and am undergoing a steep learning curve with this one. I really appreciate your assistance! I think I understand a lot more now.

I've ... moved it from Available to Assigned

What does that mean?

Sorry - what I mean by this is when I click on the idPop3 component in the designer, and open up SASLMechanism my idSASLXOauth2 component is in the Available list (left hand side). I move this to the Assigned side to confirm that it's actually being assigned to the POP3 component.

Please check this for yourself. You can either:

  • look at the contents of the TIdPOP3.Capabilities property after calling TIdPOP3.Connect() (to avoid TIdPOP3.Connect() raising the exception, you can set TIdPOP3.AutoLogin to false, and then call TIdPOP3.Login() afterwards)
  • assign a TIdLog... component to the TIdPOP3.Intercept property to capture the raw POP3 commands/responses.

The CAPA response should look something like this:

C: CAPA

S: +OK
TOP
UIDL
SASL PLAIN XOAUTH2 // <-- HERE
USER
.

Are you seeing that entry when logging in to your Microsoft server?

Thanks very much for the details. Unfortunately no - I'm not seeing that when connecting to the MS Server.

What I have in my debug log is as follows:

S:CAPA
R:+OK
TOP
UIDL
STLS
.
S:STLS
R:+OK Begin TLS negotiation.

...and then I get the Doesn't support AUTH or specified SASL handlers. So it looks like the response is definitely missing the XOAUTH2 that Indy is expecting.

I have checked out the Capabilities property after calling connect as you suggested. I get TOP, UIDL and STLS only.

I thought I'd try and be sneaky and do a force, so I went and added:

idPOP3.Connect;
idPOP3.CAPA;
idPop3.Capabilities.add('SASL PLAIN XOAUTH2'); // Trying to be sneaky here...
idPOP3.Login;

but then in the logs I get:

S:AUTH XOAUTH2
R:-ERR Protocol error. Connection is closed. 10

I'm guessing from this it seems that my approach of manually adding a capability that wasn't returned was successful to force XOAuth2- but then I get the protocol error occurs immediately after that as Microsoft rejects my S:AUTH XOAUTH2 request.

@rlebeau
Copy link
Member Author

rlebeau commented Aug 23, 2022

I've ... moved it from Available to Assigned

What does that mean?

Sorry - what I mean by this is when I click on the idPop3 component in the designer, and open up SASLMechanism my idSASLXOauth2 component is in the Available list (left hand side). I move this to the Assigned side to confirm that it's actually being assigned to the POP3 component.

Oh, OK. I wasn't aware that there was a custom design-time Form being used to edit the SASLMechanisms, I thought only the the Object Inspector's standard TCollection editor was being used. It hass been a long time since I last dealt with the SASL components at design-time.

Are you seeing that entry when logging in to your Microsoft server?

Unfortunately no - I'm not seeing that when connecting to the MS Server.

Well, then that is why you are getting the error raised.

What I have in my debug log is as follows:

Interesting, I would have expected TIdPOP3 to send another CAPA command after STLS has finished securing the connection, as a server's capabilities may change once the connection has been secured. But looking at TIdPOP3's code, it only sends CAPA one time, in TIdPOP3.Connect() before login. I have now fixed that (#427). I'll bet the XOAUTH2 capability will now show up properly after a successful STLS.

I thought I'd try and be sneaky and do a force, so I went and added:

...

but then in the logs I get:

S:AUTH XOAUTH2
R:-ERR Protocol error. Connection is closed. 10

Odd, considering that is following Microsoft's example of sending AUTH XOAUTH2 without any parameters, waiting for the server to acknowledge the request before then sending the encoded token. I wonder why it think there is a protocol error.

Well, try the latest branch (you can remove your hack), and see if the same error still occurs.

@hairy77
Copy link

hairy77 commented Aug 24, 2022

I have now fixed that (#427). I'll bet the XOAUTH2 capability will now show up properly after a successful STLS.
Odd, considering that is following Microsoft's example of sending AUTH XOAUTH2 without any parameters, waiting for the server to acknowledge the request before then sending the encoded token. I wonder why it think there is a protocol error.

Well, try the latest branch (you can remove your hack), and see if the same error still occurs.

Thanks Remy. You are correct! I've updated and tried that and on the 2nd CAPA request it shows up properly, but it is still unsuccessful.

I think I've found the problem! (Just not sure how to fix it).

I manually tried logging in with OpenSSL typing in the commands myself. If I try and replicate what Indy is doing I get the same error.

If on the other hand I execute AUTH XOAUTH2 as a command, wait for a response, and then paste in the token and send that through as a separate command - it executes successfully.

I notice in IdSASLCollection on line 235 - this is where the problem occurs:

AClient.SendCmd(ACmd + ' ' + String(ASASL.ServiceName) + ' ' + AEncoder.Encode(S), []);//[334, 504]);

Indy is sending through ...

AUTH XOAUTH2 <token>

... as a single transmission and Microsoft doesn't appear to like it.

However it would appear if AUTH XOAUTH2 is sent through, wait for a response and then the Token is sent through after - Microsoft is happy.

I'm not sure if it's of any help, but I've come up with the following code to create things at runtime (to try and make it easier for discussion and to see what I'm actually doing instead of having components on a form at design time):

procedure TForm2.IdSASLXOAuth21GetAccessToken(Sender: TObject; var AccessToken: string);
begin
  AccessToken := EmailOAuthDataModule.FOAuth2_Enhanced.AccessToken;
end;

procedure TForm2.btnCheckMsg2Click(Sender: TObject);
var
  IDPop3: TidPop3;
  xoauthSASL: TIdSASLListEntry;
  msgCount: Integer;
  SASLList: TIdSASLListEntry;
begin
  IdPop3 := TidPop3.create;
  IdPop3.AutoLogin := false;
  IdPOP3.IOHandler := TidSSLioHandlerSocketOpenSSL.create;
  xoauthSASL := IdPOP3.SASLMechanisms.Add;
  xoauthSASL.SASL := TIdSASLXOAuth2.Create(nil);
  TIdSASLXOAuth2(xoauthSASL.SASL).OnGetAccessToken := IdSASLXOAuth21GetAccessToken;
  TIdSASLXOAuth2(xoauthSASL.SASL).UserPassProvider := TIdUserPassProvider.Create();
  TIdSASLXOAuth2(xoauthSASL.SASL).UserPassProvider.Username := microsoft_clientaccount;

  IdPOP3.Host := 'outlook.office365.com';
  IdPOP3.Port := 995;
  IdPOP3.UseTLS := utUseExplicitTLS;

  IdPOP3.AuthType := patSASL;
  IdPOP3.Connect;
  IdPOP3.CAPA;
  IdPOP3.Login;
  msgCount := IdPOP3.CheckMessages;
end;

I hope this is of some help.

@KrystianBigaj
Copy link

KrystianBigaj commented Aug 24, 2022

Hi,

I can confirm that POP3, IMAP4, SMTP with MS OAUTH2 and Indy (sasl-oauth branch from about month ago, Delphi 10.4) works correctly.
I had to make 2 fixes:
IdReplyIMAP4.pas (add AssignTo, because in case of error during ouath authentication, error message is mssing), fix:

procedure TIdReplyIMAP4.AssignTo(ADest: TPersistent);
begin
  inherited AssignTo(ADest);

  // Extra.Assign must be called after inherited AssignTo (because of Clear)
  if ADest is TIdReplyIMAP4 then
    TIdReplyIMAP4(ADest).Extra.Assign(Extra);
end;

For POP3 connection I had to fix AUTH XOAUTH2 with token in new line (because I got "Protocol error" respone).
IdSASLCollection.pas:

function PerformSASLLogin ....
...
  if ACanAttemptIR then begin
    if ASASL.TryStartAuthenticate(AHost, APort, AProtocolName, S) then begin
      { KB
      https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth
      To authenticate a POP server connection, the client will have to respond with an AUTH command split into two lines in the following format:
      }
      if TextIsSame(AProtocolName, IdGSKSSN_pop) then
      begin
        AClient.SendCmd(ACmd + ' ' + String(ASASL.ServiceName) {+ ' ' + AEncoder.Encode(S)}, []);//[334, 504]);
        AClient.SendCmd(AEncoder.Encode(S), []);
      end else
        AClient.SendCmd(ACmd + ' ' + String(ASASL.ServiceName) + ' ' + AEncoder.Encode(S), []);//[334, 504]);
      if CheckStrFail(AClient.LastCmdResult.Code, AOkReplies, AContinueReplies) then begin
        if not TextIsSame(AProtocolName, IdGSKSSN_pop) then begin
          ASASL.FinishAuthenticate;
          Exit; // this mechanism is not supported
        end;
      end else begin
        AuthStarted := True;
      end;
    end;
  end;

I'm not sure it this is required for other OAUTH2 providers.

@hairy77
Copy link

hairy77 commented Aug 24, 2022

Thanks so much for your confirmation and code snippet. I have now changed line 235 in IdSASLCollection.pas from:

AClient.SendCmd(ACmd + ' ' + String(ASASL.ServiceName) + ' ' + AEncoder.Encode(S), []);//[334, 504]);

to:

     if TextIsSame(AProtocolName, IdGSKSSN_pop) then
      begin
        AClient.SendCmd(ACmd + ' ' + String(ASASL.ServiceName) {+ ' ' + AEncoder.Encode(S)}, []);//[334, 504]);
        AClient.SendCmd(AEncoder.Encode(S), []);
      end else
      AClient.SendCmd(ACmd + ' ' + String(ASASL.ServiceName) + ' ' + AEncoder.Encode(S), []);//[334, 504]);

... and can confirm that I am successfully authenticating with Microsoft 365! Again - thank you!!!

@geoffsmith82
Copy link

@KrystianBigaj I wouldn't make the change in PerformSASLLogin as the the 2 line login for Microsoft isn't used by everyone (specifically Google). I would add an property to the SASL class for a Two line POP authentication.

@rlebeau
Copy link
Member Author

rlebeau commented Aug 25, 2022

Most POP3 providers support the version of the AUTH command where the initial credentials are included in the 1st line of the request, so PerformSASLLogin() attempts that version first. This feature saves a round-trip. But unlike other protocols, that feature of AUTH is not advertised in POP3's CAPA reply, as it is assumed to be supported since it was introduced in the same RFC 2449 that introduced the CAPA command itself. But, it wasn't formalized until RFC 5034, so not all POP3 providers actually implement that feature of AUTH. So, if it fails the 1st time, then for POP3 only, PerformSASLLogin() falls back to the old logic of sending an AUTH request without sending the initial credentials until the server explicitly asks for them.

THIS IS BY DESIGN. You should NOT have had to make ANY code changes to TIdPOP3 or PerformSASLLogin() for this to work. So please, revert all of those changes.

If you really want to disable the 1st 1-line AUTH attempt for POP3, the correct way to do that is to have TIdPOP3.Login() set ACanAttemptIR=False when it calls LoginSASL(). TIdPOP3 used to do exactly that, until a year ago when the retry logic was implemented in PerformSASL() in PR #354.

I have now added a SASLCanAttemptInitialResponse property to TIdPOP3 in the sasl-oauth branch to control whether ACanAttemptIR is set to True or False on a per-login basis. Similar to the ValidateAuthLoginCapability property in TIdSMTP when AuthType=satDefault.

As for TIdReplyIMAP, I have checked in a fix for it in the main code.

@hairy77
Copy link

hairy77 commented Aug 25, 2022

Hi Remy,

Thanks for your reply, and thanks for adding in SASLCanAttemptInitialResponse. Just to clarify...

THIS IS BY DESIGN. You should NOT have had to make ANY code changes to TIdPOP3 or PerformSASLLogin() for this to work. So please, revert all of those changes.
TIdPOP3 used to do exactly that, until a year ago when the retry logic was implemented in PerformSASL() in PR #354.

I'm confused by this. With the example code shown above on how I connect - I was definitely getting errors with Microsoft until I made code changes Krystian mentioned to the code - however if I understand you correctly - I shoudn't have needed to make that change because the retry logic was implemented in PerformSASL(). So now I'm confused as to why it is failing for me with Microsoft as it certainly didn't seem to be retrying using the second method after failure of the first.

I will revert changes and give SASLCanAttemptInitialResponse a go- but I'm still curious to know that what I'm experiencing (failure to authenticate) does not match up with what your saying (retry logic was implemented and there should be no need to make any changes to the code) - that I shouldn't need SASLCanAttemptInitialResponse at all?

@marcin-bury
Copy link

Remy @rlebeau
Thanks a lot.

@marcin-bury
Copy link

@KrystianBigaj , @hairy77
How do you obtain the acces_token for reading emails. What endpoints do you use and what do you put in "scope"?

TIA
Marcin

@joostcrommert1
Copy link

@marcin-bury depends on what type of library (SMTP, Pop3, IMAP) you use in your Delphi-project. In my case i've used the TIdSMTP-component with the following scope; "https://outlook.office.com/IMAP.AccessAsUser.All https://outlook.office.com/POP.AccessAsUser.All https://outlook.office.com/SMTP.Send offline_access".

Used endpoints are https://login.microsoftonline.com/common/oauth2/v2.0/authorize and https://login.microsoftonline.com/common/oauth2/v2.0/token

@marcin-bury
Copy link

@joostcrommert1 thanks for the response.
Would you share the a piece of code to get what is the correct sequence of calling the endpoints and what I should put in each request body?
I need only IMAP - read the messages and move them to 'archive' folder.

TIA
Marcin

@joostcrommert1
Copy link

joostcrommert1 commented Nov 18, 2022

@marcin-bury I haven't used IMAP myself, but I guess the set-up would be the same as POP3;

Providers[ProviderInfo] is based on the example that @geoffsmith82 posted earlier in this thread.

DataModule.Mailbox: TIdPop3;

   xoauthSASL := DataModule.Mailbox.SASLMechanisms.Add;
   xoauthSASL.SASL := TIdSASLXOAuth.Create(nil);

   if xoauthSASL.SASL is TIdSASLXOAuth then
   begin
     TIdSASLXOAuth(xoauthSASL.SASL).Token := Self.GetAccessCode(FSelectedProvider);
     TIdSASLXOAuth(xoauthSASL.SASL).User := FUsername;
     TIdSASLXOAuth(xoauthSASL.SASL).TwoLinePOPFormat := True;
   end;

   DataModule.Mailbox.SASLCanAttemptInitialResponse := False;
   DataModule.Mailbox.AuthType := patSASL;

   DataModule.Mailbox.Connect;
   DataModule.Mailbox.Login;

GetAccessCode:

if ((Now() >= aAccessTokenValidUntil) and (aAccessTokenValidUntil <> 0)) then
begin
  AccessToken := aAccessToken;
end
else
begin
  if aRefreshToken <> '' then
  begin
    aParams := TStringList.Create;
    aParams.Add('grant_type=refresh_token');
    aParams.Add('client_id='+Providers[ProviderInfo].ClientID);
    aParams.Add('client_secret='+Providers[ProviderInfo].ClientSecret);
    aParams.Add('scope='+Providers[ProviderInfo].Scopes);
    aParams.Add('redirect_uri='+Providers[ProviderInfo].RedirectUrl);
    aParams.Add('refresh_token='+aRefreshToken);

    aResponseString := Self.Request(Providers[ProviderInfo].AccessTokenEndpoint, 'POST', aParams);

    aOauth := TOffice365OAuthClass.FromJsonString(aResponseString);

    aAccessTokenValidUntil := IncSecond(Now(), Round(aOauth.expires_in));

    AccessToken := aOauth.access_token;
  end;

  if AccessToken = '' then
  begin
    FCallBackURL := Providers[ProviderInfo].RedirectUrl;

    AuthURL := Providers[ProviderInfo].AuthorizationEndpoint + '?client_id=%s'
     + '&response_type=code'
     + '&redirect_uri=%s'
     + '&scope=%s';

    AuthURL := Format(AuthURL, [
      Providers[ProviderInfo].ClientID,
      Providers[ProviderInfo].RedirectUrl,
      Providers[ProviderInfo].Scopes
    ]);

    // Show (login) form
    aLoginForm := TFLogin.Create(nil);
    try
      Self.aLoginForm.InitForm(AuthURL, Providers[ProviderInfo].RedirectUrl, Self.processLogin());
    finally
      FreeAndNil(Self.aLoginForm);
    end;

    if FAuthCode = '' then
    begin
      Abort;
    end;

    aParams := TStringList.Create;
    aParams.Add('grant_type=authorization_code');
    aParams.Add('client_id='+Providers[ProviderInfo].ClientID);
    aParams.Add('client_secret='+Providers[ProviderInfo].ClientSecret);
    aParams.Add('scope='+Providers[ProviderInfo].Scopes);
    aParams.Add('redirect_uri='+Providers[ProviderInfo].RedirectUrl);
    aParams.Add('code='+FAuthCode);

    aResponseString := Self.Request(Providers[ProviderInfo].AccessTokenEndpoint, 'POST', aParams);

    aOauth := TOffice365OAuthClass.FromJsonString(aResponseString);

    aAccessTokenValidUntil := IncSecond(Now(), Round(aOauth.expires_in));

    AccessToken := aOauth.access_token;
  end;
end;

@rlebeau
Copy link
Member Author

rlebeau commented Nov 18, 2022

@marcin-bury
xoauthSASL.SASL := TIdSASLXOAuth.Create(nil);

Have you tried using the TIdSASLXOAuth2 class in this ticket's branch, instead of using a custom class?

if xoauthSASL.SASL is TIdSASLXOAuth then

Why are you using the is operator to check the type you just created? This will always be True, so just omit it.

 TIdSASLXOAuth(xoauthSASL.SASL).Token := Self.GetAccessCode(FSelectedProvider);
 TIdSASLXOAuth(xoauthSASL.SASL).TwoLinePOPFormat := True;

TIdSASLXOAuth2 does not have Token or TwoLinePOPFormat properties.

Also, what is the TwoLinePOPFormat property doing? Is there a problem with the new TIdPOP3.SASLCanAttemptInitialResponse property added in this ticket's branch?

@marcin-bury
Copy link

@rlebeau
Remy
The piece of code, you are refering to was presented by @joostcrommert1 as an example of "flow" to connect to Office365 mailbox.

@marcin-bury
Copy link

Btw, @joostcrommert1 , thanks for sharing

@marcin-bury
Copy link

Sorry guys for non english post
@KrystianBigaj
Podpowiedziałbyś jak właściwie pobrać token z login.microsoftonline.com, żeby zalogować się do skrzynki IMAP-owej Office365 (przez TidIMAP4). Mam tenant_Id, client_Id, client_secret,
takie mam body przy wywołaniu:
tsRequestBody.Add('grant_type=client_credentials'); tsRequestBody.Add('client_id=' + ClientID); tsRequestBody.Add('client_secret=' + ClientSecret); tsRequestBody.Add('scope=https://graph.microsoft.com/.default');
dostaję odpowiedź z access_token, ale już do IMAP zalogować się nie mogę,
Dzięki
Marcin

@joostcrommert1
Copy link

@rlebeau code is messy indeed, was happy with a working example so didn't bother to clean up the code.

@marcin-bury
Copy link

@joostcrommert1
What "scopes" do you use for obtaining access_token - some standard or dedicated ones?

@rlebeau
Copy link
Member Author

rlebeau commented Nov 23, 2022

tsRequestBody.Add('scope=https://graph.microsoft.com/.default');

This looks wrong to me, since the Graph API is not being accessed.

See: Authenticate an IMAP, POP or SMTP connection using OAuth

@Cortomatt
Copy link

Cortomatt commented Jan 2, 2023

Hello.
I tried to use the new Indy TIdSASLXOAuth2, but it doesn't work :-/
I've got a token => it's ok, but after I can't connect to my email box

IdIMAP_Test:=TIdIMAP4.Create;

RichEdit_Log.Lines.Clear;

PWDToken:=GetToken;

IdSSLIOHandlerSocketOpenSSL:=TIdSSLIOHandlerSocketOpenSSL.Create(Self);
IdSSLIOHandlerSocketOpenSSL.SSLOptions.Method:=sslvSSLv23;
IdSSLIOHandlerSocketOpenSSL.SSLOptions.Mode:=sslmClient;

IdIMAP_Test.IOHandler:=IdSSLIOHandlerSocketOpenSSL;
IdIMAP_Test.UseTLS:=utUseImplicitTLS;//utUseExplicitTLS;
IdIMAP_Test.AuthType:=iatSASL;

xoauthSASL_Test:=IdIMAP_Test.SASLMechanisms.Add;
xoauthSASL_Test.SASL:=TIdSASLXOAuth2.Create(nil);
TIdSASLXOAuth2(xoauthSASL_Test.SASL).UserPassProvider:=TIdUserPassProvider.Create(nil);
TIdSASLXOAuth2(xoauthSASL_Test.SASL).UserPassProvider.Username:='myusername@domain.fr';
TIdSASLXOAuth2(xoauthSASL_Test.SASL).UserPassProvider.Password:=PWDToken;

IdIMAP_Test.Host:='outlook.office365.com';
IdIMAP_Test.Port:=993;
IdIMAP_Test.ConnectTimeout:=6000;
IdIMAP_Test.ReadTimeout:=6000;

LogEvent:=TIdLogEvent_My.Create(Self,RichEdit_Log);

IF NOT Assigned(IdIMAP_Test.IOHandler) 
  THEN IdIMAP_Test.IOHandler := TIdIOHandler.MakeDefaultIOHandler(Self);

IdIMAP_Test.IOHandler.Intercept:=LogEvent;
IdIMAP_Test.IOHandler.OnStatus:=IdIMAP_Test.OnStatus;

IdIMAP_Test.Connect;
IF IdIMAP_Test.Connected
  then ShowMessage(IntToStr(IdIMAP_Test.MailBox.TotalMsgs));

IdIMAP_Test.Disconnect;

I've got a socket error 10038, or a timeout error
Can you help me please?

@rlebeau
Copy link
Member Author

rlebeau commented Jan 2, 2023

PWDToken:=GetToken;

Did you verify you are actually receiving a token?

IdSSLIOHandlerSocketOpenSSL.SSLOptions.Method:=sslvSSLv23;

You really shouldn't be using the SSLOptions.Method property at all, use the SSLOptions.SSLVersions property instead.

xoauthSASL_Test.SASL:=TIdSASLXOAuth2.Create(nil);
TIdSASLXOAuth2(xoauthSASL_Test.SASL).UserPassProvider:=TIdUserPassProvider.Create(nil);

On a side note: you are not assigning an Owner to these objects, so they are going to be leaked unless you Free them manually when you are done using them.

IF NOT Assigned(IdIMAP_Test.IOHandler)
THEN IdIMAP_Test.IOHandler := TIdIOHandler.MakeDefaultIOHandler(Self);

You don't need that.

IdIMAP_Test.IOHandler.Intercept:=LogEvent;

You can assign the LogEvent to the IdIMAP_Test.Intercept property instead, and then Indy will make sure to assign it to the IOHandler for you when appropriate.

IdIMAP_Test.IOHandler.OnStatus:=IdIMAP_Test.OnStatus;

Are you getting any events?

IdIMAP_Test.Connect;
IF IdIMAP_Test.Connected
then ShowMessage(IntToStr(IdIMAP_Test.MailBox.TotalMsgs));

MailBox.TotalMsgs is not valid until SelectMailBox() or StatusMailBox() is called first.

I've got a socket error 10038, or a timeout error Can you help me please?

On which line of code, exactly? Did the LogEvent log anything?

@Cortomatt
Copy link

Cortomatt commented Jan 3, 2023

Thanks for your quick reply.

Yes, i've a token, and today i've an event (some moment i've nothing else error 10038)

Here the events : I've added [SRV] & [PGM] before each line to understand the log

TOKEN
eyJ0eXAiOiJ.......... etc...
[SRV] * OK The Microsoft Exchange IMAP4 service is ready. [UABBADcAUAAyADYANABDAEEAMA...................MATwBNAA==]
[PGM]C1 CAPABILITY
[SRV] * CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=XOAUTH2 SASL-IR UIDPLUS ID UNSELECT CHILDREN IDLE NAMESPACE LITERAL+
[SRV] C1 OK CAPABILITY completed.
[PGM]C2 AUTHENTICATE XOAUTH2 dXNlcj1vM......................... etc...
[SRV] C2 NO AUTHENTICATE failed.
[PGM]C3 LOGOUT
[SRV] * BYE Microsoft Exchange Server IMAP4 server signing off.
[SRV] C3 OK LOGOUT completed.

@geoffsmith82
Copy link

If I can remember correctly when I got this error it was because the line
[PGM]C2 AUTHENTICATE XOAUTH2 dXNlcj1vM......................... etc...
didn't have the account specified in it. It should be something like
base64("user=test@contoso.onmicrosoft.com^Aauth=Bearer EwBAAl3BAAUFFpUAo7J3Ve0bjLBWZWCclRC3EoAA^A^A")

@Cortomatt
Copy link

Cortomatt commented Jan 3, 2023

If I can remember correctly when I got this error it was because the line [PGM]C2 AUTHENTICATE XOAUTH2 dXNlcj1vM......................... etc... didn't have the account specified in it. It should be something like base64("user=test@contoso.onmicrosoft.com^Aauth=Bearer EwBAAl3BAAUFFpUAo7J3Ve0bjLBWZWCclRC3EoAA^A^A")

Thanks for your answer.
I test it on debug, and i've the username/account.
But i'm not sure than the function PerformSASLLogin_IMAP is using base64

function PerformSASLLogin_IMAP(ASASL: TIdSASL; AEncoder: TIdEncoder;
  ADecoder: TIdDecoder; AClient : TIdIMAP4): Boolean;
const
  AOkReplies: array[0..0] of string = (IMAP_OK);
  AContinueReplies: array[0..0] of string = (IMAP_CONT);
var
  S: String;
  AuthStarted: Boolean;
begin
  Result := False;
  AuthStarted := False;

  // TODO: use UTF-8 when base64-encoding strings...

  if AClient.IsCapabilityListed('SASL-IR') then begin {Do not localize}
    if ASASL.TryStartAuthenticate(AClient.Host, AClient.Port, IdGSKSSN_imap, S) then begin
      AClient.SendCmd(AClient.NewCmdCounter, 'AUTHENTICATE ' + String(ASASL.ServiceName) + ' ' + AEncoder.Encode(S), [], True); {Do not Localize}
      if CheckStrFail(AClient.LastCmdResult.Code, AOkReplies, AContinueReplies) then begin
        ASASL.FinishAuthenticate;
        Exit; // this mechanism is not supported
      end;
      AuthStarted := True;
    end;
  end;

@geoffsmith82
Copy link

That line I posted looked like it was. You would need to run it through a base64 decoder though to see if your email address is at the start.

@Cortomatt
Copy link

That line I posted looked like it was. You would need to run it through a base64 decoder though to see if your email address is at the start.

Thanks !
I've done the test, it's ok :-) (and the token too)
Perhaps something is missing on the office 365 server ? An autorisation ?

@geoffsmith82
Copy link

Have a look at the bottom of my project page for what you need to configure in Microsoft's options.

https://github.com/geoffsmith82/GmailAuthSMTP

@Cortomatt
Copy link

Edit : it work's with an other java program :-/ so the pbm is in my Delphi code

@rlebeau
Copy link
Member Author

rlebeau commented Jan 5, 2023

Thanks for your answer. I test it on debug, and i've the username/account. But i'm not sure than the function PerformSASLLogin_IMAP is using base64

Yes, it does use base64. The ASASL parameter would be pointing at the TIdSASLXOAuth2 object, and the AEncoder parameter would be pointing at a TIdEncoderMIME object. ASASL.(Try)StartAuthenticate() would return the XOAuth2 authentication string without base64 applied to it, and then AEncoder.Encode() would encode that string in base64 when sending it in the AUTHENTICATE command.

For instance, in your log message C2 AUTHENTICATE XOAUTH2 dXNlcj1vM........................., if you base64-decode dXNlcj1vM... then you will see that its data begins with user=o..., which follows the format that geoffsmith82 showed earlier from Microsoft's XOAuth2 documentation.

@Hunssi
Copy link

Hunssi commented Mar 26, 2023

I wanted to try XOAUTH2 on C++ Builder (11.3). I cloned the project, built the sasl-oauth branch, and was I able to build and link my own project after updating the Indy. Now I'm having difficulties installing the component to design time palette as I don't have Delphi.Personality available and on the other hand, the header file (hpp) for this SASL is not generated by the build job. Should it be possible to use this with C++ Builder as it is at the moment?

@rlebeau
Copy link
Member Author

rlebeau commented Mar 27, 2023

The IdSASLOAuth.pas source file was not yet added to the various .DPK and .DPROJ project files (the actual TIdSASL... components in it were already being registered on the Component Palette in IdRegister.pas, though). I have now updated the project files. Try pulling down the latest sasl-oauth branch and try again.

@Hunssi
Copy link

Hunssi commented Mar 30, 2023

Thank you! I was able to add the new SASL mechanism and building the C++ application with this update.

However, I haven't been able to test it end to end yet, as I'm having difficulties getting the Azure app registrations in place and getting valid token for the bearer. I'm hitting "535 5.7.3 Authentication unsuccessful" with curl as well, so I need to sort that out next.

@geoffsmith82
Copy link

I'm hitting "535 5.7.3 Authentication unsuccessful" with curl as well, so I need to sort that out next.

Check out the documentation here. SMTP can be disabled on the Organisation and/or the user
https://learn.microsoft.com/en-us/exchange/clients-and-mobile-in-exchange-online/authenticated-client-smtp-submission

@rlebeau rlebeau added the Status: In Progress Issue is being worked on label Apr 23, 2023
@lanselof lanselof linked a pull request May 14, 2023 that will close this issue
@Neustradamus
Copy link

Any progress on it?

@rlebeau
Copy link
Member Author

rlebeau commented Jan 17, 2024

Nothing new lately. I recently installed RAD Studio 12, so I should be able to work more on it this year.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Element: SASL Issues related to SASL handling, TIdSASL and descendants, etc Status: In Progress Issue is being worked on Type: Enhancement Issue is proposing a new feature/enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.