+
+
\ No newline at end of file
diff --git a/packages/oauth2-example/public/refresh.php b/packages/oauth2-example/public/refresh.php
new file mode 100755
index 00000000..78cfa5a7
--- /dev/null
+++ b/packages/oauth2-example/public/refresh.php
@@ -0,0 +1,56 @@
+ getenv('BLACKBAUD_ACCESS_KEY'),
+
+ /**
+ * OAuth 2.0 App Credentials
+ * @link https://developer.blackbaud.com/apps/
+ */
+ // The client ID assigned to you by the provider
+ 'clientId' => getenv('OAUTH_CLIENT_ID'),
+ // The client password assigned to you by the provider
+ 'clientSecret' => getenv('OAUTH_CLIENT_SECRET')
+]);
+
+$existingAccessToken = new AccessToken($_POST); // get access token from your data store
+
+// FIXME normally we'd test $existingAccessToken->hasExpired() before refreshing
+$newAccessToken = $sky->getAccessToken('refresh_token', [
+ 'refresh_token' => $existingAccessToken->getRefreshToken()
+]);
+
+// Purge old access token and store new access token to your data store.
+
+?>
+
+
+
+ Refresh Token
+
+
+
+
+
+
diff --git a/packages/oauth2-example/public/token.php b/packages/oauth2-example/public/token.php
new file mode 100755
index 00000000..b846db22
--- /dev/null
+++ b/packages/oauth2-example/public/token.php
@@ -0,0 +1,92 @@
+ getenv('BLACKBAUD_ACCESS_KEY'),
+
+ /**
+ * OAuth 2.0 App Credentials
+ * @link https://developer.blackbaud.com/apps/
+ */
+ // The client ID assigned to you by the provider
+ 'clientId' => getenv('OAUTH_CLIENT_ID'),
+ // The client password assigned to you by the provider
+ 'clientSecret' => getenv('OAUTH_CLIENT_SECRET'),
+ // Redirect URI registered with the provider
+ 'redirectUri' => getenv('OAUTH_REDIRECT_URL')
+]);
+
+// If we don't have an authorization code then get one
+if (!isset($_GET['code'])) {
+ // Fetch the authorization URL from the provider; this returns the
+ // urlAuthorize option and generates and applies any necessary parameters
+ // (e.g. state).
+ $authorizationUrl = $sky->getAuthorizationUrl();
+
+ // Get the state generated for you and store it to the session.
+ $_SESSION['oauth2state'] = $sky->getState();
+
+ // Redirect the user to the authorization URL.
+ header('Location: ' . $authorizationUrl);
+ exit;
+
+// Check given state against previously stored one to mitigate CSRF attack
+} elseif (empty($_GET['state']) || (isset($_SESSION['oauth2state']) && $_GET['state'] !== $_SESSION['oauth2state'])) {
+ if (isset($_SESSION['oauth2state'])) {
+ unset($_SESSION['oauth2state']);
+ }
+
+ exit('Invalid state');
+} else {
+ try {
+ // Try to get an access token using the authorization code grant.
+ $accessToken = $sky->getAccessToken('authorization_code', [
+ 'code' => $_GET['code']
+ ]);
+
+ $school = $sky->endpoint('school/v1');
+ $levels = $school->get('levels');
+ } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
+ // Failed to get the access token or user details.
+ exit($e->getMessage());
+ }
+}
+
+?>
+
+
+
+ Request Token
+
+
+
+
Request Token
+
Requested an access token using authorization_code flow.
+
+
Access Token
+
= json_encode($accessToken, JSON_PRETTY_PRINT) ?>
+
+
+
GET /school/v1/levels
+
= json_encode($levels, JSON_PRETTY_PRINT) ?>
+
+
diff --git a/packages/oauth2/.gitattributes b/packages/oauth2/.gitattributes
new file mode 100644
index 00000000..e250b932
--- /dev/null
+++ b/packages/oauth2/.gitattributes
@@ -0,0 +1 @@
+/phpcs.xml export-ignore
\ No newline at end of file
diff --git a/packages/oauth2/.github/workflows/todo.yml b/packages/oauth2/.github/workflows/todo.yml
new file mode 100644
index 00000000..09bd5e51
--- /dev/null
+++ b/packages/oauth2/.github/workflows/todo.yml
@@ -0,0 +1,12 @@
+name: "Run TODO to Issue"
+on: ["push"]
+jobs:
+ build:
+ runs-on: "ubuntu-latest"
+ steps:
+ - uses: "actions/checkout@v3"
+ - name: "TODO to Issue"
+ uses: "alstr/todo-to-issue-action@v4"
+ with:
+ AUTO_ASSIGN: true
+ IDENTIFIERS: "[{\"name\": \"TODO\", \"labels\": []}, {\"name\": \"FIXME\", \"labels\": [\"bug\"]}]"
diff --git a/packages/oauth2/.gitignore b/packages/oauth2/.gitignore
new file mode 100644
index 00000000..af50cbc8
--- /dev/null
+++ b/packages/oauth2/.gitignore
@@ -0,0 +1,6 @@
+.DS_Store
+/.cache/
+/coverage/
+/docs/
+/tools/
+/vendor/
diff --git a/packages/oauth2/.nova/Artwork b/packages/oauth2/.nova/Artwork
new file mode 100644
index 00000000..579530d5
Binary files /dev/null and b/packages/oauth2/.nova/Artwork differ
diff --git a/packages/oauth2/.nova/Configuration.json b/packages/oauth2/.nova/Configuration.json
new file mode 100644
index 00000000..a445e5e9
--- /dev/null
+++ b/packages/oauth2/.nova/Configuration.json
@@ -0,0 +1,5 @@
+{
+ "workspace.art_style" : 1,
+ "workspace.color" : 9,
+ "workspace.name" : "OAuth2-BlackbaudSKY"
+}
diff --git a/packages/oauth2/.nova/Tasks/docs.json b/packages/oauth2/.nova/Tasks/docs.json
new file mode 100644
index 00000000..e21ca8f6
--- /dev/null
+++ b/packages/oauth2/.nova/Tasks/docs.json
@@ -0,0 +1,9 @@
+{
+ "actions" : {
+ "run" : {
+ "enabled" : true,
+ "script" : "composer run-script docs"
+ }
+ },
+ "openLogOnRun" : "start"
+}
diff --git a/packages/oauth2/LICENSE.md b/packages/oauth2/LICENSE.md
new file mode 100644
index 00000000..58a52a7d
--- /dev/null
+++ b/packages/oauth2/LICENSE.md
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013-2015 Alex Bilbie
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
\ No newline at end of file
diff --git a/packages/oauth2/README.md b/packages/oauth2/README.md
new file mode 100644
index 00000000..a4870170
--- /dev/null
+++ b/packages/oauth2/README.md
@@ -0,0 +1,169 @@
+# Blackbaud SKY API OAuth 2.0 Client
+
+TThis package provides Blackbaud SKY OAuth 2.0 support for the [PHP League's OAuth 2.0 Client](https://oauth2-client.thephpleague.com/)
+
+[![Version](http://poser.pugx.org/groton-school/oauth2-blackbaudsky/version)](https://packagist.org/packages/groton-school/oauth2-blackbaudsky)
+[![License](http://poser.pugx.org/groton-school/oauth2-blackbaudsky/license)](https://packagist.org/packages/groton-school/oauth2-blackbaudsky)
+[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/groton-school/OAuth2-BlackbaudSKY/badges/quality-score.png?b=main)](https://scrutinizer-ci.com/g/groton-school/OAuth2-BlackbaudSKY/?branch=main)
+[![Code Coverage](https://scrutinizer-ci.com/g/groton-school/OAuth2-BlackbaudSKY/badges/coverage.png?b=main)](https://scrutinizer-ci.com/g/groton-school/OAuth2-BlackbaudSKY/?branch=main)
+---
+
+This package is compliant with [PSR-1][], [PSR-2][], [PSR-4][], and [PSR-7][]. If you notice compliance oversights, please send a patch via pull request. If you're interesting in contributing to this library, please take a look at our [contributing guidelines](CONTRIBUTING.md).
+
+## Requirements
+
+The following versions of PHP are supported.
+
+* PHP 5.6
+* PHP 7.0
+* PHP 7.1
+* PHP 7.2
+* PHP 7.3
+* PHP 7.4
+* PHP 8.0
+
+## Usage
+
+Refer to [example](https://github.com/groton-school/OAuth2-BlackbaudSKY-example) for example usage.
+
+### Authorization Code Grant
+
+The following example uses the out-of-the-box `GenericProvider` provided by this library. If you're looking for a specific provider (i.e. Facebook, Google, GitHub, etc.), take a look at our [list of provider client libraries](docs/providers/thirdparty.md). **HINT: You're probably looking for a specific provider.**
+
+The authorization code grant type is the most common grant type used when authenticating users with a third-party service. This grant type utilizes a client (this library), a server (the service provider), and a resource owner (the user with credentials to a protected—or owned—resource) to request access to resources owned by the user. This is often referred to as _3-legged OAuth_, since there are three parties involved.
+
+The following example illustrates this using [Brent Shaffer's](https://github.com/bshaffer) demo OAuth 2.0 application named **Lock'd In**. When running this code, you will be redirected to Lock'd In, where you'll be prompted to authorize the client to make requests to a resource on your behalf.
+
+Now, you don't really have an account on Lock'd In, but for the sake of this example, imagine that you are already logged in on Lock'd In when you are redirected there.
+
+```php
+$sky = new \GrotonSchool\OAuth2\Client\Provider\BlackbaudSKY([
+ BlackbaudSKY::ACCESS_KEY => 'key', // A Blackbaud SKY API subscription access key
+ 'clientId' => 'demoapp', // The client ID assigned to your app by Blackbaud
+ 'clientSecret' => 'demopass', // The client password assigned to your app by Blackbaud
+ 'redirectUri' => 'http://example.com/your-redirect-url/'
+]);
+
+// If we don't have an authorization code then get one
+if (!isset($_GET['code'])) {
+
+ // Fetch the authorization URL from the provider; this returns the
+ // urlAuthorize option and generates and applies any necessary parameters
+ // (e.g. state).
+ $authorizationUrl = $sky->getAuthorizationUrl();
+
+ // Get the state generated for you and store it to the session.
+ $_SESSION['oauth2state'] = $sky->getState();
+
+ // Redirect the user to the authorization URL.
+ header('Location: ' . $authorizationUrl);
+ exit;
+
+// Check given state against previously stored one to mitigate CSRF attack
+} elseif (empty($_GET['state']) || (isset($_SESSION['oauth2state']) && $_GET['state'] !== $_SESSION['oauth2state'])) {
+
+ if (isset($_SESSION['oauth2state'])) {
+ unset($_SESSION['oauth2state']);
+ }
+
+ exit('Invalid state');
+
+} else {
+
+ try {
+
+ // Try to get an access token using the authorization code grant.
+ $accessToken = $sky->getAccessToken('authorization_code', [
+ 'code' => $_GET['code']
+ ]);
+
+ // We have an access token, which we may use in authenticated
+ // requests against the service provider's API.
+ echo 'Access Token: ' . $accessToken->getToken() . " ";
+ echo 'Refresh Token: ' . $accessToken->getRefreshToken() . " ";
+ echo 'Expired in: ' . $accessToken->getExpires() . " ";
+ echo 'Already expired? ' . ($accessToken->hasExpired() ? 'expired' : 'not expired') . " ";
+
+ // The provider provides a way to get an authenticated API request for
+ // the service, using the access token; it returns an object conforming
+ // to Psr\Http\Message\RequestInterface.
+ $request = $sky->getAuthenticatedRequest(
+ 'GET',
+ 'https://api.sky.blackbaud.com/school/v1/academics/departments',
+ $accessToken
+ );
+
+ // For convenience, the provider also wraps endpoints with a Guzzle client
+ $school = $sky->endpoint('school/v1');
+ var_export($school->get('levels'));
+
+ // ...and those endpoints can also nest further endpoints
+ $academics = $school->endpoint('academics');
+ var_export($academics->get('departments'));
+
+ } catch (\League\OAuth2\Client\Provider\Exception\IdentityProviderException $e) {
+
+ // Failed to get the access token or user details.
+ exit($e->getMessage());
+
+ }
+
+}
+```
+
+### Refreshing a Token
+
+Once your application is authorized, you can refresh an expired token using a refresh token rather than going through the entire process of obtaining a brand new token. To do so, simply reuse this refresh token from your data store to request a refresh.
+
+_This example uses [Brent Shaffer's](https://github.com/bshaffer) demo OAuth 2.0 application named **Lock'd In**. See authorization code example above, for more details._
+
+```php
+$sky = new \League\OAuth2\Client\Provider\GenericProvider([
+ BlackbaudSKY::ACCESS_KEY => 'key', // A Blackbaud SKY API subscription access key
+ 'clientId' => 'demoapp', // The client ID assigned to your app by Blackbaud
+ 'clientSecret' => 'demopass' // The client password assigned to your app by Blackbaud
+]);
+
+$existingAccessToken = getAccessTokenFromYourDataStore();
+
+if ($existingAccessToken->hasExpired()) {
+ $newAccessToken = $sky->getAccessToken('refresh_token', [
+ 'refresh_token' => $existingAccessToken->getRefreshToken()
+ ]);
+
+ // Purge old access token and store new access token to your data store.
+}
+```
+
+### Using a proxy
+
+It is possible to use a proxy to debug HTTP calls made to a provider. All you need to do is set the `proxy` and `verify` options when creating your Provider instance. Make sure you enable SSL proxying in your proxy.
+
+``` php
+$sky = new \League\OAuth2\Client\Provider\GenericProvider([
+ BlackbaudSKY::ACCESS_KEY => 'key', // A Blackbaud SKY API subscription access key
+ 'clientId' => 'demoapp', // The client ID assigned to your app by Blackbaud
+ 'clientSecret' => 'demopass', // The client password assigned to your app by Blackbaud
+ 'redirectUri' => 'http://example.com/your-redirect-url/'
+ 'proxy' => '192.168.0.1:8888',
+ 'verify' => false
+]);
+```
+
+## Install
+
+Via Composer
+
+``` bash
+$ composer require groton-school/oauth2-blackbaudsky
+```
+
+## License
+
+The MIT License (MIT). Please see [License File](https://github.com/thephpleague/oauth2-client/blob/master/LICENSE) for more information.
+
+
+[PSR-1]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md
+[PSR-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md
+[PSR-4]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md
+[PSR-7]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-7-http-message.md
diff --git a/packages/oauth2/composer.json b/packages/oauth2/composer.json
new file mode 100644
index 00000000..7083a2ee
--- /dev/null
+++ b/packages/oauth2/composer.json
@@ -0,0 +1,26 @@
+{
+ "name": "groton-school/oauth2-blackbaudsky",
+ "description": "This package provides Blackbaud SKY OAuth 2.0 support for the PHP League's OAuth 2.0 Client",
+ "license": "MIT",
+ "authors": [
+ {
+ "name": "Seth Battis",
+ "email": "sbattis@groton.org"
+ }
+ ],
+ "require": {
+ "ext-curl": "*",
+ "league/oauth2-client": "^2.6",
+ "guzzlehttp/guzzle": "^7.4",
+ "battis/data-utilities": "^1.2"
+ },
+ "autoload": {
+ "psr-4": {
+ "GrotonSchool\\OAuth2\\Client\\Provider\\": "src/"
+ }
+ },
+ "scripts": {
+ "docs": "./tools/phpdocumentor -d src -t docs"
+ }
+
+}
diff --git a/packages/oauth2/composer.lock b/packages/oauth2/composer.lock
new file mode 100644
index 00000000..39a59c86
--- /dev/null
+++ b/packages/oauth2/composer.lock
@@ -0,0 +1,817 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "30d3e417431000f301ababbd3ff99a80",
+ "packages": [
+ {
+ "name": "battis/data-utilities",
+ "version": "v1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/battis/data-utilities.git",
+ "reference": "b821e6f271476733d66e2a127e9938c8b3a17cd5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/battis/data-utilities/zipball/b821e6f271476733d66e2a127e9938c8b3a17cd5",
+ "reference": "b821e6f271476733d66e2a127e9938c8b3a17cd5",
+ "shasum": ""
+ },
+ "require": {
+ "battis/hydratable": "^0.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Battis\\DataUtilities\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "Seth Battis",
+ "email": "sethbattis@stmarksschool.org"
+ }
+ ],
+ "description": "A handful of useful helper functions to process data",
+ "support": {
+ "issues": "https://github.com/battis/data-utilities/issues",
+ "source": "https://github.com/battis/data-utilities/tree/v1.2.0"
+ },
+ "time": "2024-02-12T14:28:34+00:00"
+ },
+ {
+ "name": "battis/hydratable",
+ "version": "v0.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/battis/hydratable.git",
+ "reference": "a8ab2428b070dbe1f2143c7783898237727f3c79"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/battis/hydratable/zipball/a8ab2428b070dbe1f2143c7783898237727f3c79",
+ "reference": "a8ab2428b070dbe1f2143c7783898237727f3c79",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Battis\\Hydratable\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "GPL-3.0"
+ ],
+ "authors": [
+ {
+ "name": "Seth Battis",
+ "email": "seth@battis.net"
+ }
+ ],
+ "description": "Hydrate serialized objects with defaults and overrides",
+ "support": {
+ "issues": "https://github.com/battis/hydratable/issues",
+ "source": "https://github.com/battis/hydratable/tree/v0.1.1"
+ },
+ "time": "2023-07-04T21:21:58+00:00"
+ },
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "7.8.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "41042bc7ab002487b876a0683fc8dce04ddce104"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/41042bc7ab002487b876a0683fc8dce04ddce104",
+ "reference": "41042bc7ab002487b876a0683fc8dce04ddce104",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/promises": "^1.5.3 || ^2.0.1",
+ "guzzlehttp/psr7": "^1.9.1 || ^2.5.1",
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-client": "^1.0",
+ "symfony/deprecation-contracts": "^2.2 || ^3.0"
+ },
+ "provide": {
+ "psr/http-client-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "ext-curl": "*",
+ "php-http/client-integration-tests": "dev-master#2c025848417c1135031fdf9c728ee53d0a7ceaee as 3.0.999",
+ "php-http/message-factory": "^1.1",
+ "phpunit/phpunit": "^8.5.36 || ^9.6.15",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "suggest": {
+ "ext-curl": "Required for CURL handler support",
+ "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "psr-18",
+ "psr-7",
+ "rest",
+ "web service"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/7.8.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-03T20:35:24+00:00"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/bbff78d96034045e58e13dedd6ad91b5d1253223",
+ "reference": "bbff78d96034045e58e13dedd6ad91b5d1253223",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.36 || ^9.6.15"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/2.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-03T20:19:20+00:00"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "2.6.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/45b30f99ac27b5ca93cb4831afe16285f57b8221",
+ "reference": "45b30f99ac27b5ca93cb4831afe16285f57b8221",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "ralouphie/getallheaders": "^3.0"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "http-interop/http-factory-tests": "^0.9",
+ "phpunit/phpunit": "^8.5.36 || ^9.6.15"
+ },
+ "suggest": {
+ "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://sagikazarmark.hu"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/2.6.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-12-03T20:05:35+00:00"
+ },
+ {
+ "name": "league/oauth2-client",
+ "version": "2.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/oauth2-client.git",
+ "reference": "160d6274b03562ebeb55ed18399281d8118b76c8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/oauth2-client/zipball/160d6274b03562ebeb55ed18399281d8118b76c8",
+ "reference": "160d6274b03562ebeb55ed18399281d8118b76c8",
+ "shasum": ""
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^6.0 || ^7.0",
+ "paragonie/random_compat": "^1 || ^2 || ^9.99",
+ "php": "^5.6 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.3.5",
+ "php-parallel-lint/php-parallel-lint": "^1.3.1",
+ "phpunit/phpunit": "^5.7 || ^6.0 || ^9.5",
+ "squizlabs/php_codesniffer": "^2.3 || ^3.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-2.x": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\OAuth2\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Alex Bilbie",
+ "email": "hello@alexbilbie.com",
+ "homepage": "http://www.alexbilbie.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Woody Gilk",
+ "homepage": "https://github.com/shadowhand",
+ "role": "Contributor"
+ }
+ ],
+ "description": "OAuth 2.0 Client Library",
+ "keywords": [
+ "Authentication",
+ "SSO",
+ "authorization",
+ "identity",
+ "idp",
+ "oauth",
+ "oauth2",
+ "single sign on"
+ ],
+ "support": {
+ "issues": "https://github.com/thephpleague/oauth2-client/issues",
+ "source": "https://github.com/thephpleague/oauth2-client/tree/2.7.0"
+ },
+ "time": "2023-04-16T18:19:15+00:00"
+ },
+ {
+ "name": "paragonie/random_compat",
+ "version": "v9.99.100",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paragonie/random_compat.git",
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
+ "reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">= 7"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "4.*|5.*",
+ "vimeo/psalm": "^1"
+ },
+ "suggest": {
+ "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
+ },
+ "type": "library",
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paragon Initiative Enterprises",
+ "email": "security@paragonie.com",
+ "homepage": "https://paragonie.com"
+ }
+ ],
+ "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
+ "keywords": [
+ "csprng",
+ "polyfill",
+ "pseudorandom",
+ "random"
+ ],
+ "support": {
+ "email": "info@paragonie.com",
+ "issues": "https://github.com/paragonie/random_compat/issues",
+ "source": "https://github.com/paragonie/random_compat"
+ },
+ "time": "2020-10-15T08:29:30+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "e616d01114759c4c489f93b099585439f795fe35"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/e616d01114759c4c489f93b099585439f795fe35",
+ "reference": "e616d01114759c4c489f93b099585439f795fe35",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.0.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory/tree/1.0.2"
+ },
+ "time": "2023-04-10T20:10:41+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/7c3aff79d10325257a001fcf92d991f24fc967cf",
+ "reference": "7c3aff79d10325257a001fcf92d991f24fc967cf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.4-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2023-05-23T14:45:45+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": {
+ "ext-curl": "*"
+ },
+ "platform-dev": [],
+ "plugin-api-version": "2.6.0"
+}
diff --git a/packages/oauth2/phive.xml b/packages/oauth2/phive.xml
new file mode 100644
index 00000000..98ee245b
--- /dev/null
+++ b/packages/oauth2/phive.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/packages/oauth2/phpdoc.dist.xml b/packages/oauth2/phpdoc.dist.xml
new file mode 100644
index 00000000..8c2136b9
--- /dev/null
+++ b/packages/oauth2/phpdoc.dist.xml
@@ -0,0 +1,19 @@
+
+
+
+
+ .cache/phpdoc
+
+
+
+
+
+
+
diff --git a/packages/oauth2/psalm.xml b/packages/oauth2/psalm.xml
new file mode 100644
index 00000000..2ef1ac8e
--- /dev/null
+++ b/packages/oauth2/psalm.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/packages/oauth2/src/BlackbaudSKY.php b/packages/oauth2/src/BlackbaudSKY.php
new file mode 100644
index 00000000..707b8981
--- /dev/null
+++ b/packages/oauth2/src/BlackbaudSKY.php
@@ -0,0 +1,129 @@
+accessKey = $options[self::ACCESS_KEY];
+ }
+
+ if (!empty($options[self::ACCESS_TOKEN])) {
+ $this->accessToken = $options[self::ACCESS_TOKEN];
+ }
+ }
+
+ public function getBaseAuthorizationUrl()
+ {
+ return "https://oauth2.sky.blackbaud.com/authorization";
+ }
+
+ public function getBaseAccessTokenUrl(array $params)
+ {
+ return "https://oauth2.sky.blackbaud.com/token";
+ }
+
+ public function getBaseApiUrl()
+ {
+ return "https://api.sky.blackbaud.com";
+ }
+
+ public function getResourceOwnerDetailsUrl(AccessToken $token)
+ {
+ // TODO waiting on resolution of https://app.blackbaud.com/support/cases/018662802
+ }
+
+ protected function getDefaultScopes()
+ {
+ return [];
+ }
+
+ protected function checkResponse(ResponseInterface $response, $data)
+ {
+ }
+
+ protected function createResourceOwner(array $response, AccessToken $token)
+ {
+ }
+
+ /**
+ * Returns authorization headers for the 'bearer' grant.
+ *
+ * @param AccessTokenInterface|string|null $token Either a string or an access token instance
+ * @return array
+ */
+ protected function getAuthorizationHeaders($token = null)
+ {
+ return [
+ self::ACCESS_KEY => $this->accessKey,
+ "Authorization" => "Bearer " . $token,
+ ];
+ }
+
+ public function getAccessToken($grant = "", array $options = [])
+ {
+ if (!empty($grant)) {
+ $this->accessToken = parent::getAccessToken($grant, $options);
+ return $this->accessToken;
+ } elseif (!empty($this->accessToken)) {
+ return $this->accessToken->getToken();
+ } else {
+ throw new Exception("Stored access token or grant type required");
+ }
+ }
+
+ public function setAccessToken(AccessToken $accessToken)
+ {
+ $this->accessToken = $accessToken;
+ }
+
+ /** @deprecated 0.2.3 externalized to {@link https://github.com/groton-school/appengine-sky-api groton-school/appengine-sky-api} */
+ public function endpoint(
+ string $path,
+ ?AccessToken $token = null
+ ): SkyAPIEndpoint {
+ if (!$token) {
+ if ($this->accessToken) {
+ $token = $this->accessToken;
+ } else {
+ throw new Exception("No access token provided or cached");
+ }
+ }
+ return new SkyAPIEndpoint($this, $path, $token);
+ }
+}
diff --git a/packages/oauth2/src/SkyAPIEndpoint.php b/packages/oauth2/src/SkyAPIEndpoint.php
new file mode 100644
index 00000000..e311057c
--- /dev/null
+++ b/packages/oauth2/src/SkyAPIEndpoint.php
@@ -0,0 +1,89 @@
+sky = $sky;
+ $this->path = $path;
+ $this->accessToken = $accessToken;
+ $this->client = new Client([
+ "base_uri" => Path::join($this->sky->getBaseApiUrl(), $this->path) . "/",
+ ]);
+ }
+
+ public function send(string $method, string $url, array $options = []): mixed
+ {
+ /*
+ * TODO deal with refreshing tokens (need callback to store new refresh token)
+ * https://developer.blackbaud.com/skyapi/docs/in-depth-topics/api-request-throttling
+ */
+ usleep(100000);
+ $request = $this->sky->getAuthenticatedRequest(
+ $method,
+ $url,
+ $this->accessToken,
+ $options
+ );
+ return json_decode(
+ $this->client
+ ->send($request)
+ ->getBody()
+ ->getContents(),
+ true
+ );
+ }
+
+ public function get(string $url, array $options = []): mixed
+ {
+ return $this->send("get", $url, $options);
+ }
+
+ public function post(string $url, array $options = []): mixed
+ {
+ return $this->send("post", $url, $options);
+ }
+
+ public function patch(string $url, array $options = []): mixed
+ {
+ return $this->send("patch", $url, $options);
+ }
+
+ public function delete(string $url, array $options = []): mixed
+ {
+ return $this->send("delete", $url, $options);
+ }
+
+ public function endpoint(string $path): SkyAPIEndpoint
+ {
+ return new SkyAPIEndpoint(
+ $this->sky,
+ Path::join($this->path, $path),
+ $this->accessToken
+ );
+ }
+}