Repository: legalthings/sso
Branch: master
Commit: e806f50c387d
Files: 54
Total size: 107.8 KB
Directory structure:
gitextract_z0pcsndb/
├── .gitattributes
├── .github/
│ └── workflows/
│ └── php.yml
├── .gitignore
├── .scrutinizer.yml
├── LICENSE
├── README.md
├── codeception.yml
├── composer.json
├── demo/
│ ├── ajax-broker/
│ │ ├── api.php
│ │ ├── app.js
│ │ ├── attach.php
│ │ ├── index.html
│ │ └── verify.php
│ ├── broker/
│ │ ├── error.php
│ │ ├── include/
│ │ │ ├── attach.php
│ │ │ └── functions.php
│ │ ├── index.php
│ │ ├── login.php
│ │ └── logout.php
│ └── server/
│ ├── api/
│ │ ├── info.php
│ │ ├── login.php
│ │ └── logout.php
│ ├── attach.php
│ └── include/
│ ├── config.php
│ └── start_broker_session.php
├── phpcs.xml
├── phpstan.neon
├── src/
│ ├── Broker/
│ │ ├── Broker.php
│ │ ├── Cookies.php
│ │ ├── Curl.php
│ │ ├── NotAttachedException.php
│ │ ├── RequestException.php
│ │ └── Session.php
│ └── Server/
│ ├── BrokerException.php
│ ├── ExceptionInterface.php
│ ├── GlobalSession.php
│ ├── Server.php
│ ├── ServerException.php
│ └── SessionInterface.php
└── tests/
├── _bootstrap.php
├── _output/
│ └── .gitignore
├── _support/
│ ├── DemoTester.php
│ ├── Helper/
│ │ ├── Demo.php
│ │ └── Unit.php
│ ├── PhpBuiltInServer.php
│ └── UnitTester.php
├── demo/
│ └── DemoCept.php
├── demo.suite.yml
├── unit/
│ ├── Broker/
│ │ ├── AttachTest.php
│ │ └── RequestTest.php
│ ├── Server/
│ │ ├── AttachTest.php
│ │ └── BrokerSessionTest.php
│ └── TokenTrait.php
└── unit.suite.yml
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitattributes
================================================
/demo export-ignore
/tests export-ignore
/.gitattributes export-ignore
/.gitignore export-ignore
/.scrutinizer.yml export-ignore
/.travis.yml export-ignore
/phpunit.xml.dist export-ignore
/phpcs.xml.dist export-ignore
/phpstan.neon export-ignore
/README.md export-ignore
================================================
FILE: .github/workflows/php.yml
================================================
name: PHP
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
run:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- php: 8.0
- php: 8.1
- php: 8.2
coverage: '--coverage --coverage-xml'
name: PHP ${{ matrix.php }}
steps:
- uses: actions/checkout@v2
with:
fetch-depth: 10
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: xdebug
- name: Validate composer.json
run: composer validate
- name: Install dependencies
run: composer update --prefer-dist --no-progress --no-suggest
- name: Run Codeception
run: vendor/bin/codecept run ${{ matrix.coverage }}
- name: Upload coverage to Scrutinizer
if: ${{ matrix.coverage }}
uses: sudo-bot/action-scrutinizer@latest
with:
cli-args: "--format=php-clover build/logs/clover.xml --revision=${{ github.event.pull_request.head.sha || github.sha }}"
================================================
FILE: .gitignore
================================================
.DS_Store
nbproject
/vendor
composer.lock
tests/_output/*
# Elastic Beanstalk Files
.elasticbeanstalk/*
!.elasticbeanstalk/*.cfg.yml
!.elasticbeanstalk/*.global.yml
/tests/_support/_generated/
.idea
================================================
FILE: .scrutinizer.yml
================================================
#language: php
checks:
php: true
filter:
excluded_paths:
- tests
build:
nodes:
analysis:
environment:
php: 8.2
postgresql: false
redis: false
mongodb: false
tests:
override:
- phpcs-run src
-
command: vendor/bin/phpstan analyze --error-format=checkstyle | sed '/^\s*$/d' > phpstan-checkstyle.xml
analysis:
file: phpstan-checkstyle.xml
format: 'general-checkstyle'
- php-scrutinizer-run
================================================
FILE: LICENSE
================================================
Copyright (c) 2020 Arnold Daniels
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.
================================================
FILE: README.md
================================================

Single Sign-On for PHP
========
[](https://github.com/jasny/sso/actions)
[](https://scrutinizer-ci.com/g/jasny/sso/?branch=master)
[](https://scrutinizer-ci.com/g/jasny/sso/?branch=master)
[](https://packagist.org/packages/jasny/sso)
[](https://packagist.org/packages/jasny/sso)
Jasny SSO is a relatively simple and straightforward solution for single sign on (SSO).
With SSO, logging into a single website will authenticate you for all affiliate sites. The sites don't need to share a
toplevel domain.
### How it works
When using SSO, we can distinguish 3 parties:
* Client - This is the browser of the visitor
* Broker - The website which is visited
* Server - The place that holds the user info and credentials
The broker has an id and a secret. These are known to both the broker and server.
When the client visits the broker, it creates a random token, which is stored in a cookie. The broker will then send
the client to the server, passing along the broker's id and token. The server creates a hash using the broker id, broker
secret and the token. This hash is used to create a link to the user's session. When the link is created the server
redirects the client back to the broker.
The broker can create the same link hash using the token (from the cookie), the broker id and the broker secret. When
doing requests, it passes that hash as a session id.
The server will notice that the session id is a link and use the linked session. As such, the broker and client are
using the same session. When another broker joins in, it will also use the same session.
For a more in depth explanation, please [read this article](https://github.com/jasny/sso/wiki).
### How is this different from OAuth?
With OAuth, you can authenticate a user at an external server and get access to their profile info. However, you
aren't sharing a session.
A user logs in to website foo.com using Google OAuth. Next they visit website bar.org which also uses Google OAuth.
Regardless of that, they are still required to press the 'login' button on bar.org.
With Jasny SSO both websites use the same session. So when the user visits bar.org, they are automatically logged in.
When they log out (on either of the sites), they are logged out for both.
## Installation
Install this library through composer
composer require jasny/sso
## Demo
There is a demo server and two demo brokers as example. One with normal redirects and one using
[JSONP](https://en.wikipedia.org/wiki/JSONP) / AJAX.
To prove it's working you should setup the server and two or more brokers, each on their own machine and their own
(sub)domain. However, you can also run both server and brokers on your own machine, simply to test it out.
On *nix (Linux / Unix / OSX) run:
php -S localhost:8000 -t demo/server/
export SSO_SERVER=http://localhost:8000/attach.php SSO_BROKER_ID=Alice SSO_BROKER_SECRET=8iwzik1bwd; php -S localhost:8001 -t demo/broker/
export SSO_SERVER=http://localhost:8000/attach.php SSO_BROKER_ID=Greg SSO_BROKER_SECRET=7pypoox2pc; php -S localhost:8002 -t demo/broker/
export SSO_SERVER=http://localhost:8000/attach.php SSO_BROKER_ID=Julius SSO_BROKER_SECRET=ceda63kmhp; php -S localhost:8003 -t demo/ajax-broker/
Now open some tabs and visit
* http://localhost:8001
* http://localhost:8002
* http://localhost:8003
username | password
-------- | --------
jackie | jackie123
john | john123
_Note that after logging in, you need to refresh on the other brokers to see the effect._
# Usage
## Server
The `Server` class takes a callback as first constructor argument. This callback should look up the secret
for a broker based on the id.
The second argument must be a PSR-16 compatible cache object. It's used to store the link between broker token and
client session.
```php
use Jasny\SSO\Server\Server;
$brokers = [
'foo' => ['secret' => '8OyRi6Ix1x', 'domains' => ['example.com']],
// ...
];
$server = new Server(
fn($id) => $brokers[$id] ?? null, // Unique secret and allowed domains for each broker.
new Cache() // Any PSR-16 compatible cache
);
```
_In this example the brokers are simply configured as an array, but typically you want to fetch the broker info from a DB._
### Attach
A client needs to attach the broker token to the session id by doing an HTTP request to the server. This request can be
handled by calling `attach()`.
The `attach()` method returns a verification code. This code must be returned to the broker, as it's needed to
calculate the checksum.
```php
$verificationCode = $server->attach();
```
If it's not possible to attach (for instance in case of an incorrect checksum), an Exception is thrown.
### Handle broker API request
After the client session is attached to the broker token, the broker is able to send API requests on behalf of the
client. Calling the `startBrokerSession()` method with start the session of the client based on the bearer token. This
means that these request the server can access the session information of the client through `$_SESSION`.
```
$server->startBrokerSession();
```
The broker could use this to login, logout, get user information, etc. The API for handling such requests is outside
the scope of the project. However since the broker uses normal sessions, any existing the authentication can be used.
_If you're lookup for an authentication library, consider using [Jasny Auth](https://github.com/jasny/auth)._
### PSR-7
By default, the library works with superglobals like `$_GET` and `$_SERVER`. Alternatively it can use a PSR-7 server
request. This can be passed to `attach()` and `startBrokerSession()` as argument.
```php
$verificationCode = $server->attach($serverRequest);
```
### Session interface
By default, the library uses the superglobal `$_SESSION` and the `php_session_*()` functions. It does this through
the `GlobalSession` object, which implements `SessionInterface`.
For projects that use alternative sessions, it's possible to create a wrapper that implements `SessionInterface`.
```php
use Jasny\SSO\Server\SessionInterface;
class CustomerSessionHandler implements SessionInterface
{
// ...
}
```
The `withSession()` methods creates a copy of the Server object with the custom session interface.
```php
$server = (new Server($callback, $cache))
->withSession(new CustomerSessionHandler());
```
The `withSession()` method can also be used with a mock object for testing.
### Logging
Enable logging for debugging and catching issues.
```php
$server = (new Server($callback, $cache))
->withLogging(new Logger());
```
Any PSR-3 compatible logger can be used, like [Monolog](https://packagist.org/packages/monolog/monolog) or
[Loggy](https://packagist.org/packages/yubb/loggy). The `context` may contain the broker id, token, and session id.
## Broker
When creating a `Broker` instance, you need to pass the server url, broker id and broker secret. The broker id and
secret needs to match the secret registered at the server.
**CAVEAT**: *The broker id MUST be alphanumeric.*
### Attach
Before the broker can do API requests on the client's behalf, the client needs to attach the broker token to the client
session. For this, the client must do an HTTP request to the SSO Server.
The `getAttachUrl()` method will generate a broker token for the client and use it to create an attach URL. The method
takes an array of query parameters as single argument.
There are several methods in making the client do an HTTP request. The broker can redirect the client or do a request
via the browser using AJAX or loading an image.
```php
use Jasny\SSO\Broker\Broker;
// Configure the broker.
$broker = new Broker(
getenv('SSO_SERVER'),
getenv('SSO_BROKER_ID'),
getenv('SSO_BROKER_SECRET')
);
// Attach through redirect if the client isn't attached yet.
if (!$broker->isAttached()) {
$returnUrl = (!empty($_SERVER['HTTPS']) ? 'https://' : 'http://') . $_SERVER['HTTP_HOST'] . $_SERVER['REQUEST_URI'];
$attachUrl = $broker->getAttachUrl(['return_url' => $returnUrl]);
header("Location: $attachUrl", true, 303);
echo "You're redirected to $attachUrl";
exit();
}
```
### Verify
Upon verification the SSO Server will return a verification code (as a query parameter or in the JSON response). The code
is used to calculate the checksum. The verification code prevents session hijacking using an attach link.
```php
if (isset($_GET['sso_verify'])) {
$broker->verify($_GET['sso_verify']);
}
```
### API requests
Once attached, the broker is able to do API requests on behalf of the client. This can be done by
- using the broker `request()` method, or by
- using any HTTP client like Guzzle
#### Broker request
```php
// Post to modify the user info
$broker->request('POST', '/login', $credentials);
// Get user info
$user = $broker->request('GET', '/user');
```
The `request()` method uses Curl to send HTTP requests, adding the bearer token for authentication. It expects a JSON
response and will automatically decode it.
#### HTTP library (Guzzle)
To use a library like [Guzzle](http://docs.guzzlephp.org/) or [Httplug](http://httplug.io/), get the bearer token using
`getBearerToken()` and set the `Authorization` header
```php
$guzzle = new GuzzleHttp\Client(['base_uri' => 'https://sso-server.example.com']);
$res = $guzzle->request('GET', '/user', [
'headers' => [
'Authorization' => 'Bearer ' . $broker->getBearerToken()
]
]);
```
### Client state
By default, the Broker uses the cookies (`$_COOKIE` and `setcookie()`) via the `Cookies` class to persist the client's
SSO token.
#### Cookie
Instantiate a new `Cookies` object with custom parameters to modify things like cookie TTL, domain and https only.
```php
use Jasny\SSO\Broker\{Broker,Cookies};
$broker = (new Broker(getenv('SSO_SERVER'), getenv('SSO_BROKER_ID'), getenv('SSO_BROKER_SECRET')))
->withTokenIn(new Cookies(7200, '/myapp', 'example.com', true));
```
_(The cookie can never be accessed by the browser.)_
#### Session
Alternatively, you can store the SSO token in a PHP session for the broker by using `Session`.
```php
use Jasny\SSO\Broker\{Broker,Session};
session_start();
$broker = (new Broker(getenv('SSO_SERVER'), getenv('SSO_BROKER_ID'), getenv('SSO_BROKER_SECRET')))
->withTokenIn(new Session());
```
#### Custom
The method accepts any object that implements `ArrayAccess`, allowing you to create a custom handler if needed.
```php
class CustomStateHandler implements \ArrayAccess
{
// ...
}
```
This can also be used with a mock object for testing.
================================================
FILE: codeception.yml
================================================
actor: Tester
paths:
tests: tests
log: tests/_output
data: tests/_data
support: tests/_support
envs: tests/_envs
bootstrap: _bootstrap.php
settings:
colors: true
memory_limit: 1024M
extensions:
enabled:
- Codeception\Extension\RunFailed
================================================
FILE: composer.json
================================================
{
"name": "jasny/sso",
"description": "Simple Single Sign-On",
"keywords": ["sso", "auth"],
"license": "MIT",
"homepage": "https://github.com/jasny/sso/wiki",
"authors": [
{
"name": "Arnold Daniels",
"email": "arnold@jasny.net",
"homepage": "http://www.jasny.net"
}
],
"support": {
"issues": "https://github.com/jasny/sso/issues",
"source": "https://github.com/jasny/sso"
},
"require": {
"php": "^8.0",
"ext-json": "*",
"jasny/immutable": "^2.1",
"psr/simple-cache": "*",
"psr/log": "*"
},
"require-dev": {
"phpstan/phpstan": "^0.12.59",
"codeception/codeception": "^4.1",
"codeception/module-phpbrowser": "^1.0",
"codeception/module-rest": "^1.2",
"desarrolla2/cache": "^3.0",
"jasny/http-message": "^1.3",
"jasny/php-code-quality": "^2.6.0",
"jasny/phpunit-extension": "^0.3.2",
"yubb/loggy": "^2.1"
},
"autoload": {
"psr-4": {
"Jasny\\SSO\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Jasny\\Tests\\SSO\\": "tests/unit/"
}
},
"scripts": {
"test": [
"phpstan analyse",
"codecept run",
"phpcs -p src"
]
},
"config": {
"preferred-install": "dist",
"sort-packages": true,
"optimize-autoloader": true
},
"minimum-stability": "dev",
"prefer-stable": true
}
================================================
FILE: demo/ajax-broker/api.php
================================================
request($_SERVER['REQUEST_METHOD'], $path, $_POST);
} catch (Exception $e) {
$status = $e->getCode() ?: 500;
$result = ['error' => $e->getMessage()];
}
// REST
if (!$result) {
http_response_code(204);
} else {
http_response_code(isset($status) ? $status : 200);
header("Content-Type: application/json");
echo json_encode($result);
}
================================================
FILE: demo/ajax-broker/app.js
================================================
+function ($) {
// Init
attach();
/**
* Attach session.
* Will redirect to SSO server.
*/
function attach()
{
const req = $.ajax({
url: 'attach.php',
crossDomain: true,
dataType: 'jsonp'
});
req.done(function (data, code) {
if (code && code >= 400) { // jsonp failure
showError(data);
return;
}
$.ajax({method: 'POST', url: 'verify.php', data: data}).done(function () {
doApiRequest('info', null, showUserInfo);
});
});
req.fail(function (jqxhr) {
showError(jqxhr.responseJSON || jqxhr.textResponse)
});
}
/**
* Do an AJAX request to the API
*
* @param command API command
* @param params POST data
* @param callback Callback function
*/
function doApiRequest(command, params, callback)
{
const req = $.ajax({
url: 'api.php?command=' + command,
method: params ? 'POST' : 'GET',
data: params,
dataType: 'json'
});
req.done(callback);
req.fail(function (jqxhr) {
showError(jqxhr.responseJSON || jqxhr.textResponse);
});
}
/**
* Display the error message
*
* @param data
*/
function showError(data)
{
const message = typeof data === 'object' && data.error ? data.error : 'Unexpected error';
$.growl.error({message: message});
}
/**
* Display user info
*
* @param info
*/
function showUserInfo(info)
{
const body = $('body');
const userInfo = $('#user-info');
body.removeClass('anonymous authenticated');
userInfo.html('');
if (info) {
for (const key in info) {
userInfo.append($('
').text(key));
userInfo.append($('
').text(info[key]));
}
}
body.addClass(info ? 'authenticated' : 'anonymous');
}
/**
* Submit login form through AJAX
*/
$('#login-form').on('submit', function (e) {
e.preventDefault();
$('#error').text('').hide();
var data = {
username: this.username.value,
password: this.password.value
};
doApiRequest('login', data, showUserInfo);
});
$('#logout').on('click', function () {
doApiRequest('logout', {}, () => showUserInfo(null));
});
}(jQuery);
================================================
FILE: demo/ajax-broker/attach.php
================================================
isAttached()) {
echo $jsCallback . '(null, 200)';
}
// Attach through redirect if the client isn't attached yet.
$url = $broker->getAttachUrl(['callback' => $jsCallback]);
header("Location: $url", true, 303);
================================================
FILE: demo/ajax-broker/index.html
================================================
Single Sign-On Ajax demo