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

PHP setOAuth() now wants a OAuthTokenProvider? What does that mean? #2850

Closed
Stefanbracke opened this issue Dec 23, 2022 · 12 comments
Closed

Comments

@Stefanbracke
Copy link

I do this:

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

I followed this example from nearly everywhere but i always get this error:

Uncaught TypeError: Argument 1 passed to PHPMailer\PHPMailer\PHPMailer::setOAuth() must be an instance of PHPMailer\PHPMailer\OAuthTokenProvider, instance of OAuth given

@Synchro
Copy link
Member

Synchro commented Dec 24, 2022

No, it's not a built-in. It means that what you pass in as the provider property must implement the OAuthTokenProvider interface. Take a look at the gmail example.

@Synchro Synchro closed this as completed Dec 24, 2022
@Stefanbracke
Copy link
Author

Stefanbracke commented Dec 24, 2022 via email

@murraycollingwood
Copy link

Yes, exactly the same question! How do you construct the values? Preferably without using files.
The example looks like this:

//Create and pass GoogleOauthClient to PHPMailer
$oauthTokenProvider = new \GoogleOauthClient(
    '[email protected]',
    'path/to/gmail-xoauth2-credentials.json',
    'path/to/gmail-xoauth-token.json'
);
$mail->setOAuth($oauthTokenProvider);

My credentials and tokens come from a database entry, so do I have to write them to files in order to use this option (seems a bit backward). Or is there an option to create this oauthTokenProvider using variables?

Cheers
Murray

@Synchro
Copy link
Member

Synchro commented Nov 17, 2023

It wasn't clear which example you were referring to, however I see that you are talking about the wiki article that shows how to use Google's client library instead of the PHP League one.
The only requirement of what you pass into setOAuth is that it should implement the OAuthTokenProvider interface, which requires a single getOauth64 method that returns a base64-encoded access token, and that is shown in the wiki article.

The GoogleOauthClient you mention here is Google's own library, and it's that that requires external config files, not PHPMailer.

If you want to avoid the external file approach, I recommend using the default League classes, which operate entirely on literal strings, not external files.

@decomplexity
Copy link
Contributor

Don’t get too hung up on class GoogleOauthClient: you write it, and it itself is whatever you want it to be, with the only restriction that the ‘implements OAuthTokenProvider’ interface will force your class to contain the getOauth64() public method that is used by PHPMailer’s SMTP.

Your GoogleOauthClient is functionally replacing:
a. a provider client such as TheLeague’s oauth2-google client
b. theTheLeague’s own AbstractProvider and its underlying classes
c. access token acquisition (part of PHPMailer’s OAuth class)
d. getOauth64() (the remainder of PHPMailer’s OAuth2)

by
a. & b. Google’s google-api-php-client library
c. & d. as above

The implementation of OAuthTokenProvider we use internally accepts either literal strings (ClientID value and so on) or a single record php-imploded string file containing these that is automatically generated as part of the initial offline refresh-token creation. Horses for courses. But it also supports MSFT OAuth2 as well as Google, and a file is useful for storing the new MSFT refresh token created with each access token (MSFT refresh tokens expire after 90 days or less; Google’s last forever, and MSFT client_secrets also expire)

One advantage of the Google-supplied client is that, unlike TheLeague’s oauth2-google client, it supports Google service accounts (aka client_credentials) as well as authorization_code grant flow, and to provide this we intend to replace TheLeague’s oauth2-google client by Google’s google-api-php-client library.

@murraycollingwood
Copy link

Thanks for the responses.

I rolled my own MyOAuthTokenProvider class to implement OAuthTokenProvider.
Now I'm getting this error:

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.

PS I removed parts of the encoded strings so they aren't copies of the originals

From other posts in this group I can see this is a security issue at the google end, but it would be nice if google provided a bit more information about why it couldn't authenticate. While we include a username (email) there is no password, it's not relevant with XOAUTH2 - so the message is unhelpful.

I will post this question to google, but if you have anything to add please do so.

Cheers
Murray

@decomplexity
Copy link
Contributor

What scopes have you specified in the OAuth Consent screen App Registration in Google Cloud Console?

@murraycollingwood
Copy link

image

This was the only one that seemed appropriate, however it requires verification, which hasn't happened yet because I can't get it working (verification requires a video of the program operating) - I think it's a chicken and egg thing.

Do I have to specify that scope anywhere in the OAuth object I pass to PHPMailer????

@decomplexity
Copy link
Contributor

I only ask because Pete Scopes’ document ‘Gmail XOAUTH2 Using Google API Client’ (7th June 2022) contains:
$this->client->setScopes([\Google_Service_Gmail::MAIL_GOOGLE_COM]);
where MAIL_GOOGLE_COM is, unsurprisingly, equivalent to:
$client->setScopes(array('https://mail.google.com/'));

If you have a more restrictive scope setting in the OAuth Consent screen, you will either get your access token request bounced or, as in your case, get an access token with the more restrictive setting that will be bounced when presented to Gmail for authentication.

This is Inconsistent with the returned ‘Username and Password not accepted‘ message perhaps, but since XOAUTH2-type authorisation has (vide your diagnostics) been accepted in principle, the message is spurious anyway: a legacy of ‘basic’ LOGIN authentication perhaps.

Unlike MSFT access tokens, Google ones are opaque, but Google's tokeninfo endpoint will decode it for you:
curl "https://oauth2.googleapis.com/tokeninfo? access_token=[your access token string]"
that returns (among others) the scope field values.

Finally, it's worth checking that the envelope and header 'From' email address fields are consistent with the Gmail account registration: your token has '[email protected]' as bearer

@murraycollingwood
Copy link

I saw some stuff about scope, and only the generic gmail scope was found to be working by others. I've got a bit of time so I'm checking this with the gmail developer support. I'm also pushing the verification team to make some comment about whether this error is due to an unverified app. I'll post again when I get some responses.

@decomplexity
Copy link
Contributor

decomplexity commented Nov 18, 2023

RFC 6749 is clear about what should happen if your client asked for more permissive scopes than was registered:
you should get an "invalid_scope' bounce message from the token server, and this is to be taken to mean "The requested scope is invalid, unknown, malformed, or exceeds the scope granted by the resource owner.
So if the scopes were mismatched, you should not have got as far as receiving an access token at all.
If (if...) it transpires you have a scope problem, try the following (overkill) four in the client (with 'openid' first; the order of the rest is irrelevant) and Google console:
openid
https://www.googleapis.com/userinfo.email
https://www.googleapis.com/userinfo.profile
https://mail.google.com/

@decomplexity
Copy link
Contributor

One obvious point worth remembering that when an access token is issued, it has not yet been presented to the resource (Gmail) for authentication. So the token can be quite valid (it will be, assuming Google stick to RFC 6749) ) but inappropriate for what you want to use it for. When it is presented to Gmail as the resource, Gmail (perhaps with a sideways glance at the token server) will check that your intended use (SMTP outbound) is consistent with the token’s scopes, expiration date, audience and so on. It may also check the bearer ‘user’ prefix email address (the bit starting dXNlc…): Google won’t let you send from an arbitrary address.

Your SMTP server’s return is (in UTF-8):
{"status":"400","schemes":"Bearer","scope":"https:/vR6'
where the 400 is simply ‘bad request’.

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

4 participants