File sasl-xoauth2-0.26.obscpio of Package sasl-xoauth2

07070100000000000081A40000000000000000000000016783F6CC00000066000000000000000000000000000000000000002000000000sasl-xoauth2-0.26/.clang-formatBasedOnStyle: Google
FixNamespaceComments: true
DerivePointerAlignment: false
PointerAlignment: Right
07070100000001000081A40000000000000000000000016783F6CC00000018000000000000000000000000000000000000001D00000000sasl-xoauth2-0.26/.gitignore/build
/packaging-build
07070100000002000081A40000000000000000000000016783F6CC0000056F000000000000000000000000000000000000002100000000sasl-xoauth2-0.26/CMakeLists.txt# Copyright 2020 Google LLC
#
# 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.

cmake_minimum_required(VERSION 3.0.0)
cmake_policy(VERSION 3.0)

execute_process(
  COMMAND head -n 1 ChangeLog
  COMMAND sed -e "s/.*(//" -e "s/).*//"
  WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
  OUTPUT_VARIABLE CHANGELOG_VERSION
  OUTPUT_STRIP_TRAILING_WHITESPACE)

project(sasl-xoauth2 VERSION ${CHANGELOG_VERSION} LANGUAGES CXX)

include(GNUInstallDirs)

option(EnableTests "Enable tests." ON)

if(EnableTests)
  enable_testing()
endif()
add_subdirectory(docs)
add_subdirectory(scripts)
add_subdirectory(src)

set(CPACK_SOURCE_PACKAGE_FILE_NAME "${CMAKE_PROJECT_NAME}-${PROJECT_VERSION}")
set(CPACK_SOURCE_GENERATOR "TGZ")
set(CPACK_SOURCE_IGNORE_FILES "/build/;/packaging/;/packaging-build/;/packaging-output/;/packaging-scripts/;/.git/;/.gitignore;.clang-format;~$;${CPACK_SOURCE_IGNORE_FILES}")

include(CPack)
07070100000003000081A40000000000000000000000016783F6CC00000449000000000000000000000000000000000000002200000000sasl-xoauth2-0.26/CONTRIBUTING.md# How to Contribute

We'd love to accept your patches and contributions to this project. There are
just a few small guidelines you need to follow.

## Contributor License Agreement

Contributions to this project must be accompanied by a Contributor License
Agreement. You (or your employer) retain the copyright to your contribution;
this simply gives us permission to use and redistribute your contributions as
part of the project. Head over to <https://cla.developers.google.com/> to see
your current agreements on file or to sign a new one.

You generally only need to submit a CLA once, so if you've already submitted one
(even if it was for a different project), you probably don't need to do it
again.

## Code reviews

All submissions, including submissions by project members, require review. We
use GitHub pull requests for this purpose. Consult
[GitHub Help](https://help.github.com/articles/about-pull-requests/) for more
information on using pull requests.

## Community Guidelines

This project follows [Google's Open Source Community
Guidelines](https://opensource.google/conduct/).
07070100000004000081A40000000000000000000000016783F6CC00000225000000000000000000000000000000000000001A00000000sasl-xoauth2-0.26/COPYINGCopyright 2020 Google LLC

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.
07070100000005000081A40000000000000000000000016783F6CC0000167F000000000000000000000000000000000000001C00000000sasl-xoauth2-0.26/ChangeLogsasl-xoauth2 (0.26) unstable; urgency=low

  * Add refresh_window parameter to control when tokens are refreshed.

 -- Tarick Bedeir <tarick@bedeir.com>  Sun, 12 Jan 2025 09:06:00 -0800

sasl-xoauth2 (0.25) unstable; urgency=low

  * Prevent truncation when writing logs.
  * Allow token-level username overrides.
  * Script that updates CA certificates creates directories if missing.
  * Avoid opening/closing syslog (which may have been causing lost logs), and
    add syslog prefix.

 -- Tarick Bedeir <tarick@bedeir.com>  Wed, 30 Oct 2024 08:39:00 -0400

sasl-xoauth2 (0.24) unstable; urgency=low

  * Fix bug in logging code that resulted in missing messages (and the
    occasional segfault).

 -- Tarick Bedeir <tarick@bedeir.com>  Mon, 17 Jul 2023 17:57:00 -0700

sasl-xoauth2 (0.23) unstable; urgency=low

  * Add new config option, "always_log_to_syslog", to unconditionally
    log progress to syslog.
  * Update sasl-xoauth2-tool to enable simplified device flow for
    Outlook token acquisition.

 -- Tarick Bedeir <tarick@bedeir.com>  Thu, 13 Jul 2023 16:17:00 -0700

sasl-xoauth2 (0.22) unstable; urgency=low

  * Update SASL plugin flags to indicate sasl-xoauth2 doesn't send
    plaintext credentials.

 -- Tarick Bedeir <tarick@bedeir.com>  Fri, 23 Jun 2023 08:07:00 -0700

sasl-xoauth2 (0.21) unstable; urgency=low

  * Update sasl-xoauth2-tool so that Outlook client secrets are optional.

 -- Tarick Bedeir <tarick@bedeir.com>  Thu, 15 Jun 2023 15:23:00 -0700

sasl-xoauth2 (0.20) unstable; urgency=low

  * Prompt for client secret when using sasl-xoauth2-tool, if it isn't
    specified as a command-line argument.
  * Update sasl-xoauth2-tool to pass a client secret when requesting an
    initial access token for Outlook.
  * Man pages for sasl-xoauth2-tool, sasl-xoauth2.conf.

 -- Tarick Bedeir <tarick@bedeir.com>  Sun, 28 May 2023 09:09:00 -0700

sasl-xoauth2 (0.19) unstable; urgency=low

  * sasl-xoauth2-tool is the new one-stop-shop for requesting initial tokens,
    testing configuration files, and testing token refreshes. It replaces the
    standalone sasl-xoauth2-token-tool and sasl-xoauth2-test-config tools.

 -- Tarick Bedeir <tarick@bedeir.com>  Sat, 12 Nov 2022 12:05:00 -0500

sasl-xoauth2 (0.18) unstable; urgency=low

  * Fix bug in sasl-xoauth2-test-config that resulted in a crash if the token
    file could not be opened.

 -- Tarick Bedeir <tarick@bedeir.com>  Tue, 04 Oct 2022 22:08:00 -0700

sasl-xoauth2 (0.17) unstable; urgency=low

  * Reorganize packaging.
  * Fix sasl-xoauth2-token-tool for compatibility with Python 3.6.

 -- Tarick Bedeir <tarick@bedeir.com>  Tue, 04 Oct 2022 12:11:00 -0700

sasl-xoauth2 (0.16) unstable; urgency=low

  * Add ca_bundle_file, ca_certs_dir configuration options to control where
    SSL/TLS libraries look for CA certificates.

 -- Tarick Bedeir <tarick@bedeir.com>  Sat, 17 Sep 2022 09:26:00 -0700

sasl-xoauth2 (0.15) unstable; urgency=low

  * Fix packaging of sasl-xoauth2-test-config, and explain its use in the
    README.

 -- Tarick Bedeir <tarick@bedeir.com>  Sat, 10 Sep 2022 09:49:00 -0700

sasl-xoauth2 (0.14) unstable; urgency=low

  * Add sasl-xoauth2-token-tool, to allow creation of initial tokens.
  * Add proxy support.
  * Add sasl-xoauth2-test-config, to allow interactive testing of configs and
    token refreshes.
  * Rename CA certificate update script and ensure it lands in the correct
    directory during installation.

 -- Tarick Bedeir <tarick@bedeir.com>  Mon, 15 Aug 2022 08:27:00 -0700

sasl-xoauth2 (0.13) unstable; urgency=low

  * Rebuild for Ubuntu 22.04 (Jammy).

 -- Tarick Bedeir <tarick@bedeir.com>  Sun, 14 Aug 2022 13:23:00 -0700

sasl-xoauth2 (0.12) unstable; urgency=low

  * Prevent Office 365 tokens from expiring after 90 days.
  * Add script to fetch initial tokens for Gmail.
  * Install hook to update CA certificates (Ubuntu only).

 -- Tarick Bedeir <tarick@bedeir.com>  Sun, 10 Apr 2022 22:34:00 -0700

sasl-xoauth2 (0.11) unstable; urgency=low

  * Allow client ID/secret and endpoint overrides in token files.
  * Fix HTTP request seek callback signature.
  * Set SASL SSF so that XOAUTH2 is preferred over other mechanisms.

 -- Tarick Bedeir <tarick@bedeir.com>  Sat, 05 Feb 2022 09:55:00 -0800

sasl-xoauth2 (0.10) unstable; urgency=low

  * Added Python script to fetch initial tokens for Outlook/Office 365.
  * Updated README to describe use with Outlook/Office 365.

 -- Tarick Bedeir <tarick@bedeir.com>  Sun, 29 Nov 2020 22:38:00 -0800

sasl-xoauth2 (0.9) unstable; urgency=low

  * Compile fixes for Focal.

 -- Tarick Bedeir <tarick@bedeir.com>  Mon, 10 Aug 2020 13:26:00 -0700

sasl-xoauth2 (0.8) unstable; urgency=low

  * Rework packaging.
  * Improve CURL request error handling.
  * Use config file path from package installation path.

 -- Tarick Bedeir <tarick@bedeir.com>  Sun, 22 Mar 2020 18:58:00 -0700

sasl-xoauth2 (0.7) unstable; urgency=low

  * Flesh out README.

 -- Tarick Bedeir <tarick@bedeir.com>  Sun, 11 Aug 2019 21:54:00 -0700

sasl-xoauth2 (0.6) unstable; urgency=low

  * Actually (?) fix broken 32-bit build.

 -- Tarick Bedeir <tarick@bedeir.com>  Sat, 10 Aug 2019 19:22:00 -0700

sasl-xoauth2 (0.5) unstable; urgency=low

  * Fix broken 32-bit build.

 -- Tarick Bedeir <tarick@bedeir.com>  Sat, 10 Aug 2019 18:58:00 -0700

sasl-xoauth2 (0.4) unstable; urgency=low

  * Add config file in /etc/sasl-xoauth2.conf.
  * Store client secret in config file.
  * Write failure logs to syslog instead of /tmp.
  * Install library directly into SASL plugin directory.

 -- Tarick Bedeir <tarick@bedeir.com>  Sat, 10 Aug 2019 10:43:00 -0700

sasl-xoauth2 (0.2) unstable; urgency=low

  * Fix symlinks.

 -- Tarick Bedeir <tarick@bedeir.com>  Fri, 09 Aug 2019 17:13:00 -0700
07070100000006000081A40000000000000000000000016783F6CC0000000E000000000000000000000000000000000000001E00000000sasl-xoauth2-0.26/FUNDING.ymlko_fi: tarick
07070100000007000081A40000000000000000000000016783F6CC0000607C000000000000000000000000000000000000001C00000000sasl-xoauth2-0.26/README.md# sasl-xoauth2

## Disclaimer

This is not an officially supported Google product.

## Background

sasl-xoauth2 is a SASL plugin that enables client-side use of OAuth 2.0. Among
other things it enables the use of Gmail or Outlook/Office 365 SMTP relays from
Postfix.

## Building from Source

Fetch the sources, then:

```
$ mkdir build && cd build && cmake ..
# To install with a system-packaged postfix, under /usr, use:
# cmake .. -DCMAKE_INSTALL_PREFIX=/usr -DCMAKE_INSTALL_SYSCONFDIR=/etc
$ make
$ sudo make install
# Need msal for sasl-xoauth2-tool:
$ sudo pip3 install msal
```

## Pre-Built Packages for Ubuntu

Add the [sasl-xoauth2 PPA](https://launchpad.net/~sasl-xoauth2/+archive/ubuntu/stable):

```
$ sudo add-apt-repository ppa:sasl-xoauth2/stable
$ sudo apt-get update
```

Install the plugin:

```
$ sudo apt-get install sasl-xoauth2
```

## Pre-Built Packages for RHEL/EPEL/Fedora

The package is now available in latest Fedora and EPEL8/9. You can see how to enable epel here:

https://docs.fedoraproject.org/en-US/epel/

After that just install the plugin as any other package:

```
$ sudo dnf install sasl-xoauth2
```

(Thank you [@augustus-p](https://github.com/augustus-p) for confirming that
this works!)

For older Fedora versions, you can use the [sasl-xoauth2 Copr
repository](https://copr.fedorainfracloud.org/coprs/jjelen/sasl-xoauth2/):

```
$ sudo dnf copr enable jjelen/sasl-xoauth2
```

### A Note on SELinux

If SELinux is enabled, you may find that authentication is failing. This is
likely because the sasl-xoauth2 plugin, running within the Postfix `smtp`
process, is unable to read, write, or create a new token file. If in doubt,
check your SELinux audit logs.

## Configuration

### Configure Mail Agent

This plugin has only been tested with Postfix. First, configure Postfix to use
SASL, and specifically the XOAUTH2 method. In `/etc/postfix/main.cf`:

```
smtp_sasl_auth_enable = yes
smtp_sasl_password_maps = hash:/etc/postfix/sasl_passwd
smtp_sasl_security_options =
smtp_sasl_mechanism_filter = xoauth2
```

Alternatively, you could specify multiple mechanisms, i.e.

```
smtp_sasl_mechanism_filter = xoauth2,login
```

The above used to cause problems, because both "xoauth2" and the "login"
plug-in (as is used for many older, not-yet-OAuth2 providers) had the
same "SSF" setting of "0". This made SASL's automatic detection of which
plug-in to use non-deterministic. Now, with the higher SSF of "60" for
"xoauth2", providers offering OAUTH2 will be handled via the xoauth2 plug-in.

You can check the effective value by calling `pluginviewer -c` (on Debian/Ubuntu it’s installed as `/usr/sbin/saslpluginviewer` in the `sasl2-bin` package); look for
the "SSF" value:

```
Plugin "sasl-xoauth2" [loaded],         API version: 4
        SASL mechanism: XOAUTH2, best SSF: 60
        security flags: NO_ANONYMOUS|PASS_CREDENTIALS
        features: WANT_CLIENT_FIRST|PROXY_AUTHENTICATION
Plugin "login" [loaded],        API version: 4
        SASL mechanism: LOGIN, best SSF: 0
        security flags: NO_ANONYMOUS|PASS_CREDENTIALS
        features: SERVER_FIRST
```

See https://www.cyrusimap.org/sasl/sasl/authentication_mechanisms.html#authentication-mechanisms for details on the fields.

While you're at it, enable TLS:

```
smtp_tls_security_level = encrypt
smtp_tls_CAfile = /etc/ssl/certs/ca-certificates.crt
smtp_tls_session_cache_database = btree:${data_directory}/smtp_scache
```

And then set the outbound relay to Gmail's SMTP server:

```
relayhost = [smtp.gmail.com]:587
```

(For Outlook, use `[smtp.office365.com]:587` instead.)

Next, add client account details to the SASL password database in
`/etc/postfix/sasl_passwd`:

```
[smtp.gmail.com]:587 username@domain.com:/etc/tokens/username@domain.com
```

(For Outlook, replace `[smtp.gmail.com]:587` with `[smtp.office365.com]:587`.)

The path specified above tells sasl-xoauth2 where to find tokens for the account
"username@domain.com" (but see [A Note on chroot](#a-note-on-chroot) below).

Finally, regenerate the SASL password database:

```
$ sudo postmap /etc/postfix/sasl_passwd
```

#### A Note on chroot

Check if chroot is enabled:

```
$ grep -E '^(smtp|.*chroot)' /etc/postfix/master.cf
# service type  private unpriv  chroot  wakeup  maxproc command + args
smtp      inet  n       -       y       -       -       smtpd
smtp      unix  -       -       y       -       -       smtp
```

In this example `master.cf`, chroot is in fact enabled for the Postfix smtp
process. As a result, the token path specified above will be interpreted at
runtime relative to the Postfix root (`/var/spool/postfix`, usually).

This means that **even though the path in `/etc/postfix/sasl_passwd` is
(and should be) `/etc/tokens/username@domain.com`**, at runtime Postfix will
attempt to read from `/var/spool/postfix/etc/tokens/username@domain.com`.

#### SSL/TLS Certificates

If you see an error message similar to the following, you may need to copy over
root CA certificates for the TLS handshake to work within sasl-xoauth2:

```
TokenStore::Refresh: http error: error setting certificate verify locations: ...
```

To copy certificates manually, assuming the Postfix root is
`/var/spool/postfix`:

```
$ sudo mkdir -p /var/spool/postfix/etc/ssl/certs
$ sudo cp /etc/ssl/certs/ca-certificates.crt /var/spool/postfix/etc/ssl/certs/ca-certificates.crt
```

The Debian and Ubuntu packages install a script that is automatically run by
`update-ca-certificates` to ensure the certificates are copied whenever the
system certificates are updated:
`/etc/ca-certificates/update.d/postfix-sasl-xoauth2-update-ca-certs`. It is also
run when the package is installed.

sasl-xoauth2 also provides two configuration variables, `ca_bundle_file` and
`ca_certs_dir`, that may be used to manually configure where the SSL/TLS
libraries will look for a CA certificate bundle (for `ca_bundle_file`) or a set
of CA certificates (for `ca_certs_dir`). Specify one or the other, but not both.

#### A Note on postmulti

[@jamenlang](https://github.com/jamenlang) has provided a [very helpful
tutorial](https://github.com/jamenlang/sasl-xoauth2-1/wiki/Setting-up-postmulti-with-multiple-xoauth2-relays)
on setting up `postmulti` with sasl-xoauth2.

### Gmail Configuration

From a new account, Google requires several steps to enable access.
Once you are logged into your Gmail account in the browser, all these steps happen at the [Google Cloud Platform console](https://console.cloud.google.com/).

#### Basic Account Setup

- Select an existing project, or add a Project if you don't have one yet (it can be any name)

- Set up "OAuth Consent Screen" for the project

  - If this is an "External" app, make sure the "Publishing status" of the app is set to "In production" (as opposed to "Testing"), otherwise the token [will be revoked after 7 days](https://github.com/tarickb/sasl-xoauth2/issues/29).
    - You can ignore any requests to "verify" your app. The warnings shown in the console are misleading. You don't actually need to go through verification.

#### Client Credentials

From the [Google Cloud Platform console](https://console.cloud.google.com/),

- Credentials: Create Credentials: OAuth client ID

  - Application type: Desktop app

  - Choose a memorable name

Store the client ID and secret in `/etc/sasl-xoauth2.conf`:

```json
{
  "client_id": "client ID goes here",
  "client_secret": "client secret goes here"
}
```

We'll also need these credentials in the next step.

#### Initial Access Token

The sasl-xoauth2 package includes a script that can assist in the generation of
Gmail OAuth tokens. Run the script as follows:

```shell
$ sasl-xoauth2-tool get-token gmail \
    PATH_TO_TOKENS_FILE \
    --client-id=CLIENT_ID_FROM_SASL_XOAUTH2_CONF \
    --client-secret=CLIENT_SECRET_FROM_SASL_XOAUTH2_CONF \
    --scope="https://mail.google.com/"

Please open this URL in a browser ON THIS HOST:

https://accounts.google.com/o/oauth2/auth?client_id=&scope=&response_type=code&redirect_uri=http%3A%2F%2F127.0.0.1%3A12345%2Foauth2_result
```

(This script must run on the same host that is opening the URL -- it's not
possible to copy the URL and paste it into a browser on another computer. This
is because [recent
changes](https://developers.googleblog.com/2022/02/making-oauth-flows-safer.html)
to the OAuth2 authorization flow require that the browser pass the resulting
authorization code directly to the requesting application. If the Postfix
installation is running on a headless host, simply run the script on a host with
a usable browser then copy the resulting token file over to the headless host.)

Opening the URL and authorizing the application should result in a new token in
`PATH_TO_TOKENS_FILE`, which should be the file specified in
`/etc/postfix/sasl_passwd`.  In our example that file will be either
`/etc/tokens/username@domain.com` or
`/var/spool/postfix/etc/tokens/username@domain.com` (see [A Note on
chroot](#a-note-on-chroot)):

```json
{
  "access_token" : "access token goes here",
  "expiry" : "0",
  "refresh_token" : "refresh token goes here"
}
```

In my configuration, chroot is enabled and so even though
`/etc/postfix/sasl_passwd` specifies `/etc/tokens/username@domain.com`,
my token file is `/var/spool/postfix/etc/tokens/username@domain.com`.

It may be necessary to adjust permissions on the token file so that Postfix (or,
more accurately, sasl-xoauth2 running as the Postfix user) can update it:

```
$ sudo chown -R postfix:postfix /etc/tokens
```

or:

```
$ sudo chown -R postfix:postfix /var/spool/postfix/etc/tokens
```

Skip to [restart Postfix](#restart-postfix) below.

### Outlook/Office 365 Configuration (Device Flow)

As of sasl-xoauth2-0.23, this is the preferred method to authenticate with
Outlook/Office, but the [fallback legacy client
approach](#outlookoffice-365-configuration-legacy-client-deprecated) does still
work (... for now).

#### Client Credentials

Follow [Microsoft's instructions to register an
application](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#register-an-application),
with some notes:

* Use any name you like (it doesn't have to be "sasl-xoauth2").
* Do **not** add any redirect URIs or set up any platform configurations.
* You **must** toggle "Allow public client flows" to "yes".
* Be sure to select the appropriate type of account (consumer Outlook vs.
  "organizational directory") -- see
  [#89](https://github.com/tarickb/sasl-xoauth2/issues/89) for why.

Then, add API permissions for `SMTP.Send`:

1. From the app registration "API permissions" page, click "add a permission".
1. Click "Microsoft Graph".
1. Enter "SMTP.Send" in the search box.
1. Expand the `SMTP` permission, then check the `SMTP.Send` checkbox.

Store the "application (client) ID" (which you'll find in the "Overview" page
for the application you registered with Azure) in `/etc/sasl-xoauth2.conf`.
Leave `client_secret` blank. Additionally, explicitly set the token endpoint
(`sasl-xoauth2` points to Gmail's token endpoint by default):

```json
{
  "client_id": "client ID goes here",
  "client_secret": "",
  "token_endpoint": "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
}
```

> [!WARNING] When using the Public Preview of Microsoft's new HVE endpoint
> (smtp-hve.office365.com), a custom `refresh_window` should be set in the
> configuration file to at least 600 seconds (10 minutes) to prevent the "501
> 5.5.127 Invalid XOAUTH2 auth data - Token will expire soon error" error.

We'll also need these credentials in the next step.

#### A Note on Token Endpoints

The endpoint above
(`https://login.microsoftonline.com/consumers/oauth2/v2.0/token`) is suitable
for use with consumer Outlook accounts. For other types of accounts it may be
necessary to replace `consumers` with `common`, `organizations`, or a specific
tenant ID. See [Microsoft's OAuth protocol
documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints)
for more on this.

#### Initial Access Token

The sasl-xoauth2 package includes a script that can assist in the generation of
Microsoft OAuth tokens. Run the script as follows:

```shell
$ sasl-xoauth2-tool get-token outlook \
    PATH_TO_TOKENS_FILE \
    --client-id=CLIENT_ID_FROM_SASL_XOAUTH2_CONF \
    --use-device-flow
To sign in, use a web browser to open the page https://www.microsoft.com/link and enter the code REDACTED to authenticate.
```

If using a tenant other than `consumers`, pass `--tenant=common`,
`--tenant=organizations`, or `--tenant=TENANT_ID`. The client ID will
be the same one written to `/etc/sasl-xoauth2.conf`. And `PATH_TO_TOKENS_FILE`
will be the file specified in `/etc/postfix/sasl_passwd`. In our example that
file will be either `/etc/tokens/username@domain.com` or
`/var/spool/postfix/etc/tokens/username@domain.com` (see [A Note on
chroot](#a-note-on-chroot)).

Visit the link in a browser, enter the code, then accept the various prompts.
After authorizing the application, the tool will write a token to the path
specified.

```
To sign in, use a web browser to open the page https://www.microsoft.com/link and enter the code REDACTED to authenticate.
Acquired token.
```

It may be necessary to adjust permissions on the resulting token file so that
Postfix (or, more accurately, sasl-xoauth2 running as the Postfix user) can
update it:

```
$ sudo chown -R postfix:postfix /etc/tokens
```

or:

```
$ sudo chown -R postfix:postfix /var/spool/postfix/etc/tokens
```

### Outlook/Office 365 Configuration (Legacy Client) (Deprecated)

#### Client Credentials

Follow [Microsoft's instructions to register an
application](https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app#register-an-application).
Use any name you like (it doesn't have to be "sasl-xoauth2").

Be sure to select the appropriate type of account (consumer Outlook vs.
"organizational directory") -- see
[#89](https://github.com/tarickb/sasl-xoauth2/issues/89) for why.

Under "Platform configurations", add a native-client redirect URI for
mobile/desktop applications:
`https://login.microsoftonline.com/common/oauth2/nativeclient`.  Then, add API
permissions for `SMTP.Send`: from the app registration "API permissions" page,
click "add a permission", then "Microsoft Graph", and from there enter
"SMTP.Send" in the search box. Expand the `SMTP` permission, then check the
`SMTP.Send` checkbox.

Store the "application (client) ID" (which you'll find in the "Overview" page
for the application you registered with Azure) in `/etc/sasl-xoauth2.conf`.
Leave `client_secret` blank (but see [A Note on Client
Secrets](#a-note-on-client-secrets) below for non-personal-Outlook-account
situations). Additionally, explicitly set the token endpoint (`sasl-xoauth2`
points to Gmail's token endpoint by default):

```json
{
  "client_id": "client ID goes here",
  "client_secret": "",
  "token_endpoint": "https://login.microsoftonline.com/consumers/oauth2/v2.0/token"
}
```

We'll also need these credentials in the next step.

#### A Note on Token Endpoints

The endpoint above
(`https://login.microsoftonline.com/consumers/oauth2/v2.0/token`) is suitable
for use with consumer Outlook accounts. For other types of accounts it may be
necessary to replace `consumers` with `common`, `organizations`, or a specific
tenant ID. See [Microsoft's OAuth protocol
documentation](https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-protocols#endpoints)
for more on this.

#### Initial Access Token

The sasl-xoauth2 package includes a script that can assist in the generation of
Microsoft OAuth tokens. Run the script as follows:

```shell
$ sasl-xoauth2-tool get-token outlook \
    PATH_TO_TOKENS_FILE \
    --client-id=CLIENT_ID_FROM_SASL_XOAUTH2_CONF

Please visit the following link in a web browser, then paste the resulting URL:

https://login.microsoftonline.com/consumers/oauth2/v2.0/authorize?client_id=REDACTED&response_type=code&redirect_uri=https%3A//login.microsoftonline.com/common/oauth2/nativeclient&response_mode=query&scope=openid%20offline_access%20https%3A//outlook.office.com/SMTP.Send

Resulting URL: 
```

If using a tenant other than `consumers`, pass `--tenant=common`,
`--tenant=organizations`, or `--tenant=TENANT_ID` (and see [A Note on Client
Secrets](#a-note-on-client-secrets), which may be relevant). The client ID will
be the same one written to `/etc/sasl-xoauth2.conf`. And `PATH_TO_TOKENS_FILE`
will be the file specified in `/etc/postfix/sasl_passwd`. In our example that
file will be either `/etc/tokens/username@domain.com` or
`/var/spool/postfix/etc/tokens/username@domain.com` (see [A Note on
chroot](#a-note-on-chroot)).

Visit the link in a browser and accept the various prompts. After authorizing
the application you will be redirected to a blank page. This is expected--copy
the URL of the blank page back into the terminal where you're running the
script, and the script will extract the URL components needed to obtain initial
tokens:

```
Resulting URL: https://login.microsoftonline.com/common/oauth2/nativeclient?code=REDACTED
Tokens written to PATH_TO_TOKENS_FILE.
```

It may be necessary to adjust permissions on the resulting token file so that
Postfix (or, more accurately, sasl-xoauth2 running as the Postfix user) can
update it:

```
$ sudo chown -R postfix:postfix /etc/tokens
```

or:

```
$ sudo chown -R postfix:postfix /var/spool/postfix/etc/tokens
```

#### A Note on Client Secrets

Some users have [reported](https://github.com/tarickb/sasl-xoauth2/issues/61)
needing to specify client secrets when requesting access and refresh tokens for
Outlook. This would seem to be the case when registering an application that has
access to "accounts in any organizational directory" (i.e., non-personal
Microsoft accounts). If this applies to you, please specify the client secret in
`/etc/sasl-xoauth2.conf` and on the command line when using `sasl-xoauth2-tool`.

#### Further Reading

The following references were useful while developing, testing, and debugging
Outlook support:

- [Authenticate an IMAP, POP or SMTP connection using OAuth](https://docs.microsoft.com/en-us/exchange/client-developer/legacy-protocols/how-to-authenticate-an-imap-pop-smtp-application-by-using-oauth)
- [Microsoft identity platform and OAuth 2.0 authorization code flow](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow)
- [Microsoft identity platform and OAuth 2.0 Resource Owner Password Credentials](https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth-ropc)

### Proxy Support

In case the system is behind a corporate web proxy you can configure a proxy
that is used by the curl library when refreshing the token.

```json
{
  "client_id": "client ID goes here",
  "client_secret": "client secret goes here",
  "token_endpoint": "token endpoint goes here",
  "proxy" : "http://proxy:8080"
}
```

For supported proxy schemes please refer to the [curl library documentation](https://curl.se/libcurl/c/CURLOPT_PROXY.html)

### Testing Your Configuration

sasl-xoauth2 provides a tool, `sasl-xoauth2-tool`, that allows the
semi-interative testing of configuration and token files (which is a lot more
useful than parsing log files when trying to figure out why Postfix isn't
delivering mail correctly).

First, test your configuration file:

```
$ sasl-xoauth2-tool test-config --config-file ./bad-config.conf
sasl-xoauth2: Missing required value: client_secret
Config check failed.
$ sasl-xoauth2-tool test-config --config-file ./good-config.conf
Config check passed.
$ sasl-xoauth2-tool test-config --config-file /etc/sasl-xoauth2.conf
Config check passed.
```

(Specifying the path is only required if your configuration file isn't located
in the system-default path.)

Next, test your token file:

```
$ sasl-xoauth2-tool test-token-refresh ./bad-token.json
Config check passed.
2022-09-10 09:18:59: TokenStore::Read: file=./bad-token.json
2022-09-10 09:18:59: TokenStore::Read: refresh=REDACTED
2022-09-10 09:18:59: TokenStore::Refresh: attempt 1
2022-09-10 09:18:59: TokenStore::Refresh: token_endpoint: https://accounts.google.com/o/oauth2/token
2022-09-10 09:18:59: TokenStore::Refresh: request: client_id=REDACTED&client_secret=REDACTED&grant_type=refresh_token&refresh_token=REDACTED
2022-09-10 09:19:00: TokenStore::Refresh: code=400, response={
  "error": "invalid_grant",
  "error_description": "Bad Request"
}
2022-09-10 09:19:00: TokenStore::Refresh: request failed
Token refresh failed.
$ sasl-xoauth2-tool test-token-refresh ./good-token.json
Config check passed.
Token refresh succeeded.
```

(Again, you'll have to specify your configuration file with
`--config-file <config file>` if it isn't located at the system-default path.)

### Restart Postfix

```
$ service postfix restart
```

## Using Multiple Mail Providers or Users Simultaneously

One instance of sasl-xoauth2 may provide tokens for different mail providers
and/or users.
Each provider will require its own client ID, client secret, and token
endpoint. Each user may require a username to be specified, if the username
automatically obtained from postfix is not correct.
In this case, each of these may be set in the token file rather than
in `/etc/sasl-xoauth2.conf`. Set them when setting the initial access token:

```json
{
  "access_token" : "access token goes here",
  "client_id": "client ID goes here",
  "client_secret": "client secret goes here, if required",
  "token_endpoint": "token endpoint goes here, for non-Gmail",
  "expiry" : "0",
  "refresh_token" : "refresh token goes here",
  "user" : "username goes here"
}
```

`sasl-xoauth2-tool` has an argument `--overwrite-existing-token` to preserve the content of these additional fields
when manually updating an expired or invalidated token.

## Debugging

### Increasing Verbosity

By default, sasl-xoauth2 will write to syslog if authentication fails. To
disable this, set `log_to_syslog_on_failure` to `no` in
`/etc/sasl-xoauth2.conf`:

```json
{
  "client_id": "client ID goes here",
  "client_secret": "client secret goes here",
  "log_to_syslog_on_failure": "no"
}
```

Conversely, to get more verbose logging when authentication fails, set
`log_full_trace_on_failure` to `yes`.

To get *even more* logging, set `always_log_to_syslog` to `yes` to have
sasl-xoauth2 immediately and unconditionally write logs to syslog .

### Postfix Logging

It can be useful (thanks [@kpedro88](https://github.com/kpedro88)!) to increase
Postfix's logging level, following the instructions
[here](https://www.postfix.org/DEBUG_README.html#verbose).

### SASL Mechanisms

If Postfix complains about not finding a SASL mechanism (along the lines of
`warning: SASL authentication failure: No worthy mechs found`), it's possible
that either `make install` or the pre-built package put libsasl-xoauth2.so in
the wrong directory.

## Building

sasl-xoauth2 uses [git-buildpackage](https://github.com/agx/git-buildpackage)
for Debian and Ubuntu builds. The following is mostly intended as a cheat-sheet.

### Creating a New Distribution Branch for an Existing Release

```
$ TARGET_DIST=dist-name # debian, ubuntu, etc.
$ TARGET_DIST_RELEASE=release-name # focal, jammy, etc.
$ RELEASE=0.NN
$ RELEASE_VERSION="$RELEASE-1ubuntu1~${TARGET_DIST_RELEASE}1~ppa1"
$ git clone --no-checkout -o upstream git@github.com:tarickb/sasl-xoauth2.git
$ cd sasl-xoauth2
$ git checkout -b "$TARGET_DIST/$TARGET_DIST_RELEASE" "release-$RELEASE"
$ git checkout "upstream/packaging/$TARGET_DIST" debian/
$ dch --create --package "sasl-xoauth2" --newversion "$RELEASE_VERSION" \
    --distribution "$TARGET_DIST_RELEASE"
$ git add debian/
$ git commit -m \
    "Initial packaging commit for $TARGET_DIST/$TARGET_DIST_RELEASE." debian/
```

### Updating an Existing Release Branch for a New Release

```
$ TARGET_DIST=dist-name # debian, ubuntu, etc.
$ TARGET_DIST_RELEASE=release-name # focal, jammy, etc.
$ RELEASE=0.NN
$ RELEASE_VERSION="$RELEASE-1ubuntu1~${TARGET_DIST_RELEASE}1~ppa1"
$ git fetch upstream
$ git checkout "$TARGET_DIST/$TARGET_DIST_RELEASE"
$ git merge "release-$RELEASE"
$ gbp dch --release --auto --debian-branch="$TARGET_DIST/$TARGET_DIST_RELEASE" \
    -N "$RELEASE_VERSION" --distribution="$TARGET_DIST_RELEASE"
$ git commit -m "Release $RELEASE_VERSION" debian/changelog
```

### Building and Uploading a Release

```
$ TARGET_DIST=dist-name # debian, ubuntu, etc.
$ TARGET_DIST_RELEASE=release-name # focal, jammy, etc.
$ gbp buildpackage --git-debian-branch="$TARGET_DIST/$TARGET_DIST_RELEASE" \
    -S --git-tag
$ dput ppa:sasl-xoauth2/stable build/*.changes
```
07070100000008000041ED0000000000000000000000026783F6CC00000000000000000000000000000000000000000000001700000000sasl-xoauth2-0.26/docs07070100000009000081A40000000000000000000000016783F6CC00000405000000000000000000000000000000000000002600000000sasl-xoauth2-0.26/docs/CMakeLists.txtset(MD_TO_MAN
  sasl-xoauth2.conf.5)

find_program(PANDOC_PATH NAMES pandoc DOC "Path to pandoc, for Markdown-to-man-page conversion")
if(NOT EXISTS ${PANDOC_PATH})
  message(FATAL_ERROR "Unable to find pandoc")
else()
  message(STATUS "Found pandoc: ${PANDOC_PATH}")
endif()

foreach(man_page IN LISTS MD_TO_MAN)
  set(src ${CMAKE_CURRENT_SOURCE_DIR}/${man_page}.md)
  set(dst ${CMAKE_CURRENT_BINARY_DIR}/${man_page})

  if(NOT EXISTS "${src}")
    message(FATAL_ERROR "Unable to find Markdown at ${src} for ${man_page}")
  endif()

  if(man_page MATCHES "\\.([0-9])$")
    set(man_num ${CMAKE_MATCH_1})
  else()
    message(FATAL_ERROR "No man number in ${man_page}")
  endif()

  add_custom_command(
    OUTPUT ${dst}
    COMMAND ${PANDOC_PATH} -s -t man "${src}" -o "${dst}"
    DEPENDS ${src}
    COMMENT "Generating ${man_page}"
    VERBATIM)

  add_custom_target("${man_page}_man" ALL DEPENDS ${dst})
  install(
    FILES ${dst}
    DESTINATION "${CMAKE_INSTALL_FULL_MANDIR}/man${man_num}"
    COMPONENT doc)
endforeach()
0707010000000A000081A40000000000000000000000016783F6CC0000097D000000000000000000000000000000000000002E00000000sasl-xoauth2-0.26/docs/sasl-xoauth2.conf.5.md% sasl-xoauth2.conf(5) | File Formats Manual

# NAME

/etc/sasl-xoauth2.conf - configuration file for sasl-xoauth2

# DESCRIPTION

This file contains static, administrator-defined information needed for XOAUTH2 SASL authentication.

It uses a JSON format to define variables needed to complete XOAUTH2 configuration. 

A minimal configuration file looks like:

```json
{
  "client_id": "CLIENT_ID_GOES_HERE",
  "client_secret": "CLIENT_SECRET_GOES_HERE"
}
```

See the full README for guidance on initial configuration:
https://github.com/tarickb/sasl-xoauth2

# OPTIONS

The top-level JSON object can contain the following keys:

`client_id`

: identifies this client for OAuth 2 token requests

`client_secret`

: authenticates this client for OAuth 2 token requests; world-readable by default (but see below to place this in token files instead)

`always_log_to_syslog`

: always write plugin log messages to syslog, even for successful runs; may contain tokens/secrets (defaults to "no")

`log_to_syslog_on_failure`

: log to syslog if XOAUTH2 flow fails (defaults to "yes")

`log_full_trace_on_failure`

: log a full trace to syslog if XOAUTH2 flow fails; may contain tokens/secrets (defaults to "no")

`token_endpoint`

: URL to use when requesting tokens; defaults to Google, must be overridden for use with Microsoft/Outlook

`proxy`

: if set, HTTP requests will be proxied through this server

`ca_bundle_file`

: if set, overrides CURL's default certificate-authority bundle file

`ca_certs_dir`

: if set, overrides CURL's default certificate-authority directory

`refresh_window`

: if set, overrides the default 10 second refresh window with the specified time in seconds (integer)

# TOKEN FILE

In addition to this file, `sasl-xoauth2` relies on a "token file" which it updates independently.
The token file is also JSON-formatted.
The contents of this token file MAY contain values for the keys described above (except for the logging-related keys).
If they do, the value in the token file overrides the value in the main configuration file.

This makes it possible to use the same installation of `sasl-xoauth2` to connect to two different providers simultaneously.
This also has the benefit of providing storage for client secrets that is not world-readable.

# BUGS

Please report improvements in this documentation upstream at https://github.com/tarickb/sasl-xoauth2/issues

# SEE ALSO

sasl-xoauth2-tool(1)
0707010000000B000041ED0000000000000000000000026783F6CC00000000000000000000000000000000000000000000001A00000000sasl-xoauth2-0.26/scripts0707010000000C000081A40000000000000000000000016783F6CC00000803000000000000000000000000000000000000002900000000sasl-xoauth2-0.26/scripts/CMakeLists.txt# Copyright 2020 Google LLC
#
# 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.

set(SCRIPTS
  sasl-xoauth2-tool)

find_program(ARGPARSE_MANPAGE_PATH NAMES argparse-manpage DOC "Path to argparse-manpage, to generate man pages from Python scripts")
if(NOT EXISTS ${ARGPARSE_MANPAGE_PATH})
  message(WARNING "Unable to find argparse-manpage, will not generate man pages for scripts")
else()
  message(STATUS "Found argparse-manpage: ${ARGPARSE_MANPAGE_PATH}")
endif()

foreach(script IN LISTS SCRIPTS)
  set(man_num 1)

  set(src ${CMAKE_CURRENT_SOURCE_DIR}/${script}.in)
  set(dst ${CMAKE_CURRENT_BINARY_DIR}/${script})
  set(man_base ${script}.${man_num})
  set(man ${CMAKE_CURRENT_BINARY_DIR}/${man_base})

  if(NOT EXISTS "${src}")
    message(FATAL_ERROR "Unable to find template at ${src} for ${script}")
  endif()

  configure_file(${src} ${dst})

  install(
    PROGRAMS ${dst}
    DESTINATION ${CMAKE_INSTALL_BINDIR}
    COMPONENT scripts)

  if(EXISTS ${ARGPARSE_MANPAGE_PATH})
    add_custom_command(
      OUTPUT ${man}
      COMMAND ${ARGPARSE_MANPAGE_PATH} --pyfile "${dst}" --function "argparse_get_parser" --author "Tarick Bedeir" --author-email "tarick@bedeir.com" --project-name "sasl-xoauth2" --url "https://github.com/tarickb/sasl-xoauth2" > "${man}"
      DEPENDS ${dst}
      COMMENT "Generating man page for ${script}"
      VERBATIM)

    add_custom_target("${man_base}_man" ALL DEPENDS ${man})
    install(
      FILES ${man}
      DESTINATION "${CMAKE_INSTALL_FULL_MANDIR}/man${man_num}"
      COMPONENT doc)
  endif()
endforeach()
0707010000000D000081ED0000000000000000000000016783F6CC00000107000000000000000000000000000000000000003F00000000sasl-xoauth2-0.26/scripts/postfix-sasl-xoauth2-update-ca-certs#!/bin/sh

# Don't give an error if, for example, postfix is not installed.
mkdir /var/spool/postfix/etc/ssl || true
mkdir /var/spool/postfix/etc/ssl/certs || true
cp /etc/ssl/certs/ca-certificates.crt /var/spool/postfix/etc/ssl/certs/ca-certificates.crt || true
0707010000000E000081ED0000000000000000000000016783F6CC00003254000000000000000000000000000000000000002F00000000sasl-xoauth2-0.26/scripts/sasl-xoauth2-tool.in#!/usr/bin/python3
# PYTHON_ARGCOMPLETE_OK
# -*- coding: utf-8 -*-

import argparse
import http
import http.server
import json
import logging
import msal
import os
import subprocess
import sys
import urllib.parse
import urllib.request

from typing import Optional,Dict,Union,IO,Any

try:
    import argcomplete #type: ignore
except ImportError:
    argcomplete = None

##########

# These are set by CMake at project build time.

TEST_TOOL_PATH = '${CMAKE_INSTALL_FULL_LIBDIR}/${PROJECT_NAME}/test-config'
DEFAULT_CONFIG_FILE = '${CMAKE_INSTALL_FULL_SYSCONFDIR}/${PROJECT_NAME}.conf'

##########

GOOGLE_OAUTH2_AUTH_URL = 'https://accounts.google.com/o/oauth2/auth'
GOOGLE_OAUTH2_TOKEN_URL = 'https://accounts.google.com/o/oauth2/token'
GOOGLE_OAUTH2_RESULT_PATH = '/oauth2_result'


def url_safe_escape(instring:str) -> str:
    return urllib.parse.quote(instring, safe='~-._')


def dump_overwrite(token:dict, output_filename:str, input_dict:dict):
    # overwrite fields in previous token (represented by input_dict)
    # that also have values from new token
    input_dict.update(token)
    with open(output_filename,'w') as output_file:
        json.dump(input_dict, output_file, indent=4)


def gmail_redirect_uri(local_port:int) -> str:
    return 'http://127.0.0.1:%d%s' % (local_port, GOOGLE_OAUTH2_RESULT_PATH)


def gmail_get_auth_url(client_id:str, scope:str, local_port:int) -> str:
    client_id = url_safe_escape(client_id)
    scope = url_safe_escape(scope)
    redirect_uri = url_safe_escape(gmail_redirect_uri(local_port))
    return '{}?client_id={}&scope={}&response_type={}&redirect_uri={}'.format(
        GOOGLE_OAUTH2_AUTH_URL, client_id, scope, 'code', redirect_uri)


def gmail_get_token_from_code(client_id:str, client_secret:str, authorization_code:str, local_port:int) -> Any:
    params = {}
    params['client_id'] = client_id
    params['client_secret'] = client_secret
    params['code'] = authorization_code
    params['redirect_uri'] = gmail_redirect_uri(local_port)
    params['grant_type'] = 'authorization_code'
    data = urllib.parse.urlencode(params)
    response = urllib.request.urlopen(GOOGLE_OAUTH2_TOKEN_URL, data.encode('ascii')).read()
    return json.loads(response)


def gmail_get_RequestHandler(client_id:str, client_secret:str, output_filename:str, input_dict:dict) -> type:
    class GMailRequestHandler(http.server.BaseHTTPRequestHandler):
        def log_request(self, code:Union[int,str]='-', size:Union[int,str]='-') -> None:
            # Silence request logging.
            return

        def do_GET(self) -> None:
            code = self.ExtractCodeFromResponse()
            response_code = 400
            response_text = '<html><head><title>Error</title></head><body><h1>Invalid request.</h1></body></html>'

            if code:
                response_code = 200
                response_text = '<html><head><title>Done</title></head><body><h1>You may close this window now.</h1></body></html>'

            self.send_response(response_code)
            self.send_header('Content-type', 'text/html')
            self.end_headers()
            self.wfile.write(response_text.encode('utf8'))

            if code:
                token = gmail_get_token_from_code(
                    client_id,
                    client_secret,
                    code,
                    self.server.server_address[1],
                )
                dump_overwrite(token, output_filename, input_dict)
                sys.exit(0)

        def ExtractCodeFromResponse(self) -> Optional[str]:
            parse = urllib.parse.urlparse(self.path)
            if parse.path != GOOGLE_OAUTH2_RESULT_PATH:
                return None
            qs = urllib.parse.parse_qs(parse.query)
            if 'code' not in qs:
                return None
            if len(qs['code']) != 1:
                return None
            return qs['code'][0]

    return GMailRequestHandler


def get_token_gmail(client_id:str, client_secret:str, scope:str, output_filename:str, input_dict:dict) -> None:
    request_handler_class = gmail_get_RequestHandler(
        client_id,
        client_secret,
        output_filename,
        input_dict,
    )
    server = http.server.HTTPServer(('', 0), request_handler_class)
    _, port = server.server_address

    url = gmail_get_auth_url(client_id, scope, port)
    print(f"Please open this URL in a browser ON THIS HOST:\n\n{url}\n", file=sys.stderr)

    server.serve_forever()

##########


OUTLOOK_REDIRECT_URI = "https://login.microsoftonline.com/common/oauth2/nativeclient"
OUTLOOK_SCOPE = "openid offline_access https://outlook.office.com/SMTP.Send"


def outlook_get_authorization_code(client_id:str, tenant:str) -> str:
    url = f"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/authorize"
    query:Dict[str,str] = {}
    query['client_id'] = client_id
    query['response_type'] = 'code'
    query['redirect_uri'] = OUTLOOK_REDIRECT_URI
    query['response_mode'] = 'query'
    query['scope'] = OUTLOOK_SCOPE

    print("Please visit the following link in a web browser, then paste the resulting URL:\n\n" +
          f"{url}?{urllib.parse.urlencode(query)}\n",
          file=sys.stderr)

    resulting_url_input:str = input("Resulting URL: ")
    if OUTLOOK_REDIRECT_URI not in resulting_url_input:
        raise Exception(f"Resulting URL does not contain expected prefix: {OUTLOOK_REDIRECT_URI}")
    resulting_url = urllib.parse.urlparse(resulting_url_input)
    code = urllib.parse.parse_qs(resulting_url.query)
    if "code" not in code:
        raise Exception(f"Missing code in result: {resulting_url.query}")
    return code["code"][0]


def outlook_get_initial_tokens(client_id:str, client_secret:str, tenant:str, code:str) -> Dict[str,Union[str,int]]:
    url = f"https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token"
    token_request:Dict[str,str] = {}
    token_request['client_id'] = client_id
    if len(client_secret) > 0:
      token_request['client_secret'] = client_secret
    token_request['scope'] = OUTLOOK_SCOPE
    token_request['code'] = code
    token_request['redirect_uri'] = OUTLOOK_REDIRECT_URI
    token_request['grant_type'] = 'authorization_code'
    resp = urllib.request.urlopen(
        urllib.request.Request(
            url,
            data=urllib.parse.urlencode(token_request).encode('ascii'),
            headers={ "Content-Type": "application/x-www-form-urlencoded" }))
    if resp.code != 200:
        raise Exception(f"Request failed: {resp.code}")
    try:
        content = json.load(resp)
        return {
            'access_token': content["access_token"],
            'refresh_token': content["refresh_token"],
            'expiry': 0,
        }
    except:
        raise Exception(f"Tokens not found in response: {content}")


def outlook_get_initial_tokens_by_device_flow(client_id:str, tenant:str) -> Dict[str,Union[str,int]]:
    authority = f"https://login.microsoftonline.com/{tenant}"
    app = msal.PublicClientApplication(client_id, authority=authority)
    flow = app.initiate_device_flow([OUTLOOK_SCOPE])
    if "user_code" not in flow:
        raise Exception("Failed to create device flow. Ensure that public client flows are enabled for the application. Flow: %s" % json.dumps(flow, indent=4))
    print(flow["message"])
    sys.stdout.flush()
    result = app.acquire_token_by_device_flow(flow)
    if "access_token" not in result or "refresh_token" not in result:
        raise Exception("Failed to acquire token. Result: %s" % json.dumps(result, indent=4))
    print("Acquired token.")
    return {
        'access_token': result["access_token"],
        'refresh_token': result["refresh_token"],
        'expiry': 0,
    }


def get_token_outlook(client_id:str, client_secret:str, tenant:str, use_device_flow:bool, output_filename:str, input_dict:dict) -> None:
    if use_device_flow:
        tokens = outlook_get_initial_tokens_by_device_flow(client_id, tenant)
    else:
        code = outlook_get_authorization_code(client_id, tenant)
        tokens = outlook_get_initial_tokens(client_id, client_secret, tenant, code)
    dump_overwrite(tokens, output_filename, input_dict)

##########


parser = argparse.ArgumentParser()
subparse = parser.add_subparsers()


def argparse_get_parser() -> argparse.ArgumentParser:
  return parser


def subcommand_get_token(args:argparse.Namespace) -> None:
    input_dict = {}
    if args.overwrite_existing_token:
        if not os.path.isfile(args.output_file):
            raise Exception("Cannot overwrite nonexistent token file {}".format(args.output_file))
        else:
            with open(args.output_file,'r') as input_file:
                input_dict = json.load(input_file)
    if args.service == 'outlook':
        if not args.tenant:
            parser.error("'outlook' service requires 'tenant' argument.")
        if not args.client_secret and not args.use_device_flow:
            args.client_secret = input('Please enter OAuth2 client secret (not always required; Azure docs are unclear): ')
        get_token_outlook(
            args.client_id,
            args.client_secret,
            args.tenant,
            args.use_device_flow,
            args.output_file,
            input_dict,
        )
    elif args.service == 'gmail':
        if not args.client_secret:
            args.client_secret = input('Please enter OAuth2 client secret: ')
        if not args.client_secret:
            parser.error("'gmail' service requires 'client-secret' argument.")
        if not args.scope:
            parser.error("'gmail' service requires 'scope' argument.")
        get_token_gmail(
            args.client_id,
            args.client_secret,
            args.scope,
            args.output_file,
            input_dict,
        )


sp_get_token = subparse.add_parser('get-token', description='Fetches initial access and refresh tokens from an OAuth 2 provider')
sp_get_token.set_defaults(func=subcommand_get_token)
sp_get_token.add_argument(
    'service', choices=['outlook', 'gmail'],
    help="service type",
)
sp_get_token.add_argument(
    '--client-id', required=True,
    help="required for both services",
)
sp_get_token.add_argument(
    '--tenant', default='consumers',
    help="wanted by 'outlook' (defaults to 'consumers')",
)
sp_get_token.add_argument(
    '--client-secret',
    help="required for both services, will prompt the user if blank",
)
sp_get_token.add_argument(
    '--scope',
    help="required for 'gmail'",
)
sp_get_token.add_argument(
    "--use-device-flow",
    default=False,
    action='store_true',
    help="use simplified device flow for Outlook/Azure",
)
sp_get_token.add_argument(
    '--overwrite-existing-token',
    default=False,
    action='store_true',
    help='overwrite existing token file (preserves extra fields)',
)
sp_get_token.add_argument(
    'output_file', nargs='?', type=str, default='-',
    help="output file, '-' for stdout",
)


def subcommand_test_config(args:argparse.Namespace) -> None:
  subprocess_args = [TEST_TOOL_PATH]
  if args.config_file:
    subprocess_args.extend(['--config', args.config_file])
  result = subprocess.run(subprocess_args, shell=False)
  sys.exit(result.returncode)


sp_test_config = subparse.add_parser('test-config', description='Tests a sasl-xoauth2 config file for syntax errors')
sp_test_config.set_defaults(func=subcommand_test_config)
sp_test_config.add_argument(
    '--config-file',
    help="config file path (defaults to '%s')" % DEFAULT_CONFIG_FILE,
)


def subcommand_test_token_refresh(args:argparse.Namespace) -> None:
  subprocess_args = [TEST_TOOL_PATH, '--token', args.token_file]
  if args.config_file:
    subprocess_args.extend(['--config', args.config_file])
  result = subprocess.run(subprocess_args, shell=False)
  sys.exit(result.returncode)


sp_test_token_refresh = subparse.add_parser('test-token-refresh', description='Tests that a token can be refreshed (i.e., that the OAuth 2 flow is working correctly)')
sp_test_token_refresh.set_defaults(func=subcommand_test_token_refresh)
sp_test_token_refresh.add_argument(
    '--config-file',
    help="config file path (defaults to '%s')" % DEFAULT_CONFIG_FILE,
)
sp_test_token_refresh.add_argument(
    'token_file',
    help="file containing initial access token",
)

##########


def main() -> None:
    if argcomplete:
        argcomplete.autocomplete(parser)
    elif '_ARGCOMPLETE' in os.environ:
        logging.error('Argument completion requested but the "argcomplete" '
                      'module is not installed. '
                      'Maybe you want to "apt install python3-argcomplete"')
        sys.exit(1)
    try:
      args = parser.parse_intermixed_args()
    except:
      args = parser.parse_args()
    if hasattr(args, 'func'):
      args.func(args)
    else:
      parser.print_help()
      sys.exit(1)


if __name__ == '__main__':
    main()
0707010000000F000041ED0000000000000000000000026783F6CC00000000000000000000000000000000000000000000001600000000sasl-xoauth2-0.26/src07070100000010000081A40000000000000000000000016783F6CC00000ADA000000000000000000000000000000000000002500000000sasl-xoauth2-0.26/src/CMakeLists.txt# Copyright 2020 Google LLC
#
# 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.

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++14 -g -Wall -Werror")

find_package(PkgConfig REQUIRED)
find_package(CURL REQUIRED)

pkg_check_modules(JSON REQUIRED "jsoncpp")
pkg_check_modules(SASL REQUIRED "libsasl2")

include_directories(${CMAKE_SOURCE_DIR}/src)

set(SOURCES
  client.cc
  client.h
  config.cc
  config.h
  http.cc
  http.h
  log.cc
  log.h
  module.cc
  module.h
  token_store.cc
  token_store.h)

set(TEST_CONFIG_SOURCES
  test_config.cc)

set(CONFIG_FILE ${PROJECT_NAME}.conf)
set(CONFIG_FILE_FULL_PATH ${CMAKE_INSTALL_FULL_SYSCONFDIR}/${CONFIG_FILE})

link_directories(${JSON_LIBRARY_DIRS})

add_library(${PROJECT_NAME} SHARED ${SOURCES} ${CONFIG_FILE})
target_include_directories(${PROJECT_NAME} SYSTEM PUBLIC ${CURL_INCLUDE_DIRS} ${SASL_INCLUDE_DIRS} ${JSON_INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME} ${CURL_LIBRARIES} ${JSON_LIBRARIES})
target_compile_definitions(${PROJECT_NAME} PRIVATE CONFIG_FILE_FULL_PATH="${CONFIG_FILE_FULL_PATH}")

add_library(${PROJECT_NAME}-static STATIC ${SOURCES} ${CONFIG_FILE})
target_include_directories(${PROJECT_NAME}-static SYSTEM PUBLIC ${CURL_INCLUDE_DIRS} ${SASL_INCLUDE_DIRS} ${JSON_INCLUDE_DIRS})
target_link_libraries(${PROJECT_NAME}-static ${CURL_LIBRARIES} ${JSON_LIBRARIES})
target_compile_definitions(${PROJECT_NAME}-static PRIVATE CONFIG_FILE_FULL_PATH="${CONFIG_FILE_FULL_PATH}")

add_executable(test-config ${TEST_CONFIG_SOURCES} ${CONFIG_FILE})
target_include_directories(test-config SYSTEM PUBLIC ${CURL_INCLUDE_DIRS} ${SASL_INCLUDE_DIRS} ${JSON_INCLUDE_DIRS})
target_link_libraries(test-config ${PROJECT_NAME}-static ${CURL_LIBRARIES} ${JSON_LIBRARIES})
target_compile_definitions(test-config PRIVATE CONFIG_FILE_FULL_PATH="${CONFIG_FILE_FULL_PATH}")

install(
  TARGETS ${PROJECT_NAME}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}/sasl2)

install(
  TARGETS test-config
  RUNTIME DESTINATION ${CMAKE_INSTALL_LIBDIR}/${PROJECT_NAME})

install(
  FILES ${CONFIG_FILE}
  DESTINATION ${CMAKE_INSTALL_SYSCONFDIR}
  COMPONENT config)

add_executable(${PROJECT_NAME}_test xoauth2_test.cc)
target_link_libraries(${PROJECT_NAME}_test ${PROJECT_NAME})

add_test(
  NAME ${PROJECT_NAME}_test
  COMMAND ${PROJECT_NAME}_test)
07070100000011000081A40000000000000000000000016783F6CC00002357000000000000000000000000000000000000002000000000sasl-xoauth2-0.26/src/client.cc// Copyright 2020 Google LLC
//
// 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.

#include "client.h"

#include <json/json.h>
#include <string.h>

#include <sstream>

#include "config.h"
#include "log.h"
#include "token_store.h"

namespace sasl_xoauth2 {

namespace {

void ReadPrompt(Log *log, sasl_interact_t **prompts, const unsigned int id,
                std::string *value) {
  if (!prompts || !*prompts) return;
  for (const auto *p = *prompts; p->id != SASL_CB_LIST_END; ++p) {
    if (p->id == id) {
      value->assign(static_cast<const char *>(p->result), p->len);
      log->Write("ReadPrompt: found id %d with value [%s]", id, value->c_str());
      return;
    }
  }
  log->Write("ReadPrompt: unable to find id %d", id);
}

int TriggerAuthNameCallback(Log *log, const sasl_utils_t *utils,
                            std::string *value) {
  sasl_getsimple_t *get_simple_cb = nullptr;
  void *context;
  int err = utils->getcallback(
      utils->conn, SASL_CB_AUTHNAME,
      reinterpret_cast<sasl_callback_ft *>(&get_simple_cb), &context);
  if (err != SASL_OK) {
    log->Write("TriggerAuthNameCallback: getcallback err=%d", err);
    return err;
  }
  if (!get_simple_cb) {
    log->Write("TriggerAuthNameCallback: null callback");
    return SASL_INTERACT;
  }

  const char *response = nullptr;
  unsigned int response_len = 0;
  err = get_simple_cb(context, SASL_CB_AUTHNAME, &response, &response_len);
  if (err != SASL_OK) {
    log->Write("TriggerAuthNameCallback: callback err=%d", err);
    return err;
  }

  value->assign(response, response_len);
  return SASL_OK;
}

int TriggerPasswordCallback(Log *log, const sasl_utils_t *utils,
                            std::string *value) {
  sasl_getsecret_t *get_secret_cb = nullptr;
  void *context;
  int err = utils->getcallback(
      utils->conn, SASL_CB_PASS,
      reinterpret_cast<sasl_callback_ft *>(&get_secret_cb), &context);
  if (err != SASL_OK) {
    log->Write("TriggerPasswordCallback: getcallback err=%d", err);
    return err;
  }
  if (!get_secret_cb) {
    log->Write("TriggerPasswordCallback: null callback");
    return SASL_BADPROT;
  }

  sasl_secret_t *password = nullptr;
  err = get_secret_cb(utils->conn, context, SASL_CB_PASS, &password);
  if (err != SASL_OK) {
    log->Write("TriggerPasswordCallback: callback err=%d", err);
    return err;
  }
  if (!password) {
    log->Write("TriggerPasswordCallback: null password");
    return SASL_BADPROT;
  }

  value->assign(reinterpret_cast<const char *>(password->data), password->len);
  return SASL_OK;
}

int RequestPrompts(sasl_client_params_t *params, sasl_interact_t **prompts,
                   const bool need_auth_name, const bool need_password) {
  if (!prompts) return SASL_BADPARAM;
  if (!need_auth_name && !need_password) return SASL_BADPARAM;

  // +1 for trailing SASL_CB_LIST_END.
  const int num_prompts = need_auth_name + need_password + 1;

  auto *req_prompts = static_cast<sasl_interact_t *>(
      params->utils->malloc(sizeof(sasl_interact_t) * num_prompts));
  if (!req_prompts) return SASL_NOMEM;
  memset(req_prompts, 0, sizeof(*req_prompts) * num_prompts);
  sasl_interact_t *p = req_prompts;

  if (need_auth_name) {
    p->id = SASL_CB_AUTHNAME;
    p->challenge = "Authentication Name";
    p->prompt = "Authentication Name";
    p++;
  }

  if (need_password) {
    p->id = SASL_CB_PASS;
    p->challenge = "Password";
    p->prompt = "Password";
    p++;
  }

  p->id = SASL_CB_LIST_END;

  *prompts = req_prompts;
  return SASL_INTERACT;
}

Log::Options GetLogOptions() {
  if (Config::Get()->always_log_to_syslog()) return Log::OPTIONS_IMMEDIATE;
  if (Config::Get()->log_full_trace_on_failure())
    return Log::OPTIONS_FULL_TRACE_ON_FAILURE;
  return Log::OPTIONS_NONE;
}

Log::Target GetLogTarget() {
  if (Config::Get()->always_log_to_syslog())
    return Log::TARGET_SYSLOG;
  if (!Config::Get()->log_to_syslog_on_failure())
    return Log::TARGET_NONE;
  return Log::TARGET_DEFAULT;
}

}  // namespace

Client::Client() {
  log_ = Log::Create(GetLogOptions(), GetLogTarget());
  log_->Write("Client: created");
}

Client::~Client() { log_->Write("Client: destroyed"); }

int Client::DoStep(sasl_client_params_t *params, const char *from_server,
                   const unsigned int from_server_len,
                   sasl_interact_t **prompt_need, const char **to_server,
                   unsigned int *to_server_len, sasl_out_params_t *out_params) {
  log_->Write("Client::DoStep: called with state %d", static_cast<int>(state_));

  int err = SASL_BADPROT;

  switch (state_) {
    case State::kInitial:
      err = InitialStep(params, prompt_need, to_server, to_server_len,
                        out_params);
      break;

    case State::kTokenSent:
      err = TokenSentStep(params, prompt_need, from_server, from_server_len,
                          to_server, to_server_len, out_params);
      break;

    default:
      log_->Write("Client::DoStep: invalid state");
  }

  if (err != SASL_OK && err != SASL_INTERACT) log_->SetFlushOnDestroy();
  log_->Write("Client::DoStep: new state %d and err %d",
              static_cast<int>(state_), err);
  return err;
}

int Client::InitialStep(sasl_client_params_t *params,
                        sasl_interact_t **prompt_need, const char **to_server,
                        unsigned int *to_server_len,
                        sasl_out_params_t *out_params) {
  *to_server = nullptr;
  *to_server_len = 0;

  std::string auth_name;
  ReadPrompt(log_.get(), prompt_need, SASL_CB_AUTHNAME, &auth_name);
  if (auth_name.empty()) {
    int err = TriggerAuthNameCallback(log_.get(), params->utils, &auth_name);
    log_->Write("Client::InitialStep: TriggerAuthNameCallback err=%d", err);
  }

  std::string password;
  ReadPrompt(log_.get(), prompt_need, SASL_CB_PASS, &password);
  if (password.empty()) {
    int err = TriggerPasswordCallback(log_.get(), params->utils, &password);
    log_->Write("Client::InitialStep: TriggerPasswordCallback err=%d", err);
  }

  if (prompt_need && *prompt_need) {
    params->utils->free(*prompt_need);
    *prompt_need = nullptr;
  }

  if (prompt_need && (auth_name.empty() || password.empty())) {
    return RequestPrompts(params, prompt_need, auth_name.empty(),
                          password.empty());
  }

  int err = params->canon_user(params->utils->conn, auth_name.data(),
                               auth_name.size(),
                               SASL_CU_AUTHID | SASL_CU_AUTHZID, out_params);
  if (err != SASL_OK) return err;

  user_ = auth_name;
  token_ = TokenStore::Create(log_.get(), password);
  if (!token_) return SASL_FAIL;
  if (token_->has_user()) user_ = token_->user();

  err = SendToken(to_server, to_server_len);
  if (err != SASL_OK) return err;

  state_ = State::kTokenSent;
  return SASL_OK;
}

int Client::TokenSentStep(sasl_client_params_t *params,
                          sasl_interact_t **prompt_need,
                          const char *from_server,
                          const unsigned int from_server_len,
                          const char **to_server, unsigned int *to_server_len,
                          sasl_out_params_t *out_params) {
  *to_server = nullptr;
  *to_server_len = 0;

  log_->Write("Client::TokenSentStep: from server: %s", from_server);

  if (from_server_len == 0) return SASL_OK;

  std::string from_server_str(from_server, from_server_len);
  std::stringstream stream(from_server_str);
  std::string status;

  try {
    Json::Value root;
    stream >> root;
    if (root.isMember("status")) status = root["status"].asString();
  } catch (const std::exception &e) {
    log_->Write("Client::TokenSentStep: caught exception: %s", e.what());
    return SASL_BADPROT;
  }

  if (status == "400" || status == "401") {
    int err = token_->Refresh();
    if (err != SASL_OK) return err;
    return SASL_TRYAGAIN;
  }

  if (status.empty()) {
    log_->Write("Client::TokenSentStep: blank status, assuming we're okay");
    return SASL_OK;
  }

  log_->Write("Client::TokenSentStep: status: %s", status.c_str());
  return SASL_BADPROT;
}

int Client::SendToken(const char **to_server, unsigned int *to_server_len) {
  std::string token;
  int err = token_->GetAccessToken(&token);
  if (err != SASL_OK) return err;

  response_ = "user=" + user_ + "\1auth=Bearer " + token + "\1\1";
  log_->Write("Client::SendToken: response: %s", response_.c_str());

  *to_server = response_.data();
  *to_server_len = response_.size();

  return SASL_OK;
}

}  // namespace sasl_xoauth2
07070100000012000081A40000000000000000000000016783F6CC000007E6000000000000000000000000000000000000001F00000000sasl-xoauth2-0.26/src/client.h/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#ifndef SASL_XOAUTH2_CLIENT_H
#define SASL_XOAUTH2_CLIENT_H

#include <sasl/sasl.h>
#include <sasl/saslplug.h>

#include <memory>
#include <string>

namespace sasl_xoauth2 {

class Log;
class TokenStore;

class Client {
 public:
  Client();
  ~Client();

  int DoStep(sasl_client_params_t *params, const char *from_server,
             const unsigned int from_server_len, sasl_interact_t **prompt_need,
             const char **to_server, unsigned int *to_server_len,
             sasl_out_params_t *out_params);

 private:
  enum class State {
    kInitial,
    kTokenSent,
  };

  int InitialStep(sasl_client_params_t *params, sasl_interact_t **prompt_need,
                  const char **to_server, unsigned int *to_server_len,
                  sasl_out_params_t *out_params);

  int TokenSentStep(sasl_client_params_t *params, sasl_interact_t **prompt_need,
                    const char *from_server, const unsigned int from_server_len,
                    const char **to_server, unsigned int *to_server_len,
                    sasl_out_params_t *out_params);

  int SendToken(const char **to_server, unsigned int *to_server_len);

  State state_ = State::kInitial;
  std::string user_;
  std::string response_;

  // Order of destruction matters -- token_ holds a pointer to log_.
  std::unique_ptr<Log> log_;
  std::unique_ptr<TokenStore> token_;
};

}  // namespace sasl_xoauth2

#endif  // SASL_XOAUTH2_CLIENT_H
07070100000013000081A40000000000000000000000016783F6CC00001203000000000000000000000000000000000000002000000000sasl-xoauth2-0.26/src/config.cc// Copyright 2020 Google LLC
//
// 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.

#include "config.h"

#include <errno.h>
#include <sasl/sasl.h>
#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <syslog.h>

#include <algorithm>
#include <fstream>
#include <sstream>

namespace sasl_xoauth2 {

namespace {

constexpr char kConfigFilePath[] = CONFIG_FILE_FULL_PATH;

bool s_log_to_stderr = false;
Config *s_config = nullptr;

void Log(const char *fmt, ...) {
  va_list args;
  va_start(args, fmt);

  if (s_log_to_stderr)
    vfprintf(stderr, fmt, args);
  else
    vsyslog(LOG_WARNING, fmt, args);

  va_end(args);
}

template <typename T>
int Transform(std::string in, T *out) {
  Log("sasl-xoauth2: Unknown value type.\n");
  return SASL_FAIL;
}

template <>
int Transform(std::string in, bool *out) {
  std::for_each(in.begin(), in.end(), [](char c) { return std::tolower(c); });
  if (in == "yes" || in == "true") {
    *out = true;
    return SASL_OK;
  }
  if (in == "no" || in == "false") {
    *out = false;
    return SASL_OK;
  }
  Log("sasl-xoauth2: Invalid value '%s'. Need either 'yes'/'true' or "
      "'no'/'false'.\n",
      in.c_str());
  return SASL_FAIL;
}

template <>
int Transform(std::string in, std::string *out) {
  *out = in;
  return SASL_OK;
}

template <>
int Transform(std::string in, int *out) {
  *out = stoi(in);
  return SASL_OK;
}

template <typename T>
int Fetch(const Json::Value &root, const std::string &name, bool optional,
          T *out) {
  if (!root.isMember(name)) {
    if (optional) return SASL_OK;
    Log("sasl-xoauth2: Missing required value: %s\n", name.c_str());
    return SASL_FAIL;
  }
  return Transform(root[name].asString(), out);
}

}  // namespace

void Config::EnableLoggingToStderr() { s_log_to_stderr = true; }

int Config::Init(std::string path) {
  // Fail silently if we've already been initialized (via InitForTesting, say).
  if (s_config) return SASL_OK;

  if (path.empty()) {
    path = kConfigFilePath;
  }

  try {
    std::ifstream f(path);
    if (!f.good()) {
      Log("sasl-xoauth2: Unable to open config file %s: %s\n", path.c_str(),
          strerror(errno));
      return SASL_FAIL;
    }

    Json::Value root;
    f >> root;
    s_config = new Config();
    return s_config->Init(root);

  } catch (const std::exception &e) {
    Log("sasl-xoauth2: Exception during init: %s\n", e.what());
    return SASL_FAIL;
  }
}

int Config::InitForTesting(const Json::Value &root) {
  if (s_config) {
    Log("sasl-xoauth2: Already initialized!\n");
    exit(1);
  }

  s_config = new Config();
  return s_config->Init(root);
}

Config *Config::Get() {
  if (!s_config) {
    Log("sasl-xoauth2: Attempt to fetch before calling Init()!\n");
    exit(1);
  }
  return s_config;
}

int Config::Init(const Json::Value &root) {
  try {
    int err;

    err = Fetch(root, "client_id", false, &client_id_);
    if (err != SASL_OK) return err;

    err = Fetch(root, "client_secret", false, &client_secret_);
    if (err != SASL_OK) return err;

    err = Fetch(root, "always_log_to_syslog", true,
                &always_log_to_syslog_);
    if (err != SASL_OK) return err;

    err = Fetch(root, "log_to_syslog_on_failure", true,
                &log_to_syslog_on_failure_);
    if (err != SASL_OK) return err;

    err = Fetch(root, "log_full_trace_on_failure", true,
                &log_full_trace_on_failure_);
    if (err != SASL_OK) return err;

    err = Fetch(root, "token_endpoint", true, &token_endpoint_);
    if (err != SASL_OK) return err;

    err = Fetch(root, "refresh_window", true, &refresh_window_);
    if (err != SASL_OK) return err;

    err = Fetch(root, "proxy", true, &proxy_);
    if (err != SASL_OK) return err;

    err = Fetch(root, "ca_bundle_file", true, &ca_bundle_file_);
    if (err != SASL_OK) return err;

    err = Fetch(root, "ca_certs_dir", true, &ca_certs_dir_);
    if (err != SASL_OK) return err;

    return 0;

  } catch (const std::exception &e) {
    Log("sasl-xoauth2: Exception during init: %s\n", e.what());
    return SASL_FAIL;
  }
}

}  // namespace sasl_xoauth2
07070100000014000081A40000000000000000000000016783F6CC00000825000000000000000000000000000000000000001F00000000sasl-xoauth2-0.26/src/config.h/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#ifndef SASL_XOAUTH2_CONFIG_H
#define SASL_XOAUTH2_CONFIG_H

#include <json/json.h>

#include <string>

namespace sasl_xoauth2 {

class Config {
 public:
  static void EnableLoggingToStderr();

  static int Init(std::string path = "");
  static int InitForTesting(const Json::Value &root);

  static Config *Get();

  std::string client_id() const { return client_id_; }
  std::string client_secret() const { return client_secret_; }
  bool always_log_to_syslog() const { return always_log_to_syslog_; }
  bool log_to_syslog_on_failure() const { return log_to_syslog_on_failure_; }
  bool log_full_trace_on_failure() const { return log_full_trace_on_failure_; }
  std::string token_endpoint() const { return token_endpoint_; }
  std::string proxy() const { return proxy_; }
  std::string ca_bundle_file() const { return ca_bundle_file_; }
  std::string ca_certs_dir() const { return ca_certs_dir_; }
  int refresh_window() const { return refresh_window_; }

 private:
  Config() = default;

  int Init(const Json::Value &root);

  std::string client_id_;
  std::string client_secret_;
  bool always_log_to_syslog_ = false;
  bool log_to_syslog_on_failure_ = true;
  bool log_full_trace_on_failure_ = false;
  std::string token_endpoint_ = "https://accounts.google.com/o/oauth2/token";
  std::string proxy_ = "";
  std::string ca_bundle_file_ = "";
  std::string ca_certs_dir_ = "";
  int refresh_window_ = 10;  // seconds
};

}  // namespace sasl_xoauth2

#endif  // SASL_XOAUTH2_CONFIG_H
07070100000015000081A40000000000000000000000016783F6CC00001288000000000000000000000000000000000000001E00000000sasl-xoauth2-0.26/src/http.cc// Copyright 2020 Google LLC
//
// 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.

#include "http.h"

#include <curl/curl.h>
#include <sasl/sasl.h>
#include <string.h>

#include <vector>

namespace sasl_xoauth2 {

namespace {

constexpr char kUserAgent[] = "sasl xoauth2 token refresher";

class RequestContext {
 public:
  static size_t Read(char *data, size_t size, size_t items, void *context) {
    size *= items;

    auto *request = static_cast<RequestContext *>(context);
    size_t remaining = std::min(request->to_server_remaining_, size);
    memcpy(data, request->to_server_next_, remaining);
    request->to_server_next_ += remaining;
    request->to_server_remaining_ -= remaining;

    return remaining;
  }

  static size_t Write(char *data, size_t size, size_t items, void *context) {
    size *= items;

    auto *request = static_cast<RequestContext *>(context);
    size_t old_size = request->from_server_.size();
    request->from_server_.resize(old_size + size);
    memcpy(&request->from_server_[old_size], data, size);

    return size;
  }

  static int Seek(void *context, curl_off_t offset, int origin) {
    auto *request = static_cast<RequestContext *>(context);
    if (origin != SEEK_SET || offset != 0) return CURL_SEEKFUNC_FAIL;
    request->Rewind();
    return CURL_SEEKFUNC_OK;
  }

  RequestContext(const std::string &data) : to_server_(data) { Rewind(); }

  size_t to_server_size() const { return to_server_.size(); }

  std::string from_server() const {
    return std::string(from_server_.begin(), from_server_.end());
  }

 private:
  void Rewind() {
    to_server_next_ = to_server_.c_str();
    to_server_remaining_ = to_server_.size();
  }

  std::string to_server_;
  const char *to_server_next_ = nullptr;
  size_t to_server_remaining_ = 0;

  std::vector<char> from_server_;
};

HttpIntercept s_intercept = {};

}  // namespace

void SetHttpInterceptForTesting(HttpIntercept intercept) {
  s_intercept = intercept;
}

int HttpPost(HttpPostOptions options) {
  if (s_intercept) return s_intercept(options);

  *options.response_code = 0;
  options.response->clear();

  CURL *curl = curl_easy_init();
  if (!curl) {
    *options.error = "Unable to create CURL handle.";
    return SASL_BADPROT;
  }

  RequestContext context(options.data);

  char transport_error[CURL_ERROR_SIZE] = {'\0'};

  // Behavior.
  curl_easy_setopt(curl, CURLOPT_VERBOSE, false);
  curl_easy_setopt(curl, CURLOPT_NOPROGRESS, true);
  curl_easy_setopt(curl, CURLOPT_NOSIGNAL, true);

  // Errors.
  curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, transport_error);

  // Network.
  curl_easy_setopt(curl, CURLOPT_URL, options.url.c_str());

  // Certs.
  if (options.ca_certs_dir.empty()) {
    if (options.ca_bundle_file.empty()) {
      // Use default CA location.
    } else {
      curl_easy_setopt(curl, CURLOPT_CAINFO, options.ca_bundle_file.c_str());
    }
  } else {
    curl_easy_setopt(curl, CURLOPT_CAPATH, options.ca_certs_dir.c_str());
  }

  // HTTP.
  curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, true);
  curl_easy_setopt(curl, CURLOPT_USERAGENT, kUserAgent);
  if (!options.proxy.empty())
    curl_easy_setopt(curl, CURLOPT_PROXY, options.proxy.c_str());
  curl_easy_setopt(curl, CURLOPT_POST, true);
  curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE,
                   static_cast<curl_off_t>(context.to_server_size()));

  // Callbacks.
  curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, &RequestContext::Write);
  curl_easy_setopt(curl, CURLOPT_WRITEDATA, &context);
  curl_easy_setopt(curl, CURLOPT_READFUNCTION, &RequestContext::Read);
  curl_easy_setopt(curl, CURLOPT_READDATA, &context);
  curl_easy_setopt(curl, CURLOPT_SEEKFUNCTION, &RequestContext::Seek);
  curl_easy_setopt(curl, CURLOPT_SEEKDATA, &context);

  CURLcode err = curl_easy_perform(curl);
  curl_easy_cleanup(curl);

  if (err != CURLE_OK) {
    *options.error = transport_error;
    if (options.error->empty()) {
      *options.error = curl_easy_strerror(err);
      *options.error += " (no further error information)";
    }
    return SASL_BADPROT;
  }

  curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, options.response_code);
  *options.response = context.from_server();
  return SASL_OK;
}

}  // namespace sasl_xoauth2
07070100000016000081A40000000000000000000000016783F6CC000004A9000000000000000000000000000000000000001D00000000sasl-xoauth2-0.26/src/http.h/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#ifndef SASL_XOAUTH2_HTTP_H
#define SASL_XOAUTH2_HTTP_H

#include <functional>
#include <string>

namespace sasl_xoauth2 {

struct HttpPostOptions {
  const std::string &url;
  const std::string &data;
  const std::string &proxy;
  const std::string &ca_bundle_file;
  const std::string &ca_certs_dir;

  long *response_code;
  std::string *response;
  std::string *error;
};

using HttpIntercept = std::function<int(HttpPostOptions)>;

void SetHttpInterceptForTesting(HttpIntercept intercept);

int HttpPost(HttpPostOptions options);

}  // namespace sasl_xoauth2

#endif  // SASL_XOAUTH2_HTTP_H
07070100000017000081A40000000000000000000000016783F6CC00000E86000000000000000000000000000000000000001D00000000sasl-xoauth2-0.26/src/log.cc// Copyright 2020 Google LLC
//
// 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.

#include "log.h"

#include <stdarg.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <syslog.h>
#include <time.h>

#include <memory>

namespace sasl_xoauth2 {

namespace {

Log::Options s_default_options = Log::OPTIONS_NONE;
Log::Target s_default_target = Log::TARGET_SYSLOG;

std::string Now() {
  time_t t = time(nullptr);
  char time_str[32];
  tm local_time = {};
  localtime_r(&t, &local_time);
  strftime(time_str, sizeof(time_str), "%F %T", &local_time);
  return std::string(time_str);
}

class NoOpLogger : public LogImpl {
 public:
  NoOpLogger() = default;
  ~NoOpLogger() override = default;

  void WriteLine(const std::string &line) override {}
};

class SysLogLogger : public LogImpl {
 public:
  SysLogLogger() = default;
  ~SysLogLogger() override = default;

  void WriteLine(const std::string &line) override {
    syslog(LOG_WARNING, "[sasl-xoauth2] %s\n", line.c_str());
  }
};

class StdErrLogger : public LogImpl {
 public:
  StdErrLogger() = default;
  ~StdErrLogger() override = default;

  void WriteLine(const std::string &line) override {
    fprintf(stderr, "%s\n", line.c_str());
  }
};

std::unique_ptr<LogImpl> CreateLogImpl(Log::Target target) {
  switch (target) {
    case Log::TARGET_NONE:
      return std::make_unique<NoOpLogger>();
    case Log::TARGET_SYSLOG:
      return std::make_unique<SysLogLogger>();
    case Log::TARGET_STDERR:
      return std::make_unique<StdErrLogger>();
    default:
      exit(1);
  };
}

}  // namespace

void EnableLoggingForTesting() {
  s_default_options = Log::OPTIONS_IMMEDIATE;
  s_default_target = Log::TARGET_STDERR;
}

std::unique_ptr<Log> Log::Create(Options options, Target target) {
  options = static_cast<Options>(options | s_default_options);
  if (target == TARGET_DEFAULT) target = s_default_target;
  return std::unique_ptr<Log>(new Log(CreateLogImpl(target), options));
}

Log::~Log() {
  if (options_ & OPTIONS_FLUSH_ON_DESTROY) Flush();
}

void Log::Write(const char *fmt, ...) {
  va_list args;

  va_start(args, fmt);
  int buf_len = vsnprintf(nullptr, 0, fmt, args);
  va_end(args);

  // +1 for the trailing \0.
  std::vector<char> buf(buf_len + 1);
  va_start(args, fmt);
  vsnprintf(buf.data(), buf.size(), fmt, args);
  va_end(args);

  const std::string line(buf.begin(), buf.end());
  if (options_ & OPTIONS_IMMEDIATE) {
    impl_->WriteLine(line);
  } else {
    lines_.push_back(Now() + ": " + line);
  }
}

void Log::Flush() {
  if (lines_.empty()) return;
  if (options_ & OPTIONS_FULL_TRACE_ON_FAILURE) {
    impl_->WriteLine("auth failed:");
    for (const auto &line : lines_) impl_->WriteLine("  " + line);
  } else {
    if (summary_.empty()) summary_ = lines_.back();
    impl_->WriteLine("auth failed: " + summary_);
    if (lines_.size() > 1) {
      impl_->WriteLine("set log_full_trace_on_failure to see full " +
                       std::to_string(lines_.size()) + " line(s) of tracing.");
    }
  }
}

void Log::SetFlushOnDestroy() {
  options_ = static_cast<Options>(options_ | OPTIONS_FLUSH_ON_DESTROY);
  if (!lines_.empty()) summary_ = lines_.back();
}

}  // namespace sasl_xoauth2
07070100000018000081A40000000000000000000000016783F6CC000006E0000000000000000000000000000000000000001C00000000sasl-xoauth2-0.26/src/log.h/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#ifndef SASL_XOAUTH2_LOG_H
#define SASL_XOAUTH2_LOG_H

#include <memory>
#include <string>
#include <vector>

namespace sasl_xoauth2 {

void EnableLoggingForTesting();

// Log implementation interface, not for direct use.
class LogImpl {
 public:
  virtual ~LogImpl() = default;

  virtual void WriteLine(const std::string &line) = 0;
};

class Log {
 public:
  enum Options {
    OPTIONS_NONE = 0,
    OPTIONS_IMMEDIATE = 1,
    OPTIONS_FULL_TRACE_ON_FAILURE = 2,
    OPTIONS_FLUSH_ON_DESTROY = 4,
  };

  enum Target {
    TARGET_DEFAULT = 0,
    TARGET_NONE = 1,
    TARGET_SYSLOG = 2,
    TARGET_STDERR = 3,
  };

  static std::unique_ptr<Log> Create(Options options = OPTIONS_NONE,
                                     Target target = TARGET_DEFAULT);

  ~Log();

  void Write(const char *fmt, ...);
  void Flush();
  void SetFlushOnDestroy();

 protected:
  Log(std::unique_ptr<LogImpl> impl, Options options)
      : impl_(std::move(impl)), options_(options) {}

 private:
  const std::unique_ptr<LogImpl> impl_;

  Options options_;
  std::string summary_;
  std::vector<std::string> lines_;
};

}  // namespace sasl_xoauth2

#endif  // SASL_XOAUTH2_LOG_H
07070100000019000081A40000000000000000000000016783F6CC00000C17000000000000000000000000000000000000002000000000sasl-xoauth2-0.26/src/module.cc// Copyright 2020 Google LLC
//
// 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.

#include "module.h"

#include <sasl/sasl.h>
#include <sasl/saslplug.h>

#include "client.h"
#include "config.h"

namespace {

int mech_new(void *, sasl_client_params_t *params, void **context) {
  sasl_xoauth2::Client *client = new sasl_xoauth2::Client();
  if (!client) {
    params->utils->seterror(params->utils->conn, 0,
                            "Failed to create Client instance.");
    return SASL_NOMEM;
  }
  *context = client;
  return SASL_OK;
}

int mech_step(void *context, sasl_client_params_t *params,
              const char *from_server, unsigned int from_server_len,
              sasl_interact_t **prompt_need, const char **to_server,
              unsigned int *to_server_len, sasl_out_params_t *out_params) {
  if (!context) return SASL_BADPARAM;
  return static_cast<sasl_xoauth2::Client *>(context)->DoStep(
      params, from_server, from_server_len, prompt_need, to_server,
      to_server_len, out_params);
}

void mech_dispose(void *context, const sasl_utils_t *utils) {
  if (!context) return;
  delete static_cast<sasl_xoauth2::Client *>(context);
}

sasl_client_plug_t s_plugin = {
    /* mech_name = */ "XOAUTH2",
    /* max_ssf = */ 60,
    /* security_flags = */
      SASL_SEC_NOANONYMOUS | SASL_SEC_NOPLAINTEXT | SASL_SEC_PASS_CREDENTIALS,
    /* features = */ SASL_FEAT_WANT_CLIENT_FIRST | SASL_FEAT_ALLOWS_PROXY,
    /* required_prompts = */ nullptr,
    /* glob_context = */ nullptr,
    /* mech_new = */ &mech_new,
    /* mech_step = */ &mech_step,
    /* mech_dispose = */ &mech_dispose,
    /* mech_free = */ nullptr,
    /* idle = */ nullptr,
    /* spare_fptr1 = */ nullptr,
    /* spare_fptr2 = */ nullptr};

sasl_client_plug_t s_plugins[] = {s_plugin};

}  // namespace

extern "C" int sasl_client_plug_init(const sasl_utils_t *utils, int max_version,
                                     int *out_version,
                                     sasl_client_plug_t **plug_list,
                                     int *plug_count) {
  if (max_version < SASL_CLIENT_PLUG_VERSION) {
    utils->seterror(utils->conn, 0, "sasl-xoauth2: need version %d, got %d",
                    SASL_CLIENT_PLUG_VERSION, max_version);
    return SASL_BADVERS;
  }

  // Do this here because subsequent calls are chroot-ed (for Postfix, at
  // least).
  int err = sasl_xoauth2::Config::Init();
  if (err != SASL_OK) return err;

  *out_version = SASL_CLIENT_PLUG_VERSION;
  *plug_list = s_plugins;
  *plug_count = sizeof(s_plugins) / sizeof(s_plugins[0]);
  return SASL_OK;
}
0707010000001A000081A40000000000000000000000016783F6CC000003E9000000000000000000000000000000000000001F00000000sasl-xoauth2-0.26/src/module.h/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#ifndef SASL_XOAUTH2_MODULE_H
#define SASL_XOAUTH2_MODULE_H

#include <sasl/sasl.h>
#include <sasl/saslplug.h>

#ifdef __cplusplus
extern "C" {
#endif

int sasl_client_plug_init(const sasl_utils_t *utils, int max_version,
                          int *out_version, sasl_client_plug_t **plug_list,
                          int *plug_count);

#ifdef __cplusplus
}
#endif

#endif  // SASL_XOAUTH2_MODULE_H
0707010000001B000081A40000000000000000000000016783F6CC00000057000000000000000000000000000000000000002800000000sasl-xoauth2-0.26/src/sasl-xoauth2.conf{
  "client_id": "CLIENT_ID_GOES_HERE",
  "client_secret": "CLIENT_SECRET_GOES_HERE"
}
0707010000001C000081A40000000000000000000000016783F6CC00000A2B000000000000000000000000000000000000002500000000sasl-xoauth2-0.26/src/test_config.cc#include <getopt.h>
#include <sasl/sasl.h>
#include <string.h>

#include "config.h"
#include "log.h"
#include "token_store.h"

namespace {

struct Options {
  std::string config_path;
  std::string token_path;
};

bool TryParseCommandLine(int argc, char **argv, Options *out) {
  const char *kShortOptions = "c:r:";
  const option kLongOptions[] = {{"config", required_argument, nullptr, 'c'},
                                 {"token", required_argument, nullptr, 'r'},
                                 {nullptr, 0, nullptr, 0}};

  while (true) {
    int opt = getopt_long(argc, argv, kShortOptions, kLongOptions, nullptr);
    if (opt == -1) break;

    switch (opt) {
      case 'c':
        out->config_path = optarg;
        break;

      case 'r':
        out->token_path = optarg;
        break;

      default:
        return false;
    }
  }

  return true;
}

void PrintUsage(const std::string &base_name) {
  fprintf(stderr,
          "Usage: %s [options]\n\n"
          "Options:\n"
          "  -c, --config=<file>  use <file> for configuration rather than\n"
          "                       system default\n"
          "  -r, --token=<file>   attempt to request a token from the OAuth\n"
          "                       provider using the refresh token in <file>\n",
          base_name.c_str());
}

Options ParseCommandLine(int argc, char **argv) {
  const std::string base_name = basename(argv[0]);
  Options parsed_options;

  if (!TryParseCommandLine(argc, argv, &parsed_options)) {
    PrintUsage(base_name);
    exit(EXIT_FAILURE);
  }

  return parsed_options;
}

}  // namespace

int main(int argc, char **argv) {
  const Options options = ParseCommandLine(argc, argv);

  sasl_xoauth2::Config::EnableLoggingToStderr();
  if (sasl_xoauth2::Config::Init(options.config_path) != SASL_OK) {
    printf("Config check failed.\n");
    return EXIT_FAILURE;
  }
  printf("Config check passed.\n");

  if (!options.token_path.empty()) {
    auto logger = sasl_xoauth2::Log::Create(
        sasl_xoauth2::Log::OPTIONS_FULL_TRACE_ON_FAILURE,
        sasl_xoauth2::Log::TARGET_STDERR);
    auto token_store =
        sasl_xoauth2::TokenStore::Create(logger.get(), options.token_path,
                                         /*enable_updates=*/false);
    if (!token_store) {
      logger->Flush();
      printf("Failed to read token.\n");
      return EXIT_FAILURE;
    }
    if (token_store->Refresh() != SASL_OK) {
      logger->Flush();
      printf("Token refresh failed.\n");
      return EXIT_FAILURE;
    }
    printf("Token refresh succeeded.\n");
  }

  return EXIT_SUCCESS;
}
0707010000001D000081A40000000000000000000000016783F6CC000022D5000000000000000000000000000000000000002500000000sasl-xoauth2-0.26/src/token_store.cc// Copyright 2020 Google LLC
//
// 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.

#include "token_store.h"

#include <errno.h>
#include <inttypes.h>
#include <json/json.h>
#include <sasl/sasl.h>
#include <stdio.h>
#include <string.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

#include <fstream>
#include <sstream>

#include "config.h"
#include "http.h"
#include "log.h"

namespace sasl_xoauth2 {

namespace {

constexpr int kMaxRefreshAttempts = 2;

std::string GetTempSuffix() {
  timeval t = {};
  gettimeofday(&t, nullptr);
  const uint64_t time_ms = t.tv_sec * 1000 + t.tv_usec / 1000;

  char buf[128];
  snprintf(buf, sizeof(buf), "%d.%" PRIu64, getpid(), time_ms);

  return std::string(buf);
}

void ReadOverride(const Json::Value &root, const std::string &key,
                  std::string *output) {
  if (root.isMember(key)) {
    *output = root[key].asString();
  }
}

void WriteOverride(const std::string &key, const std::string &value,
                   Json::Value *output) {
  if (!value.empty()) {
    (*output)[key] = value;
  }
}

}  // namespace

/* static */ std::unique_ptr<TokenStore> TokenStore::Create(
    Log *log, const std::string &path, bool enable_updates) {
  std::unique_ptr<TokenStore> store(new TokenStore(log, path, enable_updates));
  if (store->Read() != SASL_OK) return {};
  return store;
}

int TokenStore::GetAccessToken(std::string *token) {
  const int refresh_window =
      (override_refresh_window_ == 0 ? Config::Get()->refresh_window()
                                   : override_refresh_window_);

  if ((time(nullptr) + refresh_window) >= expiry_) {
    log_->Write("TokenStore::GetAccessToken: token expired. refreshing.");
    int err = Refresh();
    if (err != SASL_OK) return err;
  }

  *token = access_;
  return SASL_OK;
}

int TokenStore::Refresh() {
  if (refresh_attempts_ > kMaxRefreshAttempts) {
    log_->Write("TokenStore::Refresh: exceeded maximum attempts");
    return SASL_BADPROT;
  }
  refresh_attempts_++;
  log_->Write("TokenStore::Refresh: attempt %d", refresh_attempts_);

  const std::string client_id =
      (override_client_id_.empty() ? Config::Get()->client_id()
                                   : override_client_id_);
  const std::string client_secret =
      (override_client_secret_.empty() ? Config::Get()->client_secret()
                                       : override_client_secret_);
  const std::string token_endpoint =
      (override_token_endpoint_.empty() ? Config::Get()->token_endpoint()
                                        : override_token_endpoint_);

  const std::string proxy =
      (override_proxy_.empty() ? Config::Get()->proxy() : override_proxy_);

  const std::string ca_bundle_file =
      (override_ca_bundle_file_.empty() ? Config::Get()->ca_bundle_file()
                                        : override_ca_bundle_file_);

  const std::string ca_certs_dir =
      (override_ca_certs_dir_.empty() ? Config::Get()->ca_certs_dir()
                                      : override_ca_certs_dir_);

  const std::string request =
      std::string("client_id=") + client_id +
      "&client_secret=" + client_secret +
      "&grant_type=refresh_token&refresh_token=" + refresh_;
  std::string response;
  long response_code = 0;
  log_->Write("TokenStore::Refresh: token_endpoint: %s",
              token_endpoint.c_str());
  log_->Write("TokenStore::Refresh: request: %s", request.c_str());

  std::string http_error;
  int err = HttpPost({.url = token_endpoint,
                      .data = request,
                      .proxy = proxy,
                      .ca_bundle_file = ca_bundle_file,
                      .ca_certs_dir = ca_certs_dir,
                      .response_code = &response_code,
                      .response = &response,
                      .error = &http_error});
  if (err != SASL_OK) {
    log_->Write("TokenStore::Refresh: http error: %s", http_error.c_str());
    return err;
  }

  log_->Write("TokenStore::Refresh: code=%d, response=%s", response_code,
              response.c_str());

  if (response_code != 200) {
    log_->Write("TokenStore::Refresh: request failed");
    return SASL_BADPROT;
  }

  try {
    std::stringstream ss(response);
    Json::Value root;
    ss >> root;
    if (!root.isMember("access_token") || !root.isMember("expires_in")) {
      log_->Write("TokenStore::Refresh: response doesn't contain access_token");
      return SASL_BADPROT;
    }
    access_ = root["access_token"].asString();
    int expiry_sec = stoi(root["expires_in"].asString());
    if (expiry_sec <= 0) {
      log_->Write("TokenStore::Refresh: invalid expiry");
      return SASL_BADPROT;
    }
    if (root.isMember("refresh_token")) {
      const std::string refresh_token = root["refresh_token"].asString();
      if (refresh_token != refresh_) {
        log_->Write(
            "TokenStore::Refresh: response includes updated refresh token");
        refresh_ = refresh_token;
      }
    }
    expiry_ = time(nullptr) + expiry_sec;
  } catch (const std::exception &e) {
    log_->Write("TokenStore::Refresh: exception=%s", e.what());
    return SASL_FAIL;
  }

  return Write();
}

TokenStore::TokenStore(Log *log, const std::string &path, bool enable_updates)
    : log_(log), path_(path), enable_updates_(enable_updates) {}

int TokenStore::Read() {
  refresh_.clear();
  access_.clear();
  expiry_ = 0;
  user_.clear();

  try {
    log_->Write("TokenStore::Read: file=%s", path_.c_str());

    std::ifstream file(path_);
    if (!file.good()) {
      log_->Write("TokenStore::Read: failed to open file %s: %s", path_.c_str(),
                  strerror(errno));
      return SASL_FAIL;
    }

    Json::Value root;
    file >> root;
    if (!root.isMember("refresh_token")) {
      log_->Write("TokenStore::Read: missing refresh_token");
      return SASL_FAIL;
    }

    ReadOverride(root, "client_id", &override_client_id_);
    ReadOverride(root, "client_secret", &override_client_secret_);
    ReadOverride(root, "token_endpoint", &override_token_endpoint_);
    ReadOverride(root, "proxy", &override_proxy_);
    ReadOverride(root, "ca_bundle_file", &override_ca_bundle_file_);
    ReadOverride(root, "ca_certs_dir", &override_ca_certs_dir_);

    if (root.isMember("refresh_window"))
      override_refresh_window_ = stoi(root["refresh_window"].asString());

    refresh_ = root["refresh_token"].asString();
    if (root.isMember("access_token"))
      access_ = root["access_token"].asString();
    if (root.isMember("expiry")) expiry_ = stoi(root["expiry"].asString());
    if (root.isMember("user")) user_ = root["user"].asString();

    log_->Write("TokenStore::Read: refresh=%s, access=%s, user=%s",
                refresh_.c_str(), access_.c_str(), user_.c_str());
    return SASL_OK;

  } catch (const std::exception &e) {
    log_->Write("TokenStore::Read: exception=%s", e.what());
    return SASL_FAIL;
  }
}

int TokenStore::Write() {
  const std::string new_path = path_ + "." + GetTempSuffix();

  if (!enable_updates_) {
    log_->Write("TokenStore::Write: skipping write to %s", new_path.c_str());
    return SASL_OK;
  }

  try {
    Json::Value root;
    root["refresh_token"] = refresh_;
    root["access_token"] = access_;
    root["expiry"] = std::to_string(expiry_);
    if (has_user()) root["user"] = user_;

    WriteOverride("client_id", override_client_id_, &root);
    WriteOverride("client_secret", override_client_secret_, &root);
    WriteOverride("token_endpoint", override_token_endpoint_, &root);
    WriteOverride("proxy", override_proxy_, &root);
    WriteOverride("ca_bundle_file", override_ca_bundle_file_, &root);
    WriteOverride("ca_certs_dir", override_ca_certs_dir_, &root);

    if (override_refresh_window_ > 0) {
      root["refresh_window"] = std::to_string(override_refresh_window_);
    }

    std::ofstream file(new_path);
    if (!file.good()) {
      log_->Write("TokenStore::Write: failed to open file %s for writing: %s",
                  new_path.c_str(), strerror(errno));
      return SASL_FAIL;
    }
    file << root;

  } catch (const std::exception &e) {
    log_->Write("TokenStore::Write: exception=%s", e.what());
    return SASL_FAIL;
  }

  if (rename(new_path.c_str(), path_.c_str()) != 0) {
    log_->Write("TokenStore::Write: rename failed with %s", strerror(errno));
    return SASL_FAIL;
  }

  return 0;
}

}  // namespace sasl_xoauth2
0707010000001E000081A40000000000000000000000016783F6CC00000726000000000000000000000000000000000000002400000000sasl-xoauth2-0.26/src/token_store.h/*
 * Copyright 2020 Google LLC
 *
 * 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.
 */

#ifndef SASL_XOAUTH2_TOKEN_STORE_H
#define SASL_XOAUTH2_TOKEN_STORE_H

#include <time.h>

#include <memory>
#include <string>

namespace sasl_xoauth2 {

class Log;

class TokenStore {
 public:
  static std::unique_ptr<TokenStore> Create(Log *log, const std::string &path,
                                            bool enable_updates = true);

  int GetAccessToken(std::string *token);
  int Refresh();

  std::string user() const { return user_; }
  bool has_user() const { return !user_.empty(); }

 private:
  TokenStore(Log *log, const std::string &path, bool enable_updates);

  int Read();
  int Write();

  Log *const log_ = nullptr;
  const std::string path_;
  const bool enable_updates_;

  // Normally these values come from the config file, but they can be overriden.
  std::string override_client_id_;
  std::string override_client_secret_;
  std::string override_token_endpoint_;
  std::string override_proxy_;
  std::string override_ca_bundle_file_;
  std::string override_ca_certs_dir_;
  int override_refresh_window_ = 0;

  std::string access_;
  std::string refresh_;
  std::string user_;
  time_t expiry_ = 0;

  int refresh_attempts_ = 0;
};

}  // namespace sasl_xoauth2

#endif  // SASL_XOAUTH2_TOKEN_STORE_H
0707010000001F000081A40000000000000000000000016783F6CC00004FD0000000000000000000000000000000000000002600000000sasl-xoauth2-0.26/src/xoauth2_test.cc// Copyright 2020 Google LLC
//
// 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.

#include <assert.h>
#include <json/json.h>
#include <sasl/sasl.h>
#include <sasl/saslplug.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include <string>
#include <vector>

#include "config.h"
#include "http.h"
#include "log.h"
#include "module.h"
#include "token_store.h"

const std::string kUserName = "abc@def.com";

constexpr char kTempFileTemplate[] = "/tmp/sasl_xoauth2_test_token.XXXXXX";
constexpr char kTokenTemplate[] =
    R"({"access_token": "%s", "refresh_token": "%s", "expiry": "%s"})";
constexpr char kTokenTemplateWithUser[] =
    R"({"access_token": "%s", "refresh_token": "%s", "expiry": "%s",
        "user": "%s"})";

constexpr char kServerPermanentError[] =
    R"({"status":"500","schemes":"Bearer","scope":"https://mail.google.com/"})";
constexpr char kServerTokenExpired[] =
    R"({"status":"401","schemes":"Bearer","scope":"https://mail.google.com/"})";

std::string s_password;
std::vector<std::string> s_cleanup_files;

FILE *OpenTempTokenFile() {
  char temp_template[sizeof(kTempFileTemplate)];
  strcpy(temp_template, kTempFileTemplate);
  int fd = mkstemp(temp_template);
  s_password = temp_template;
  s_cleanup_files.push_back(s_password);
  return fdopen(fd, "w");
}

void SetPasswordToValidToken() {
  FILE *f = OpenTempTokenFile();
  std::string expiry_str = std::to_string(time(nullptr) + 3600);
  fprintf(f, kTokenTemplate, "access", "refresh", expiry_str.c_str());
  fclose(f);
}

void SetPasswordToValidTokenWithUserOverride(const std::string &user) {
  FILE *f = OpenTempTokenFile();
  std::string expiry_str = std::to_string(time(nullptr) + 3600);
  fprintf(f, kTokenTemplateWithUser, "access", "refresh", expiry_str.c_str(),
          user.c_str());
  fclose(f);
}

void SetPasswordToInvalidToken() {
  FILE *f = OpenTempTokenFile();
  std::string expiry_str = std::to_string(0);
  fprintf(f, "invalid");
  fclose(f);
}

void SetPasswordToExpiredToken() {
  FILE *f = OpenTempTokenFile();
  std::string expiry_str = std::to_string(0);
  fprintf(f, kTokenTemplate, "access", "refresh", expiry_str.c_str());
  fclose(f);
}

void SetPasswordToInvalidPath() { s_password = "/tmp/this/path/is/not/valid"; }

void Cleanup() {
  for (const auto &file : s_cleanup_files) {
    unlink(file.c_str());
  }
}

int DefaultHttpIntercept(sasl_xoauth2::HttpPostOptions options) {
  fprintf(stderr, "TEST: default http intercept for url=%s\n",
          options.url.c_str());
  return SASL_FAIL;
}

#define TEST_ABORT(x)                                                     \
  do {                                                                    \
    bool __result = (x);                                                  \
    if (!__result) {                                                      \
      fprintf(stderr, "TEST ASSERTION FAILED at %s:%d: %s -- ABORTING\n", \
              __FILE__, __LINE__, #x);                                    \
      Cleanup();                                                          \
      exit(1);                                                            \
    }                                                                     \
  } while (0)

#define TEST_ASSERT(x)                                                  \
  do {                                                                  \
    bool __result = (x);                                                \
    if (!__result) {                                                    \
      fprintf(stderr, "TEST ASSERTION FAILED at %s:%d: %s\n", __FILE__, \
              __LINE__, #x);                                            \
      return false;                                                     \
    }                                                                   \
  } while (0)

#define TEST_ASSERT_OK(x)                                                 \
  do {                                                                    \
    int __result = (x);                                                   \
    if (__result != SASL_OK) {                                            \
      fprintf(stderr, "TEST ASSERTION FAILED at %s:%d: %s returned %d\n", \
              __FILE__, __LINE__, #x, __result);                          \
      return false;                                                       \
    }                                                                     \
  } while (0)

void FakeFree(void *ptr) { free(ptr); }

void *FakeMalloc(size_t size) { return malloc(size); }

int FakeGetAuthName(void *, int id, const char **result, unsigned int *len) {
  assert(id == SASL_CB_AUTHNAME);
  *result = kUserName.c_str();
  *len = kUserName.size();
  return 0;
}

int FakeGetPassword(sasl_conn_t *, void *, int id, sasl_secret_t **pass) {
  assert(id == SASL_CB_PASS);
  auto *p = static_cast<sasl_secret_t *>(
      malloc(sizeof(sasl_secret_t) + s_password.size() + 1));
  p->len = s_password.size();
  strcpy(reinterpret_cast<char *>(p->data), s_password.c_str());
  *pass = p;
  return SASL_OK;
}

int FakeGetCallbackAll(sasl_conn_t *, unsigned long id, sasl_callback_ft *ft,
                       void **context) {
  if (id == SASL_CB_AUTHNAME)
    *ft = reinterpret_cast<sasl_callback_ft>(&FakeGetAuthName);
  else if (id == SASL_CB_PASS)
    *ft = reinterpret_cast<sasl_callback_ft>(&FakeGetPassword);
  else
    return SASL_FAIL;
  return SASL_OK;
}

int FakeGetCallbackNone(sasl_conn_t *, unsigned long, sasl_callback_ft *,
                        void **) {
  return SASL_FAIL;
}

int FakeCanonUser(sasl_conn_t *, const char *, unsigned int, unsigned int,
                  sasl_out_params_t *) {
  return SASL_OK;
}

class PlugCleanup {
 public:
  PlugCleanup(sasl_utils_t *utils, sasl_client_plug_t plug, void *context)
      : utils_(utils), plug_(plug), context_(context) {}

  ~PlugCleanup() { plug_.mech_dispose(context_, utils_); }

 private:
  sasl_utils_t *utils_;
  sasl_client_plug_t plug_;
  void *context_;
};

void PrintTestName(const char *name) {
  fprintf(stderr, "\n");
  fprintf(stderr, "TEST: %s\n", name);
  fprintf(stderr, "%s\n", std::string(strlen(name) + 6, '=').c_str());
}

bool TestWithPrompts(sasl_client_plug_t plug) {
  PrintTestName(__func__);
  SetPasswordToValidToken();
  sasl_xoauth2::SetHttpInterceptForTesting(&DefaultHttpIntercept);

  sasl_utils_t utils = {};
  utils.free = &FakeFree;
  utils.getcallback = &FakeGetCallbackNone;
  utils.malloc = &FakeMalloc;

  void *context = nullptr;
  TEST_ASSERT_OK(plug.mech_new(nullptr, nullptr, &context));
  PlugCleanup _(&utils, plug, context);

  sasl_client_params_t params = {};
  params.utils = &utils;
  params.canon_user = &FakeCanonUser;

  auto *prompt_need =
      static_cast<sasl_interact_t *>(malloc(sizeof(sasl_interact_t) * 3));
  memset(prompt_need, 0, sizeof(sasl_interact_t) * 3);

  sasl_interact_t *cred_auth_name = prompt_need + 0;
  cred_auth_name->id = SASL_CB_AUTHNAME;
  cred_auth_name->result = kUserName.data();
  cred_auth_name->len = kUserName.size();

  sasl_interact_t *cred_pass = prompt_need + 1;
  cred_pass->id = SASL_CB_PASS;
  cred_pass->result = s_password.data();
  cred_pass->len = s_password.size();

  sasl_interact_t *end = prompt_need + 2;
  end->id = SASL_CB_LIST_END;

  const char *to_server = nullptr;
  unsigned int to_server_len = 0;
  sasl_out_params_t out_params = {};

  TEST_ASSERT_OK(plug.mech_step(context, &params, nullptr, 0, &prompt_need,
                                &to_server, &to_server_len, &out_params));
  fprintf(stderr, "to_server=[%s], len=%d\n", to_server, to_server_len);
  TEST_ASSERT(strstr(to_server, "Bearer") != nullptr);
  TEST_ASSERT(strstr(to_server, kUserName.c_str()) != nullptr);

  TEST_ASSERT_OK(plug.mech_step(context, &params, "", 0, nullptr, &to_server,
                                &to_server_len, &out_params));
  TEST_ASSERT(to_server_len == 0);

  return true;
}

bool TestWithoutPrompts(sasl_client_plug_t plug) {
  PrintTestName(__func__);
  SetPasswordToValidToken();
  sasl_xoauth2::SetHttpInterceptForTesting(&DefaultHttpIntercept);

  sasl_utils_t utils = {};
  utils.free = &FakeFree;
  utils.getcallback = &FakeGetCallbackNone;
  utils.malloc = &FakeMalloc;

  void *context = nullptr;
  TEST_ASSERT_OK(plug.mech_new(nullptr, nullptr, &context));
  PlugCleanup _(&utils, plug, context);

  sasl_client_params_t params = {};
  params.utils = &utils;
  params.canon_user = &FakeCanonUser;

  auto *prompt_need =
      static_cast<sasl_interact_t *>(malloc(sizeof(sasl_interact_t) * 1));
  memset(prompt_need, 0, sizeof(sasl_interact_t) * 1);

  sasl_interact_t *end = prompt_need + 0;
  end->id = SASL_CB_LIST_END;

  const char *to_server = nullptr;
  unsigned int to_server_len = 0;
  sasl_out_params_t out_params = {};

  int err = plug.mech_step(context, &params, nullptr, 0, &prompt_need,
                           &to_server, &to_server_len, &out_params);
  TEST_ASSERT(err == SASL_INTERACT);
  TEST_ASSERT(to_server_len == 0);

  sasl_interact_t *p = prompt_need;
  while (p && p->id != SASL_CB_LIST_END) {
    fprintf(stderr, "p: id=%lu, challenge=%s\n", p->id, p->challenge);
    if (p->id == SASL_CB_AUTHNAME) {
      p->result = kUserName.c_str();
      p->len = kUserName.size();
    } else if (p->id == SASL_CB_PASS) {
      p->result = s_password.c_str();
      p->len = s_password.size();
    } else {
      fprintf(stderr, "unexpected id=%lu\n", p->id);
      TEST_ASSERT(false);
    }
    p++;
  }

  TEST_ASSERT_OK(plug.mech_step(context, &params, nullptr, 0, &prompt_need,
                                &to_server, &to_server_len, &out_params));
  fprintf(stderr, "to_server=[%s], len=%d\n", to_server, to_server_len);
  TEST_ASSERT(strstr(to_server, "Bearer") != nullptr);
  TEST_ASSERT(strstr(to_server, kUserName.c_str()) != nullptr);

  TEST_ASSERT_OK(plug.mech_step(context, &params, "", 0, nullptr, &to_server,
                                &to_server_len, &out_params));
  TEST_ASSERT(to_server_len == 0);

  return true;
}

bool TestWithCallbacks(sasl_client_plug_t plug) {
  PrintTestName(__func__);
  SetPasswordToValidToken();
  sasl_xoauth2::SetHttpInterceptForTesting(&DefaultHttpIntercept);

  sasl_utils_t utils = {};
  utils.free = &FakeFree;
  utils.getcallback = &FakeGetCallbackAll;
  utils.malloc = &FakeMalloc;

  void *context = nullptr;
  TEST_ASSERT_OK(plug.mech_new(nullptr, nullptr, &context));
  PlugCleanup _(&utils, plug, context);

  sasl_client_params_t params = {};
  params.utils = &utils;
  params.canon_user = &FakeCanonUser;

  const char *to_server = nullptr;
  unsigned int to_server_len = 0;
  sasl_out_params_t out_params = {};

  TEST_ASSERT_OK(plug.mech_step(context, &params, nullptr, 0, nullptr,
                                &to_server, &to_server_len, &out_params));
  fprintf(stderr, "to_server=[%s], len=%d\n", to_server, to_server_len);
  TEST_ASSERT(strstr(to_server, "Bearer") != nullptr);
  TEST_ASSERT(strstr(to_server, kUserName.c_str()) != nullptr);

  TEST_ASSERT_OK(plug.mech_step(context, &params, "", 0, nullptr, &to_server,
                                &to_server_len, &out_params));
  TEST_ASSERT(to_server_len == 0);

  return true;
}

bool TestWithCallbacksAndUserOverride(sasl_client_plug_t plug) {
  const std::string kUserNameOverride = "override@foo.com";

  PrintTestName(__func__);
  SetPasswordToValidTokenWithUserOverride(kUserNameOverride);
  sasl_xoauth2::SetHttpInterceptForTesting(&DefaultHttpIntercept);

  sasl_utils_t utils = {};
  utils.free = &FakeFree;
  utils.getcallback = &FakeGetCallbackAll;
  utils.malloc = &FakeMalloc;

  void *context = nullptr;
  TEST_ASSERT_OK(plug.mech_new(nullptr, nullptr, &context));
  PlugCleanup _(&utils, plug, context);

  sasl_client_params_t params = {};
  params.utils = &utils;
  params.canon_user = &FakeCanonUser;

  const char *to_server = nullptr;
  unsigned int to_server_len = 0;
  sasl_out_params_t out_params = {};

  TEST_ASSERT_OK(plug.mech_step(context, &params, nullptr, 0, nullptr,
                                &to_server, &to_server_len, &out_params));
  fprintf(stderr, "to_server=[%s], len=%d\n", to_server, to_server_len);
  TEST_ASSERT(strstr(to_server, "Bearer") != nullptr);
  TEST_ASSERT(strstr(to_server, kUserName.c_str()) == nullptr);
  TEST_ASSERT(strstr(to_server, kUserNameOverride.c_str()) != nullptr);

  TEST_ASSERT_OK(plug.mech_step(context, &params, "", 0, nullptr, &to_server,
                                &to_server_len, &out_params));
  TEST_ASSERT(to_server_len == 0);

  return true;
}

bool TestWithPermanentError(sasl_client_plug_t plug) {
  PrintTestName(__func__);
  SetPasswordToValidToken();
  sasl_xoauth2::SetHttpInterceptForTesting(&DefaultHttpIntercept);

  sasl_utils_t utils = {};
  utils.free = &FakeFree;
  utils.getcallback = &FakeGetCallbackAll;
  utils.malloc = &FakeMalloc;

  void *context = nullptr;
  TEST_ASSERT_OK(plug.mech_new(nullptr, nullptr, &context));
  PlugCleanup _(&utils, plug, context);

  sasl_client_params_t params = {};
  params.utils = &utils;
  params.canon_user = &FakeCanonUser;

  const char *to_server = nullptr;
  unsigned int to_server_len = 0;
  sasl_out_params_t out_params = {};

  TEST_ASSERT_OK(plug.mech_step(context, &params, nullptr, 0, nullptr,
                                &to_server, &to_server_len, &out_params));
  fprintf(stderr, "to_server=[%s], len=%d\n", to_server, to_server_len);
  TEST_ASSERT(strstr(to_server, "Bearer") != nullptr);
  TEST_ASSERT(strstr(to_server, kUserName.c_str()) != nullptr);

  int err = plug.mech_step(context, &params, kServerPermanentError,
                           sizeof(kServerPermanentError), nullptr, &to_server,
                           &to_server_len, &out_params);
  fprintf(stderr, "err=%d\n", err);
  fprintf(stderr, "to_server=[%s], len=%d\n", to_server, to_server_len);
  TEST_ASSERT(err != SASL_OK);
  TEST_ASSERT(to_server_len == 0);

  return true;
}

bool TestWithTokenExpiredError(sasl_client_plug_t plug) {
  PrintTestName(__func__);
  SetPasswordToValidToken();
  sasl_xoauth2::SetHttpInterceptForTesting(&DefaultHttpIntercept);

  sasl_utils_t utils = {};
  utils.free = &FakeFree;
  utils.getcallback = &FakeGetCallbackAll;
  utils.malloc = &FakeMalloc;

  void *context = nullptr;
  TEST_ASSERT_OK(plug.mech_new(nullptr, nullptr, &context));
  PlugCleanup _(&utils, plug, context);

  sasl_client_params_t params = {};
  params.utils = &utils;
  params.canon_user = &FakeCanonUser;

  const char *to_server = nullptr;
  unsigned int to_server_len = 0;
  sasl_out_params_t out_params = {};

  TEST_ASSERT_OK(plug.mech_step(context, &params, nullptr, 0, nullptr,
                                &to_server, &to_server_len, &out_params));
  fprintf(stderr, "to_server=[%s], len=%d\n", to_server, to_server_len);
  TEST_ASSERT(strstr(to_server, "Bearer") != nullptr);
  TEST_ASSERT(strstr(to_server, kUserName.c_str()) != nullptr);

  bool intercept_called = false;
  sasl_xoauth2::SetHttpInterceptForTesting(
      [&intercept_called](sasl_xoauth2::HttpPostOptions options) {
        *options.response = R"({"access_token": "access", "expires_in": 3600})";
        *options.response_code = 200;
        intercept_called = true;
        return SASL_OK;
      });

  int err = plug.mech_step(context, &params, kServerTokenExpired,
                           sizeof(kServerTokenExpired), nullptr, &to_server,
                           &to_server_len, &out_params);
  fprintf(stderr, "err=%d\n", err);
  fprintf(stderr, "to_server=[%s], len=%d\n", to_server, to_server_len);
  TEST_ASSERT(err == SASL_TRYAGAIN);
  TEST_ASSERT(to_server_len == 0);
  TEST_ASSERT(intercept_called);

  return true;
}

bool TestPreemptiveTokenRefresh(sasl_client_plug_t plug) {
  PrintTestName(__func__);
  SetPasswordToExpiredToken();
  sasl_xoauth2::SetHttpInterceptForTesting(&DefaultHttpIntercept);

  sasl_utils_t utils = {};
  utils.free = &FakeFree;
  utils.getcallback = &FakeGetCallbackAll;
  utils.malloc = &FakeMalloc;

  void *context = nullptr;
  TEST_ASSERT_OK(plug.mech_new(nullptr, nullptr, &context));
  PlugCleanup _(&utils, plug, context);

  sasl_client_params_t params = {};
  params.utils = &utils;
  params.canon_user = &FakeCanonUser;

  const char *to_server = nullptr;
  unsigned int to_server_len = 0;
  sasl_out_params_t out_params = {};

  bool intercept_called = false;
  sasl_xoauth2::SetHttpInterceptForTesting(
      [&intercept_called](sasl_xoauth2::HttpPostOptions options) {
        *options.response =
            R"({"access_token": "refreshed_access", "expires_in": 3600})";
        *options.response_code = 200;
        intercept_called = true;
        return SASL_OK;
      });

  TEST_ASSERT_OK(plug.mech_step(context, &params, nullptr, 0, nullptr,
                                &to_server, &to_server_len, &out_params));
  fprintf(stderr, "to_server=[%s], len=%d\n", to_server, to_server_len);
  TEST_ASSERT(strstr(to_server, "Bearer") != nullptr);
  TEST_ASSERT(strstr(to_server, "refreshed_access") != nullptr);
  TEST_ASSERT(strstr(to_server, kUserName.c_str()) != nullptr);
  TEST_ASSERT(intercept_called);

  TEST_ASSERT_OK(plug.mech_step(context, &params, "", 0, nullptr, &to_server,
                                &to_server_len, &out_params));
  TEST_ASSERT(to_server_len == 0);

  return true;
}

bool TestFailedPreemptiveTokenRefresh(sasl_client_plug_t plug) {
  PrintTestName(__func__);
  SetPasswordToExpiredToken();
  sasl_xoauth2::SetHttpInterceptForTesting(&DefaultHttpIntercept);

  sasl_utils_t utils = {};
  utils.free = &FakeFree;
  utils.getcallback = &FakeGetCallbackAll;
  utils.malloc = &FakeMalloc;

  void *context = nullptr;
  TEST_ASSERT_OK(plug.mech_new(nullptr, nullptr, &context));
  PlugCleanup _(&utils, plug, context);

  sasl_client_params_t params = {};
  params.utils = &utils;
  params.canon_user = &FakeCanonUser;

  const char *to_server = nullptr;
  unsigned int to_server_len = 0;
  sasl_out_params_t out_params = {};

  bool intercept_called = false;
  sasl_xoauth2::SetHttpInterceptForTesting(
      [&intercept_called](sasl_xoauth2::HttpPostOptions options) {
        *options.response = "";
        *options.response_code = 400;
        intercept_called = true;
        return SASL_OK;
      });

  int err = plug.mech_step(context, &params, nullptr, 0, nullptr, &to_server,
                           &to_server_len, &out_params);
  fprintf(stderr, "err=%d\n", err);
  TEST_ASSERT(err != SASL_OK);
  TEST_ASSERT(to_server_len == 0);
  TEST_ASSERT(intercept_called);

  return true;
}

int main(int argc, char **argv) {
  sasl_xoauth2::EnableLoggingForTesting();

  Json::Value config;
  config["client_id"] = "dummy client id";
  config["client_secret"] = "dummy client secret";
  sasl_xoauth2::Config::EnableLoggingToStderr();
  TEST_ASSERT_OK(sasl_xoauth2::Config::InitForTesting(config));

  int version = 0;
  sasl_client_plug_t *plug_list = nullptr;
  int plug_count = 0;

  sasl_utils_t utils = {};
  utils.free = &FakeFree;
  utils.getcallback = &FakeGetCallbackNone;
  utils.malloc = &FakeMalloc;

  TEST_ABORT(sasl_client_plug_init(&utils, SASL_CLIENT_PLUG_VERSION, &version,
                                   &plug_list, &plug_count) == SASL_OK);
  fprintf(stderr,
          "INIT: sasl_client_plug_init returned version=%d, plug_count=%d\n",
          version, plug_count);
  TEST_ABORT(plug_count == 1);

  sasl_client_plug_t plug = *plug_list;
  fprintf(stderr, "INIT: module name: %s\n", plug.mech_name);

  TEST_ABORT(TestWithPrompts(plug));
  TEST_ABORT(TestWithoutPrompts(plug));
  TEST_ABORT(TestWithCallbacks(plug));
  TEST_ABORT(TestWithCallbacksAndUserOverride(plug));
  TEST_ABORT(TestWithPermanentError(plug));
  TEST_ABORT(TestWithTokenExpiredError(plug));
  TEST_ABORT(TestPreemptiveTokenRefresh(plug));
  TEST_ABORT(TestFailedPreemptiveTokenRefresh(plug));

  Cleanup();
  fprintf(stderr, "\nALL TESTS PASS.\n");

  return 0;
}
07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!249 blocks
openSUSE Build Service is sponsored by