├── .docker └── Dockerfile ├── .editorconfig ├── .github ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── workflows │ └── sonarcloud.yml ├── .gitignore ├── .php-cs-fixer.php ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── docker-compose.yml ├── phpunit.xml.dist ├── sonar-project.properties ├── src ├── Created.php ├── Element.php ├── Expires.php ├── Nonce.php ├── Password.php ├── Security.php ├── Timestamp.php ├── Username.php ├── UsernameToken.php └── WsSecurity.php └── tests ├── TestCase.php └── WsSecurityTest.php /.docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.4-apache 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y libxml2-dev git zip \ 5 | && docker-php-ext-install soap 6 | 7 | COPY --from=composer:latest /usr/bin/composer /usr/bin/composer 8 | COPY . /var/www/ 9 | 10 | WORKDIR /var/www/ 11 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # For more information about the properties used in 2 | # this file, please see the EditorConfig documentation: 3 | # http://editorconfig.org/ 4 | 5 | root = true 6 | 7 | [*] 8 | end_of_line = lf 9 | 10 | [*.php] 11 | indent_style = space 12 | indent_size = 4 -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at contact@wsdltophp.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via pull requests on [Github]. 6 | Please make all pull requests to the `develop` branch, not the `master` branch. 7 | 8 | ## Before posting an issue 9 | 10 | - If a command is failing, post the full output you get when running the command, with the `--verbose` argument 11 | 12 | ## Pull Requests 13 | 14 | - **Create an issue** - Explain as detailed as possible the issue you encountered so we can understand the context of your pull request 15 | - **[Symfony Coding Standard]** - The easiest way to apply the conventions is to run `composer lint` 16 | - **Add tests!** - Your patch won't be accepted if it doesn't have tests. 17 | - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. 18 | - **Consider our release cycle** - We try to follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 19 | - **Create feature branches** - Don't ask us to pull from your master branch. 20 | - **One pull request per feature** - If you want to do more than one thing, send multiple pull requests. 21 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. 22 | 23 | ## Running Tests 24 | 25 | ``` bash 26 | $ composer test 27 | ``` 28 | 29 | **Happy coding**! 30 | 31 | [Github]: https://github.com/wsdltophp/wssecurity 32 | [Symfony Coding Standard]: http://symfony.com/doc/current/contributing/code/standards.html 33 | -------------------------------------------------------------------------------- /.github/workflows/sonarcloud.yml: -------------------------------------------------------------------------------- 1 | name: SonarCloud 2 | on: 3 | push: 4 | branches: 5 | - develop 6 | - feature/* 7 | - feat/* 8 | pull_request: 9 | types: [ opened, synchronize, reopened ] 10 | jobs: 11 | sonarcloud: 12 | name: SonarCloud 13 | runs-on: ubuntu-latest 14 | permissions: 15 | pull-requests: write 16 | steps: 17 | - uses: actions/checkout@v2 18 | with: 19 | fetch-depth: 0 20 | 21 | - name: Setup PHP with Xdebug 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: 7.4 25 | coverage: xdebug 26 | 27 | - name: Install dependencies with composer 28 | run: composer update --no-ansi --no-interaction --no-progress 29 | 30 | - name: Generate coverage report with phpunit/phpunit 31 | run: vendor/bin/phpunit --coverage-clover coverage.xml --log-junit report.xml 32 | 33 | - name: Monitor coverage 34 | if: false 35 | uses: slavcodev/coverage-monitor-action@v1 36 | with: 37 | github_token: ${{ secrets.SECRET_GITHUB_TOKEN }} 38 | coverage_path: coverage.xml 39 | threshold_alert: 75 40 | threshold_warning: 95 41 | 42 | - name: Codecov analyze 43 | uses: codecov/codecov-action@v3 44 | with: 45 | files: coverage.xml 46 | 47 | - name: Fix phpunit files paths 48 | run: sed -i 's@'$GITHUB_WORKSPACE/'@''@g' coverage.xml report.xml 49 | 50 | - name: SonarCloud Scan 51 | uses: SonarSource/sonarcloud-github-action@master 52 | env: 53 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 54 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | vendor 2 | composer.lock 3 | phpunit.xml 4 | .phpunit.result.cache 5 | -------------------------------------------------------------------------------- /.php-cs-fixer.php: -------------------------------------------------------------------------------- 1 | exclude('vendor') 5 | ->in(__DIR__); 6 | 7 | return (new PhpCsFixer\Config()) 8 | ->setUsingCache(false) 9 | ->setRules(array( 10 | '@PhpCsFixer' => true, 11 | )) 12 | ->setFinder($finder); 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | ## 2.0.6 - 2024/09/04 4 | - PR #20, issue #19 - Attributes on Timestamp 5 | 6 | ## 2.0.5 - 2022/05/05 7 | - Merged PR #15 8 | 9 | ## 2.0.4 - 2022/03/24 10 | - Upgrade PHP CS Fixer 11 | 12 | ## 2.0.3 - 2022/03/24 13 | - Update badges 14 | - Remove Travis CI settings 15 | - Add PHPStan analyze 16 | - Minor source code improvements 17 | 18 | ## 2.0.2 - 2021/02/03 19 | - Review .php_cs settings, apply PHP CS Fixer 20 | - Fix minor typo 21 | 22 | ## 2.0.1 - 2021/01/28 23 | - Improve Travis CI settings and badge 24 | 25 | ## 2.0.0 - 2021/01/26 26 | - Use `splitbrain/phpfarm:jessie` as Docker image and fix docker image settings 27 | - Code requires PHP >= 7.4 28 | - Code cleaning 29 | - Update README 30 | - Update Travis CI settings 31 | - Update PHPUnit settings 32 | - Update LICENSE file 33 | - Version 1.0 is no more maintained 34 | 35 | ## 1.2.2 36 | - Merged PR #9 37 | 38 | ## 1.2.1 39 | - Fixed issue #7 40 | 41 | ## 1.2.0 42 | - Merged PR #5 43 | - Fix minor typo 44 | 45 | ## 1.1.0 46 | - Provide Docker settings 47 | - Merged PR #2 and #4 48 | - Fixed issue #3 49 | - Drop support of PHP 5.3 50 | - Update contributors list 51 | - Update lint options 52 | 53 | ## 1.0.1 54 | - Improve code, add SensioLabs Insight badge, remove useless value 55 | 56 | ## 1.0.0 57 | Initial release: 58 | - WsSecurity fixes 59 | - Unit tests added 60 | - Username id added on header creation 61 | - Update readme 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012-2015 Mikaël DELSOL 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WsSecurity 2 | 3 | > WsSecurity adds the WSSE authentication header to your SOAP request 4 | 5 | [![License](https://poser.pugx.org/wsdltophp/wssecurity/license)](https://packagist.org/packages/wsdltophp/wssecurity) 6 | [![Latest Stable Version](https://poser.pugx.org/wsdltophp/wssecurity/version.png)](https://packagist.org/packages/wsdltophp/wssecurity) 7 | [![TeamCity build status](https://teamcity.mikael-delsol.fr/app/rest/builds/buildType:id:WsSecurity_Build/statusIcon.svg)](https://github.com/WsdlToPhp/WsSecurity) 8 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/WsdlToPhp/WsSecurity/badges/quality-score.png)](https://scrutinizer-ci.com/g/WsdlToPhp/WsSecurity/) 9 | [![Code Coverage](https://scrutinizer-ci.com/g/WsdlToPhp/WsSecurity/badges/coverage.png)](https://scrutinizer-ci.com/g/WsdlToPhp/WsSecurity/) 10 | [![Total Downloads](https://poser.pugx.org/wsdltophp/wssecurity/downloads)](https://packagist.org/packages/wsdltophp/wssecurity) 11 | [![StyleCI](https://styleci.io/repos/43811404/shield)](https://styleci.io/repos/43811404) 12 | [![SymfonyInsight](https://insight.symfony.com/projects/57dd432d-3d42-449f-baa0-4568abeb3a32/mini.svg)](https://insight.symfony.com/projects/57dd432d-3d42-449f-baa0-4568abeb3a32) 13 | 14 | ## How to use it 15 | This repository contains multiple classes that may be used indepdently but for now it is easier/better to only use the WsSecurity class. 16 | 17 | The WsSecurity class provides a static method that takes the parameters that should suffice to create your Ws-Security Username Authentication header required in your SOAP request. 18 | 19 | Concretly, you must include this repository in your project using composer (`composer require wsdltophp/wssecurity`) then use it such as: 20 | 21 | ```php 22 | use WsdlToPhp\WsSecurity\WsSecurity; 23 | 24 | /** 25 | * @var \SoapHeader 26 | */ 27 | $soapHeader = WsSecurity::createWsSecuritySoapHeader('login', 'password', true); 28 | /** 29 | * Send the request 30 | */ 31 | $soapClient = new \SoapClient('wsdl_url'); 32 | $soapClient->__setSoapHeaders($soapHeader); 33 | $soapClient->__soapCall('echoVoid', []); 34 | ``` 35 | 36 | The `WsSecurity::createWsSecuritySoapHeader` parameters are defined in this order `($username, $password, $passwordDigest = false, $addCreated = 0, $addExpires = 0, $returnSoapHeader = true, $mustunderstand = false, $actor = null, $usernameId = null, $addNonce = true)`: 37 | 38 | - **$username**: your login/username 39 | - **$password**: your password 40 | - **$passwordDigest**: set it to `true` if your password must be encrypted 41 | - **$addCreated**: set it to the time you created this header using the PHP [time](http://php.net/manual/en/function.time.php) function for example, otherwise pass 0 42 | - **$addExpires**: set it to the number of seconds in which the header will expire, 0 otherwise 43 | - **$returnSoapHeader**: set it to false if you want to get the [\SoapVar](http://php.net/manual/en/class.soapvar.php) object that is used to create the [\SoapHeader](http://php.net/manual/en/class.soapheader.php) object, then you'll have to use to create by yourself the [\SoapHeader](http://php.net/manual/en/class.soapheader.php) object 44 | - **$mustunderstand**: classic option of the [\SoapClient](http://php.net/manual/en/soapclient.soapclient.php) class 45 | - **$actor**: classic option of the [\SoapClient](http://php.net/manual/en/soapclient.soapclient.php) class 46 | - **$usernameId**: the id to attach to the UsernameToken element, optional 47 | - **$addNonce**: _true_ by default, if true, it adds the nonce element to the header, if false it does not add the nonce element to the header 48 | 49 | ## Alternative usage ## 50 | Create an instance of the Security class 51 | ```php 52 | use WsdlToPhp\WsSecurity\WsSecurity; 53 | 54 | $wsSecurity = new WsSecurity('login', 'password', true, /*$addCreated*/ time()); 55 | 56 | // access its properties to alter them 57 | $wsSecurity->getSecurity()->getTimestamp()->setAttribute('wsu:Id', 'AnyRequestValue'); 58 | 59 | // Get the SoapHeader 60 | $header = $security->getSoapHeader($returnSoapHeader = true, $mustunderstand = false, $actor = null); 61 | ``` 62 | 63 | 64 | ## Testing using [Docker](https://www.docker.com/) 65 | Thanks to the [Docker image](https://hub.docker.com/r/splitbrain/phpfarm) of [phpfarm](https://github.com/fpoirotte/phpfarm), tests can be run locally under *any* PHP version using the cli: 66 | - php-7.4 67 | 68 | First of all, you need to create your container which you can do using [docker-compose](https://docs.docker.com/compose/) by running the below command line from the root directory of the project: 69 | ```bash 70 | $ docker-compose up -d --build 71 | ``` 72 | 73 | You then have a container named `ws_security` in which you can run `composer` commands and `php cli` commands such as: 74 | ```bash 75 | # install deps in container (using update ensure it does use the composer.lock file if there is any) 76 | $ docker exec -it ws_security php /usr/bin/composer update 77 | # run tests in container 78 | $ docker exec -it ws_security php -dmemory_limit=-1 vendor/bin/phpunit 79 | ``` 80 | 81 | ## FAQ 82 | 83 | If you have a question, feel free to [create an issue](https://github.com/WsdlToPhp/WsSecurity/issues/new). 84 | 85 | ## License 86 | 87 | The MIT License (MIT). Please see [License File](LICENSE) for more information. 88 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name" : "wsdltophp/wssecurity", 3 | "description" : "Allows to easily add Ws Security header to a SOAP Request", 4 | "type" : "library", 5 | "keywords" : ["soap","php","wsse"], 6 | "license" : "MIT", 7 | "authors" : [ 8 | { 9 | "name": "Mikaël DELSOL", 10 | "email": "contact@wsdltophp.com", 11 | "homepage": "https://www.wsdltophp.com", 12 | "role": "Owner" 13 | }, 14 | { 15 | "name": "Alexey Anisimov", 16 | "email": "mne@lexa.in", 17 | "role": "Contributor" 18 | }, 19 | { 20 | "name": "Tom Vaughan", 21 | "email": "tom.vaughan@farpoint.com", 22 | "role": "Contributor" 23 | }, 24 | { 25 | "name": "NtlBldrv", 26 | "role": "Contributor" 27 | }, 28 | { 29 | "name": "baijunyao", 30 | "email": "baijunyao@baijunyao.com", 31 | "role": "Contributor" 32 | }, 33 | { 34 | "name": "liquid207", 35 | "role": "Contributor" 36 | } 37 | ], 38 | "support" : { 39 | "email" : "contact@wsdltophp.com" 40 | }, 41 | "require" : { 42 | "php": ">=7.4", 43 | "ext-dom": "*", 44 | "ext-soap": "*" 45 | }, 46 | "scripts": { 47 | "test": "vendor/bin/phpunit", 48 | "lint": "vendor/bin/php-cs-fixer fix --ansi --diff --verbose", 49 | "phpstan": "vendor/bin/phpstan analyze src --level=6" 50 | }, 51 | "require-dev": { 52 | "friendsofphp/php-cs-fixer": "^3.0", 53 | "phpstan/phpstan": "^1.4", 54 | "phpunit/phpunit": "^9" 55 | }, 56 | "autoload" : { 57 | "psr-4" : { 58 | "WsdlToPhp\\WsSecurity\\" : "src" 59 | } 60 | }, 61 | "autoload-dev" : { 62 | "psr-4" : { 63 | "WsdlToPhp\\WsSecurity\\Tests\\": "tests" 64 | } 65 | }, 66 | "config": { 67 | "sort-packages": true 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.4' 2 | 3 | services: 4 | php: 5 | build: 6 | context: . 7 | dockerfile: .docker/Dockerfile 8 | volumes: 9 | - .:/var/www:rw 10 | container_name: ws_security 11 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./src 6 | 7 | 8 | 9 | 10 | ./tests/ 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=WsdlToPhp_WsSecurity 2 | sonar.organization=wsdltophp 3 | sonar.php.coverage.reportPaths=coverage.xml 4 | sonar.php.tests.reportPath=report.xml 5 | 6 | sonar.sources=src/ 7 | sonar.tests=tests/ 8 | -------------------------------------------------------------------------------- /src/Created.php: -------------------------------------------------------------------------------- 1 | setTimestampValue($timestamp); 14 | parent::__construct(self::NAME, $namespace, $this->getTimestampValue(true)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Element.php: -------------------------------------------------------------------------------- 1 | 50 | */ 51 | protected array $attributes = []; 52 | 53 | /** 54 | * The namespace the element belongs to. 55 | */ 56 | protected string $namespace = ''; 57 | 58 | /** 59 | * Nonce used to generate digest password. 60 | */ 61 | protected string $nonceValue; 62 | 63 | /** 64 | * Timestamp used to generate digest password. 65 | */ 66 | protected int $timestampValue; 67 | 68 | /** 69 | * Current DOMDocument used to generate XML content. 70 | */ 71 | protected static ?DOMDocument $dom = null; 72 | 73 | /** 74 | * @param mixed $value 75 | * @param array $attributes 76 | */ 77 | public function __construct(string $name, string $namespace, $value = null, array $attributes = []) 78 | { 79 | $this 80 | ->setName($name) 81 | ->setNamespace($namespace) 82 | ->setValue($value) 83 | ->setAttributes($attributes) 84 | ; 85 | } 86 | 87 | /** 88 | * Method called to generate the string XML request to be sent among the SOAP Header. 89 | * 90 | * @param bool $asDomElement returns elements as a \DOMElement or as a string 91 | * 92 | * @return DOMElement|false|string 93 | */ 94 | protected function __toSend(bool $asDomElement = false) 95 | { 96 | // Create element tag. 97 | $element = self::getDom()->createElement($this->getNamespacedName()); 98 | $element->setAttributeNS('http://www.w3.org/2000/xmlns/', sprintf('xmlns:%s', $this->getNamespacePrefix()), $this->getNamespace()); 99 | 100 | // Define element value, add attributes if there are any 101 | $this 102 | ->appendValueToElementToSend($this->getValue(), $element) 103 | ->appendAttributesToElementToSend($element) 104 | ; 105 | 106 | return $asDomElement ? $element : self::getDom()->saveXML($element); 107 | } 108 | 109 | public function getName(): string 110 | { 111 | return $this->name; 112 | } 113 | 114 | public function setName(string $name): self 115 | { 116 | $this->name = $name; 117 | 118 | return $this; 119 | } 120 | 121 | /** 122 | * @return array 123 | */ 124 | public function getAttributes(): array 125 | { 126 | return $this->attributes; 127 | } 128 | 129 | /** 130 | * @param array $attributes 131 | * 132 | * @return Element 133 | */ 134 | public function setAttributes(array $attributes): self 135 | { 136 | $this->attributes = $attributes; 137 | 138 | return $this; 139 | } 140 | 141 | /** 142 | * @param mixed $value 143 | * 144 | * @return $this 145 | */ 146 | public function setAttribute(string $name, $value): self 147 | { 148 | $this->attributes[$name] = $value; 149 | 150 | return $this; 151 | } 152 | 153 | public function hasAttributes(): bool 154 | { 155 | return 0 < count($this->attributes); 156 | } 157 | 158 | public function getNamespace(): string 159 | { 160 | return $this->namespace; 161 | } 162 | 163 | public function setNamespace(string $namespace): self 164 | { 165 | $this->namespace = $namespace; 166 | 167 | return $this; 168 | } 169 | 170 | /** 171 | * @return Element|string 172 | */ 173 | public function getValue() 174 | { 175 | return $this->value; 176 | } 177 | 178 | /** 179 | * @param mixed $value 180 | * 181 | * @return Element 182 | */ 183 | public function setValue($value): self 184 | { 185 | $this->value = $value; 186 | 187 | return $this; 188 | } 189 | 190 | public function getNonceValue(): string 191 | { 192 | return $this->nonceValue; 193 | } 194 | 195 | public function setNonceValue(string $nonceValue): self 196 | { 197 | $this->nonceValue = $nonceValue; 198 | 199 | return $this; 200 | } 201 | 202 | /** 203 | * @return int|string 204 | */ 205 | public function getTimestampValue(bool $formatted = false) 206 | { 207 | return ($formatted && $this->timestampValue > 0) ? gmdate('Y-m-d\TH:i:s\Z', $this->timestampValue) : $this->timestampValue; 208 | } 209 | 210 | public function setTimestampValue(int $timestampValue): self 211 | { 212 | $this->timestampValue = $timestampValue; 213 | 214 | return $this; 215 | } 216 | 217 | /** 218 | * Returns the element to send as WS-Security header. 219 | * 220 | * @return DOMElement|false|string 221 | */ 222 | public function toSend() 223 | { 224 | self::setDom(new DOMDocument('1.0', 'UTF-8')); 225 | 226 | return $this->__toSend(); 227 | } 228 | 229 | /** 230 | * Handle adding value to element according to the value type. 231 | * 232 | * @param mixed $value 233 | * 234 | * @return Element 235 | */ 236 | protected function appendValueToElementToSend($value, DOMElement $element): self 237 | { 238 | if ($value instanceof Element) { 239 | $this->appendElementToElementToSend($value, $element); 240 | } elseif (is_array($value)) { 241 | $this->appendValuesToElementToSend($value, $element); 242 | } elseif (!empty($value)) { 243 | $element->appendChild(self::getDom()->createTextNode($value)); 244 | } 245 | 246 | return $this; 247 | } 248 | 249 | protected function appendElementToElementToSend(Element $value, DOMElement $element): void 250 | { 251 | $toSend = $value->__toSend(true); 252 | if ($toSend instanceof DOMElement) { 253 | $element->appendChild($toSend); 254 | } 255 | } 256 | 257 | /** 258 | * @param array $values 259 | */ 260 | protected function appendValuesToElementToSend(array $values, DOMElement $element): void 261 | { 262 | foreach ($values as $value) { 263 | $this->appendValueToElementToSend($value, $element); 264 | } 265 | } 266 | 267 | protected function appendAttributesToElementToSend(DOMElement $element): self 268 | { 269 | if (!$this->hasAttributes()) { 270 | return $this; 271 | } 272 | 273 | foreach ($this->getAttributes() as $attributeName => $attributeValue) { 274 | $matches = []; 275 | if (0 === preg_match(sprintf('/(%s|%s):/', self::NS_WSU_NAME, self::NS_WSSE_NAME), $attributeName, $matches)) { 276 | $element->setAttribute($attributeName, (string) $attributeValue); 277 | } else { 278 | $element->setAttributeNS(self::NS_WSSE_NAME === $matches[1] ? self::NS_WSSE : self::NS_WSU, $attributeName, $attributeValue); 279 | } 280 | } 281 | 282 | return $this; 283 | } 284 | 285 | /** 286 | * Returns the name with its namespace. 287 | */ 288 | protected function getNamespacedName(): string 289 | { 290 | return sprintf('%s:%s', $this->getNamespacePrefix(), $this->getName()); 291 | } 292 | 293 | private function getNamespacePrefix(): string 294 | { 295 | $namespacePrefix = ''; 296 | 297 | switch ($this->getNamespace()) { 298 | case self::NS_WSSE: 299 | $namespacePrefix = self::NS_WSSE_NAME; 300 | 301 | break; 302 | 303 | case self::NS_WSU: 304 | $namespacePrefix = self::NS_WSU_NAME; 305 | 306 | break; 307 | } 308 | 309 | return $namespacePrefix; 310 | } 311 | 312 | private static function getDom(): ?DOMDocument 313 | { 314 | return self::$dom; 315 | } 316 | 317 | private static function setDom(DOMDocument $dom): void 318 | { 319 | self::$dom = $dom; 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /src/Expires.php: -------------------------------------------------------------------------------- 1 | setTimestampValue($timestamp + $expiresIn); 14 | parent::__construct(self::NAME, $namespace, $this->getTimestampValue(true)); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Nonce.php: -------------------------------------------------------------------------------- 1 | self::NS_ENCODING, 19 | ]); 20 | } 21 | 22 | public static function encodeNonce(string $nonce): string 23 | { 24 | return base64_encode(pack('H*', $nonce)); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Password.php: -------------------------------------------------------------------------------- 1 | setTypeValue($typeValue) 23 | ->setTimestampValue($timestampValue ?: time()) 24 | ->setNonceValue((string) mt_rand()) 25 | ; 26 | 27 | parent::__construct(self::NAME, $namespace, $this->convertPassword($password), [ 28 | self::ATTRIBUTE_TYPE => $typeValue, 29 | ]); 30 | } 31 | 32 | public function convertPassword(string $password): string 33 | { 34 | if (self::TYPE_PASSWORD_DIGEST === $this->getTypeValue()) { 35 | $password = $this->digestPassword($password); 36 | } 37 | 38 | return $password; 39 | } 40 | 41 | /** 42 | * When generating the password digest, we define values (nonce and timestamp) that can be used in other place. 43 | */ 44 | public function digestPassword(string $password): string 45 | { 46 | $packedNonce = pack('H*', $this->getNonceValue()); 47 | $packedTimestamp = pack('a*', $this->getTimestampValue(true)); 48 | $packedPassword = pack('a*', $password); 49 | $hash = sha1($packedNonce.$packedTimestamp.$packedPassword); 50 | $packedHash = pack('H*', $hash); 51 | 52 | return base64_encode($packedHash); 53 | } 54 | 55 | public function getTypeValue(): string 56 | { 57 | return $this->typeValue; 58 | } 59 | 60 | public function setTypeValue(string $typeValue): self 61 | { 62 | $this->typeValue = $typeValue; 63 | 64 | return $this; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/Security.php: -------------------------------------------------------------------------------- 1 | setAttribute($envelopeNamespace.self::ATTRIBUTE_MUST_UNDERSTAND, $mustUnderstand); 29 | } 30 | 31 | if (!empty($actor)) { 32 | $this->setAttribute($envelopeNamespace.self::ATTRIBUTE_ACTOR, $actor); 33 | } 34 | } 35 | 36 | /** 37 | * Overrides methods in order to set the values. 38 | * 39 | * @param bool $asDomElement returns elements as a DOMElement or as a string 40 | * 41 | * @return DOMElement|false|string 42 | */ 43 | protected function __toSend(bool $asDomElement = false) 44 | { 45 | $this->setValue([ 46 | $this->getUsernameToken(), 47 | $this->getTimestamp(), 48 | ]); 49 | 50 | return parent::__toSend($asDomElement); 51 | } 52 | 53 | public function getUsernameToken(): ?UsernameToken 54 | { 55 | return $this->usernameToken; 56 | } 57 | 58 | public function setUsernameToken(UsernameToken $usernameToken): self 59 | { 60 | $this->usernameToken = $usernameToken; 61 | 62 | return $this; 63 | } 64 | 65 | public function getTimestamp(): ?Timestamp 66 | { 67 | return $this->timestamp; 68 | } 69 | 70 | public function setTimestamp(Timestamp $timestamp): self 71 | { 72 | $this->timestamp = $timestamp; 73 | 74 | return $this; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Timestamp.php: -------------------------------------------------------------------------------- 1 | setValue([ 32 | $this->getCreated(), 33 | $this->getExpires(), 34 | ]); 35 | 36 | return parent::__toSend($asDomElement); 37 | } 38 | 39 | public function getCreated(): ?Created 40 | { 41 | return $this->created; 42 | } 43 | 44 | public function setCreated(Created $created): self 45 | { 46 | $this->created = $created; 47 | 48 | return $this; 49 | } 50 | 51 | public function getExpires(): ?Expires 52 | { 53 | return $this->expires; 54 | } 55 | 56 | public function setExpires(Expires $expires): self 57 | { 58 | $this->expires = $expires; 59 | 60 | return $this; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Username.php: -------------------------------------------------------------------------------- 1 | $id, 27 | ]); 28 | } 29 | 30 | /** 31 | * Overrides method in order to add username, password and created values if they are set. 32 | * 33 | * @param bool $asDomElement returns elements as a DOMElement or as a string 34 | * 35 | * @return DOMElement|false|string 36 | */ 37 | protected function __toSend(bool $asDomElement = false) 38 | { 39 | $this->setValue([ 40 | $this->getUsername(), 41 | $this->getPassword(), 42 | $this->getCreated(), 43 | $this->getNonce(), 44 | ]); 45 | 46 | return parent::__toSend($asDomElement); 47 | } 48 | 49 | public function getUsername(): ?Username 50 | { 51 | return $this->username; 52 | } 53 | 54 | public function setUsername(Username $username): self 55 | { 56 | $this->username = $username; 57 | 58 | return $this; 59 | } 60 | 61 | public function getPassword(): ?Password 62 | { 63 | return $this->password; 64 | } 65 | 66 | public function setPassword(Password $password): self 67 | { 68 | $this->password = $password; 69 | 70 | return $this; 71 | } 72 | 73 | public function getCreated(): ?Created 74 | { 75 | return $this->created; 76 | } 77 | 78 | public function setCreated(Created $created): self 79 | { 80 | $this->created = $created; 81 | 82 | return $this; 83 | } 84 | 85 | public function getNonce(): ?Nonce 86 | { 87 | return $this->nonce; 88 | } 89 | 90 | public function setNonce(Nonce $nonce): self 91 | { 92 | $this->nonce = $nonce; 93 | 94 | return $this; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/WsSecurity.php: -------------------------------------------------------------------------------- 1 | initSecurity($mustUnderstand, $actor, $envelopeNamespace) 28 | ->setUsernameToken($username, $usernameId) 29 | ->setPassword($password, $passwordDigest, $addCreated) 30 | ->setNonce($addNonce) 31 | ->setCreated($addCreated) 32 | ->setTimestamp($addCreated, $addExpires) 33 | ; 34 | } 35 | 36 | public function getSecurity(): ?Security 37 | { 38 | return $this->security; 39 | } 40 | 41 | /** 42 | * @return SoapHeader|SoapVar 43 | */ 44 | public static function createWsSecuritySoapHeader( 45 | string $username, 46 | string $password, 47 | bool $passwordDigest = false, 48 | int $addCreated = 0, 49 | int $addExpires = 0, 50 | bool $returnSoapHeader = true, 51 | bool $mustUnderstand = false, 52 | ?string $actor = null, 53 | ?string $usernameId = null, 54 | bool $addNonce = true, 55 | string $envelopeNamespace = Security::ENV_NAMESPACE 56 | ) { 57 | $self = new WsSecurity($username, $password, $passwordDigest, $addCreated, $addExpires, $mustUnderstand, $actor, $usernameId, $addNonce, $envelopeNamespace); 58 | 59 | return $self->getSoapHeader($returnSoapHeader, $mustUnderstand, $actor); 60 | } 61 | 62 | public function getSoapHeader(bool $returnSoapHeader = true, bool $mustUnderstand = false, ?string $actor = null): object 63 | { 64 | if ($returnSoapHeader) { 65 | if (!empty($actor)) { 66 | return new SoapHeader(Element::NS_WSSE, Security::NAME, new SoapVar($this->getSecurity()->toSend(), XSD_ANYXML), $mustUnderstand, $actor); 67 | } 68 | 69 | return new SoapHeader(Element::NS_WSSE, Security::NAME, new SoapVar($this->getSecurity()->toSend(), XSD_ANYXML), $mustUnderstand); 70 | } 71 | 72 | return new SoapVar($this->getSecurity()->toSend(), XSD_ANYXML); 73 | } 74 | 75 | protected function initSecurity(bool $mustUnderstand = false, ?string $actor = null, string $envelopeNamespace = Security::ENV_NAMESPACE): self 76 | { 77 | $this->security = new Security($mustUnderstand, $actor, Security::NS_WSSE, $envelopeNamespace); 78 | 79 | return $this; 80 | } 81 | 82 | protected function setUsernameToken(string $username, ?string $usernameId = null): self 83 | { 84 | $usernameToken = new UsernameToken($usernameId); 85 | $usernameToken->setUsername(new Username($username)); 86 | $this->security->setUsernameToken($usernameToken); 87 | 88 | return $this; 89 | } 90 | 91 | protected function setPassword(string $password, bool $passwordDigest = false, int $addCreated = 0): self 92 | { 93 | $this->getUsernameToken()->setPassword(new Password($password, $passwordDigest ? Password::TYPE_PASSWORD_DIGEST : Password::TYPE_PASSWORD_TEXT, $addCreated)); 94 | 95 | return $this; 96 | } 97 | 98 | protected function setNonce(bool $addNonce): self 99 | { 100 | if ($addNonce) { 101 | $nonceValue = $this->getPassword()->getNonceValue(); 102 | if (!empty($nonceValue)) { 103 | $this->getUsernameToken()->setNonce(new Nonce($nonceValue)); 104 | } 105 | } 106 | 107 | return $this; 108 | } 109 | 110 | protected function setCreated(int $addCreated): self 111 | { 112 | $passwordDigest = $this->getPassword()->getTypeValue(); 113 | $timestampValue = $this->getPassword()->getTimestampValue(); 114 | if (($addCreated || Password::TYPE_PASSWORD_DIGEST === $passwordDigest) && 0 < $timestampValue) { 115 | $this->getUsernameToken()->setCreated(new Created($timestampValue)); 116 | } 117 | 118 | return $this; 119 | } 120 | 121 | protected function setTimestamp(int $addCreated = 0, int $addExpires = 0): self 122 | { 123 | $timestampValue = $this->getPassword()->getTimestampValue(); 124 | if (!$timestampValue || (0 === $addCreated && 0 === $addExpires)) { 125 | return $this; 126 | } 127 | 128 | $timestamp = new Timestamp(); 129 | if (0 < $addCreated) { 130 | $timestamp->setCreated(new Created($timestampValue)); 131 | } 132 | if (0 < $addExpires) { 133 | $timestamp->setExpires(new Expires($timestampValue, $addExpires)); 134 | } 135 | $this->security->setTimestamp($timestamp); 136 | 137 | return $this; 138 | } 139 | 140 | protected function getUsernameToken(): ?UsernameToken 141 | { 142 | return $this->security->getUsernameToken(); 143 | } 144 | 145 | protected function getPassword(): ?Password 146 | { 147 | return $this->getUsernameToken()->getPassword(); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | \s*<', str_replace([ 14 | "\r", 15 | "\n", 16 | "\t", 17 | ], '', $string))); 18 | } 19 | 20 | public static function assertMatches($pattern, $string) 21 | { 22 | parent::assertMatchesRegularExpression(sprintf('/%s/', str_replace('/', '\/', $pattern)), $string); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /tests/WsSecurityTest.php: -------------------------------------------------------------------------------- 1 | assertInstanceOf(SoapHeader::class, $header); 20 | $this->assertMatches(self::innerTrim(' 21 | 22 | 23 | foo 24 | bar 25 | 2016-03-31T19:17:04Z 26 | ([a-zA-Z0-9=]*) 27 | 28 | 29 | 2016-03-31T19:17:04Z 30 | 2016-03-31T19:27:04Z 31 | 32 | '), $header->data->enc_value); 33 | } 34 | 35 | public function testCreateWithoutExpiresIn() 36 | { 37 | $header = WsSecurity::createWsSecuritySoapHeader('foo', 'bar', false, 1459451824); 38 | $this->assertInstanceOf(SoapHeader::class, $header); 39 | $this->assertMatches(self::innerTrim(' 40 | 41 | 42 | foo 43 | bar 44 | 2016-03-31T19:17:04Z 45 | ([a-zA-Z0-9=]*) 46 | 47 | 48 | 2016-03-31T19:17:04Z 49 | 50 | '), $header->data->enc_value); 51 | } 52 | 53 | public function testCreateWithMustUnderstand() 54 | { 55 | $header = WsSecurity::createWsSecuritySoapHeader('foo', 'bar', false, 1459451824, 0, true, true); 56 | $this->assertInstanceOf(SoapHeader::class, $header); 57 | $this->assertMatches(self::innerTrim(' 58 | 59 | 60 | foo 61 | bar 62 | 2016-03-31T19:17:04Z 63 | ([a-zA-Z0-9=]*) 64 | 65 | 66 | 2016-03-31T19:17:04Z 67 | 68 | '), $header->data->enc_value); 69 | } 70 | 71 | public function testCreateWithMustUnderstandAndActor() 72 | { 73 | $header = WsSecurity::createWsSecuritySoapHeader('foo', 'bar', false, 1459451824, 0, true, true, 'BAR'); 74 | $this->assertInstanceOf(SoapHeader::class, $header); 75 | $this->assertMatches(self::innerTrim(' 76 | 77 | 78 | foo 79 | bar 80 | 2016-03-31T19:17:04Z 81 | ([a-zA-Z0-9=]*) 82 | 83 | 84 | 2016-03-31T19:17:04Z 85 | 86 | '), $header->data->enc_value); 87 | } 88 | 89 | public function testCreateSoapVar() 90 | { 91 | $header = WsSecurity::createWsSecuritySoapHeader('foo', 'bar', false, 1459451824, 0, false, true, 'BAR'); 92 | $this->assertInstanceOf('\SoapVar', $header); 93 | $this->assertMatches(self::innerTrim(' 94 | 95 | 96 | foo 97 | bar 98 | 2016-03-31T19:17:04Z 99 | ([a-zA-Z0-9=]*) 100 | 101 | 102 | 2016-03-31T19:17:04Z 103 | 104 | '), $header->enc_value); 105 | } 106 | 107 | public function testCreateWithPasswordDigest() 108 | { 109 | $header = WsSecurity::createWsSecuritySoapHeader('foo', 'bar', true, 1459451824, 0, false, true, 'BAR'); 110 | $this->assertInstanceOf('\SoapVar', $header); 111 | $this->assertMatches(self::innerTrim(' 112 | 113 | 114 | foo 115 | ([a-zA-Z0-9=/+]*) 116 | 2016-03-31T19:17:04Z 117 | ([a-zA-Z0-9=]*) 118 | 119 | 120 | 2016-03-31T19:17:04Z 121 | 122 | '), $header->enc_value); 123 | } 124 | 125 | public function testCreateWithUsernameId() 126 | { 127 | $header = WsSecurity::createWsSecuritySoapHeader('foo', 'bar', false, 1459451824, 0, true, true, 'BAR', 'X90I3u8'); 128 | $this->assertInstanceOf(SoapHeader::class, $header); 129 | $this->assertMatches(self::innerTrim(' 130 | 131 | 132 | foo 133 | bar 134 | 2016-03-31T19:17:04Z 135 | ([a-zA-Z0-9=]*) 136 | 137 | 138 | 2016-03-31T19:17:04Z 139 | 140 | '), $header->data->enc_value); 141 | } 142 | 143 | public function testCreateWithoutNonce() 144 | { 145 | $header = WsSecurity::createWsSecuritySoapHeader('foo', 'bar', false, 1459451824, 0, true, true, 'BAR', 'X90I3u8', false); 146 | $this->assertInstanceOf(SoapHeader::class, $header); 147 | $this->assertMatches(self::innerTrim(' 148 | 149 | 150 | foo 151 | bar 152 | 2016-03-31T19:17:04Z 153 | 154 | 155 | 2016-03-31T19:17:04Z 156 | 157 | '), $header->data->enc_value); 158 | } 159 | 160 | public function testWithTimestampAttribute() 161 | { 162 | $security = new WsSecurity('foo', 'bar', false, 1459451824, 0, false, null, null, false); 163 | $security->getSecurity()->getTimestamp()->setAttribute('wsu:Id', 'Timestamp-XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX'); 164 | $header = $security->getSoapHeader(); 165 | $this->assertInstanceOf(SoapHeader::class, $header); 166 | $this->assertMatches(self::innerTrim(' 167 | 168 | 169 | foo 170 | bar 171 | 2016-03-31T19:17:04Z 172 | 173 | 174 | 2016-03-31T19:17:04Z 175 | 176 | '), $header->data->enc_value); 177 | } 178 | 179 | public function testCreateWithEnvelopeNamespace() 180 | { 181 | $header = WsSecurity::createWsSecuritySoapHeader( 182 | 'foo', 183 | 'bar', 184 | false, 185 | 1459451824, 186 | 0, 187 | true, 188 | true, 189 | 'BAR', 190 | null, 191 | true, 192 | 'env' 193 | ); 194 | 195 | $this->assertInstanceOf(SoapHeader::class, $header); 196 | $this->assertMatches(self::innerTrim(' 197 | 198 | 199 | foo 200 | bar 201 | 2016-03-31T19:17:04Z 202 | ([a-zA-Z0-9=]*) 203 | 204 | 205 | 2016-03-31T19:17:04Z 206 | 207 | '), $header->data->enc_value); 208 | } 209 | } 210 | --------------------------------------------------------------------------------