plai2010/php-oauth2

OAuth2 utilities for PHP.

v1.3 2024-04-27 21:10 UTC

This package is not auto-updated.

Last update: 2024-04-27 21:22:12 UTC


README

This is a utility package for handling OAuth2 in PHP applications. It allows obtaining OAuth2 access token through an authorization code grant with just a browser and a command shell running side by side; there is no need to set up a working web endpoint for the OAuth2 provider to call back.

On-line authorization flow with working redirect is also supported. See this section for how it may be set up in an Laravel application.

The use case for this package was SMTP XOAUTH2 authentication in a Laravel (10.x) application. A Laravel service provider is included.

This package depends on league/oauth2-client, its GenericProvider in particular.

Installation

The package can be installed from Packagist:

$ composer require plai2010/php-oauth2

One may also clone the source repository from Github:

$ git clone https://github.com/plai2010/php-oauth2.git
$ cd php-oauth2
$ composer install

Example: Obtaining Access Token for Outlook SMTP

Let us say a web application has been registered in Micrsoft Azure AD:

* Application ID - 11111111-2222-3333-4444-567890abcdef
* Application secret - v8rstf8eVD5My89xDOTw8CoKG6rIw9dukIjHYzPU
* Redirect URI - http://localhost/example

For our purpose the redirect URI should not point to an actual web site. It can be any URL in proper format; just test with a browser to ake sure it yields a 404 error.

Create a PHP script, say outlook-oauth2.php:

<?php
return [
	'provider' => [
		// As registered with the OAuth2 provider.
		'client_id' => '11111111-2222-3333-4444-567890abcdef',
		'client_secret' => 'v8rstf8eVD5My89xDOTw8CoKG6rIw9dukIjHYzPU',
		'redirect_uri' => 'http://localhost/example',

		// These items are OAuth2 provider specific.
		// The values here are for Microsoft OAuth2.
		'scope_separator' => ' ',
		'url_access_token' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
		'url_authorize' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',

		// Other options.
		'pkce_method' => 'S256',
		'timeout' => 30,
	]
];

To obtain an OAuth2 token for SMTP login (XOAUTH2), start an interactive PHP shell:

$ php -a
Interactive shell

php >

Obtain an authorization URL:

php > require_once 'vendor/autoload.php';
php > config = require('outlook-oauth2.php');
php > // The name 'outlook_smtp' does not matter in this example.
php > $oauth2 = new PL2010\OAuth2\OAuth2Provider('outlook_smtp', $config);
php > // Scope is OAuth2 provider specific.
php > // The value here is for Outlook SMTP login authorization by
php > // Microsoft OAuth2; offline_access to request refresh token.
php > $scope = [ 'https://outlook.office.com/SMTP.Send', 'offline_access' ];
php > $url = $oauth2->authorize('code', $scope);
php > echo $url, PHP_EOL;

An URL like this is printed as the result (line breaks inserted):

https://login.microsoftonline.com/common/oauth2/v2.0/authorize
	?scope=...
	&state=...
	&response_type=code
	&client_id=...
	&...

Do not end the interactive PHP shell. Copy the URL into a browser and go through the steps to authorize access. At the end the browser will be redirected to an non-existing page with URL that matches the redirect URI. Go back to the the interactive PHP shell to process that URL:

php > // Get from browser the URL of the 'not found' page.
php > $redir = 'http://localhost/example?code=...&state=...';
php > $token = $oauth2->receive($redir);
php > echo json_encode($token, JSON_PRETTY_PRINT+JSON_UNESCAPED_SLASHES);
{
	"token_type": "Bearer",
	"scope": "https://outlook.office.com/SMTP.Send",
	"ext_expires_in": 3600,
	"access_token": "EwBFB+l3BAK...",
	"refresh_token": "M.C732_B...",
	"expires": 1689702075
}
php > 

The token can be saved to some storage (e.g. database) for use by an application. If the token has expiration and is refreshable, the application would request a refresh. This is illustrated as follows:

// Retrieve token from storage.
$token = [ ... ];

// Refresh token if it is expiring in say a minute.
$ttl = 60;
$oauth2 = new PL2010\OAuth2\OAuth2Provider(...);
$refreshed = $oauth2->refresh($token, $ttl);
if ($refreshed !== null) {
	// Save refreshed token to storage.
	...
	$token = $refreshed;
}

// Use $token ...

Provider Manager

An application may interact with multiple OAuth2 providers. For example, it may need authorization from Google to access files in Google Drive and from Microsoft to send email via Outlook SMTP. OAuth2Manager makes multiple OAuth2 providers available. For example,

php > // Create manager.
php > $manager = new PL2010\OAuth2\OAuth2Manager;
php > // Configure 'google' provider for 'drive' and 'openid' purposes.
php > $manager->configure('google', [
	'provider' => [
		'client_id' => ...,
		'client_secret' => ...,
		'url_access_token' => 'https://oauth2.googleapis.com/token',
		'url_authorize' => 'https://accounts.google.com/o/oauth2/auth',
	],
	'usage' => [
		'drive' => [
			'scopes' => [
				'https://www.googleapis.com/auth/drive.file',
				'https://www.googleapis.com/auth/drive.resource',
				...
			],
		],
		'signin' => [
			'scopes' => [
				'openid',
				'email',
				...
			],
		],
	],
]);
php > // Configure 'microsoft' provider for 'smtp'.
php > $manager->configure('microsoft', [
	'provider' => [
		'client_id' => ...,
		'client_secret' => ...,
		'url_access_token' => 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
		'url_authorize' => 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
	],
	'usage' => [
		'smtp' => [
			'scopes' => [
				'https://graph.microsoft.com/mail.send',
			],
		],
	]
]);

Then to interact with Microsoft to obtain access token for SMTP login, one would do this:

php > $oauth2 = $manager->get('microsoft', 'smtp');
php > // Like before ...
php > $scope = [ 'https://outlook.office.com/SMTP.Send', 'offline_access' ];
php > $url = $oauth2->authorize('code', $scope);
...

Token Repository

It is up to an application to manage the storage of OAuth2 tokens. This package does includes TokenRepository and some implementation as a model, however. In this model, a token is identified by a two-part key: provider:usage. The two parts correspond to parameters of OAuth2Manager::get()(src/OAuth2Manager.php), so that a repository and refresh tokens as needed. The key scheme can be extended to suit an application. For example, to allow per-user OAuth2 tokens, provider:usage:user_id may suffice.

AbstractTokenRepository is an abstract implementation. Just provide tokenLoad() and tokenSave() methods.

Laravel Integration

This package includes a Laravel service provider that makes a singleton OAuth2Manager available two ways:

* Abstract 'oauth2' in the application container, i.e. `app('oauth2')`.
* Facade alias 'OAuth2', i.e. `OAuth2::`.

The configuration file is config/oauth2.php. It returns an associative array of provider configurations by names, like this:

<?php
return [
	'google' => [
		'provider' => [
			'client_id' => ...,
			'client_secret' => ...,
			...
		],
		'usage' => [
			'drive' => [
				...
			],
			'signin' => [
				...
			],
		],
	],

	'microsoft' => [
		'provider' => [
			'client_id' => ...,
			'client_secret' => ...,
			...
		],
		'usage' => [
			'smtp' => [
				...
			],
		]
	],
];

There is no TokenRepository setup, but here is an example of making available as 'oauth2_tokens' a singleton DirectoryTokenRepository:

use PL2010\OAuth2\Repositories\DirectoryTokenRepository;

app()->singleton('oauth2_tokens', function() {
	return new DirectoryTokenRepository(
		storage_path('app/oauth2_tokens'),
		app('oauth2')
	);
});

Example: Online Flow in Laravel Application

Let's say a Laravel application at example.com is using this package. One can add a route in routes/web.php for starting an authorization code grant flow:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Route;

// Presumably there would a button on some page to do make a POST request,
// but for simplicity we just use `GET` in our example.
Route::get('/oauth2/authorize/{provider}/{usage?}', function(
	Request $request,
	string $provider,
	?string $usage=null
) {
	/**
	 * @var \PL2010\OAuth2\OAuth2Manager $mgr
	 * @var \PL2010\OAuth2\OAuth2Provider $oauth2
	 */
	$mgr = app('oauth2');
	$oauth2 = $mgr->get($provider, $usage);
	$redirect = route('oauth2.callback', [
		'provider' => $provider,
		'usage' => $usage,
	]);
	$url = $oauth2->authorize('code', '', $redirect, function($state, $data) {
		// Preserve state data in cache for a short while.
		Cache::put("oauth2:flow:state:{$state}", $data, now()->addMinutes(15));
	});
	return redirect($url);
})->middleware([
	// There would be middlewares appropriate for the use case.
	'can:configureOAuth2',
])->name('oauth2.authorize');

Using Microsoft SMTP as in the Provider Manager example, request https://example.com/oauth2/authorize/microsoft/smtp would redirect to Microsoft's login.microsoftonline.com.

A route named oauth2.callback is assumed above for receiving authorization code. Here is what it would look like:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Route;

// Handle redirect from OAuth2 authorization provider.
Route::get('/oauth2/callback/{provider}/{usage?}', function(
	Request $request,
	string $provider,
	?string $usage=null
) {
	/**
	 * @var \PL2010\OAuth2\OAuth2Manager $mgr
	 * @var \PL2010\OAuth2\OAuth2Provider $oauth2
	 * @var \PL2010\OAuth2\Contracts\TokenRepository $tkrepo
	 */
	// Expect authorization state in the request.
	$state = $request->get('state');
	if (!is_string($state))
		return redirect('/')->with('error', 'Missing OAuth2 state');

	// Retrieve preserved state data.
	$data = Cache::get("oauth2:flow:state:{$state}");
	if (!$data)
		return redirect('/')->with('error', 'Invalid OAuth2 state');

	// Obtain access token.
	$mgr = app('oauth2');
	$oauth2 = $mgr->get($provider, $usage);
	$token = $oauth2->receive($request->fullUrl(), preserved:$data);

	// Save access token.
	$key = $provider.($usage != ''
		? ":{$usage}"
		: ''
	);
	$tkrepo = app('oauth2_tokens');
	$tkrepo->putOAuth2Token($key, $token);

	return redirect('/')->with('success', 'OAuth2 access token saved');
})->middleware([
	// There would be middlewares appropriate for the use case.
	'can:configureOAuth2',
])->name('oauth2.callback');

On Microsoft side, OAuth2 should be configured to allow redirect URI https://example.com/oauth2/callback/microsoft/smtp.