TheNetworg / oauth2-azure

Azure AD provider for the OAuth 2.0 Client.
https://packagist.org/packages/thenetworg/oauth2-azure
MIT License
230 stars 108 forks source link

Invalid_grant AADSTS9002313: Invalid request.Request is malformed or invalid #114

Open decomplexity opened 4 years ago

decomplexity commented 4 years ago

[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

hajekj commented 4 years ago

Original: https://github.com/thephpleague/oauth2-client/issues/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

decomplexity commented 4 years ago

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]
hajekj commented 4 years ago

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.

decomplexity commented 4 years ago

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

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.

hajekj commented 4 years ago

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.

hajekj commented 4 years ago

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

decomplexity commented 4 years ago

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!

decomplexity commented 4 years ago

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

decomplexity commented 4 years ago

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();
}
hajekj commented 4 years ago

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