-
Notifications
You must be signed in to change notification settings - Fork 1
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
Providers for client_credentials grant flow? #1
Comments
The only two providers I have used are Jesper Skytte Marcussen’s greew/oauth2-azure-provider and Jan Hajek’s TheNetworg/[oauth2-azure]. Since MSFT did not, as the time, support client_credentials grant for SMTP, I couldn’t experiment to see at what point authorization failed (if indeed it did), and I recall that IMAP and POP were supported by not SMTP. I had missed the recent (10th July) this year Exchange Team’s announcement of SMTP support, so thank you for letting me know. Google had not, at the time, made any statement about future implementation, and I assume this is still the case. Looking at TheLeague’s code, it was obvious that client_credentials grants were supported, and I remember wondering whether the typical provider class (e.g. thenetworg/Provider/Azure.php) would handle client_credentials transparently since it extended theleague/Provider/AbstractProvider.php. The decomplexity wrapper code was, as you know, extended to handle client_credentials grant, but the PHPMailer’s OAuth2.php was written to handle authorization_code grant only. The wrapper’s setup file SendOauth2D-settings.php allowed the grant type to be set by service provider (e.g. MSFT), and the relevant parameter 'grantTypeValue' was passed to PHPMailer’s OAuth2.php My updated PHPMailer OAuth2.php looked something like:
|
so i tried to use client_credentials flow using theleague's provider but it errors with: but if u make a request to the token endpoint to get an AccessToken it does not return a refreshToken (like tha auth code grant) see: https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow#use-a-token. I don't know PHP enough to be able to make a PR in the PHPMailer repo or theleagues repo... Btw i appreciate your help and i hope u have time to get back to this and maybe update these repos. |
I have tested this myself today using the decomplexity wrapper, TheNetworg provider and the modified PHPMailer OAuth2.php I copied to you. |
Having now registered a fresh app specifically for client_credentials flow and registered a new service principal, I am still getting 'failed to authenticate' although the TheNetworg provider and TheLeague code didn't throw errors. The access token content also looks correct although I intend trying to see the effect of forcing an AUD claim of outlook.office.com instead of outlook.office365.com. |
so i followed the docs and created a new app: serviceprincipal:
getaccesstoken: Mail Sent! i think the issue was the serviceprincipal config that i created a lot of time ago. Now the problem is that PHPMailer expect a refresh_token but with the client_credentials flow no refresh_token is issued |
Excellent. Since without trouble I had obtained an access token that looked OK in all the usual respects, the likely culprits were either the wrong Service Principal or wrong Object ID. As I said earlier, the two usual reasons for 'failure to authenticate' are having the wrong AUD claim (an AUD of Graph is a guaranteed failure) and using the wrong scope operands or operands in the wrong order (!! - an undocumented restriction), but since scope for client_credentials flow is a single fixed URL and not a list of scopes, the fault probably lay elsewhere. One issue with TheLeague's oauth2-client is that there is not, as far as I am aware, any mechanism to refresh a refresh token, either when one was expiring or when obtaining each new access token. The wrapper handles this but it also needs a one-line addition to TheLeague's AccessToken.php. Using client_credentials instead makes this change unnecessary! [It is counter-intuitive that permissions for authorisation_code flow are set in Graph, but the scopes to be used must refer to outlook.office.com. If the first scope operand refers to Graph or is a valid Graph scope, (e.g. user.read), any remaining operands are also assumed to be Graph scopes unless they are prefixed with a URL. More details in my "Microsoft OAuth2 SMTP issues.md"] |
Over the weekend I created yet another app. I then amended PHPMailer OAuth.php to use Curl - rather as you did - to get an access token directly instead of relying on TheNetworg provider and TheLeague code:
The access token thus created still caused SMTP to fail authentication!
that does not end /v2.0 in spite of the URL being aimed at the v2.0 endpoint. |
make sure no MFA policy is set on the mailbox, security defaults off. this is my accesstoken decoded from jwt.ms:
|
My finger trouble! I reran the Curl and it worked.
And whereas the Curl token contained a role claim of [SMTP.SendAsApp], there is no role claim in the getAccessToken token, presumably because the SMTP.SendAsApp permission is set only in my tenant. So I now need to check whether the tenantID is obtained by the TheNetWorg provider or the TheLeague's Abstract provider. |
So how hard would it be to add the client_credential flow to thenetworg's provider and make PHPMailer to accept access_token only instead of a refresh_token? |
I don't think PHPMailer itself would need changing outside of Oauth.php, but (from memory) TheNetworg provider default tenantID is 'common' (i.e. a multi-organisation 'tenant'). It may just need a simple override when called: I don't think $tenant is a parameter of Azure.php's constructor but will need to check. |
$tenant is fortunately a public property (but not a constructor parameter) of TheNetworg provider Azure.php.
|
I have upgraded the decomplexity/SendOauth2 wrapper on GitHub and Packagist to accept an optional tenant ID. This should not be needed for client_credentials grant if, by default, the provider specified a tenant ID instead of 'common' as Organisation in the URL parameters when calling the token endpoint. I will put together an update to Oauth.php such that it isn't a breaking change, and offer it as a PR to @Synchro for PHPMailer |
Hi, I hope it's fine I update this issue because it's about client_credentials grant flow. Since our application is a server application I tried using client_credentials grant but whatever I tried it's always giving me "AUTH command failed: 535 5.7.3 Authentication unsuccessful". P.S. I added FullAccess rights to the mailbox via Exchange Online Powershell. @decomplexity Any hints where I can have a look for a complete example how I can use client_credentials grant with PHPMailer and SendOauth2? Thank you in advance! |
Hi! since i was in the same situation as u basically i solved it without using this library.
so u just use send_mail() everywhere u need. Hope it helps! P.S. IIRC the clientid refers to the objectid of the created serviceprincipal. |
Firstly, Mea culpa. I regret changing the port from 587 to 465 (implicit TLS): I have myself always used 587, but when I submitted the example as a PR to PHPMailer in October '23 , @Synchro suggested using 465 "... per RFC8314:
My fault forgetting that MSFT does not like 465. Secondly, a couple of days ago, we released V4.0.0 on the decomplexity/sendoauth2 repo. This release supports Google's official 'Client' API for both authorization_code and client_credentials grant, and for both client_secret and X.509 certificates; but the support for MSFT is essentially unchanged, and I don't think there even any bug fixes in that area. The 'global code' example that will be updated on the PHPMailer repo is awaiting review and approval by @Synchro, but we will put a copy on the decomplexity/sendoauth2 repo as well - tonight or tomorrow. There is no additional worked example since the only additional setup is detailed in MSFT's document on the Exchange Team blog "Announcing Client Credential flow for SMTP AUTH in Exchange Online" which you will have read. When you registered your app for authorization_code grant, you implicitly registered a user principal (or delegated access). Since client_credentials grant has no user, the app has instead to be registered as a service principal, and the last couple of pages of blog pages details this. And they highlight the trap of using the wrong OBJECT_ID - using the one from the app Registration page rather than the correct one from the Enterprise Application page (which is essentially just a list of principals). I remember one oddity in MSFT's example of service principal registration: in the line $AADServicePrincipalDetails = ...., , YourAppName must be in quotes. Since you are getting an authentication failure, and since module SendOauth2C handles scope (permissions) and any overrides needed, it may be helpful to turn on PHPMailer SMTP DEBUG diagnostics and check (or send me) the access code. |
That's very weird about port 465! SMTPS on 465 was originally deprecated only a few months after it was introduced in favour of STARTTLS on 587, however more or less the only implementer that didn't make this switch was Microsoft who never stopped using it for SMTP on Exchange, Outlook, Live, Hotmail, O365, and whatever they are calling it this week. It all came full circle in RFC8314 when the roles were reversed and SMTPS on 465 was effectively undeprecated. Are you saying that just when everyone is switching back to the port that MS never stopped using, they suddenly decided to stop using it?? |
I can’t keep track of MSFT’s changes; I assume there is some logic to them. |
Thank you everyone for the help! I succeeded making it work, the code was fine the problem was the way I registered the service principal. Not in a million years could have fixed it, so thank you very much @decomplexity for taking the time and mention this quirk.
|
I have had both MS (client_credentials) and Gmail (authorization_code) working for some time. I am interested in implementing client_credentials for gmail. I updated the library to V4, configured a service account and Domain-wide delegation with google. When I run the code I get the following exception: [14-Jun-2024 00:47:21 UTC] PHP Warning: file_put_contents(gmail-xoauth2-credentials.json): Failed to open stream: Permission denied in /home/mwb/public_html/api/lib/vendor/decomplexity/sendoauth2/src/SendOauth2ETrait.php on line 185 It looks like a json file is being written out and failing due to lack of permissions. The vendor directory is read only. Are you sure this code works in a linux environment? Perhaps I missed a config setting somewhere. Thanks for your time. |
Hi @eradin SendOauth2 was developed and runs on Linux. Subdirectory vendor and its children decomplexity/sendoauth2/src should indeed be read-only. But the parent of vendor - often just ‘php’ – is intended to be read-write as it will contain (if you are using the ‘full’ version of SendOauth2) not just the Google API .json but also the settings interchange file Oauth2parms_n.txt (where ‘n’ is the settings ‘case’ value), SendOauth2D_invoke.php (that calls the settings file SendOauth2D.php and uses ‘n’) and SendOauth2D.php itself (which is used to specify OAuth2 parameters). If you wish to put the .json somewhere else read/write, especially if you were using the multi-parameter calling sequence in the example shown in the PHPMailer repo rather than the ‘full’ version of SendOauth2, you could amend the SendOauth2ETrait $GMAIL_XOUTH2_CREDENTIALS with a suitable subdirectory prefix. If you wish to use the Google-generated .json that you have manually copied rather than the one generated by SendOauth2ETrait.php, simply set $WRITE_GMAIL_CREDENTIALS_FILE in SendOauth2ETrait to false. (Note that because consts were not supported in Traits by php until V8.2.0, the two $s above were defined as properties). |
Thanks. To be clear, I am using the phpmailer example as my base (SendOauth2B). Another issue is the current implementation isn't thread safe as the current generated file has a fixed name. What I would like to see is a parameter where we could specify our own .json file. My application supports "connectors" where many users who can send email via their MS or google account (each specifying their own credentials) When an email goes out, I would generate a user specific json file in the tmp directory and provide SendOauth2B with that file reference. I think this is cleaner and also cuts down the number of parameters since they are contained in the json file. It's also thread safe and doesn't require specific vendor/ location and permissions. I'm happy to create a pull request and you can integrate it into your implementation. I prefer not maintaining forks. Another thought is to have Trait write the json to a tmp/ file but 2C would need to know the file path. Currently these are hard coded. UPDATE: I made a couple of simple changes to write json out to the system tmp directory. It currently writes to the current directory which in my case is restricted. This also solves the thread safe issue as well. When I have a little time, I will create a pull request. |
I am currently travelling and don’t have access to a test rig. Your point about being thread-safe is interesting in that we piloted a version that created the .json with a six-digit random-number filename prefix (that then necessitated housekeeping for aborted sessions), but chose to release the simpler ‘single file’ version as it suits the environment many use it for: mail-out from a website’s Contact form, PayPal IPN and PDT notifications, order confirmations and so on, where the sender is the same on each instantiation. I blame Google for using file-based credentials! What follows gives the caller from PHPMailer an option to specify their own .json and/or use either a generated file or Google’s default. The defaults will remain if not overridden. Note that changes are needed to the ‘full’ system in addition to the section in SendOauth2B that handles calls direct from PHPMailer. This is because SendOauth2C is invoked in both cases; changes are thus also needed to the full system’s interchange file and to SendOauth2D. What follows is not tested and a bit rough; you may wish to compare it with your proposed PR. The code lines are unlikely to conform to PSR-12…
after
add:
after (at 149 or thereabouts)
add:
after (at 328 or thereabouts)
add:
after 409 or thereabouts:
add:
after 436 or thereabouts (comments):
add:
after (at 587 or thereabouts)
add:
after (at 651 or thereabouts)
add:
at 47 or thereabouts
by:
and add:
after (180 or thereabouts)
add:
after (181 or thereabouts)
add:
at 346 or thereabouts
by:
at 63 or thereabouts:
by:
at 70 or thereabouts:
by:
at 97 or thereabouts:
by
at 185 or thereabouts:
by:
from 181 or thereabouts:
add:
after 271 or thereabouts:
add:
after 508 or therebouts:
add:
after 154 or thereabouts:
add:
|
Nice. This is the solution that's needed. My /tmp solution noted earlier was only a quickie so I could do some testing but wasn't intended to be the final solution. A couple of observations from above:
$this->gmail_xouth2_credentials_file = tempnam(sys_get_temp_dir(), 'gmail-credentials-');
I'm going to play with your code above and hold off on the PR. Looking forward to an official update. Really appreciate this project. Oauth2 ain't easy. |
In reply to your points:
Finally, kindly note that there are at least 14 different combinations of provider (MSFT, TheLeague's Google provider + client, Google API), grant flow (authorization_code and client_credentials), credential (client_secret, cl;ient_certificate), and caller (PHPMailer or ‘full’) to test; there would be umpteen more but some are not supported by the provider, e.g. Google API appeared not to support authorization_code flow with certificates. To your comment ‘Oauth2 ain't easy’, Google API was a doddle compared with MSFT’s MSAL with SMTP. My document “Microsoft OAuth2 SMTP issues” in the PHPMailer Wiki says it all, but it needed a MSFT email round trip (India, Redmond etc etc and finally Portugal) to find someone who properly understood what was going on. |
Thanks for the comments. BTW, I implemented your code changes and it works well. I had 2 small issues that needed correcting.
protected $writeGmailCredentialsFile = "true"; should be protected $writeGmailCredentialsFile = true;
An additional reference to WRITE_GMAIL_CREDENTIALS_FILE should be changed to writeGmailCredentialsFile |
Thanks for these corrections - both silly errors. |
Rough untested mods needed to allow ‘impersonate’ to default to ‘mailSMTPAddress’. amend SendOauth2B:
add:
at 445 or thereabouts [corrects a Comment error]
by
at 466 or thereabouts et seq:
move all this to 454 - currently blank; immediately after $this->GoogleAPIOauth2File(); after 653 or thereabouts
add:
amend SendOauth2C: after 149 or thereabouts
add:
after 183 or thereabouts:
add:
at 354 or thereabouts
by:
amend SendOauth2D:
|
@eradin - I plan next week (w.c. 1st July) to aggregate the various changes above into a subversion for testing. |
Nope. I think this is a welcome addition. Look forward to integrating your official changes. |
Master now updated as promised. In practice, having the two operands writeGmailCredentialsFile and gmailXoauth2Credentials gives more flexibility to the developer than I had realised at the outset, and I have changed the logic in the Trait so as to allow the dynamically-created .json also to use the filename set in gmailXoauth2Credentials. This would allow the developer to use a dynamically-created .json with their own filenames (perhaps with a random-number prefix or user prefix to pre-empt a thread clash). |
One small nitpicky comment. I ran composer update and the json file still says ^4.0. Shouldn't it be ^4.1? I had to look in the files to make sure my production code was picking up the latest version. |
Strange... Git's published .json script doesn't (or shouldn't) contain any versioning - apart of course from the 'requires' versions for dependencies. And we rarely use 'carat' versioning. A ^4.0 would, I guess, be equivalent to >=4.0 & < 5.0.0 and Git should anyway download the current release (9), but I'm not sure where your Composer update run is getting the ^4.0 from. Any ideas? (I will check early next week (w.c. 22nd Jul) to see whatever Composer instructions are in the README; there may perhaps be a >=4.0 still there (?), but would be surprised if there was a ^4.0) |
My mistake. I was looking at the wrong file. The lock file is correct. |
‘Composer install’ checks for a composer.lock file in the current directory. If it finds one it continues to use the dependency versions given there instead of evaluating latest versions (or whatever) from composer.json. But, confusingly, ‘npm install’ works more or less the other way around: it uses package.json to create a list of dependencies, and then uses the versions given in the package-lock.json file; I think it first creates a new lock file based on the contents of package.json and then uses the new lock file to install. If a dependency appears in the package.json but not the lock file, the new lock file will naturally have been updated to add it. Early versions of the sendoauth2 modules did indeed include a module 'version' comment at the head; this was later removed, but there is probably still an (unnecessary) PHP version dependency comment. Sendoauth2 now relies on Git's semantic versioning. Updating the version to major version V4.0.0 was needed as there was a fail-gracefully breaking change from V3 for any 'MSFT' developer who used client_credentials when calling PHPMailer's 'traditional' API (it wasn't a BC for anyone using the 'full' (SendOauth2A) API . V4.1.0 should not have any BCs, which is why there are umpteen 'tests for null or blank' to accommodate the additional operands needed for using an externally-supplied credentials .json. |
it seeems that https://github.com/thephpleague/oauth2-client supports client_credentials flow but with a Genericprovider.
is there any provider for Azure that supports it?
it seems that Azure SMTP has been updated to use client_credentials
after configuring a Service-principal.The text was updated successfully, but these errors were encountered: