Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Access token o365, php-imap doesn't work. #315

Closed
Aldi1990 opened this issue Nov 3, 2022 · 31 comments
Closed

Access token o365, php-imap doesn't work. #315

Aldi1990 opened this issue Nov 3, 2022 · 31 comments
Labels

Comments

@Aldi1990
Copy link

Aldi1990 commented Nov 3, 2022

I don't know where is the problem. I did everything in Microsoft Azure and below code, generation access token and it's works. When I will assign token to password php-imap doesn't work.

When I disabled auth and log in by basic authenication by normal password php-imap works.

Debug show info:
<< * OK The Microsoft Exchange IMAP4 service is ready. [REDACTED==] >> TAG1 AUTHENTICATE XOAUTH2 REDACTED.............................== << TAG1 NO AUTHENTICATE failed. >> TAG2 LOGOUT << * BYE Microsoft Exchange Server IMAP4 server signing off. << TAG2 OK LOGOUT completed.

@thin-k-design
Copy link
Contributor

This is an issue with the Microsoft Oauth2 process, not with the php-imap repository. Please make sure you have used the correct Microsoft IMAP scopes to generate the token under:

'scope' => 'https://graph.microsoft.com/User.Read',

@Aldi1990
Copy link
Author

Aldi1990 commented Nov 4, 2022

Thanks for the suggestion. Below I am sending the screen from Azure and the PHP version. What do you think of my code that generates the access token, code is correct?

image
image

@thin-k-design
Copy link
Contributor

You need to specify the scope "https://outlook.office.com/IMAP.AccessAsUser.All" in your oauth workflow.

@Aldi1990
Copy link
Author

Aldi1990 commented Nov 6, 2022

Thanks for your help, now I can login by Oauth2 but can't get the e-mail.

<< * OK The Microsoft Exchange IMAP4 service is ready. [REDACTED==] >> TAG1 AUTHENTICATE XOAUTH2 REDACTED== << TAG1 OK AUTHENTICATE completed. >> TAG2 LIST "" "%" << TAG2 BAD User is authenticated but not connected.

Json decode
object(stdClass)#1 (1) { ["error"]=> object(stdClass)#2 (2) { ["code"]=> string(27) "MailboxNotEnabledForRESTAPI" ["message"]=> string(70) "The mailbox is either inactive, soft-deleted, or is hosted on-premise." } } >> TAG3 LOGOUT << * BYE Microsoft Exchange Server IMAP4 server signing off. << TAG3 OK LOGOUT completed.

@thin-k-design
Copy link
Contributor

Sorry, can't really help, contact the microsoft support.

@Aldi1990
Copy link
Author

Aldi1990 commented Nov 7, 2022

I found a solution to my problem, now everything works :)

@moikzz213
Copy link

I found a solution to my problem, now everything works :)

Hi @Aldi1990 may know what is your solution as I have same issue

@ufo1990
Copy link

ufo1990 commented Nov 8, 2022

@moikzz213 write more information about your an issue and write code which you use.

@moikzz213
Copy link

moikzz213 commented Nov 8, 2022

Hi @ufo1990,

I am using PHP Laravel Framework.
"webklex/laravel-imap": "^2.4",
"webklex/php-imap": "2.4.2"

currently this is my code, reference (https://www.php-imap.com/examples/oauth)

        $client = new ClientManager();
        $client->make([
                        'host' => 'outlook.office365.com',
                        'port' => 993,
                        'encryption' => 'ssl', // 'tls',
                        'validate_cert' => true,
                        'username' => '[email protected]',
                        'password' => $response['access_token'],
                        'protocol'   => 'imap',
                        'authentication' => "oauth",
                ]); 
        $client->connect();
        if(!$client->isConnected()){
            echo json_encode($client);
            return;
        }  

I received an error

-  Webklex\PHPIMAP\Exceptions\ConnectionFailedException
- 
- ErrorException: stream_socket_client(): Peer certificate CN=`40-123-194-112.cprapid.com' did not match expected CN=`localhost' in /domain/vendor/webklex/php-imap/src/Connection/Protocols/Protocol.php:191
- Stack trace:
- #0 [internal function]: Illuminate\Foundation\Bootstrap\HandleExceptions->handleError(2, 'stream_socket_c...', '/home/domain...', 191, Array)
- #1 /domain/vendor/webklex/php-imap/src/Connection/Protocols/Protocol.php(191): stream_socket_client('ssl://localhost...', 0, '', 30, 4, Resource id #651)
- #2 /domain/vendor/webklex/php-imap/src/Connection/Protocols/ImapProtocol.php(73): Webklex\PHPIMAP\Connection\Protocols\Protocol->createStream('ssl', 'localhost', 993, 30)
- #3 /domain/vendor/webklex/php-imap/src/Client.php(350): Webklex\PHPIMAP\Connection\Protocols\ImapProtocol->connect('localhost', 993)
- #4 [internal function]: Webklex\PHPIMAP\Client->connect()
- #5 /domain/vendor/webklex/php-imap/src/ClientManager.php(55): call_user_func_array(Array, Array)
- #6 /domain/app/Console/Commands/ReceivedEvent.php(74): Webklex\PHPIMAP\ClientManager->__call('connect', Array)
- #7 /domain/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(36): App\Console\Commands\ReceivedEvent->handle()
- #8 /domain/vendor/laravel/framework/src/Illuminate/Container/Util.php(40): Illuminate\Container\BoundMethod::Illuminate\Container\{closure}()
- #9 /domain/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(93): Illuminate\Container\Util::unwrapIfClosure(Object(Closure))
- #10 /domain/vendor/laravel/framework/src/Illuminate/Container/BoundMethod.php(37): Illuminate\Container\BoundMethod::callBoundMethod(Object(Illuminate\Foundation\Application), Array, Object(Closure))
- #11 /domain/vendor/laravel/framework/src/Illuminate/Container/Container.php(653): Illuminate\Container\BoundMethod::call(Object(Illuminate\Foundation\Application), Array, Array, NULL)
- #12 /domain/vendor/laravel/framework/src/Illuminate/Console/Command.php(136): Illuminate\Container\Container->call(Array)
- #13 /domain/vendor/symfony/console/Command/Command.php(298): Illuminate\Console\Command->execute(Object(Symfony\Component\Console\Input\ArgvInput), Object(Illuminate\Console\OutputStyle))
- #14 /domain/vendor/laravel/framework/src/Illuminate/Console/Command.php(121): Symfony\Component\Console\Command\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Illuminate\Console\OutputStyle))
- #15 /domain/vendor/symfony/console/Application.php(1024): Illuminate\Console\Command->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
- #16 /domain/vendor/symfony/console/Application.php(299): Symfony\Component\Console\Application->doRunCommand(Object(App\Console\Commands\ReceivedEvent), Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
- #17 /domain/vendor/symfony/console/Application.php(171): Symfony\Component\Console\Application->doRun(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
- #18 /domain/vendor/laravel/framework/src/Illuminate/Console/Application.php(94): Symfony\Component\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
- #19 /domain/vendor/laravel/framework/src/Illuminate/Foundation/Console/Kernel.php(129): Illuminate\Console\Application->run(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
- #20 /domain/artisan(37): Illuminate\Foundation\Console\Kernel->handle(Object(Symfony\Component\Console\Input\ArgvInput), Object(Symfony\Component\Console\Output\ConsoleOutput))
- #21 {main}

`
  at vendor/webklex/php-imap/src/Connection/Protocols/ImapProtocol.php:81
     77▕    if ($encryption == "tls") {
     78▕        $this->enableTls();
     79▕    }
     80▕} catch (Exception $e) {
  ➜  81▕    throw new ConnectionFailedException($e);
     82▕}
     83▕     }
     84▕
     85▕     /**

      app/Console/Commands/SomeCommand.php:74
      Webklex\PHPIMAP\ClientManager::__call("connect", [])

      Illuminate\Foundation\Console\Kernel::handle(Object(Symfony\Component\Console\Input\ArgvInput), 
      Object(Symfony\Component\Console\Output\ConsoleOutput))

@ufo1990
Copy link

ufo1990 commented Nov 8, 2022

Can you access via Basic Authentication? How do you get a token? show code

@moikzz213
Copy link

moikzz213 commented Nov 9, 2022

Before I was able to access using basic but now I cannot login. I got my token by using the code below

    $url = 'https://login.microsoftonline.com/{tenantID}/oauth2/v2.0/token';
    $postInput = array(
                        'grant_type'    => 'client_credentials',
                        'scope'         => 'https://outlook.office365.com/.default',
                        'client_secret' => 'clientID',
                        'client_id'     => 'secretID'
                    );

    $headers = array(
        "Content-Type: application/json",
        "Connection: keep-alive",
        "Accept: application/json"
    );
    $client = new \GuzzleHttp\Client();
    $response = $client->request('POST', $url, ['form_params' => $postInput]); 

    $statusCode = $response->getStatusCode();
    $responseBody = json_decode($response->getBody(), true);

@Webklex
Copy link
Owner

Webklex commented Nov 9, 2022

Hi @moikzz213 ,
please update your dependencies to the latest release and try again.

If you are still facing issues, please take a look at #262 and make sure you're using the correct scope (I believe [](https://outlook.office365.com/.default)` isn't the one you want).

Best regards & happy coding,

@moikzz213
Copy link

moikzz213 commented Nov 15, 2022

Hi @Webklex I already update npm and composer but I still cant figure out the solution.
I changed scopes, but it gets me and Invalid Scope only aside from "https://graph.microsoft.com/.default"

Client error: `POST https://login.microsoftonline.com/{tenant}/oauth2/v2.0/token` resulted in a `400 Bad Request` response:
{"error":"invalid_scope","error_description":"AADSTS1002012: The provided value for scope https://outlook.office.com/IMAP.AccessAsUser.All

attached image is my code.
image

@Webklex
Copy link
Owner

Webklex commented Nov 15, 2022

@moikzz213 I don't know. I have no way of testing or verifying it.
Which webklex/php-imap version are you currently using? Make sure its the latest one (> v4.0).

Maybe this thread can help you as well: #264

My blind guess: some checkbox somewhere within your setup needs to be ticked.

Best regards,

@irufus
Copy link

irufus commented Dec 16, 2022

I tried with the updated dependencies and get the same issue. Attached is a list of my configuration in Azure AD

MicrosoftTeams-image (12)

@lostence
Copy link

lostence commented Jan 3, 2023

For those having "User is authenticated but not connected", it seems that o365 imap over ipv6 is not working.
If I force the ipv4 resolve for outlook.office365.com, it works fine.

@andres9604
Copy link

For those having "User is authenticated but not connected", it seems that o365 imap over ipv6 is not working. If I force the ipv4 resolve for outlook.office365.com, it works fine.

Hello, can you tell me how I can do this?

@lostence
Copy link

Hello, can you tell me how I can do this?

I added the hostname and it's ipv4 address in the local hosts file.
But in the mean time microsoft fixed their ipv6 imap servers so it is not needed anymore

@Pinkbus2020
Copy link

plz can anyone give the full proper working code?

@jupitern
Copy link

I implemented this last week. Let me try to help.

first you have to register an app in azure.portal.com

Copy application id and tenant id from the application.
In the created app go to Authentication > Add a platform > select "Web" option.
here in redirect URIs I added a callback to a valid endpoint in your public domain website to receive the callback information. I inserted https://[YOUR_WEBSITE_HERE.COM]/mail/callback
Go to certificates and secrets and create a secret. give it a name and save the value somewhere.
In the API permissions menu add permission for IMAP.AccessAsUser.All
click Save.

Code example:

On my website I created 4 routes/endpoints that point to a controller class with four methods:
store this variables with the correct values in your controller:

    private string $tenant = "...";
    private string $clientId = "....";
    private string $redirectUri = "https://[YOUR_WEBSITE_HERE.COM]/mail/callback";
    private string $secret = '....';

/mail/auth => this endpoint is called manually by you the first time. it generates the url to login and redirects to microsoft login page. first time you insert the user and password of the user that you will use to acess (one or many) mailbox(s)

public function auth()
    {
        $authUri = 'https://login.microsoftonline.com/' . $this->tenant
            . '/oauth2/v2.0/authorize?client_id=' . $this->clientId
            . '&scope=https://outlook.office365.com/IMAP.AccessAsUser.All'
            . '&redirect_uri=' . urlencode($this->redirectUri)
            . '&response_type=code'
            . '&approval_prompt=auto';

        // redirect to login
        return $this->response->withHeader('Location', $authUri);
    }

/mail/callback => this url is the url you configured in the redirect URIs section of the app and is called by microsoft if auth is correct. microsoft sends code and session state in GET that you will then use to get a valid access token that will be used as password to autenticate

public function callback()
    {
        $code = $_GET['code'];
        $sessionState = $_GET['session_state'];

        $url= "https://login.microsoftonline.com/".$this->tenant."/oauth2/v2.0/token";

        $param_post_curl = [
            'client_id'     => $this->clientId,
            'scope'         => 'https://outlook.office365.com/IMAP.AccessAsUser.All offline_access',
            'code'          => $code,
            'session_state' => $sessionState,
            'client_secret' => $this->secret, // this is the secret generated in the app menu certificates and secrets.
            'redirect_uri'  => $this->redirectUri,
            'grant_type'    => 'authorization_code'
        ];

        $ch = curl_init();
        curl_setopt($ch,CURLOPT_URL,$url);
        curl_setopt($ch,CURLOPT_POSTFIELDS, http_build_query($param_post_curl));
        curl_setopt($ch,CURLOPT_POST, 1);
        curl_setopt($ch,CURLOPT_RETURNTRANSFER, true);

        $oResult = curl_exec($ch);
        $tokens = json_decode($oResult);

        $db->table('tokens')->insert([
            'AccessToken'  => $tokens->access_token,
            'RefreshToken' => $tokens->refresh_token,
            'ExpiresIn'    => $tokens->expires_in,
        ]);

        // at this point you have stored a valid access token that can be used to login.
    }

/mail/refresh => enpoint to refresh acess token. acess token is valid for about 1.5 hours so I call this endpoint from a cron every hour using the refresh token to generate a new access token. If this never fails there is no need to ever do the previous two steps again.

    public function refresh()
    {
        $db = // connect to your db and get last valid refresh token
        $res = $db->selectOne("select RefreshToken from tokens order by id desc limit 1");

        $param_post_curl = [
            'client_id'     => $this->clientId,
            'client_secret' => $this->secret,
            'refresh_token' => $res->RefreshToken,
            'grant_type'    => 'refresh_token'
        ];

        $ch = curl_init();

        curl_setopt($ch,CURLOPT_URL, "https://login.microsoftonline.com/".$this->tenant."/oauth2/v2.0/token");
        curl_setopt($ch,CURLOPT_POSTFIELDS, http_build_query($param_post_curl));
        curl_setopt($ch,CURLOPT_POST, 1);
        curl_setopt($ch,CURLOPT_RETURNTRANSFER, true);
        // curl_setopt($ch,CURLOPT_SSL_VERIFYPEER, false); // uncoment this line if localhost

        $oResult = curl_exec($ch);
        $tokens = json_decode($oResult);

        // save new valid tokens
        $db->table('tokens')->insert([
            'AccessToken'  => $tokens->access_token,
            'RefreshToken' => $tokens->refresh_token,
            'ExpiresIn'    => $tokens->expires_in,
        ]);
    }

from here just get the access token from the db and connect to the mailbox:

$cm = new ClientManager();
        $client = $cm->make([
            'host'          => 'outlook.office365.com',
            'port'          => 993,
            'encryption'    => 'ssl',
            'validate_cert' => false,
            'username'      => $email,
            'password'      => $accessToken,
            'protocol'      => 'imap',
            'authentication' => 'oauth'
        ]);

        $client->connect();

done!

@thanjeys
Copy link

I found a solution to my problem, now everything works :)

Hi, I am facing same issues on office365 mail. that is

<< TAG1 OK AUTHENTICATE completed. >> TAG2 LIST "" "*" << TAG2 BAD User is authenticated but not connected. >> TAG3 LOGOUT << * BYE Microsoft Exchange Server IMAP4 server signing off. << TAG3 OK LOGOUT completed.

Can you give me solutions please ?

Thank you

@MouMoutMan
Copy link

@Aldi1990 , can you share your solution plz ? @thanjeys too ?

@thanjeys
Copy link

@Aldi1990 Still i didnt get any solution, If you found it, Pls Post here. Thank you.

@Aldi1990
Copy link
Author

@MouMoutMan @thanjeys In Ms Azure Are you set all correct ?

@thanjeys
Copy link

image

@Aldi1990 Yes, I did it, I was working good suddenly its not working from this month onwards. Any idea ?

@Aldi1990
Copy link
Author

@thanjeys secret key is actual ?

@thanjeys
Copy link

@Aldi1990 No luck, I tried that too. Even i tried with different account. How did u you solve issue ?

Thanks

@Aldi1990
Copy link
Author

Aldi1990 commented Jun 23, 2023

@thanjeys Look to my solution

$endpoint_authorize  = 'https://login.microsoftonline.com/XXXXXXXXXXX/oauth2/v2.0/authorize';
$endpoint_token  = 'https://login.microsoftonline.com/XXXXXXXXXXX/oauth2/v2.0/token';
$redirect_uri = 'https://XXXXXXXXXXX/imap/mail_oauth2.php';
$client_id = 'XXXXXXXXXXX';
$client_secret = 'XXXXXXXXXXX';

session_start();

$_SESSION['state'] = session_id();

/*Build url*/
$login = $endpoint_authorize.'?'.http_build_query([
    'client_id'		=> $client_id,
    'redirect_uri'	=> $redirect_uri,              
    'scope'			=> 'https://outlook.office.com/IMAP.AccessAsUser.All offline_access',
    'response_type'	=> 'code ',
    'state'			=> $_SESSION['state'],
]);

//Logout Ms o365
if($_GET['action'] == 'logout')
{
   unlink('token.json');
   session_unset();
   header('Location: '.$redirect_uri);
}

//Check if exists file token.json
if(!file_exists('token.json'))
{
    echo '<a href = "'.$login.'">Log in</a>';
    	
	if(isset($_GET['code']))
	{
        $code = $_GET['code'];
    }
	else
	{
       exit();
    } 
	
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL,$endpoint_token);
    curl_setopt($ch, CURLOPT_POST, TRUE);
    curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
        'code'          => $code,
        'client_id'     => $client_id,
        'client_secret' => $client_secret,
        'redirect_uri'  => $redirect_uri,
        'grant_type'    => 'authorization_code',
    ]));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
	$response = curl_exec($ch);
    curl_close($ch); 
	file_put_contents('token.json', $response);
	header('Location: '.$redirect_uri);
}
else
{
    echo '<a href="?action=logout">Logout</a>';	
    
    $response = file_get_contents('token.json');
    $array = json_decode($response);
    $access_token = $array->access_token;
    $refresh_token = $array->refresh_token;
	$ch = curl_init(); 
	curl_setopt($ch, CURLOPT_HTTPHEADER, array ('Authorization: Bearer '.$access_token,'Conent-type: application/json'));
	curl_setopt($ch, CURLOPT_URL, "https://outlook.office365.com/api/v2.0/me/");
	curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
	$error_response = curl_exec($ch);
	$array = json_decode($error_response);
	
	if(isset($array->error))
	{   
        // Generate new Access Token using old Refresh Token
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL,$endpoint_token);
        curl_setopt($ch, CURLOPT_POST, TRUE);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/x-www-form-urlencoded']);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
        curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
            'client_id'		=> $client_id,
            'client_secret' => $client_secret,
            'refresh_token'	=> $refresh_token,
            'grant_type'    => 'refresh_token',
        ]));
		
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        $response = curl_exec($ch);
        file_put_contents('token.json', $response);
		header('Location: '.$redirect_uri);
    }
	else
	{
		$response = json_decode(curl_exec($ch), 1);
		$_SESSION['EmailAddress'] = $response['EmailAddress'];
	}

	curl_close ($ch);
}

@Webklex Webklex closed this as completed Jun 24, 2023
@thanjeys
Copy link

@Aldi1990 Thanks for your solution, I will check it and let you know. if any luck. Thanks

@thanjeys
Copy link

I implemented this last week. Let me try to help.

first you have to register an app in azure.portal.com

Copy application id and tenant id from the application. In the created app go to Authentication > Add a platform > select "Web" option. here in redirect URIs I added a callback to a valid endpoint in your public domain website to receive the callback information. I inserted https://[YOUR_WEBSITE_HERE.COM]/mail/callback Go to certificates and secrets and create a secret. give it a name and save the value somewhere. In the API permissions menu add permission for IMAP.AccessAsUser.All click Save.

Code example:

On my website I created 4 routes/endpoints that point to a controller class with four methods: store this variables with the correct values in your controller:

    private string $tenant = "...";
    private string $clientId = "....";
    private string $redirectUri = "https://[YOUR_WEBSITE_HERE.COM]/mail/callback";
    private string $secret = '....';

/mail/auth => this endpoint is called manually by you the first time. it generates the url to login and redirects to microsoft login page. first time you insert the user and password of the user that you will use to acess (one or many) mailbox(s)

public function auth()
    {
        $authUri = 'https://login.microsoftonline.com/' . $this->tenant
            . '/oauth2/v2.0/authorize?client_id=' . $this->clientId
            . '&scope=https://outlook.office365.com/IMAP.AccessAsUser.All'
            . '&redirect_uri=' . urlencode($this->redirectUri)
            . '&response_type=code'
            . '&approval_prompt=auto';

        // redirect to login
        return $this->response->withHeader('Location', $authUri);
    }

/mail/callback => this url is the url you configured in the redirect URIs section of the app and is called by microsoft if auth is correct. microsoft sends code and session state in GET that you will then use to get a valid access token that will be used as password to autenticate

public function callback()
    {
        $code = $_GET['code'];
        $sessionState = $_GET['session_state'];

        $url= "https://login.microsoftonline.com/".$this->tenant."/oauth2/v2.0/token";

        $param_post_curl = [
            'client_id'     => $this->clientId,
            'scope'         => 'https://outlook.office365.com/IMAP.AccessAsUser.All offline_access',
            'code'          => $code,
            'session_state' => $sessionState,
            'client_secret' => $this->secret, // this is the secret generated in the app menu certificates and secrets.
            'redirect_uri'  => $this->redirectUri,
            'grant_type'    => 'authorization_code'
        ];

        $ch = curl_init();
        curl_setopt($ch,CURLOPT_URL,$url);
        curl_setopt($ch,CURLOPT_POSTFIELDS, http_build_query($param_post_curl));
        curl_setopt($ch,CURLOPT_POST, 1);
        curl_setopt($ch,CURLOPT_RETURNTRANSFER, true);

        $oResult = curl_exec($ch);
        $tokens = json_decode($oResult);

        $db->table('tokens')->insert([
            'AccessToken'  => $tokens->access_token,
            'RefreshToken' => $tokens->refresh_token,
            'ExpiresIn'    => $tokens->expires_in,
        ]);

        // at this point you have stored a valid access token that can be used to login.
    }

/mail/refresh => enpoint to refresh acess token. acess token is valid for about 1.5 hours so I call this endpoint from a cron every hour using the refresh token to generate a new access token. If this never fails there is no need to ever do the previous two steps again.

    public function refresh()
    {
        $db = // connect to your db and get last valid refresh token
        $res = $db->selectOne("select RefreshToken from tokens order by id desc limit 1");

        $param_post_curl = [
            'client_id'     => $this->clientId,
            'client_secret' => $this->secret,
            'refresh_token' => $res->RefreshToken,
            'grant_type'    => 'refresh_token'
        ];

        $ch = curl_init();

        curl_setopt($ch,CURLOPT_URL, "https://login.microsoftonline.com/".$this->tenant."/oauth2/v2.0/token");
        curl_setopt($ch,CURLOPT_POSTFIELDS, http_build_query($param_post_curl));
        curl_setopt($ch,CURLOPT_POST, 1);
        curl_setopt($ch,CURLOPT_RETURNTRANSFER, true);
        // curl_setopt($ch,CURLOPT_SSL_VERIFYPEER, false); // uncoment this line if localhost

        $oResult = curl_exec($ch);
        $tokens = json_decode($oResult);

        // save new valid tokens
        $db->table('tokens')->insert([
            'AccessToken'  => $tokens->access_token,
            'RefreshToken' => $tokens->refresh_token,
            'ExpiresIn'    => $tokens->expires_in,
        ]);
    }

from here just get the access token from the db and connect to the mailbox:

$cm = new ClientManager();
        $client = $cm->make([
            'host'          => 'outlook.office365.com',
            'port'          => 993,
            'encryption'    => 'ssl',
            'validate_cert' => false,
            'username'      => $email,
            'password'      => $accessToken,
            'protocol'      => 'imap',
            'authentication' => 'oauth'
        ]);

        $client->connect();

done!

@jupitern I have followed your code, It was working fine. But recently its not working..

So Can you confirm Is it working Good for you ?

Thanks

@stevebauman
Copy link

stevebauman commented Sep 5, 2023

For anyone encountering this issue which tripped me up for many hours -- when requesting an access token, you cannot supply Microsoft Graph scopes with Outlook scopes. For example, with the below scopes, an access token will be returned, but it cannot actually be used to connect to Outlook over IMAP:

$scopes = [
    'User.Read',
    'offline_access',
    'https://outlook.office.com/IMAP.AccessAsUser.All',
];

You must re-request a new access token with only the offline_access and 'https://outlook.office.com/IMAP.AccessAsUser.All scopes after the initial consent has been given, using the refresh token provided in the request (due to the offline_access scope).

See Microsoft employee reply here:

IMAP, SMTP scopes are targeted for Exchange resource and not Graph. Whereas User.Read, Mail.ReadWrite are meant for Graph resource.

We do not support generation of tokens that are meant for two resources. Hence the error "Provided value for the input parameter scope is not valid because it contains more than one resource."

You should generate two tokens separately by two calls to /token. 1. One with the IMAP, SMTP scopes generated for the Exchange resource. 2. The other with Graph scopes (User.Read, Mail.ReadWrite) meant for Graph resource.

https://stackoverflow.com/questions/61597263/office-365-xoauth2-for-imap-and-smtp-authentication-fails

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests