Trying to authenticate with smtp.gmail.com via XOAUTH2

375 Views Asked by At

I'm using PHPMailer to authenticate via XOAUTH2 with smtp.gmail.com in order to send an email. However, I'm getting an authentication error with an inadequate response:

2023-11-17 06:32:29 SERVER -> CLIENT: 250-smtp.gmail.com at your service, [2402:b280:a1f:ba65:ce96:e5ff:feee:f13e]250-SIZE 35882577250-8BITMIME250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH250-ENHANCEDSTATUSCODES250-PIPELINING250-CHUNKING250 SMTPUTF8
2023-11-17 06:32:29 CLIENT -> SERVER: AUTH XOAUTH2 dXNlcj1tdXJyYXlAZm9jdXMtY29tcHV0aW5nLmNvbS5hdQFhdXRoPUJlYXJlciB5YTI5LmEwQWZCX2J5QXAxU2twbzdJSXBGTVJLWVNPYlZqWXZMaVBCZkVXLUUweEtwOUVmUTNHTWhnV3d5UDUwM09VdGY2NVZkeW0xcHNsd05lR0JLZElwNUI4eFFISjJZdC00U1o5amQxWGtIUjBVZ0tMN29wZWt5NlJzZFFfcHRVSmU4RGU2X0dYbS1xN2lmcVFzV1VIRTI2ZHl5OUhKNWxudE1NQ2kybkhpd2FDZlEzeFY2Q1hLcmpMU0VXbnRKUEEwMTczAQE=
2023-11-17 06:32:30 SERVER -> CLIENT: 334 eyJzdGF0dXMiOiI0MDAiLCJzY2hlbWVzIjoiQmVhcmVyIiwic2NvcGUiOiJodHRwczov9vZ2xlLmNvbS8ifQ==
2023-11-17 06:32:30 SMTP ERROR: AUTH command failed: 334 eyJzdGF0dXMiOiI0MDAiLCJzY2hlbWVzIjoiQmVhcmVyIiwic2NvovL21haWwuZ29vZ2xlLmNvbS8ifQ==
SMTP Error: Could not authenticate.
2023-11-17 06:32:30 CLIENT -> SERVER: QUIT
2023-11-17 06:32:30 SERVER -> CLIENT: 535-5.7.8 Username and Password not accepted. Learn more at535 5.7.8 https://support.google.com/mail/?p=BadCredentials k6-20020a17090a404600b00277371fd346sm2482723pjg.30 - gsmtp
2023-11-17 06:32:30 SMTP ERROR: QUIT command failed: 535-5.7.8 Username and Password not accepted. Learn more at535 5.7.8 https://support.google.com/mail/?p=BadCredentials k6-20020a17090a404600b00277371fd346sm2482723pjg.30 - gsmtp
SMTP Error: Could not authenticate.

When using XOAUTH2 I'm sending a username and an access token, not a password. So why are these messages indicating in valid username and password credentials. The username and access token are correct, so these responses are really unhelpful.

Can we please get some better messages or some helpful information from Gmail about why this isn't authenticating?

Section of the code I am using:

$mailer = new PHPMailer(true);
$mailer->SMTPDebug = SMTP::DEBUG_SERVER;
$mailer->isSMTP();        
$mailer->Host = ini_get("SMTP");
$mailer->Port = ini_get("smtp_port");
$mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mailer->SMTPAuth   = true;
$mailer->Username   = null;
$mailer->Password   = null;
$mailer->AuthType = $smtp["authtype"]; // "XOAUTH2"

$oauth = new MyOAuthTokenProvider();
$oauth->setEmail($smtp["username"]);
$oauth->setAccessToken($smtp["accessToken"]);        
$mailer->setOAuth($oauth);

$mailer->SMTPKeepAlive = true;
$mailer->From = $headers["From"];
$mailer->addReplyTo($headers["From"]);
$mailer->addAddress($recipient);
$mailer->isHTML(true);
$mailer->Subject = $headers["Subject"];
$mailer->Body = $body;           
$mailer->send();


class MyOAuthTokenProvider implements OAuthTokenProvider {
    protected $email;
    protected $accessToken;

    public function setEmail($value) {
        $this->email = $value;
    }
    public function setAccessToken($value) {
        $this->accessToken = $value;
    }

    /**
     * @see \PHPMailer\PHPMailer\OAuth::getOauth64()
     */
    public function getOauth64(): string
    {
        return base64_encode(
            'user=' .
            $this->email .
            "\001auth=Bearer " .
            $this->accessToken .
            "\001\001"
        );
    }
}

EDIT: Linda was really helpful sharing her code, however this only works where the security scope allows access to all your Gmail mailboxes and for sending email. The limited scopes simply don't work, none of them. This is a major issue for us as it breaches the ISO27001 standard we are wanting to meet, which requires that access to data should only be required for the functions necessary.

This now remains an issue firmly with Google/Gmail that they need to fix their security scopes.

2

There are 2 best solutions below

4
On

This is my sample for SMTP XOauth2 with PHP.

It requires that you have a client id and client secret for an installed app created on Google cloud console with gmail enabled.

You also need a refresh token. This code doesn't create the initial refresh token. I recommend using either oauth2 playground or something like the php client library to create that.

//Import PHPMailer classes into the global namespace
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
use PHPMailer\PHPMailer\OAuth;
//Alias the League Google OAuth2 provider class
// composer require league/oauth2-google
use League\OAuth2\Client\Provider\Google;


try{
    $client = getClient();
    print_r($client->getClientId());
    print_r($client->getClientSecret());
    print_r($client->getRefreshToken());

    //Use league/oauth2-client as OAuth2 token provider
    //                Fill in authentication details here
    //                Either the gmail account owner, or the user that gave consent
    $email = EMAIL_FROM;
    $clientId = $client->getClientId();
    $clientSecret = $client->getClientSecret();
    $refreshToken = $client->getRefreshToken();
    $send_to = EMAIL_TO;

    //Create a new PHPMailer instance
    $mail = new PHPMailer();

    //Tell PHPMailer to use SMTP
    $mail->isSMTP();

    //Enable SMTP debugging
    //SMTP::DEBUG_OFF = off (for production use)
    //SMTP::DEBUG_CLIENT = client messages
    //SMTP::DEBUG_SERVER = client and server messages
    $mail->SMTPDebug = SMTP::DEBUG_SERVER;

    //Set the hostname of the mail server
    $mail->Host = 'smtp.gmail.com';

    //Set the SMTP port number:
    // - 465 for SMTP with implicit TLS, a.k.a. RFC8314 SMTPS or
    // - 587 for SMTP+STARTTLS
    $mail->Port = 465;

    //Set the encryption mechanism to use:
    // - SMTPS (implicit TLS on port 465) or
    // - STARTTLS (explicit TLS on port 587)
    $mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;

    //Whether to use SMTP authentication
    $mail->SMTPAuth = true;

    //Set AuthType to use XOAUTH2
    $mail->AuthType = 'XOAUTH2';

    //Obtained by configuring and running get_oauth_token.php
    //after setting up an app in Google Developer Console.

    //Create a new OAuth2 provider instance
    $provider = new Google(
        [
            'clientId' => $clientId,
            'clientSecret' => $clientSecret,
        ]
    );

    //Pass the OAuth provider instance to PHPMailer
    $mail->setOAuth(
        new OAuth(
            [
                'provider' => $provider,
                'clientId' => $clientId,
                'clientSecret' => $clientSecret,
                'refreshToken' => $refreshToken,
                'userName' => $email,
            ]
        )
    );

    //Set who the message is to be sent from
    //For gmail, this generally needs to be the same as the user you logged in as
    $mail->setFrom($email, 'First Last');

    //Set who the message is to be sent to
    $mail->addAddress('[REDACTED]', 'John Doe');

    //Set the subject line
    $mail->Subject = 'PHPMailer GMail XOAUTH2 SMTP test';

    //Read an HTML message body from an external file, convert referenced images to embedded,
    //convert HTML into a basic plain-text alternative body
    $mail->CharSet = PHPMailer::CHARSET_UTF8;
    $mail->msgHTML(file_get_contents('test.html'), __DIR__);

    //Replace the plain text body with one created manually
    $mail->AltBody = 'This is a plain-text message body';

    //Attach an image file
    $mail->addAttachment('images/phpmailer_mini.png');

    //send the message, check for errors
    if (!$mail->send()) {
        echo 'Mailer Error: ' . $mail->ErrorInfo;
    } else {
        echo 'Message sent!';
    }
}
catch(Exception $e) {
    // TODO(developer) - handle error appropriately
    echo 'Message: ' .$e->getMessage();
}

debug

2023-11-30 12:59:21 SERVER -> CLIENT: 220 smtp.gmail.com ESMTP n10-20020a05600c4f8a00b004053e9276easm5559806wmq.32 - gsmtp
2023-11-30 12:59:21 CLIENT -> SERVER: EHLO DESKTOP-R0C0P2L
2023-11-30 12:59:21 SERVER -> CLIENT: 250-smtp.gmail.com at your service, [86.52.89.221]
                                      250-SIZE 35882577
                                      250-8BITMIME
                                      250-AUTH LOGIN PLAIN XOAUTH2 PLAIN-CLIENTTOKEN OAUTHBEARER XOAUTH
                                      250-ENHANCEDSTATUSCODES
                                      250-PIPELINING
                                      250-CHUNKING
                                      250 SMTPUTF8
2023-11-30 12:59:22 CLIENT -> SERVER: AUTH XOAUTH2 [REDACTED]
2023-11-30 12:59:22 SERVER -> CLIENT: 235 2.7.0 Accepted
0
On

I got the same problem when I used the scope $client->addScope('https://www.googleapis.com/auth/gmail.send'); to get the refresh token

but when i changed that to $client->addScope('https://mail.google.com'); and got a new refresh token it worked fine, here is my function to get a refresh token

$client = new Google_Client();
$client->setClientId('YOUR GOOGLE CLIENT ID');
$client->setClientSecret('YOUR GOOGLE CLIENT SECRET');
$client->setRedirectUri('YOUR REDIRECT URI');
// this didnt work $client->addScope('https://www.googleapis.com/auth/gmail.send');
$client->addScope('https://mail.google.com');
$client->setAccessType('offline'); // Important to get the refresh token
$client->setApprovalPrompt('force');

if (!isset($_GET['code'])) {
    $auth_url = $client->createAuthUrl();
    header('Location: ' . filter_var($auth_url, FILTER_SANITIZE_URL));
} else {
    $client->authenticate($_GET['code']);
    $_SESSION['access_token'] = $client->getAccessToken();
    $refreshToken = $client->getRefreshToken();

    echo "Refresh token: " . $refreshToken;
    // Save this refresh token securely in your application
}