TheNetworg/oauth2-azure

Invalid_grant AADSTS9002313: Invalid request.Request is malformed or invalid

decomplexity opened this issue · 10 comments

[Shadow post]
I am trying to get PHPMailer to authenticate with SMTP AUTH. I am using the thephpleague’s OAuth2 and thenetworg’s Azure provider via MSFT’s V2 authorisation and token endpoints.
I receive the Invalid Grant error (above).
To avoid double-posting, more detail is the thephpleague’s OAuth2 Issues #850
https://github.com/thephpleague/oauth2-client/issues/new

Original: thephpleague/oauth2-client#850

Could you please post a sample code which you are using to obtain the token and the version of oauth2-azure which you are using?

/cc: @decomplexity

Tnx Jan.
I am using V1.4.2 of oauth2-azure.
Authorising code follows. I an obviously doing something stupid and I apologise in advance!

require 'vendor/autoload.php';
require_once('vendor/phpmailer/phpmailer/src/OAuth.php');
require_once('vendor/phpmailer/phpmailer/src/PHPMailer.php');
require_once('vendor/phpmailer/phpmailer/src/SMTP.php');
require_once('vendor/phpmailer/phpmailer/src/Exception.php');
require_once('vendor/thenetworg/oauth2-azure/src/Provider/Azure.php');

use phpmailer\phpmailer\OAuth;
use phpmailer\phpmailer\PHPMailer;
use phpmailer\phpmailer\SMTP;
use phpmailer\phpmailer\Exception;
use TheNetworg\OAuth2\Client\Provider\Azure;


$username = [my email address – which is also the Azure AD userid];    
$clientId = [from Azure AD];
$clientSecret = [from Azure AD];
$redirectURI = [the URL of my get_oauth_token  module];
$refreshToken = ‘0.[51 characters].[696 characters]’;

$mail = new PHPMailer;
$mail->isSMTP();                                      
$mail->Host = 'smtp.office365.com';
$mail->SMTPAuth = true;
$mail->AuthType = 'XOAUTH2';
$mail->SMTPDebug = SMTP::DEBUG_LOWLEVEL;

$provider = new Azure([
    'clientId'     => $clientId,
    'clientSecret' => $clientSecret,
    'redirectUri'  => $redirectURI
    ]);

$provider->urlAPI = "https://graph.microsoft.com/";
$provider->scope = "openid  SMTP.Send Mail.Send offline_access profile email User.Read";
$provider->defaultEndPointVersion = TheNetworg\OAuth2\Client\Provider\Azure::ENDPOINT_VERSION_2_0;

$mail->setOAuth(
        new OAuth(
            [
                'provider'     => $provider,
                'clientId'     => $clientId,
                'clientSecret' => $clientSecret,
                'refreshToken' => $refreshToken, 
                'userName'     => $username
            ]
        )
   );

$mail -> refresh_token = $refreshToken;
$mail -> Username = $username;               
$mail->  Password = [my email password]; 
$mail->  SMTPSecure = 'tls';
$mail->  Port = 587;


[more PHPMailer stuff]

So couple questions... Why are you using SMTP sending instead of Microsoft Graph - which is more preferred way of sending mail? I would suggest to rely on client_credentials flow instead, since refresh tokens can get invalidated due to password change and also expire every 90 days unless set otherwise.

I noticed that you have the refresh_token hardcoded inside the code, are you sure you have the refresh token with all the required information? From which endpoint does the refresh token come from? V2? I guess so, because you are including scopes, which aren't supported in V1. The way to set v2 endpoint with v1.4.2 is similar to this don't use the p param.

Tnx Jan.
Some background is appended below, but to pick up on your points:

  • OAuth2 client credentials grant flow is not supported by MSFT for SMTP AUTH with Oauth2 (MSFT announcement 30th April). For Contact Forms and the like (see below) we would appear to need ROPC grants

  • my initial refresh token came from my get_oauth_token.php pointing at V2 authorisation and token endpoints.
    Am I sure I have all the required information? No!! – and I suspect that is my problem. It is not obvious from the documentation whether the $refresh_token (as copied to my PHPMailer authorisation module from running get_oauth_token) should be a full JWT or just its central payload, and whether other parameters passed in the URI return from get_oauth_token such as &state and &session_state should be included. And if a hard-coded refresh token (obtained via get_oauth_token) is needed to initialise the system, how future refresh tokens can be automatically obtained (i.e. without offline manual intervention). I should be most grateful if you could clarify any of this.

  • MSFT Graph is obviously the way to go longer term as there is a lot of functionality there built on the best mail system on the planet (which must annoy Google, but even GSuite can only offer EAS to email/calendar/contacts desktop clients instead of full-blown MAPI over HTTPS; and EAS cannot for example synch distribution lists) and we have a pilot project to implement it.
    But we still, of course, need openid and Oauth2 irrespective of whatever mail transport we use.

  • at your suggestion, we have tried the ‘AAD B2C Experimental’ modification of using “idtoken” as scope, but the error we experience is occurring before the endpoint has tried to interpret ‘scope’ (verified by using a nonce scope!)

Background
We also have a huge collection of older PHPMail apps, ranging from simple Contact Forms and PayPal IPNs (where the user doesn’t get authenticated) to ones internal to the wider business where we want to authenticate the user (which we do by kludgy means at present and where the multi-tenant AD account type would be useful).

Hence our tests to see if a straightforward authentication change to PHPMailer were possible, leaving all the PHPMailer header, To/Ccc/Bcc, AddUser etc etc stuff intact.

The trial of amending PHPMailer to support Oauth2 were partly driven by the need for better security but mainly by MSFT deprecating SMTP AUTH Basic Authentication with its cessation planned for Q3'ish 2021. And a MSFT bombshell post a few days ago states that new tenants will be blocked by default from SMTP AUTH using both Basic Authentication AND Oauth2, – i.e. they will block it at the protocol level. Although this block can be unset using Powershell, it will cause lots of “it has just stopped working” problems for those who don’t realise what has happened.

The StevenMaguire provider for PHPMailer that has been used for several years for Hotmail, Windows Live Mail and similar is failing with V2 endpoints, and MSFT say that V2 is a prerequisite for SMTP AUTH with Oauth2. Hence our trial of your Azure provider.

Yes, I am aware of the SMTP not supporting client_credentials, that was mostly aimed at Microsoft Graph usage.

The refresh token should be the full refresh token, you should never make changes to it, since it will break the token. State and session_state don't need to be included.

If you were to use MS Graph, you could use client_credentials. But let's focus on this to work with SMTP:

  1. Can you please show me the contents of get_oauth_token - eg. what it does?
  2. I wouldn't change the scopes, just use these:
$provider->pathAuthorize = "/oauth2/v2.0/authorize";
$provider->pathToken = "/oauth2/v2.0/token";

and I would suggest also this one:

$provider->tenant = "<tenant-id>"; // Either the GUID or one of your domains.

Also, the provider for requesting the token should have same parameters as the one used with phpmailer. I will try to make a demo for you over the weekend (I won't be able to do it sooner, sorry) if we don't managed to figure it out correctly.

Also, I just noticed... You are doing

$provider->defaultEndPointVersion = TheNetworg\OAuth2\Client\Provider\Azure::ENDPOINT_VERSION_2_0;

which is not supported with v1.4.2 but dev-master version. This could be the confusion.

Since in dev-master there were some breaking changes, it is rather a v2 candidate not to break everyone's code. Please check the v1.4.2 docs - which are relevant for you if you are using v1.4.2 - https://github.com/TheNetworg/oauth2-azure/tree/v1.4.2

Could quite possibly be the cause, with the V1 auth endpoint not recognising a V2 refresh token.
Assuming you meant that dev-master did support V2 endpoints, I will tomorrow (29th) rebuild my trial using the dev-master build and report back.
And my thanks for your help so far; all too often help gets unrecognised!

I have rebuilt using dev-master and with your suggested changes to my PHPMailer module:
$provider->pathAuthorize = "/oauth2/v2.0/authorize";
$provider->pathToken = "/oauth2/v2.0/token";
$provider->tenant = "[my tenant domain name];

But Invalid_Grant AADSTS9002313: Invalid request is still being flagged up.
As requested, I will post my get_oauth_token code as soon as I can

Jan - you asked for the contents of my get_oauth_token.
Version A below - which is based on the one given in StevenMaguire's producer - appears to work OK (it produces a token)
Version B below - which is based on the Azure dev-master one - loops when asking for user signin. The AAD Sign-In monitor gives the familiar error message:
Error 500011 - The resource principal named {name} was not found in the tenant named {tenant}.
['name' and 'tenant' above are what AAD returns; they are not my redaction!]
I assume this results from the calling parameters in the URL not being what the V2 endpoints expect.

VERSION A

use TheNetworg\OAuth2\Client\Provider\Azure; 

session_start();
$provider = new Azure([

    'clientId'                  => [my client ID from AAD],
    'clientSecret'              => [my client secret from AAD],
    'redirectUri'               => [URI of this module],
    'accessType'                => 'offline'

]);

$baseGraphUri = $provider->getRootMicrosoftGraphUri(null);
$provider->scope = 'Mail.Send SMTP.Send offline_access openid profile email User.Read';
$provider->pathAuthorize = "/oauth2/v2.0/authorize";
$provider->pathToken = "/oauth2/v2.0/token";
$provider->tenant = "decomplexity.com"; 

if (!isset($_GET['code'])) {

    // If we don't have an authorization code then get one
    $authUrl = $provider->getAuthorizationUrl();
    $_SESSION['oauth2state'] = $provider->getState();
    header('Location: '.$authUrl);
    exit;

// Check given state against previously stored one to mitigate CSRF attack
} elseif (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {

    unset($_SESSION['oauth2state']);
    exit('Invalid state');

} else {

    // Try to get an access token (using the authorization code grant)
    $token = $provider->getAccessToken('authorization_code', [
        'code' => $_GET['code'],
        'scope' => $provider->scope,
    ]);


    // Use this to interact with an API on the users behalf
//    echo $token->getToken();
      return $token->getToken();
}

VERSION B

require 'vendor/autoload.php';
use TheNetworg\OAuth2\Client\Provider\Azure;
 
session_start();
$provider = new Azure([
    'clientId'          => [my client ID from AAD],
    'clientSecret'      => [my client secret from AAD],
    'redirectUri'       => [URI of this module],
    'accessType'        => 'offline'
]);


$provider->defaultEndPointVersion = TheNetworg\OAuth2\Client\Provider\Azure::ENDPOINT_VERSION_2_0;
$baseGraphUri = $provider->getRootMicrosoftGraphUri(null);
$provider->scope = 'Mail.Send SMTP.Send offline_access openid profile email' . $baseGraphUri . '/User.Read';
$provider->pathAuthorize = "/oauth2/v2.0/authorize";
$provider->pathToken = "/oauth2/v2.0/token";
$provider->tenant = "decomplexity.com"; 


if (isset($_GET['code']) && isset($_SESSION['OAuth2.state']) && isset($_GET['state'])) {
    if ($_GET['state'] == $_SESSION['OAuth2.state']) {
        unset($_SESSION['OAuth2.state']);

        // Try to get an access token (using the authorization code grant)
        /** @var AccessToken $token */
        $token = $provider->getAccessToken('authorization_code', [
            'scope' => $provider->scope,
            'code' => $_GET['code'],
        ]);

        // Verify token
        // Save it to local server session data
        
        return $token->getToken();
    } else {
        echo 'Invalid state';

        return null;
    }
} else {

        $authorizationUrl = $provider->getAuthorizationUrl(['scope' => $provider->scope]);

        $_SESSION['OAuth2.state'] = $provider->getState();

        header('Location: ' . $authorizationUrl);

        exit;


    return $token->getToken();
}

This just looks like misconfiguration. I will try to make a sample for you.