├── .gitignore ├── .semver ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── Exception │ └── PayloadException.php └── SSOHelper.php └── tests ├── SSOHelperTest └── SSOHelperTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | .idea 6 | -------------------------------------------------------------------------------- /.semver: -------------------------------------------------------------------------------- 1 | --- 2 | :major: 0 3 | :minor: 9 4 | :patch: 3 5 | :special: '' 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - '5.4' 5 | - '5.5' 6 | - '5.6' 7 | - '7.0' 8 | - hhvm 9 | - nightly 10 | 11 | before_script: 12 | - composer install 13 | 14 | notifications: 15 | email: false 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Colin Viebrock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Discourse Single-Sign-On Helper for PHP 2 | 3 | This is a small class to help with providing an SSO source for Discourse forums. It provides 3 helper functions for validating incoming requests, extracting nonce, and building the returning query string. 4 | 5 | For more information on the SSO settings in Discourse, visit 6 | 7 | > Original code from Johan Jatko: https://github.com/ArmedGuy/discourse_sso_php 8 | 9 | 10 | ## Installation 11 | 12 | The package is registered at Packagist as [cviebrock/discourse-php](https://packagist.org/packages/cviebrock/discourse-php) and can be installed using [composer](http://getcomposer.org/): 13 | 14 | ``` 15 | composer require "cviebrock/discourse-php" 16 | ``` 17 | 18 | 19 | ## Usage 20 | 21 | ```php 22 | $sso = new Cviebrock\DiscoursePHP\SSOHelper(); 23 | 24 | // this should be the same in your code and in your Discourse settings: 25 | $secret = 'super_secret_sso_key'; 26 | $sso->setSecret( $secret ); 27 | 28 | // load the payload passed in by Discourse 29 | $payload = $_GET['sso']; 30 | $signature = $_GET['sig']; 31 | 32 | // validate the payload 33 | if (!($sso->validatePayload($payload,$signature))) { 34 | // invaild, deny 35 | header("HTTP/1.1 403 Forbidden"); 36 | echo("Bad SSO request"); 37 | die(); 38 | } 39 | 40 | $nonce = $sso->getNonce($payload); 41 | 42 | // Insert your user authentication code here ... 43 | 44 | // Required and must be unique to your application 45 | $userId = '...'; 46 | 47 | // Required and must be consistent with your application 48 | $userEmail = '...'; 49 | 50 | // Optional - if you don't set these, Discourse will generate suggestions 51 | // based on the email address 52 | 53 | $extraParameters = array( 54 | 'username' => $userUsername, 55 | 'name' => $userFullName 56 | ); 57 | 58 | // build query string and redirect back to the Discourse site 59 | $query = $sso->getSignInString($nonce, $userId, $userEmail, $extraParameters); 60 | header('Location: http://discourse.example.com/session/sso_login?' . $query); 61 | exit(0); 62 | ``` 63 | 64 | 65 | ## Bugs, Suggestions and Contributions 66 | 67 | Please use Github for bugs, comments, suggestions. 68 | 69 | 1. Fork the project. 70 | 2. Create your bugfix/feature branch and write your (well-commented) code. 71 | 3. Commit your changes and push to your repository. 72 | 4. Create a new pull request against this project's `master` branch. 73 | 74 | 75 | 76 | ## Copyright and License 77 | 78 | **discourse-php** was written by Colin Viebrock and released under the MIT License. See the LICENSE file for details. 79 | 80 | Copyright 2015 Colin Viebrock 81 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cviebrock/discourse-php", 3 | "description": "Helper class for building a single sign-on source for Discourse forums", 4 | "keywords": [ 5 | "discourse", 6 | "forum" 7 | ], 8 | "license": "MIT", 9 | "homepage": "https://github.com/cviebrock/discourse-php", 10 | "authors": [ 11 | { 12 | "name": "Colin Viebrock", 13 | "email": "colin@viebrock.ca" 14 | }, 15 | { 16 | "name": "Johan Jatko", 17 | "homepage": "https://xr.gs/" 18 | } 19 | ], 20 | "support": { 21 | "issues": "https://github.com/cviebrock/discourse-php/issues", 22 | "source": "https://github.com/cviebrock/discourse-php" 23 | }, 24 | "require": { 25 | "php": ">=5.3.0" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Cviebrock\\DiscoursePHP\\": "src/" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 12 | 13 | ./tests/SSOHelperTest 14 | 15 | 16 | 17 | 18 | ./src 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Exception/PayloadException.php: -------------------------------------------------------------------------------- 1 | secret = $secret; 22 | 23 | return $this; 24 | } 25 | 26 | /** 27 | * @param $payload 28 | * @param $signature 29 | * @return mixed 30 | */ 31 | public function validatePayload($payload, $signature) 32 | { 33 | $payload = urldecode($payload); 34 | 35 | return $this->signPayload($payload) === $signature; 36 | } 37 | 38 | /** 39 | * @param $payload 40 | * @return mixed 41 | * @throws PayloadException 42 | */ 43 | public function getNonce($payload) 44 | { 45 | $payload = urldecode($payload); 46 | $query = array(); 47 | parse_str(base64_decode($payload), $query); 48 | if (!array_key_exists('nonce', $query)) { 49 | throw new PayloadException('Nonce not found in payload'); 50 | } 51 | 52 | return $query['nonce']; 53 | } 54 | 55 | /** 56 | * @param $payload 57 | * @return mixed 58 | * @throws PayloadException 59 | */ 60 | public function getReturnSSOURL($payload) 61 | { 62 | $payload = urldecode($payload); 63 | $query = array(); 64 | parse_str(base64_decode($payload), $query); 65 | if (!array_key_exists('return_sso_url', $query)) { 66 | throw new PayloadException('Return SSO URL not found in payload'); 67 | } 68 | 69 | return $query['return_sso_url']; 70 | } 71 | 72 | /** 73 | * @param $nonce 74 | * @param $id 75 | * @param $email 76 | * @param array $extraParameters 77 | * @return string 78 | */ 79 | public function getSignInString($nonce, $id, $email, $extraParameters = []) 80 | { 81 | 82 | $parameters = array( 83 | 'nonce' => $nonce, 84 | 'external_id' => $id, 85 | 'email' => $email, 86 | ) + $extraParameters; 87 | 88 | $payload = base64_encode(http_build_query($parameters)); 89 | 90 | $data = array( 91 | 'sso' => $payload, 92 | 'sig' => $this->signPayload($payload), 93 | ); 94 | 95 | return http_build_query($data); 96 | } 97 | 98 | /** 99 | * @param $payload 100 | * @return string 101 | */ 102 | protected function signPayload($payload) 103 | { 104 | return hash_hmac('sha256', $payload, $this->secret); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /tests/SSOHelperTest/SSOHelperTest.php: -------------------------------------------------------------------------------- 1 | sso = new SSOHelper; 25 | $this->sso->setSecret(self::SECRET); 26 | } 27 | 28 | public function testInOut() 29 | { 30 | $this->assertTrue( 31 | $this->sso->validatePayload(self::PAYLOAD, self::SIGNATURE) 32 | ); 33 | 34 | $userId = 1234; 35 | $userEmail = 'sso@example.com'; 36 | $response = $this->sso->getSignInString( 37 | $this->sso->getNonce(self::PAYLOAD), $userId, $userEmail 38 | ); 39 | $expected = 'sso=bm9uY2U9ZTE4YmRiYTgxOTdhZGUwOTZkOTY0' 40 | . 'NTdkNDg2NzViYjkmZXh0ZXJuYWxfaWQ9MTIzNCZlbWFpbD1zc2' 41 | . '8lNDBleGFtcGxlLmNvbQ%3D%3D&sig=9db5456c6d21b8bad96' 42 | . 'a9071edfd0fd87160f7b71687dbbed2050d4c7750b643'; 43 | $this->assertEquals($expected, $response); 44 | } 45 | 46 | public function testNonceGood() 47 | { 48 | $payload = base64_encode('nonce=1111'); 49 | $this->assertEquals(1111, $this->sso->getNonce($payload)); 50 | 51 | $payload = base64_encode('nonce=2222&asdf=true'); 52 | $this->assertEquals(2222, $this->sso->getNonce($payload)); 53 | } 54 | 55 | /** 56 | * @expectedException \Cviebrock\DiscoursePHP\Exception\PayloadException 57 | */ 58 | public function testNonceBad1() 59 | { 60 | $payload = base64_encode('nonc=1111'); 61 | $this->sso->getNonce($payload); 62 | } 63 | 64 | /** 65 | * @expectedException \Cviebrock\DiscoursePHP\Exception\PayloadException 66 | */ 67 | public function testNonceBad2() 68 | { 69 | $this->sso->getNonce('junk'); 70 | } 71 | 72 | public function testExtraParametersPlayNice() 73 | { 74 | $userId = 1234; 75 | $userEmail = 'sso@example.com'; 76 | $extraParams = array( 77 | 'nonce' => 'junk', 78 | 'external_id' => 'junk', 79 | 'email' => 'junk', 80 | 'only_me' => 'gets_through', 81 | ); 82 | $response = $this->sso->getSignInString( 83 | $this->sso->getNonce(self::PAYLOAD), $userId, $userEmail, $extraParams 84 | ); 85 | parse_str($response, $response); 86 | $parts = array(); 87 | parse_str(base64_decode($response['sso']), $parts); 88 | $this->assertEquals($userId, $parts['external_id']); 89 | $this->assertEquals($userEmail, $parts['email']); 90 | $this->assertEquals(self::NONCE, $parts['nonce']); 91 | $this->assertEquals('gets_through', $parts['only_me']); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 |