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

Azure oauth implementation within jelix php framework #2931

Closed
overghost opened this issue Jul 15, 2023 · 10 comments
Closed

Azure oauth implementation within jelix php framework #2931

overghost opened this issue Jul 15, 2023 · 10 comments

Comments

@overghost
Copy link

overghost commented Jul 15, 2023

Hello,
i'm implementing oauth in a project based on jelix framework (php framework).
Jelix has a jMailer class that depends on PHPMAILER.

so, i have following class that authentificates a user within azure ad (i added a function to return getOauth64 as in example of phpmailer)

<?php

class svoauthConfig {
    private $OAUTH_APP_ID = null;
    private $OAUTH_APP_SECRET = null;
    private $OAUTH_REDIRECT_URI = null;
    private $OAUTH_SCOPES = null;
    private $OAUTH_AUTHORITY = null;
    private $OAUTH_AUTHORIZE_ENDPOINT = null;
    private $OAUTH_TOKEN_ENDPOINT = null;
    private $after_login = null;

    private function getEnv(){
        $this->OAUTH_APP_ID = jProfiles::get('Azure','Default')['OAUTH_APP_ID'];
        $this->OAUTH_APP_SECRET = jProfiles::get('Azure','Default')['OAUTH_APP_SECRET'];
        $this->OAUTH_REDIRECT_URI = jProfiles::get('Azure','Default')['OAUTH_REDIRECT_URI'];
        $this->OAUTH_SCOPES = jProfiles::get('Azure','Default')['OAUTH_SCOPES'];
        $this->OAUTH_AUTHORITY = jProfiles::get('Azure','Default')['OAUTH_AUTHORITY'];
        $this->OAUTH_AUTHORIZE_ENDPOINT = jProfiles::get('Azure','Default')['OAUTH_AUTHORIZE_ENDPOINT'];
        $this->OAUTH_TOKEN_ENDPOINT = jProfiles::get('Azure','Default')['OAUTH_TOKEN_ENDPOINT'];
        $this->after_login = '';
        return [
            'appId' => $this->OAUTH_APP_ID,
            'appSecret' => $this->OAUTH_APP_SECRET,
            'redirectUri' => $this->OAUTH_REDIRECT_URI,
            'scopes' => $this->OAUTH_SCOPES,
            'authority' => $this->OAUTH_AUTHORITY,
            'authorizeEndpoint' => $this->OAUTH_AUTHORIZE_ENDPOINT,
            'tokenEndpoint' => $this->OAUTH_TOKEN_ENDPOINT,
            'after_login' => $this->after_login
        ];
    }

    public function returnEnv(){
        return $this->getEnv();
    }

    public function login(){
        $conf = $this->getEnv();
        $oauthClient = new \League\OAuth2\Client\Provider\GenericProvider([
            'clientId'                => $conf['appId'],
            'clientSecret'            => $conf['appSecret'],
            'redirectUri'             => $conf['redirectUri'],
            'urlAuthorize'            => $conf['authority'].$conf['authorizeEndpoint'],
            'urlAccessToken'          => $conf['authority'].$conf['tokenEndpoint'],
            'urlResourceOwnerDetails' => '',
            'scopes'                  => $conf['scopes']
        ]);

        $authUrl = $oauthClient->getAuthorizationUrl();
        // Save client state so we can validate in callback
        $_SESSION['oauthState'] = $oauthClient->getState();
        return $authUrl;
    }

    public function storeTokens($accessToken, $user) {
        $_SESSION['AZURE'] = new stdClass();
        $_SESSION['AZURE']->accessToken = $accessToken->getToken();
        $_SESSION['AZURE']->refreshToken = $accessToken->getRefreshToken();
        $_SESSION['AZURE']->tokenExpires = $accessToken->getExpires();
        $_SESSION['AZURE']->userName = $user->getDisplayName();
        $_SESSION['AZURE']->userEmail = null !== $user->getMail() ? $user->getMail() : $user->getUserPrincipalName();
        $_SESSION['AZURE']->userTimeZone = $user->getMailboxSettings()->getTimeZone();
    }

    public function clearTokens() {
        unset($_SESSION['AZURE']);
    }

    public function getAccessToken() {
        // Check if tokens exist
        if (empty($_SESSION['AZURE']->accessToken) ||
            empty($_SESSION['AZURE']->refreshToken) ||
            empty($_SESSION['AZURE']->tokenExpires)) {
            return '';
        }

        // Check if token is expired
        //Get current time + 5 minutes (to allow for time differences)
        $now = time() + 300;
        if ($_SESSION['AZURE']->tokenExpires <= $now) {
            // Token is expired (or very close to it)
            // so let's refresh
            $conf = $this->getEnv();
            // Initialize the OAuth client
            $oauthClient = new \League\OAuth2\Client\Provider\GenericProvider([
                'clientId'                => $conf['appId'],
                'clientSecret'            => $conf['appSecret'],
                'redirectUri'             => $conf['redirectUri'],
                'urlAuthorize'            => $conf['authority'].$conf['authorizeEndpoint'],
                'urlAccessToken'          => $conf['authority'].$conf['tokenEndpoint'],
                'urlResourceOwnerDetails' => '',
                'scopes'                  => $conf['scopes']
            ]);

            try {
                $newToken = $oauthClient->getAccessToken('refresh_token', [
                    'refresh_token' => $_SESSION['AZURE']->refreshToken
                ]);

                // Store the new values
                $this->updateTokens($newToken);

                return $newToken->getToken();
            }
            catch (League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
                return '';
            }
        }

        // Token is still valid, just return it
        return $_SESSION['AZURE']->accessToken;
    }

    public function updateTokens($accessToken) {
        $_SESSION['AZURE']->accessToken = $accessToken->getToken();
        $_SESSION['AZURE']->refreshToken = $accessToken->getRefreshToken();
        $_SESSION['AZURE']->tokenExpires = $accessToken->getExpires();
    }

    public function getOauth64(){
        $t = $this->getAccessToken();
        return base64_encode(
            'user=' .
            $_SESSION['AZURE']->userEmail .
            "\001auth=Bearer " .
            $t .
            "\001\001"
        );
    }
}

in a controller i tried following code :

jClasses::inc('core~svoauthConfig');

        $mail = new PHPMailer\PHPMailer\PHPMailer();
        $mail->isSMTP();
        $mail->Host = 'smtp.office365.com';
        $mail->Port = 587;
        $mail->SMTPSecure = 'tls';
        $mail->SMTPAuth = true;
        $token = new svoauthConfig();
        $mail->AuthType = 'XOAUTH2';
        $mail->setOAuth($token->getOauth64());
        $mail->setFrom($_SESSION['AZURE']->userEmail, $_SESSION['AZURE']->userName);
        $mail->addAddress('[email protected]');
        $mail->Subject = 'PHPMailer Outlook XOAUTH2 SMTP test';
        $mail->CharSet = PHPMailer::CHARSET_UTF8;
        $mail->Body    = 'This is the HTML message body <b>in bold!</b>';
        if (!$mail->send()) {
            echo 'Mailer Error: ' . $mail->ErrorInfo;
        } else {
            echo 'Message sent!';
        }

And this is returning following error:

TypeError: PHPMailer\PHPMailer\PHPMailer::setOAuth(): Argument #1 ($oauth) must be of type PHPMailer\PHPMailer\OAuthTokenProvider, string given, called in pathtocontroller on line 31 in pathtophpmailervendor\vendor\phpmailer\phpmailer\src\PHPMailer.php on line 5122

any ideas on how i could solve this problem?

@Synchro
Copy link
Member

Synchro commented Jul 15, 2023

Well, look at what the error says - it wants an instance of the OAuthTokenProvider, not a string, and you have one right there in $token, so you should be doing this:

$mail->setOAuth($token);

@Synchro Synchro closed this as completed Jul 15, 2023
@Synchro
Copy link
Member

Synchro commented Jul 15, 2023

Ah, you're missing one thing. It's not enough to simply have a method named getOauth64 in your class, you need to specify that you are explicitly providing compatibility with the OAuthTokenProvider interface from PHPMailer, so also update your class declaration to:

class svoauthConfig implements OAuthTokenProvider {

and don't forget that this is namespaced, so you'll need to add this at the top of your file:

use PHPMailer\PHPMailer\OAuthTokenProvider;

@overghost
Copy link
Author

overghost commented Jul 15, 2023

Hey there,
thanks for the quick reply,
i made the changes and i can't achieve it so far...

i have following smtp log output

2023-07-15 20:51:14 Connection: opening to smtp.office365.com:587, timeout=300, options=array()
2023-07-15 20:51:14 Connection: opened
2023-07-15 20:51:14 SMTP INBOUND: "220 AS4PR09CA0007.outlook.office365.com Microsoft ESMTP MAIL Service ready at Sat, 15 Jul 2023 20:51:13 +0000"
2023-07-15 20:51:14 SERVER -> CLIENT: 220 AS4PR09CA0007.outlook.office365.com Microsoft ESMTP MAIL Service ready at Sat, 15 Jul 2023 20:51:13 +0000
2023-07-15 20:51:14 CLIENT -> SERVER: EHLO localhost
2023-07-15 20:51:14 SMTP INBOUND: "250-AS4PR09CA0007.outlook.office365.com Hello [2a02:a03f:69de:2c00:f07c:778c:8369:c4ba]"
2023-07-15 20:51:14 SMTP INBOUND: "250-SIZE 157286400"
2023-07-15 20:51:14 SMTP INBOUND: "250-PIPELINING"
2023-07-15 20:51:14 SMTP INBOUND: "250-DSN"
2023-07-15 20:51:14 SMTP INBOUND: "250-ENHANCEDSTATUSCODES"
2023-07-15 20:51:14 SMTP INBOUND: "250-STARTTLS"
2023-07-15 20:51:14 SMTP INBOUND: "250-8BITMIME"
2023-07-15 20:51:14 SMTP INBOUND: "250-BINARYMIME"
2023-07-15 20:51:14 SMTP INBOUND: "250-CHUNKING"
2023-07-15 20:51:14 SMTP INBOUND: "250 SMTPUTF8"
2023-07-15 20:51:14 SERVER -> CLIENT: 250-AS4PR09CA0007.outlook.office365.com Hello [2a02:a03f:69de:2c00:f07c:778c:8369:c4ba]250-SIZE 157286400250-PIPELINING250-DSN250-ENHANCEDSTATUSCODES250-STARTTLS250-8BITMIME250-BINARYMIME250-CHUNKING250 SMTPUTF8
2023-07-15 20:51:14 CLIENT -> SERVER: STARTTLS
2023-07-15 20:51:14 SMTP INBOUND: "220 2.0.0 SMTP server ready"
2023-07-15 20:51:14 SERVER -> CLIENT: 220 2.0.0 SMTP server ready
2023-07-15 20:51:14 CLIENT -> SERVER: EHLO localhost
2023-07-15 20:51:14 SMTP INBOUND: "250-AS4PR09CA0007.outlook.office365.com Hello [2a02:a03f:69de:2c00:f07c:778c:8369:c4ba]"
2023-07-15 20:51:14 SMTP INBOUND: "250-SIZE 157286400"
2023-07-15 20:51:14 SMTP INBOUND: "250-PIPELINING"
2023-07-15 20:51:14 SMTP INBOUND: "250-DSN"
2023-07-15 20:51:14 SMTP INBOUND: "250-ENHANCEDSTATUSCODES"
2023-07-15 20:51:14 SMTP INBOUND: "250-AUTH LOGIN XOAUTH2"
2023-07-15 20:51:14 SMTP INBOUND: "250-8BITMIME"
2023-07-15 20:51:14 SMTP INBOUND: "250-BINARYMIME"
2023-07-15 20:51:14 SMTP INBOUND: "250-CHUNKING"
2023-07-15 20:51:14 SMTP INBOUND: "250 SMTPUTF8"
2023-07-15 20:51:14 SERVER -> CLIENT: 250-AS4PR09CA0007.outlook.office365.com Hello [2a02:a03f:69de:2c00:f07c:778c:8369:c4ba]250-SIZE 157286400250-PIPELINING250-DSN250-ENHANCEDSTATUSCODES250-AUTH LOGIN XOAUTH2250-8BITMIME250-BINARYMIME250-CHUNKING250 SMTPUTF8
2023-07-15 20:51:14 Auth method requested: XOAUTH2
2023-07-15 20:51:14 Auth methods available on the server: LOGIN,XOAUTH2
2023-07-15 20:51:14 CLIENT -> SERVER: AUTH XOAUTH2 XNlcj1zdmFlcmVuZG9uY2tAY25sZGIuYmUBYXV0aD1CZWFyZXIgZXlKMGVYQWlPaUpLVjFRaUxDSnViMjVqWlNJNklsZGFNRTlDTWtocWVrVkdhMVZzZGxwSE9ESkpkSGgzTUdSamFVTkNZMjVtV0hCWlQxa3RYMHcyUzJjaUxDSmhiR2NpT2lKU1V6STFOaUlzSW5nMWRDSTZJaTFMU1ROUk9XNU9VamRpVW05bWVHMWxXbTlZY1dKSVdrZGxkeUlzSW10cFpDSTZJaTFMU1ROUk9X
...
Ul1SXhyRExyVkhfeVF6cjJJemNmVjBSdG9IQk1jR0U2S2hNenNrUERFbzB4bW9WRnkxZERoczUwbF9VMGkwbTM4SFJuVjV5anoyS3dUT0t4S3ZwcU9jMy1nX1d6TDJnNU4wMzF5RWdJWlNkVXRBRks4TVZiWFNRM0tPZkFrMHo2NHVsb2hiVDcyV1A5M3JlcFh3MFo1NldybTM3ZjVOcWZqQkJYWnVWVjRXUFBRWEp0cVk3eVRXLWcBAQ==
2023-07-15 20:51:20 SMTP INBOUND: "535 5.7.3 Authentication unsuccessful [AS4PR09CA0007.eurprd09.prod.outlook.com 2023-07-15T20:51:20.176Z 08DB853A34E71ACE]"
2023-07-15 20:51:20 SERVER -> CLIENT: 535 5.7.3 Authentication unsuccessful [AS4PR09CA0007.eurprd09.prod.outlook.com 2023-07-15T20:51:20.176Z 08DB853A34E71ACE]
2023-07-15 20:51:20 SMTP ERROR: AUTH command failed: 535 5.7.3 Authentication unsuccessful [AS4PR09CA0007.eurprd09.prod.outlook.com 2023-07-15T20:51:20.176Z 08DB853A34E71ACE]

@Synchro
Copy link
Member

Synchro commented Jul 15, 2023

Your code is now working correctly, but you still have an auth problem. I suggest you look at the wiki about this, and also read about how others have solved auth problems with Microsoft services.

@decomplexity
Copy link
Contributor

What 'scope' permissions are you using?

@overghost
Copy link
Author

What 'scope' permissions are you using?

OAUTH_SCOPES = 'openid profile offline_access user.read mailboxsettings.read calendars.readwrite SMTP.Send Mail.Send email';

@decomplexity
Copy link
Contributor

Try just “offline_access https://outlook.office.com/SMTP.Send”

@overghost
Copy link
Author

Try just “offline_access https://outlook.office.com/SMTP.Send”

i tried it didn't work... gona have some sleep and take it back on tomorrow, i might be busy on this for to long so far (started this workout 14 hours ago)

Thanks for the help, really greatefull...

@overghost
Copy link
Author

Your code is now working correctly, but you still have an auth problem. I suggest you look at the wiki about this, and also read about how others have solved auth problems with Microsoft services.

thanks for the links, i'm digging them out (i read them before as well, but i might have missed something somehow)

@decomplexity
Copy link
Contributor

Worth checking that you are presenting an access token and not a refresh token, since the one shown in your diagnostics is not jwt parsable with or without a base64 decode.

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

No branches or pull requests

3 participants