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

Research: OATH2 support for accessing O365 mailboxes via IMAP #313

Closed
atc0005 opened this issue Oct 14, 2022 · 15 comments
Closed

Research: OATH2 support for accessing O365 mailboxes via IMAP #313

atc0005 opened this issue Oct 14, 2022 · 15 comments
Assignees
Milestone

Comments

@atc0005
Copy link
Owner

atc0005 commented Oct 14, 2022

Overview

I was reminded today of the impending retirement of basic authentication for O365. From what I've read IMAP support isn't going away, you'll just now need to provide a token in place of username/password pair as is, for a time, still supported.

I've not researched yet what it will take to work with OAuth2 tokens.

Very light scanning of a Redmine issue on the topic provided multiple CLI examples (via curl) for fetching a token and then using that token to access a mailbox. An equivalent implementation might be sufficient for our needs.

References

General:

Redmine:

OAuth 2.0 Resource Owner Password Credentials (ROPC) grant:

Some Go-specific references:

RFCS:

Other projects:

@atc0005 atc0005 added enhancement New feature or request question Further information is requested flag config App: list-emails App: check_imap_mailbox labels Oct 14, 2022
@atc0005 atc0005 added this to the Future milestone Oct 14, 2022
@atc0005 atc0005 self-assigned this Oct 14, 2022
@atc0005
Copy link
Owner Author

atc0005 commented Oct 14, 2022

Unfortunately, I no longer have access to an IMAP-enabled O365 account for testing, so this might be more challenging than in the past.

@atc0005 atc0005 changed the title Research: OATH2 support to permit accessing O365 mailboxes via IMAP Research: OATH2 support for accessing O365 mailboxes via IMAP Oct 14, 2022
@aucompbiker
Copy link

Adam,

I'll be happy to test the code against the Libraries' email accounts.

@atc0005 atc0005 pinned this issue Nov 1, 2022
@atc0005
Copy link
Owner Author

atc0005 commented Nov 2, 2022

Other projects:

https://github.com/tgulacsi/imapclient

This one seems very promising.

A quick scan of recent changes suggests that it may be worth splitting off O365 functionality into a separate plugin. Will require further review to be sure.

@atc0005
Copy link
Owner Author

atc0005 commented Nov 11, 2022

Worked on this some yesterday as part of assisting with a Redmine installation.

Based on the yesterday's work, it looks like supporting the Resource Owner Password Credentials grant will be doable sooner than later. For better or worse, this grant type is deprecated and will be removed in the OAuth 2.1 standard. Even so, it appears to be a viable approach for initially supporting OAuth-based IMAP logins to O365.

Longer term, support for "daemon applications" appears to be what is needed:

On a different note, while assisting with the Redmine installation I can see the need for a small tool that performs the following functions:

  1. submits a request for a token
  2. (optionally) records the token in unmodified form
  3. (optionally) records the token in xoauth2 encoded form
  4. emits the xoauth2 encoded token to stdout
  5. uses a series of explicitly defined error codes that a wrapper script can use to determine whether it should attempt a login using the token

This would allow existing Redmine installations to pretty much work as-is, assuming that the sysadmins of those instances already use a wrapper script of some kind to drive the process.

The scope of such a tool may warrant a separate project.

The reason it may fit here is that the logic used to drive the tool could be used by a Nagios plugin (either the existing one or a new one) and the list-emails tool.

@atc0005
Copy link
Owner Author

atc0005 commented Nov 11, 2022

Continuing with the "thinking out loud" bit, a small workaround tool could be produced that emits a properly encoded (xoauth2) token. This encoded token could be used in place of the original password in the list-emails configuration file. The user would have to remember that the token would have a limited lifetime before it expires and would need to be manually replaced by using the small workaround tool. Name as of yet undecided.

A minor enhancement to the list-emails tool would have it use an updated INI file to record the encoded token. The encoded token could be used until an error is returned indicating that it is expired causing the list-emails app to fetch a new token, encode it and record in the INI file for further use.

Longer term the list-emails tool would likely need to switch to another authorization flow more suitable to interactive CLI use.

@atc0005 atc0005 modified the milestones: Future, Next Feature Release Nov 11, 2022
@atc0005
Copy link
Owner Author

atc0005 commented Nov 11, 2022

From https://learn.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth#authenticate-connection-requests:

IMAP Protocol Exchange

To authenticate an IMAP server connection, the client must respond with an AUTHENTICATE command in the following format:

AUTHENTICATE XOAUTH2 <base64 string in XOAUTH2 format>

Sample client-server message exchange that results in an authentication success:

[connection begins]
C: C01 CAPABILITY
S: * CAPABILITY … AUTH=XOAUTH2
S: C01 OK Completed
C: A01 AUTHENTICATE XOAUTH2 dXNlcj1zb21ldXNlckBleGFtcGxlLmNvbQFhdXRoPUJlYXJlciB5YTI5LnZGOWRmdDRxbVRjMk52YjNSbGNrQmhkSFJoZG1semRHRXVZMjl0Q2cBAQ==
S: A01 OK AUTHENTICATE completed.

Sample client-server message exchange that results in an authentication failure:

[connection begins]
S: * CAPABILITY … AUTH=XOAUTH2
S: C01 OK Completed
C: A01 AUTHENTICATE XOAUTH2 dXNlcj1zb21ldXNlckBleGFtcGxlLmNvbQFhdXRoPUJlYXJlciB5YTI5LnZGOWRmdDRxbVRjMk52YjNSbGNrQmhkSFJoZG1semRHRXVZMjl0Q2cBAQ==
S: A01 NO AUTHENTICATE failed.

The XOAUTH2 mechanism is particularly important.

@atc0005
Copy link
Owner Author

atc0005 commented Nov 15, 2022

Based on these sources:

XOAUTH2 is considered a legacy authentication mechanism and is superseded by OAUTHBEARER.

Per about 30 minutes ago, outlook.office365.com indicates these capabilities:

  • AUTH=PLAIN
  • AUTH=XOAUTH2
  • CHILDREN
  • ID
  • IDLE
  • IMAP4
  • IMAP4rev1
  • LITERAL+
  • MOVE
  • NAMESPACE
  • SASL-IR
  • UIDPLUS
  • UNSELECT

Unfortunately, OAUTHBEARER is not included among them, so isn't an option for us. Since the auth mechanism is officially dropped, we'll have to bundle the functionality locally.

The first one is surprising. Haven't tested yet to see whether it actually works as advertised.

@atc0005
Copy link
Owner Author

atc0005 commented Nov 16, 2022

Based on the yesterday's work, it looks like supporting the Resource Owner Password Credentials grant will be doable sooner than later. For better or worse, this grant type is deprecated and will be removed in the OAuth 2.1 standard. Even so, it appears to be a viable approach for initially supporting OAuth-based IMAP logins to O365.

Longer term, support for "daemon applications" appears to be what is needed:

After some trial & error I've prototyped the process for retrieving a token using the client credentials auth flow and it appears to be working reliably.

One "gotcha" encountered was the scopes. With these scopes set the process worked:

API Permissions name Type Description Admin consent required
Microsoft Graph IMAP.AccessAsUser.All Delegated Read and write access to mailboxes via IMAP. No
Microsoft Graph User.Read Delegated Sign in and read user profile No
Office 365 Exchange Online IMAP.AccessAsApp Application IMAP.AccessAsApp Yes

The last one has to be granted by a tenant administrator.

Per https://blog.rebex.net/office365-ews-oauth-unattended:

Optionally, you can remove the delegated User.Read permission which is not needed for app-only application - click the context menu on the right side of the permission and select Remove permission.

Other sources have said the same thing: Delegated scopes are not needed for the client credentials flow.

The service principal also had to be granted access to a list of applicable mailboxes that we wished to access using the registered application.

From the application, I had to request a token with the scope of https://outlook.office365.com/.default. Requesting a scope of .default (as I've seen mentioned elsewhere) did not work.

In short, there are multiple "must have" items for this to work properly. Failing to meet any of the requirements results in failure to access the intended mailboxes.

@atc0005
Copy link
Owner Author

atc0005 commented Nov 16, 2022

I'm planning to build a separate plugin specific to O365 IMAP access, at least until the dust settles and the design is clear.

For the list-emails app I'm not yet sure of the direction. One approach is to cache the token either in a local SQLite database, a local file or in the INI file already used to provide configuration settings. The other (and simpler) approach is to fetch a new token each time the app is used. For the initial implementation, this last approach is probably best for now.

Unfortunately, OAUTHBEARER is not included among them, so isn't an option for us. Since the auth mechanism is officially dropped, we'll have to bundle the functionality locally.

This wasn't bad at all; this is probably why the library authors assumed it would be a small lift for library consumers to bundle the functionality directly. Perhaps in the future XOAUTH2 support could be returned to the upstream library if enough developers express an interest.

@atc0005
Copy link
Owner Author

atc0005 commented Nov 16, 2022

On a related note, one of our O365 administrators provided a copy of the https://github.com/DanijelkMSFT/ThisandThat/blob/main/Get-IMAPAccessToken.ps1 script (commit bc7649953761cd2d1b462172df7df6be9bb9eb9b) for testing. Worked great on Windows, but the commands "hung" on Ubuntu test systems.

Based on prior experience and research, it appeared that IPv6 was to blame. In the end, the fix is to explicitly enable CRLF line termination for the IMAP commands issued instead of letting PowerShell guess which should be used. Pull Request submitted to fix the script for others using the script on non-Windows systems.

@atc0005
Copy link
Owner Author

atc0005 commented Nov 16, 2022

Learned that the Ruby IMAP NET::IMAP::authenticate method handles base64 encoding the "data" before it is used with the AUTHENTICATE command.

From /usr/lib/ruby/2.7.0/net/imap.rb (Ubuntu 20.04):

    # Sends an AUTHENTICATE command to authenticate the client.
    # The +auth_type+ parameter is a string that represents
    # the authentication mechanism to be used. Currently Net::IMAP
    # supports the authentication mechanisms:
    #
    #   LOGIN:: login using cleartext user and password.
    #   CRAM-MD5:: login with cleartext user and encrypted password
    #              (see [RFC-2195] for a full description).  This
    #              mechanism requires that the server have the user's
    #              password stored in clear-text password.
    #
    # For both of these mechanisms, there should be two +args+: username
    # and (cleartext) password.  A server may not support one or the other
    # of these mechanisms; check #capability() for a capability of
    # the form "AUTH=LOGIN" or "AUTH=CRAM-MD5".
    #
    # Authentication is done using the appropriate authenticator object:
    # see @@authenticators for more information on plugging in your own
    # authenticator.
    #
    # For example:
    #
    #    imap.authenticate('LOGIN', user, password)
    #
    # A Net::IMAP::NoResponseError is raised if authentication fails.
    def authenticate(auth_type, *args)
      auth_type = auth_type.upcase
      unless @@authenticators.has_key?(auth_type)
        raise ArgumentError,
          format('unknown auth type - "%s"', auth_type)
      end
      authenticator = @@authenticators[auth_type].new(*args)
      send_command("AUTHENTICATE", auth_type) do |resp|
        if resp.instance_of?(ContinuationRequest)
          data = authenticator.process(resp.data.text.unpack("m")[0])
          s = [data].pack("m0")
          send_string_data(s)
          put_string(CRLF)
        end
      end
    end

This means that the custom authentication mechanism does not need to handle base64 encoding on its own. It just needs to handle formatting the provided username/password (or in the case of XOAUTH2 the username/unencoded token) and passing it back (where it is then base64 encoded).

refs:

@atc0005
Copy link
Owner Author

atc0005 commented Nov 17, 2022

For reference, here is a curl command used to fetch a token:

curl https://login.microsoftonline.com/TENAT_ID_HERE/oauth2/v2.0/token -X POST -H "Content-type: application/x-www-form-urlencoded" -d "client_id=CLIENT_ID_HERE&scope=https%3A%2F%2Foutlook.office365.com%2F.default&grant_type=client_credentials&username=me@example.com&client_secret=CLIENT_SECRET_HERE"

and the "pretty printed" JSON response:

{
    "token_type": "Bearer",
    "expires_in": 3599,
    "ext_expires_in": 3599,
    "access_token": "TOKEN_HERE"
}

Note that a refresh token isn't specified. Perhaps this isn't available for this specific grant type?

@atc0005
Copy link
Owner Author

atc0005 commented Nov 17, 2022

and the "pretty printed" JSON response:

{
    "token_type": "Bearer",
    "expires_in": 3599,
    "ext_expires_in": 3599,
    "access_token": "TOKEN_HERE"
}

Note that a refresh token isn't specified. Perhaps this isn't available for this specific grant type?

Per RFC6749, Section 4.4.3:

If the access token request is valid and authorized, the authorization server issues an access token as described in Section 5.1. A refresh token SHOULD NOT be included.

refs:

@atc0005
Copy link
Owner Author

atc0005 commented Nov 24, 2022

Of note:

Published Jun 30 2022 11:04 AM 27.6K Views
Today, we’re excited to announce the availability of OAuth 2.0 authentication via client credentials grant flow for the POP and IMAP protocols for accessing Exchange Online mailboxes.

The necessary OAuth2 flow was added in June 30 of this year. From the time I filed this GH issue the needed support was added only 4 months prior.

refs:

@atc0005 atc0005 unpinned this issue Nov 24, 2022
atc0005 added a commit that referenced this issue Nov 24, 2022
- rename existing monitoring plugin to make explicitly
  clear that it supports Basic Auth only
- add new monitoring plugin to support the Client Credentials
  OAuth2 flow
- add xoauth2 prototype tool to convert given username and
  token to XOAuth2 formatted string (optionally encoded to
  support SASL XOAUTH)
- update `list-emails` tool to support either of Basic Auth
  or Client Credentials OAuth2 flow depending on which
  config file settings are used
- refresh README to provide coverage for new settings/plugin
  and liberal collection of ref links for context
- refresh `list-emails` config file example coverage
  - update existing file to make clear that it is intended
    for Basic Auth
  - add new file to cover settings for Client Credentials
    OAuth2 flow

refs GH-313
@atc0005
Copy link
Owner Author

atc0005 commented Nov 24, 2022

Considering GH-335 to be the culmination of the research/prototyping work noted on this and related GH issues.

@atc0005 atc0005 closed this as completed Nov 24, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

2 participants