├── .github └── workflows │ └── php.yml ├── .gitignore ├── README.md ├── bin └── migrate.php ├── composer.json ├── docs ├── classes │ ├── Taproot-IndieAuth-Callback-AuthorizationFormInterface.html │ ├── Taproot-IndieAuth-Callback-DefaultAuthorizationForm.html │ ├── Taproot-IndieAuth-Callback-SingleUserPasswordAuthenticationCallback.html │ ├── Taproot-IndieAuth-IndieAuthException.html │ ├── Taproot-IndieAuth-Middleware-ClosureRequestHandler.html │ ├── Taproot-IndieAuth-Middleware-DoubleSubmitCookieCsrfMiddleware.html │ ├── Taproot-IndieAuth-Middleware-NoOpMiddleware.html │ ├── Taproot-IndieAuth-Middleware-ResponseRequestHandler.html │ ├── Taproot-IndieAuth-Server.html │ ├── Taproot-IndieAuth-Storage-FilesystemJsonStorage.html │ └── Taproot-IndieAuth-Storage-TokenStorageInterface.html ├── coverage │ ├── Callback │ │ ├── AuthorizationFormInterface.php.html │ │ ├── DefaultAuthorizationForm.php.html │ │ ├── SingleUserPasswordAuthenticationCallback.php.html │ │ ├── dashboard.html │ │ └── index.html │ ├── IndieAuthException.php.html │ ├── Middleware │ │ ├── ClosureRequestHandler.php.html │ │ ├── DoubleSubmitCookieCsrfMiddleware.php.html │ │ ├── NoOpMiddleware.php.html │ │ ├── ResponseRequestHandler.php.html │ │ ├── dashboard.html │ │ └── index.html │ ├── Server.php.html │ ├── Storage │ │ ├── FilesystemJsonStorage.php.html │ │ ├── Sqlite3Storage.php.html │ │ ├── TokenStorageInterface.php.html │ │ ├── dashboard.html │ │ └── index.html │ ├── dashboard.html │ ├── functions.php.html │ ├── index.html │ ├── phpunit_css │ │ ├── bootstrap.min.css │ │ ├── custom.css │ │ ├── nv.d3.min.css │ │ ├── octicons.css │ │ └── style.css │ ├── phpunit_icons │ │ ├── file-code.svg │ │ └── file-directory.svg │ └── phpunit_js │ │ ├── bootstrap.min.js │ │ ├── d3.min.js │ │ ├── file.js │ │ ├── jquery.min.js │ │ ├── nv.d3.min.js │ │ └── popper.min.js ├── css │ ├── base.css │ ├── normalize.css │ └── template.css ├── files │ ├── src-callback-authorizationforminterface.html │ ├── src-callback-defaultauthorizationform.html │ ├── src-callback-singleuserpasswordauthenticationcallback.html │ ├── src-functions.html │ ├── src-indieauthexception.html │ ├── src-middleware-closurerequesthandler.html │ ├── src-middleware-doublesubmitcookiecsrfmiddleware.html │ ├── src-middleware-noopmiddleware.html │ ├── src-middleware-responserequesthandler.html │ ├── src-server.html │ ├── src-storage-filesystemjsonstorage.html │ ├── src-storage-sqlite3storage.html │ └── src-storage-tokenstorageinterface.html ├── graphs │ └── classes.html ├── index.html ├── indices │ └── files.html ├── js │ ├── search.js │ └── searchIndex.js ├── namespaces │ ├── default.html │ ├── taproot-indieauth-callback.html │ ├── taproot-indieauth-middleware.html │ ├── taproot-indieauth-storage.html │ ├── taproot-indieauth.html │ └── taproot.html ├── packages │ ├── Application.html │ └── default.html └── reports │ ├── deprecated.html │ ├── errors.html │ └── markers.html ├── infection.json.dist ├── phpdoc.dist.xml ├── phpunit.xml ├── psalm.xml ├── run_coverage.sh ├── src ├── Callback │ ├── AuthorizationFormInterface.php │ ├── DefaultAuthorizationForm.php │ └── SingleUserPasswordAuthenticationCallback.php ├── IndieAuthException.php ├── Middleware │ ├── ClosureRequestHandler.php │ ├── DoubleSubmitCookieCsrfMiddleware.php │ ├── NoOpMiddleware.php │ └── ResponseRequestHandler.php ├── Server.php ├── Storage │ ├── FilesystemJsonStorage.php │ ├── Sqlite3Storage.php │ └── TokenStorageInterface.php └── functions.php ├── templates ├── default_authorization_page.html.php ├── default_exception_response.html.php ├── index.php └── single_user_password_authentication_form.html.php └── tests ├── DefaultAuthorizationFormTest.php ├── DoubleSubmitCookieCsrfMiddlewareTest.php ├── FilesystemJsonStorageTest.php ├── FunctionTest.php ├── ServerTest.php ├── SingleUserPasswordAuthenticationCallbackTest.php └── templates ├── authorization_form_json_response.json.php └── code_exception_response.txt.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | 13 | strategy: 14 | matrix: 15 | php: ['7.3', '7.4', '8.0', '8.1'] 16 | 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: shivammathur/setup-php@2.11.0 21 | with: 22 | php-version: ${{ matrix.php }} 23 | extensions: 'xdebug' 24 | 25 | - uses: actions/checkout@v2 26 | 27 | - name: Validate composer.json and composer.lock 28 | run: composer validate --strict 29 | 30 | - name: Cache Composer packages 31 | id: composer-cache 32 | uses: actions/cache@v2 33 | with: 34 | path: vendor 35 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-php- 38 | 39 | - name: Install dependencies 40 | run: composer install --prefer-dist --no-progress 41 | 42 | - name: Run Test Suite 43 | run: XDEBUG_MODE=coverage ./vendor/bin/phpunit tests --coverage-filter src --coverage-text 44 | 45 | - name: Run Static Analysis 46 | run: ./vendor/bin/psalm 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | vendor 3 | composer.lock 4 | tests/tmp/* 5 | phpDocumentor.phar 6 | phpdoc.xml 7 | .phpdoc 8 | .phpunit.result.cache -------------------------------------------------------------------------------- /bin/migrate.php: -------------------------------------------------------------------------------- 1 | function(string $path): array { 13 | $logs = []; 14 | 15 | foreach (new DirectoryIterator($path) as $fileInfo) { 16 | if ($fileInfo->isFile() && $fileInfo->getExtension() == 'json') { 17 | $tokenPath = $fileInfo->getPathname(); 18 | $token = json_decode(file_get_contents($tokenPath), true); 19 | 20 | if (array_key_exists('exp', $token) or array_key_exists('code_exp', $token)) { 21 | // This token is new or has already been migrated. 22 | continue; 23 | } 24 | 25 | // The exact migration depends on whether the code was exchanged for a token already. 26 | if ($token['exchanged_at'] ?? false) { 27 | $token['exp'] = $token['valid_until']; 28 | $token['iat'] = $token['exchanged_at']; 29 | } else { 30 | $token['code_exp'] = $token['valid_until']; 31 | } 32 | 33 | file_put_contents($tokenPath, json_encode($token)); 34 | } 35 | } 36 | 37 | return $logs; 38 | } 39 | ]; 40 | 41 | function showHelp() { 42 | global $migrations; // ew, global variables :( 43 | 44 | echo << v0.2.0). The utility 50 | will be expanded when more migrations become necessary, and the command line interface 51 | will change, so it’s not recommended to automate running this tool. 52 | 53 | EOD; 54 | } 55 | 56 | // If this file is being executed directly… 57 | if (!empty($argv) and trim($argv[0]) !== '' and strpos($argv[0], 'migrate.php') !== false) { 58 | // Script currently only accepts a single JSON path argument. Expand when required. 59 | if ($argc != 2) { 60 | showHelp(); 61 | die; 62 | } 63 | 64 | $jsonPath = rtrim($argv[1], '/'); 65 | 66 | $logs = $migrations['json_v0.1.0_v0.2.0']($jsonPath); 67 | 68 | foreach ($logs as $logline) { 69 | echo "$logline\n"; 70 | } 71 | } 72 | 73 | // For testing, and automating migrations (in the unlikely event anyone wants to do that). 74 | return $migrations; 75 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "taproot/indieauth", 3 | "description": "PHP PSR-7-compliant IndieAuth Server and Client implementation.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Barnaby Walters", 9 | "email": "barnaby@waterpigs.co.uk" 10 | } 11 | ], 12 | "autoload": { 13 | "files": ["src/functions.php"], 14 | "psr-4": { 15 | "Taproot\\IndieAuth\\": "src" 16 | } 17 | }, 18 | "autoload-dev": { 19 | "psr-4": { 20 | "Taproot\\IndieAuth\\Test\\": "tests" 21 | } 22 | }, 23 | "require": { 24 | "php": ">= 7.3.0", 25 | "psr/http-message": "^1.0", 26 | "psr/log": "^1.1||^2.0||^3.0", 27 | "psr/http-server-middleware": "^1.0", 28 | "indieauth/client": "^1.1", 29 | "dflydev/fig-cookies": "^3.0", 30 | "mf2/mf2": "^0.4.6||^0.5", 31 | "barnabywalters/mf-cleaner": "^0.2", 32 | "guzzlehttp/psr7": "^1.8||^2.0" 33 | }, 34 | "require-dev": { 35 | "guzzlehttp/guzzle": "^7.3", 36 | "phpunit/phpunit": "^9.5", 37 | "vimeo/psalm": "^4.7" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /docs/coverage/Storage/Sqlite3Storage.php.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Code Coverage for /Users/barnabywalters/Documents/Programming/taproot/indieauth/src/Storage/Sqlite3Storage.php 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 25 |
26 |
27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 |
 
Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
n/a
0 / 0
n/a
0 / 0
CRAP
n/a
0 / 0
62 |
63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 |
1<?php declare(strict_types=1);
2
3namespace Taproot\IndieAuth\Storage;
4
5/*class Sqlite3Storage implements TokenStorageInterface {
6    
7}*/
75 | 76 | 77 | 88 |
89 | 90 | 91 | 92 | 93 | 94 | 95 | -------------------------------------------------------------------------------- /docs/coverage/Storage/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Code Coverage for /Users/barnabywalters/Documents/Programming/taproot/indieauth/src/Storage 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 |
15 |
16 |
17 | 25 |
26 |
27 |
28 |
29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 53 | 54 | 55 | 61 | 62 | 63 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 81 | 82 | 83 | 89 | 90 | 91 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 |
 
Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
48 |
49 | 83.48% covered (warning) 50 |
51 |
52 |
83.48%
96 / 115
56 |
57 | 46.15% covered (danger) 58 |
59 |
60 |
46.15%
6 / 13
64 |
65 | 0.00% covered (danger) 66 |
67 |
68 |
0.00%
0 / 1
FilesystemJsonStorage.php
76 |
77 | 83.48% covered (warning) 78 |
79 |
80 |
83.48%
96 / 115
84 |
85 | 46.15% covered (danger) 86 |
87 |
88 |
46.15%
6 / 13
92 |
93 | 0.00% covered (danger) 94 |
95 |
96 |
0.00%
0 / 1
Sqlite3Storage.php
n/a
0 / 0
n/a
0 / 0
n/a
0 / 0
TokenStorageInterface.php
n/a
0 / 0
n/a
0 / 0
n/a
0 / 0
130 |
131 | 143 |
144 | 145 | 146 | -------------------------------------------------------------------------------- /docs/coverage/phpunit_css/custom.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Taproot/indieauth/68ae82125dde3cf5e476dc5fc62b35ebd3d9a1d2/docs/coverage/phpunit_css/custom.css -------------------------------------------------------------------------------- /docs/coverage/phpunit_css/octicons.css: -------------------------------------------------------------------------------- 1 | .octicon { 2 | display: inline-block; 3 | vertical-align: text-top; 4 | fill: currentColor; 5 | } 6 | -------------------------------------------------------------------------------- /docs/coverage/phpunit_css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding-top: 10px; 3 | } 4 | 5 | .popover { 6 | max-width: none; 7 | } 8 | 9 | .octicon { 10 | margin-right:.25em; 11 | } 12 | 13 | .table-bordered>thead>tr>td { 14 | border-bottom-width: 1px; 15 | } 16 | 17 | .table tbody>tr>td, .table thead>tr>td { 18 | padding-top: 3px; 19 | padding-bottom: 3px; 20 | } 21 | 22 | .table-condensed tbody>tr>td { 23 | padding-top: 0; 24 | padding-bottom: 0; 25 | } 26 | 27 | .table .progress { 28 | margin-bottom: inherit; 29 | } 30 | 31 | .table-borderless th, .table-borderless td { 32 | border: 0 !important; 33 | } 34 | 35 | .table tbody tr.covered-by-large-tests, li.covered-by-large-tests, tr.success, td.success, li.success, span.success { 36 | background-color: #dff0d8; 37 | } 38 | 39 | .table tbody tr.covered-by-medium-tests, li.covered-by-medium-tests { 40 | background-color: #c3e3b5; 41 | } 42 | 43 | .table tbody tr.covered-by-small-tests, li.covered-by-small-tests { 44 | background-color: #99cb84; 45 | } 46 | 47 | .table tbody tr.danger, .table tbody td.danger, li.danger, span.danger { 48 | background-color: #f2dede; 49 | } 50 | 51 | .table tbody tr.warning, .table tbody td.warning, li.warning, span.warning { 52 | background-color: #fcf8e3; 53 | } 54 | 55 | .table tbody td.info { 56 | background-color: #d9edf7; 57 | } 58 | 59 | td.big { 60 | width: 117px; 61 | } 62 | 63 | td.small { 64 | } 65 | 66 | td.codeLine { 67 | font-family: "Source Code Pro", "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 68 | white-space: pre-wrap; 69 | } 70 | 71 | td span.comment { 72 | color: #888a85; 73 | } 74 | 75 | td span.default { 76 | color: #2e3436; 77 | } 78 | 79 | td span.html { 80 | color: #888a85; 81 | } 82 | 83 | td span.keyword { 84 | color: #2e3436; 85 | font-weight: bold; 86 | } 87 | 88 | pre span.string { 89 | color: #2e3436; 90 | } 91 | 92 | span.success, span.warning, span.danger { 93 | margin-right: 2px; 94 | padding-left: 10px; 95 | padding-right: 10px; 96 | text-align: center; 97 | } 98 | 99 | #toplink { 100 | position: fixed; 101 | left: 5px; 102 | bottom: 5px; 103 | outline: 0; 104 | } 105 | 106 | svg text { 107 | font-family: "Lucida Grande", "Lucida Sans Unicode", Verdana, Arial, Helvetica, sans-serif; 108 | font-size: 11px; 109 | color: #666; 110 | fill: #666; 111 | } 112 | 113 | .scrollbox { 114 | height:245px; 115 | overflow-x:hidden; 116 | overflow-y:scroll; 117 | } 118 | 119 | table + .structure-heading { 120 | border-top: 1px solid lightgrey; 121 | padding-top: 0.5em; 122 | } 123 | -------------------------------------------------------------------------------- /docs/coverage/phpunit_icons/file-code.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/coverage/phpunit_icons/file-directory.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/coverage/phpunit_js/file.js: -------------------------------------------------------------------------------- 1 | $(function() { 2 | var $window = $(window) 3 | , $top_link = $('#toplink') 4 | , $body = $('body, html') 5 | , offset = $('#code').offset().top 6 | , hidePopover = function ($target) { 7 | $target.data('popover-hover', false); 8 | 9 | setTimeout(function () { 10 | if (!$target.data('popover-hover')) { 11 | $target.popover('hide'); 12 | } 13 | }, 300); 14 | }; 15 | 16 | $top_link.hide().click(function(event) { 17 | event.preventDefault(); 18 | $body.animate({scrollTop:0}, 800); 19 | }); 20 | 21 | $window.scroll(function() { 22 | if($window.scrollTop() > offset) { 23 | $top_link.fadeIn(); 24 | } else { 25 | $top_link.fadeOut(); 26 | } 27 | }).scroll(); 28 | 29 | $('.popin') 30 | .popover({trigger: 'manual'}) 31 | .on({ 32 | 'mouseenter.popover': function () { 33 | var $target = $(this); 34 | var $container = $target.children().first(); 35 | 36 | $target.data('popover-hover', true); 37 | 38 | // popover already displayed 39 | if ($target.next('.popover').length) { 40 | return; 41 | } 42 | 43 | // show the popover 44 | $container.popover('show'); 45 | 46 | // register mouse events on the popover 47 | $target.next('.popover:not(.popover-initialized)') 48 | .on({ 49 | 'mouseenter': function () { 50 | $target.data('popover-hover', true); 51 | }, 52 | 'mouseleave': function () { 53 | hidePopover($container); 54 | } 55 | }) 56 | .addClass('popover-initialized'); 57 | }, 58 | 'mouseleave.popover': function () { 59 | hidePopover($(this).children().first()); 60 | } 61 | }); 62 | }); 63 | -------------------------------------------------------------------------------- /docs/css/template.css: -------------------------------------------------------------------------------- 1 | .phpdocumentor-summary { 2 | font-style: italic; 3 | } 4 | .phpdocumentor-description { 5 | margin-bottom: var(--spacing-md); 6 | } 7 | .phpdocumentor-element { 8 | position: relative; 9 | } 10 | 11 | .phpdocumentor .phpdocumentor-element__name { 12 | line-height: 1; 13 | } 14 | 15 | .phpdocumentor-element__package, 16 | .phpdocumentor-element__extends, 17 | .phpdocumentor-element__implements { 18 | display: block; 19 | font-size: var(--text-xxs); 20 | font-weight: normal; 21 | opacity: .7; 22 | } 23 | 24 | .phpdocumentor-element__package .phpdocumentor-breadcrumbs { 25 | display: inline; 26 | } 27 | 28 | .phpdocumentor-element:not(:last-child) { 29 | border-bottom: 1px solid var(--primary-color-lighten); 30 | padding-bottom: var(--spacing-lg); 31 | } 32 | 33 | .phpdocumentor-element.-deprecated .phpdocumentor-element__name { 34 | text-decoration: line-through; 35 | } 36 | 37 | .phpdocumentor-element__modifier { 38 | font-size: var(--text-xxs); 39 | padding: calc(var(--spacing-base-size) / 4) calc(var(--spacing-base-size) / 2); 40 | color: var(--text-color); 41 | background-color: var(--light-gray); 42 | border-radius: 3px; 43 | text-transform: uppercase; 44 | } 45 | .phpdocumentor-signature { 46 | display: inline-block; 47 | font-size: var(--text-sm); 48 | margin-bottom: var(--spacing-md); 49 | } 50 | 51 | .phpdocumentor-signature.-deprecated .phpdocumentor-signature__name { 52 | text-decoration: line-through; 53 | } 54 | .phpdocumentor-table-of-contents { 55 | } 56 | 57 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry { 58 | padding-top: var(--spacing-xs); 59 | margin-left: 2rem; 60 | display: flex; 61 | } 62 | 63 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry > a { 64 | flex: 0 1 auto; 65 | } 66 | 67 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry > span { 68 | flex: 1; 69 | white-space: nowrap; 70 | text-overflow: ellipsis; 71 | overflow: hidden; 72 | } 73 | 74 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry:after { 75 | content: ''; 76 | height: 12px; 77 | width: 12px; 78 | left: 16px; 79 | position: absolute; 80 | } 81 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-private:after { 82 | background: url('data:image/svg+xml;utf8,') no-repeat; 83 | } 84 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-protected:after { 85 | left: 13px; 86 | background: url('data:image/svg+xml;utf8,') no-repeat; 87 | } 88 | 89 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry:before { 90 | width: 1.25rem; 91 | height: 1.25rem; 92 | line-height: 1.25rem; 93 | background: transparent url('data:image/svg+xml;utf8,') no-repeat center center; 94 | content: ''; 95 | position: absolute; 96 | left: 0; 97 | border-radius: 50%; 98 | font-weight: 600; 99 | color: white; 100 | text-align: center; 101 | font-size: .75rem; 102 | margin-top: .2rem; 103 | } 104 | 105 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-method:before { 106 | content: 'M'; 107 | background-image: url('data:image/svg+xml;utf8,'); 108 | } 109 | 110 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-function:before { 111 | content: 'M'; 112 | background-image: url('data:image/svg+xml;utf8,'); 113 | } 114 | 115 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-property:before { 116 | content: 'P' 117 | } 118 | 119 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-constant:before { 120 | content: 'C'; 121 | background-color: transparent; 122 | background-image: url('data:image/svg+xml;utf8,'); 123 | } 124 | 125 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-class:before { 126 | content: 'C' 127 | } 128 | 129 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-interface:before { 130 | content: 'I' 131 | } 132 | 133 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-trait:before { 134 | content: 'T' 135 | } 136 | 137 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-namespace:before { 138 | content: 'N' 139 | } 140 | 141 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-package:before { 142 | content: 'P' 143 | } 144 | 145 | .phpdocumentor-table-of-contents .phpdocumentor-table-of-contents__entry.-enum:before { 146 | content: 'E' 147 | } 148 | 149 | .phpdocumentor-table-of-contents dd { 150 | font-style: italic; 151 | margin-left: 2rem; 152 | } 153 | .phpdocumentor-element-found-in { 154 | position: absolute; 155 | top: 0; 156 | right: 0; 157 | font-size: var(--text-sm); 158 | color: gray; 159 | } 160 | 161 | .phpdocumentor-element-found-in .phpdocumentor-element-found-in__source { 162 | flex: 0 1 auto; 163 | display: inline-flex; 164 | } 165 | 166 | .phpdocumentor-element-found-in .phpdocumentor-element-found-in__source:after { 167 | width: 1.25rem; 168 | height: 1.25rem; 169 | line-height: 1.25rem; 170 | background: transparent url('data:image/svg+xml;utf8,') no-repeat center center; 171 | content: ''; 172 | left: 0; 173 | border-radius: 50%; 174 | font-weight: 600; 175 | text-align: center; 176 | font-size: .75rem; 177 | margin-top: .2rem; 178 | } 179 | .phpdocumentor-class-graph { 180 | width: 100%; height: 600px; border:1px solid black; overflow: hidden 181 | } 182 | 183 | .phpdocumentor-class-graph__graph { 184 | width: 100%; 185 | } 186 | .phpdocumentor-tag-list__definition { 187 | display: flex; 188 | } 189 | 190 | .phpdocumentor-tag-link { 191 | margin-right: var(--spacing-sm); 192 | } 193 | -------------------------------------------------------------------------------- /docs/files/src-callback-authorizationforminterface.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 |

Documentation

28 | 29 | 32 | 42 | 43 | 47 |
48 | 49 |
50 |
51 | 52 | 55 | 89 | 90 |
91 |
    92 |
93 | 94 |
95 |

AuthorizationFormInterface.php

96 | 97 | 98 |
99 | 100 | 101 | 102 | 103 | 104 | 105 |

106 | Interfaces, Classes, Traits and Enums 107 | 108 |

109 | 110 |
111 |
AuthorizationFormInterface
112 |
Authorization Form Interface
113 | 114 | 115 | 116 |
117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
126 |
127 |
128 |
129 |

Search results

130 | 131 |
132 |
133 |
    134 |
    135 |
    136 |
    137 |
    138 |
    139 | 140 | 141 |
    142 | 143 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /docs/files/src-callback-defaultauthorizationform.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
    27 |

    Documentation

    28 | 29 | 32 | 42 | 43 | 47 |
    48 | 49 |
    50 |
    51 | 52 | 55 | 89 | 90 |
    91 |
      92 |
    93 | 94 |
    95 |

    DefaultAuthorizationForm.php

    96 | 97 | 98 |
    99 | 100 | 101 | 102 | 103 | 104 | 105 |

    106 | Interfaces, Classes, Traits and Enums 107 | 108 |

    109 | 110 |
    111 | 112 |
    DefaultAuthorizationForm
    113 |
    Default Authorization Form
    114 | 115 | 116 |
    117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
    126 |
    127 |
    128 |
    129 |

    Search results

    130 | 131 |
    132 |
    133 |
      134 |
      135 |
      136 |
      137 |
      138 |
      139 | 140 | 141 |
      142 | 143 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /docs/files/src-indieauthexception.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
      27 |

      Documentation

      28 | 29 | 32 | 42 | 43 | 47 |
      48 | 49 |
      50 |
      51 | 52 | 55 | 89 | 90 |
      91 |
        92 |
      93 | 94 |
      95 |

      IndieAuthException.php

      96 | 97 | 98 |
      99 | 100 | 101 | 102 | 103 | 104 | 105 |

      106 | Interfaces, Classes, Traits and Enums 107 | 108 |

      109 | 110 |
      111 | 112 |
      IndieAuthException
      113 |
      114 | 115 | 116 |
      117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
      126 |
      127 |
      128 |
      129 |

      Search results

      130 | 131 |
      132 |
      133 |
        134 |
        135 |
        136 |
        137 |
        138 |
        139 | 140 | 141 |
        142 | 143 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /docs/files/src-middleware-closurerequesthandler.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
        27 |

        Documentation

        28 | 29 | 32 | 42 | 43 | 47 |
        48 | 49 |
        50 |
        51 | 52 | 55 | 89 | 90 |
        91 |
          92 |
        93 | 94 |
        95 |

        ClosureRequestHandler.php

        96 | 97 | 98 |
        99 | 100 | 101 | 102 | 103 | 104 | 105 |

        106 | Interfaces, Classes, Traits and Enums 107 | 108 |

        109 | 110 |
        111 | 112 |
        ClosureRequestHandler
        113 |
        114 | 115 | 116 |
        117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
        126 |
        127 |
        128 |
        129 |

        Search results

        130 | 131 |
        132 |
        133 |
          134 |
          135 |
          136 |
          137 |
          138 |
          139 | 140 | 141 |
          142 | 143 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /docs/files/src-middleware-doublesubmitcookiecsrfmiddleware.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
          27 |

          Documentation

          28 | 29 | 32 | 42 | 43 | 47 |
          48 | 49 |
          50 |
          51 | 52 | 55 | 89 | 90 |
          91 |
            92 |
          93 | 94 |
          95 |

          DoubleSubmitCookieCsrfMiddleware.php

          96 | 97 | 98 |
          99 | 100 | 101 | 102 | 103 | 104 | 105 |

          106 | Interfaces, Classes, Traits and Enums 107 | 108 |

          109 | 110 |
          111 | 112 |
          DoubleSubmitCookieCsrfMiddleware
          113 |
          Double-Submit Cookie CSRF Middleware
          114 | 115 | 116 |
          117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
          126 |
          127 |
          128 |
          129 |

          Search results

          130 | 131 |
          132 |
          133 |
            134 |
            135 |
            136 |
            137 |
            138 |
            139 | 140 | 141 |
            142 | 143 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /docs/files/src-middleware-noopmiddleware.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
            27 |

            Documentation

            28 | 29 | 32 | 42 | 43 | 47 |
            48 | 49 |
            50 |
            51 | 52 | 55 | 89 | 90 |
            91 |
              92 |
            93 | 94 |
            95 |

            NoOpMiddleware.php

            96 | 97 | 98 |
            99 | 100 | 101 | 102 | 103 | 104 | 105 |

            106 | Interfaces, Classes, Traits and Enums 107 | 108 |

            109 | 110 |
            111 | 112 |
            NoOpMiddleware
            113 |
            No-Op Middleware
            114 | 115 | 116 |
            117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
            126 |
            127 |
            128 |
            129 |

            Search results

            130 | 131 |
            132 |
            133 |
              134 |
              135 |
              136 |
              137 |
              138 |
              139 | 140 | 141 |
              142 | 143 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /docs/files/src-middleware-responserequesthandler.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
              27 |

              Documentation

              28 | 29 | 32 | 42 | 43 | 47 |
              48 | 49 |
              50 |
              51 | 52 | 55 | 89 | 90 |
              91 |
                92 |
              93 | 94 |
              95 |

              ResponseRequestHandler.php

              96 | 97 | 98 |
              99 | 100 | 101 | 102 | 103 | 104 | 105 |

              106 | Interfaces, Classes, Traits and Enums 107 | 108 |

              109 | 110 |
              111 | 112 |
              ResponseRequestHandler
              113 |
              114 | 115 | 116 |
              117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
              126 |
              127 |
              128 |
              129 |

              Search results

              130 | 131 |
              132 |
              133 |
                134 |
                135 |
                136 |
                137 |
                138 |
                139 | 140 | 141 |
                142 | 143 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /docs/files/src-server.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
                27 |

                Documentation

                28 | 29 | 32 | 42 | 43 | 47 |
                48 | 49 |
                50 |
                51 | 52 | 55 | 89 | 90 |
                91 |
                  92 |
                93 | 94 |
                95 |

                Server.php

                96 | 97 | 98 |
                99 | 100 | 101 | 102 | 103 | 104 | 105 |

                106 | Interfaces, Classes, Traits and Enums 107 | 108 |

                109 | 110 |
                111 | 112 |
                Server
                113 |
                IndieAuth Server
                114 | 115 | 116 |
                117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
                126 |
                127 |
                128 |
                129 |

                Search results

                130 | 131 |
                132 |
                133 |
                  134 |
                  135 |
                  136 |
                  137 |
                  138 |
                  139 | 140 | 141 |
                  142 | 143 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /docs/files/src-storage-filesystemjsonstorage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
                  27 |

                  Documentation

                  28 | 29 | 32 | 42 | 43 | 47 |
                  48 | 49 |
                  50 |
                  51 | 52 | 55 | 89 | 90 |
                  91 |
                    92 |
                  93 | 94 |
                  95 |

                  FilesystemJsonStorage.php

                  96 | 97 | 98 |
                  99 | 100 | 101 | 102 | 103 | 104 | 105 |

                  106 | Interfaces, Classes, Traits and Enums 107 | 108 |

                  109 | 110 |
                  111 | 112 |
                  FilesystemJsonStorage
                  113 |
                  Filesystem JSON Token Storage
                  114 | 115 | 116 |
                  117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
                  126 |
                  127 |
                  128 |
                  129 |

                  Search results

                  130 | 131 |
                  132 |
                  133 |
                    134 |
                    135 |
                    136 |
                    137 |
                    138 |
                    139 | 140 | 141 |
                    142 | 143 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /docs/files/src-storage-sqlite3storage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
                    27 |

                    Documentation

                    28 | 29 | 32 | 42 | 43 | 47 |
                    48 | 49 |
                    50 |
                    51 | 52 | 55 | 89 | 90 |
                    91 |
                      92 |
                    93 | 94 |
                    95 |

                    Sqlite3Storage.php

                    96 | 97 | 98 |
                    99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
                    114 |
                    115 |
                    116 |
                    117 |

                    Search results

                    118 | 119 |
                    120 |
                    121 |
                      122 |
                      123 |
                      124 |
                      125 |
                      126 |
                      127 | 128 | 129 |
                      130 | 131 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/files/src-storage-tokenstorageinterface.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
                      27 |

                      Documentation

                      28 | 29 | 32 | 42 | 43 | 47 |
                      48 | 49 |
                      50 |
                      51 | 52 | 55 | 89 | 90 |
                      91 |
                        92 |
                      93 | 94 |
                      95 |

                      TokenStorageInterface.php

                      96 | 97 | 98 |
                      99 | 100 | 101 | 102 | 103 | 104 | 105 |

                      106 | Interfaces, Classes, Traits and Enums 107 | 108 |

                      109 | 110 |
                      111 |
                      TokenStorageInterface
                      112 |
                      Token Storage Interface
                      113 | 114 | 115 | 116 |
                      117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 |
                      126 |
                      127 |
                      128 |
                      129 |

                      Search results

                      130 | 131 |
                      132 |
                      133 |
                        134 |
                        135 |
                        136 |
                        137 |
                        138 |
                        139 | 140 | 141 |
                        142 | 143 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /docs/graphs/classes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
                        16 |

                        Documentation

                        17 | 18 | 21 | 31 | 32 | 36 |
                        37 | 38 |
                        39 |
                        40 | 41 | 44 | 78 | 79 |
                        80 |
                        81 | 82 |
                        83 | 91 |
                        92 |
                        93 |
                        94 |

                        Search results

                        95 | 96 |
                        97 |
                        98 |
                          99 |
                          100 |
                          101 |
                          102 |
                          103 |
                          104 | 105 | 106 |
                          107 | 108 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
                          27 |

                          Documentation

                          28 | 29 | 32 | 42 | 43 | 47 |
                          48 | 49 |
                          50 |
                          51 | 52 | 55 | 89 | 90 |
                          91 |

                          Documentation

                          92 | 93 | 94 |

                          95 | Packages 96 | 97 |

                          98 | 99 |
                          100 |
                          Application
                          101 |
                          102 | 103 |

                          104 | Namespaces 105 | 106 |

                          107 | 108 |
                          109 |
                          Taproot
                          110 |
                          111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 |
                          119 |
                          120 |
                          121 |

                          Search results

                          122 | 123 |
                          124 |
                          125 |
                            126 |
                            127 |
                            128 |
                            129 |
                            130 |
                            131 | 132 | 133 |
                            134 | 135 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /docs/js/search.js: -------------------------------------------------------------------------------- 1 | // Search module for phpDocumentor 2 | // 3 | // This module is a wrapper around fuse.js that will use a given index and attach itself to a 4 | // search form and to a search results pane identified by the following data attributes: 5 | // 6 | // 1. data-search-form 7 | // 2. data-search-results 8 | // 9 | // The data-search-form is expected to have a single input element of type 'search' that will trigger searching for 10 | // a series of results, were the data-search-results pane is expected to have a direct UL child that will be populated 11 | // with rendered results. 12 | // 13 | // The search has various stages, upon loading this stage the data-search-form receives the CSS class 14 | // 'phpdocumentor-search--enabled'; this indicates that JS is allowed and indices are being loaded. It is recommended 15 | // to hide the form by default and show it when it receives this class to achieve progressive enhancement for this 16 | // feature. 17 | // 18 | // After loading this module, it is expected to load a search index asynchronously, for example: 19 | // 20 | // 21 | // 22 | // In this script the generated index should attach itself to the search module using the `appendIndex` function. By 23 | // doing it like this the page will continue loading, unhindered by the loading of the search. 24 | // 25 | // After the page has fully loaded, and all these deferred indexes loaded, the initialization of the search module will 26 | // be called and the form will receive the class 'phpdocumentor-search--active', indicating search is ready. At this 27 | // point, the input field will also have it's 'disabled' attribute removed. 28 | var Search = (function () { 29 | var fuse; 30 | var index = []; 31 | var options = { 32 | shouldSort: true, 33 | threshold: 0.6, 34 | location: 0, 35 | distance: 100, 36 | maxPatternLength: 32, 37 | minMatchCharLength: 1, 38 | keys: [ 39 | "fqsen", 40 | "name", 41 | "summary", 42 | "url" 43 | ] 44 | }; 45 | 46 | // Credit David Walsh (https://davidwalsh.name/javascript-debounce-function) 47 | // Returns a function, that, as long as it continues to be invoked, will not 48 | // be triggered. The function will be called after it stops being called for 49 | // N milliseconds. If `immediate` is passed, trigger the function on the 50 | // leading edge, instead of the trailing. 51 | function debounce(func, wait, immediate) { 52 | var timeout; 53 | 54 | return function executedFunction() { 55 | var context = this; 56 | var args = arguments; 57 | 58 | var later = function () { 59 | timeout = null; 60 | if (!immediate) func.apply(context, args); 61 | }; 62 | 63 | var callNow = immediate && !timeout; 64 | clearTimeout(timeout); 65 | timeout = setTimeout(later, wait); 66 | if (callNow) func.apply(context, args); 67 | }; 68 | } 69 | 70 | function close() { 71 | // Start scroll prevention: https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/ 72 | const scrollY = document.body.style.top; 73 | document.body.style.position = ''; 74 | document.body.style.top = ''; 75 | window.scrollTo(0, parseInt(scrollY || '0') * -1); 76 | // End scroll prevention 77 | 78 | var form = document.querySelector('[data-search-form]'); 79 | var searchResults = document.querySelector('[data-search-results]'); 80 | 81 | form.classList.toggle('phpdocumentor-search--has-results', false); 82 | searchResults.classList.add('phpdocumentor-search-results--hidden'); 83 | var searchField = document.querySelector('[data-search-form] input[type="search"]'); 84 | searchField.blur(); 85 | } 86 | 87 | function search(event) { 88 | // Start scroll prevention: https://css-tricks.com/prevent-page-scrolling-when-a-modal-is-open/ 89 | document.body.style.position = 'fixed'; 90 | document.body.style.top = `-${window.scrollY}px`; 91 | // End scroll prevention 92 | 93 | // prevent enter's from autosubmitting 94 | event.stopPropagation(); 95 | 96 | var form = document.querySelector('[data-search-form]'); 97 | var searchResults = document.querySelector('[data-search-results]'); 98 | var searchResultEntries = document.querySelector('[data-search-results] .phpdocumentor-search-results__entries'); 99 | 100 | searchResultEntries.innerHTML = ''; 101 | 102 | if (!event.target.value) { 103 | close(); 104 | return; 105 | } 106 | 107 | form.classList.toggle('phpdocumentor-search--has-results', true); 108 | searchResults.classList.remove('phpdocumentor-search-results--hidden'); 109 | var results = fuse.search(event.target.value, {limit: 25}); 110 | 111 | results.forEach(function (result) { 112 | var entry = document.createElement("li"); 113 | entry.classList.add("phpdocumentor-search-results__entry"); 114 | entry.innerHTML += '

                            ' + result.name + "

                            \n"; 115 | entry.innerHTML += '' + result.fqsen + "\n"; 116 | entry.innerHTML += '
                            ' + result.summary + '
                            '; 117 | searchResultEntries.appendChild(entry) 118 | }); 119 | } 120 | 121 | function appendIndex(added) { 122 | index = index.concat(added); 123 | 124 | // re-initialize search engine when appending an index after initialisation 125 | if (typeof fuse !== 'undefined') { 126 | fuse = new Fuse(index, options); 127 | } 128 | } 129 | 130 | function init() { 131 | fuse = new Fuse(index, options); 132 | 133 | var form = document.querySelector('[data-search-form]'); 134 | var searchField = document.querySelector('[data-search-form] input[type="search"]'); 135 | 136 | var closeButton = document.querySelector('.phpdocumentor-search-results__close'); 137 | closeButton.addEventListener('click', function() { close() }.bind(this)); 138 | 139 | var searchResults = document.querySelector('[data-search-results]'); 140 | searchResults.addEventListener('click', function() { close() }.bind(this)); 141 | 142 | form.classList.add('phpdocumentor-search--active'); 143 | 144 | searchField.setAttribute('placeholder', 'Search (Press "/" to focus)'); 145 | searchField.removeAttribute('disabled'); 146 | searchField.addEventListener('keyup', debounce(search, 300)); 147 | 148 | window.addEventListener('keyup', function (event) { 149 | if (event.key === '/') { 150 | searchField.focus(); 151 | } 152 | if (event.code === 'Escape') { 153 | close(); 154 | } 155 | }.bind(this)); 156 | } 157 | 158 | return { 159 | appendIndex, 160 | init 161 | } 162 | })(); 163 | 164 | window.addEventListener('DOMContentLoaded', function () { 165 | var form = document.querySelector('[data-search-form]'); 166 | 167 | // When JS is supported; show search box. Must be before including the search for it to take effect immediately 168 | form.classList.add('phpdocumentor-search--enabled'); 169 | }); 170 | 171 | window.addEventListener('load', function () { 172 | Search.init(); 173 | }); 174 | -------------------------------------------------------------------------------- /docs/namespaces/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
                            27 |

                            Documentation

                            28 | 29 | 32 | 42 | 43 | 47 |
                            48 | 49 |
                            50 |
                            51 | 52 | 55 | 89 | 90 |
                            91 |
                              92 |
                            93 | 94 |
                            95 |

                            API Documentation

                            96 | 97 | 98 |

                            99 | Namespaces 100 | 101 |

                            102 | 103 |
                            104 |
                            Taproot
                            105 |
                            106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
                            114 |
                            115 |
                            116 |
                            117 |

                            Search results

                            118 | 119 |
                            120 |
                            121 |
                              122 |
                              123 |
                              124 |
                              125 |
                              126 |
                              127 | 128 | 129 |
                              130 | 131 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/namespaces/taproot.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
                              27 |

                              Documentation

                              28 | 29 | 32 | 42 | 43 | 47 |
                              48 | 49 |
                              50 |
                              51 | 52 | 55 | 89 | 90 |
                              91 |
                                92 |
                              93 | 94 |
                              95 |

                              Taproot

                              96 | 97 | 98 |

                              99 | Namespaces 100 | 101 |

                              102 | 103 |
                              104 |
                              IndieAuth
                              105 |
                              106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
                              114 |
                              115 |
                              116 |
                              117 |

                              Search results

                              118 | 119 |
                              120 |
                              121 |
                                122 |
                                123 |
                                124 |
                                125 |
                                126 |
                                127 | 128 | 129 |
                                130 | 131 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/packages/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 |
                                27 |

                                Documentation

                                28 | 29 | 32 | 42 | 43 | 47 |
                                48 | 49 |
                                50 |
                                51 | 52 | 55 | 89 | 90 |
                                91 |
                                  92 |
                                93 | 94 |
                                95 |

                                API Documentation

                                96 | 97 |

                                98 | Packages 99 | 100 |

                                101 | 102 |
                                103 |
                                Application
                                104 |
                                105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |
                                114 |
                                115 |
                                116 |
                                117 |

                                Search results

                                118 | 119 |
                                120 |
                                121 |
                                  122 |
                                  123 |
                                  124 |
                                  125 |
                                  126 |
                                  127 | 128 | 129 |
                                  130 | 131 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /docs/reports/deprecated.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation » Deprecated elements 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
                                  28 |

                                  Documentation

                                  29 | 30 | 33 | 43 | 44 | 48 |
                                  49 | 50 |
                                  51 |
                                  52 | 53 | 56 | 90 | 91 |
                                  92 | 95 | 96 |
                                  97 |

                                  Deprecated

                                  98 | 99 | 100 |
                                  101 | No deprecated elements have been found in this project. 102 |
                                  103 |
                                  104 |
                                  105 |
                                  106 |
                                  107 |

                                  Search results

                                  108 | 109 |
                                  110 |
                                  111 |
                                    112 |
                                    113 |
                                    114 |
                                    115 |
                                    116 |
                                    117 | 118 | 119 |
                                    120 | 121 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | -------------------------------------------------------------------------------- /docs/reports/errors.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Documentation » Compilation errors 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 |
                                    28 |

                                    Documentation

                                    29 | 30 | 33 | 43 | 44 | 48 |
                                    49 | 50 |
                                    51 |
                                    52 | 53 | 56 | 90 | 91 |
                                    92 | 95 | 96 |
                                    97 |

                                    Errors

                                    98 | 99 | 100 |
                                    No errors have been found in this project.
                                    101 | 102 |
                                    103 |
                                    104 |
                                    105 |
                                    106 |

                                    Search results

                                    107 | 108 |
                                    109 |
                                    110 |
                                      111 |
                                      112 |
                                      113 |
                                      114 |
                                      115 |
                                      116 | 117 | 118 |
                                      119 | 120 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /infection.json.dist: -------------------------------------------------------------------------------- 1 | { 2 | "source": { 3 | "directories": [ 4 | "src" 5 | ], 6 | "excludes": [ 7 | "test" 8 | ] 9 | }, 10 | "mutators": { 11 | "@default": true 12 | }, 13 | "logs": { 14 | "text": "tests/tmp/infection.log" 15 | } 16 | } -------------------------------------------------------------------------------- /phpdoc.dist.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | docs 10 | 11 | 12 | 13 | 14 | src 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | tests 5 | 6 | 7 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /run_coverage.sh: -------------------------------------------------------------------------------- 1 | rm -rf docs/coverage/ 2 | XDEBUG_MODE=coverage ./vendor/bin/phpunit tests --coverage-filter src --coverage-html docs/coverage 3 | mv docs/coverage/_css docs/coverage/phpunit_css 4 | mv docs/coverage/_icons docs/coverage/phpunit_icons 5 | mv docs/coverage/_js docs/coverage/phpunit_js 6 | grep -rl _css docs/coverage | xargs sed -i "" -e 's/_css/phpunit_css/g' 7 | grep -rl _icons docs/coverage | xargs sed -i "" -e 's/_icons/phpunit_icons/g' 8 | grep -rl _js docs/coverage | xargs sed -i "" -e 's/_js/phpunit_js/g' -------------------------------------------------------------------------------- /src/Callback/AuthorizationFormInterface.php: -------------------------------------------------------------------------------- 1 | getQueryParams()`. The parameters most likely to be of use to the authorization 26 | * form are: 27 | * 28 | * * `scope`: a space-separated list of scopes which the client app is requesting. May be absent. 29 | * * `client_id`: the URL of the client app. Should be shown to the user. This also makes a good “cancel” link. 30 | * * `redirect_uri`: the URI which the user will be redirected to on successful authorization. 31 | * 32 | * The form MUST submit a POST request to `$formAction`, with the `taproot_indieauth_action` 33 | * parameter set to `approve`. 34 | * 35 | * The form MUST additionally include any CSRF tokens required to protect the submission. 36 | * Refer to whatever CSRF protection code you’re using (e.g. {@see \Taproot\IndieAuth\Middleware\DoubleSubmitCookieCsrfMiddleware}) 37 | * and make sure to include the required element. This will usually involve getting a 38 | * CSRF token with `$request->getAttribute()` and including it in an ``. 39 | * 40 | * The form SHOULD offer the user the opportunity to choose which of the request scopes, 41 | * if any, they wish to grant. It should describe what effect each scope grants. If no scopes are 42 | * requested, tell the user that the app is only requesting authorization, not access to their data. 43 | * 44 | * The form MAY offer the user UIs for additional token configuration, e.g. a custom token lifetime. 45 | * You may have to refer to the documentation for your instance of {@see \Taproot\IndieAuth\Storage\TokenStorageInterface} to ensure 46 | * that lifetime configuration works correctly. Any other additional data is not used by the IndieAuth 47 | * library, but, if stored on the access token, will be available to your app for use. 48 | * 49 | * {@see \Taproot\IndieAuth\Server} adds the following headers to the response returned from `showForm()`: 50 | * 51 | * ``` 52 | * Cache-Control: no-store 53 | * Pragma: no-cache 54 | * X-Frame-Options: DENY 55 | * Content-Security-Policy: frame-ancestors 'none' 56 | * ``` 57 | * 58 | * These headers prevent the authorization form from being cached or embedded into a malicious webpage. 59 | * It may make sense for you to add additional `Content-Security-Policy` values appropriate to your implementation, 60 | * for example to prevent the execution of inline or 3rd party scripts. 61 | * 62 | * @param ServerRequestInterface $request The current request. 63 | * @param array $authenticationResult The array returned from the Authentication Handler. Guaranteed to contain a 'me' key, may also contain additional keys e.g. 'profile'. 64 | * @param string $formAction The URL which your form MUST submit to. Can also be used as the redirect URL for a logout process. 65 | * @param array|Exception|null $clientHApp If available, the microformats-2 structure representing the client app. An instance of Exception if fetching the `client_id` failed. 66 | * @return ResponseInterface A response containing the authorization form. 67 | */ 68 | public function showForm(ServerRequestInterface $request, array $authenticationResult, string $formAction, $clientHAppOrException): ResponseInterface; 69 | 70 | /** 71 | * Transform Authorization Code 72 | * 73 | * This method is called on a successful authorization form submission. The `$code` array 74 | * is a partially-constructed authorization code array, which is guaranteed to have the 75 | * following keys: 76 | * 77 | * * `client_id`: the validated `client_id` request parameter 78 | * * `redirect_uri`: the validated `redirect_uri` request parameter 79 | * * `state`: the `state` request parameter 80 | * * `code_challenge`: the `code_challenge` request parameter 81 | * * `code_challenge_method`: the `code_challenge_method` request parameter 82 | * * `requested_scope`: the value of the `scope` request parameter 83 | * * `me`: the value of the `me` key from the authentication result returned from the authentication request handler callback 84 | * 85 | * It may also have additional keys, which can come from the following locations: 86 | * 87 | * * All keys from the the authentication request handler callback result which do not clash 88 | * with the keys listed above (with the exception of `me`, which is always present). Usually 89 | * this is a `profile` key, but you may choose to return additional data from the authentication 90 | * callback, which will be present in `$data`. 91 | * 92 | * This method should add any additional data to the auth code, before it is persisted and 93 | * returned to the client app. Typically, this involves setting the `scope` key to be a 94 | * valid space-separated scope string of any scopes granted by the user in the form. 95 | * 96 | * If the form offers additional token configuration, this method should set any relevant 97 | * keys in `$code` based on the form data in `$request`. 98 | * 99 | * @param array $code The base authorization code data, to be added to. 100 | * @param ServerRequestInterface $request The current request. 101 | * @return array The $code data after making any necessary changes. 102 | */ 103 | public function transformAuthorizationCode(ServerRequestInterface $request, array $code): array; 104 | } 105 | -------------------------------------------------------------------------------- /src/Callback/DefaultAuthorizationForm.php: -------------------------------------------------------------------------------- 1 | csrfKey = $csrfKey ?? \Taproot\IndieAuth\Server::DEFAULT_CSRF_KEY; 55 | $this->logger = $logger ?? new NullLogger; 56 | 57 | $formTemplate = $formTemplate ?? __DIR__ . '/../../templates/default_authorization_page.html.php'; 58 | if (is_string($formTemplate)) { 59 | if (!file_exists($formTemplate)) { 60 | $this->logger->warning("\$formTemplate string passed to DefaultAuthorizationForm was not a valid path.", ['formTemplate' => $formTemplate]); 61 | } 62 | $formTemplate = function (array $context) use ($formTemplate): string { 63 | return renderTemplate($formTemplate, $context); 64 | }; 65 | } 66 | 67 | if (!is_callable($formTemplate)) { 68 | throw new BadMethodCallException("\$formTemplate must be a string (path), callable, or null."); 69 | } 70 | 71 | $this->formTemplateCallable = $formTemplate; 72 | } 73 | 74 | public function showForm(ServerRequestInterface $request, array $authenticationResult, string $formAction, $clientHAppOrException): ResponseInterface { 75 | // Show an authorization page. List all requested scopes, as this default 76 | // function has no way of knowing which scopes are supported by the consumer. 77 | $scopes = []; 78 | foreach(explode(' ', $request->getQueryParams()['scope'] ?? '') as $s) { 79 | $scopes[$s] = null; // Ideally there would be a description of the scope here, we don’t have one though. 80 | } 81 | 82 | $exception = null; 83 | $appData = null; 84 | if ($clientHAppOrException instanceof Exception) { 85 | $exception = $clientHAppOrException; 86 | } elseif (M\isMicroformat($clientHAppOrException)) { 87 | $appData = [ 88 | 'name' => M\getPlaintext($clientHAppOrException, 'name'), 89 | 'url' => M\getPlaintext($clientHAppOrException, 'url'), 90 | 'photo' => M\getPlaintext($clientHAppOrException, 'photo'), 91 | 'author' => null 92 | ]; 93 | 94 | if (M\hasProp($clientHAppOrException, 'author')) { 95 | $rawAuthor = $clientHAppOrException['properties']['author'][0]; 96 | if (is_string($rawAuthor)) { 97 | $appData['author'] = $rawAuthor; 98 | } elseif (M\isMicroformat($rawAuthor)) { 99 | $appData['author'] = [ 100 | 'name' => M\getPlaintext($rawAuthor, 'name'), 101 | 'url' => M\getPlaintext($rawAuthor, 'url'), 102 | 'photo' => M\getPlaintext($rawAuthor, 'photo') 103 | ]; 104 | } 105 | } 106 | } 107 | 108 | return new Response(200, ['content-type' => 'text/html'], call_user_func($this->formTemplateCallable, [ 109 | 'scopes' => $scopes, 110 | 'user' => $authenticationResult, 111 | 'formAction' => $formAction, 112 | 'request' => $request, 113 | 'clientHApp' => $appData, 114 | 'exception' => $exception, 115 | 'clientId' => $request->getQueryParams()['client_id'], 116 | 'clientRedirectUri' => $request->getQueryParams()['redirect_uri'], 117 | 'csrfFormElement' => '' 118 | ])); 119 | } 120 | 121 | public function transformAuthorizationCode(ServerRequestInterface $request, array $code): array { 122 | // Add any granted scopes from the form to the code. 123 | $grantedScopes = $request->getParsedBody()['taproot_indieauth_server_scope'] ?? []; 124 | 125 | // This default implementation naievely accepts any scopes it receives from the form. 126 | // You may wish to perform some sort of validation. 127 | $code['scope'] = join(' ', $grantedScopes); 128 | 129 | // You may wish to additionally make any other necessary changes to the the code based on 130 | // the form submission, e.g. if the user set a custom token lifetime, or wanted extra data 131 | // stored on the token to affect how it behaves. 132 | return $code; 133 | } 134 | 135 | public function setLogger(LoggerInterface $logger): void { 136 | $this->logger = $logger; 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/Callback/SingleUserPasswordAuthenticationCallback.php: -------------------------------------------------------------------------------- 1 | new IndieAuth\Callback\SingleUserPasswordAuthenticationCallback( 37 | * YOUR_SECRET, 38 | * ['me' => 'https://me.example.com/'], 39 | * YOUR_HASHED_PASSWORD 40 | * ) 41 | * … 42 | * ]); 43 | * ``` 44 | * 45 | * See documentation for `__construct()` for information about customising behaviour. 46 | */ 47 | class SingleUserPasswordAuthenticationCallback { 48 | const PASSWORD_FORM_PARAMETER = 'taproot_indieauth_server_password'; 49 | const LOGIN_HASH_COOKIE = 'taproot_indieauth_server_supauth_hash'; 50 | const DEFAULT_COOKIE_TTL = 60 * 5; 51 | 52 | /** @var string $csrfKey */ 53 | public $csrfKey; 54 | 55 | /** @var callable $formTemplateCallable */ 56 | protected $formTemplateCallable; 57 | 58 | /** @var array $user */ 59 | protected $user; 60 | 61 | /** @var string $hashedPassword */ 62 | protected $hashedPassword; 63 | 64 | /** @var string $secret */ 65 | protected $secret; 66 | 67 | /** @var int $ttl */ 68 | protected $ttl; 69 | 70 | /** 71 | * Constructor 72 | * 73 | * @param string $secret A secret key used to encrypt cookies. Can be the same as the secret passed to IndieAuth\Server. 74 | * @param array $user An array representing the user, which will be returned on a successful authentication. MUST include a 'me' key, may also contain a 'profile' key, or other keys at your discretion. 75 | * @param string $hashedPassword The password used to authenticate as $user, hashed by `password_hash($pass, PASSWORD_DEFAULT)` 76 | * @param string|callable|null $formTemplate The path to a template used to render the sign-in form, or a template callable with the signature `function (array $context): string`. Uses default if null. 77 | * @param string|null $csrfKey The key under which to fetch a CSRF token from `$request` attributes, and as the CSRF token name in submitted form data. Defaults to the Server default, only change if you’re using a custom CSRF middleware. 78 | * @param int|null $ttl The lifetime of the authentication cookie, in seconds. Defaults to five minutes. 79 | */ 80 | public function __construct(string $secret, array $user, string $hashedPassword, $formTemplate=null, ?string $csrfKey=null, ?int $ttl=null) { 81 | if (strlen($secret) < 64) { 82 | throw new BadMethodCallException("\$secret must be a string with a minimum length of 64 characters."); 83 | } 84 | $this->secret = $secret; 85 | 86 | $this->ttl = $ttl ?? self::DEFAULT_COOKIE_TTL; 87 | 88 | if (!isset($user['me'])) { 89 | throw new BadMethodCallException('The $user array MUST contain a “me” key, the value which must be the user’s canonical URL as a string.'); 90 | } 91 | 92 | $hashAlgo = password_get_info($hashedPassword)['algo']; 93 | // Invalid algorithms are null in PHP 7.4, 0 in PHP 7.3. 94 | if (is_null($hashAlgo) or 0 === $hashAlgo) { 95 | throw new BadMethodCallException('The provided $hashedPassword was not a valid hash created by the password_hash() function.'); 96 | } 97 | $this->user = $user; 98 | $this->hashedPassword = $hashedPassword; 99 | 100 | $formTemplate = $formTemplate ?? __DIR__ . '/../../templates/single_user_password_authentication_form.html.php'; 101 | if (is_string($formTemplate)) { 102 | $formTemplate = function (array $context) use ($formTemplate): string { 103 | return renderTemplate($formTemplate, $context); 104 | }; 105 | } 106 | 107 | if (!is_callable($formTemplate)) { 108 | throw new BadMethodCallException("\$formTemplate must be a string (path), a callable, or null."); 109 | } 110 | 111 | $this->formTemplateCallable = $formTemplate; 112 | $this->csrfKey = $csrfKey ?? \Taproot\IndieAuth\Server::DEFAULT_CSRF_KEY; 113 | } 114 | 115 | public function __invoke(ServerRequestInterface $request, string $formAction, ?string $normalizedMeUrl=null) { 116 | // If the request is logged in, return authentication data. 117 | $cookies = $request->getCookieParams(); 118 | if ( 119 | isset($cookies[self::LOGIN_HASH_COOKIE]) 120 | && hash_equals(hash_hmac('SHA256', json_encode($this->user), $this->secret), $cookies[self::LOGIN_HASH_COOKIE]) 121 | ) { 122 | return $this->user; 123 | } 124 | 125 | // If the request is a form submission with a matching password, return a redirect to the indieauth 126 | // flow, setting a cookie. 127 | if ($request->getMethod() == 'POST' && password_verify($request->getParsedBody()[self::PASSWORD_FORM_PARAMETER] ?? '', $this->hashedPassword)) { 128 | $response = new Response(302, ['Location' => $formAction]); 129 | 130 | // Set the user data hash cookie. 131 | $response = FigCookies\FigResponseCookies::set($response, FigCookies\SetCookie::create(self::LOGIN_HASH_COOKIE) 132 | ->withValue(hash_hmac('SHA256', json_encode($this->user), $this->secret)) 133 | ->withMaxAge($this->ttl) 134 | ->withSecure($request->getUri()->getScheme() == 'https') 135 | ->withDomain($request->getUri()->getHost()) 136 | ); 137 | 138 | return $response; 139 | } 140 | 141 | // Otherwise, return a response containing the password form. 142 | return (new Response(200, ['content-type' => 'text/html'], call_user_func($this->formTemplateCallable, [ 143 | 'formAction' => $formAction, 144 | 'request' => $request, 145 | 'csrfFormElement' => '' 146 | ])))->withAddedHeader('Cache-control', 'no-store') 147 | ->withAddedHeader('Pragma', 'no-cache') 148 | ->withAddedHeader('X-Frame-Options', 'DENY') 149 | ->withAddedHeader('Content-Security-Policy', "frame-ancestors 'none'"); 150 | } 151 | } -------------------------------------------------------------------------------- /src/IndieAuthException.php: -------------------------------------------------------------------------------- 1 | ['statusCode' => 500, 'name' => 'Internal Server Error', 'explanation' => 'An internal server error occurred.'], 29 | self::INTERNAL_ERROR_REDIRECT => ['statusCode' => 302, 'name' => 'Internal Server Error', 'error' => 'internal_error'], 30 | self::AUTHENTICATION_CALLBACK_INVALID_RETURN_VALUE => ['statusCode' => 302, 'name' => 'Internal Server Error', 'error' => 'internal_error'], 31 | self::AUTHENTICATION_CALLBACK_MISSING_ME_PARAM => ['statusCode' => 302, 'name' => 'Internal Server Error', 'error' => 'internal_error'], 32 | self::AUTHORIZATION_APPROVAL_REQUEST_MISSING_HASH => ['statusCode' => 302, 'name' => 'Request Missing Hash', 'error' => 'internal_error'], 33 | self::AUTHORIZATION_APPROVAL_REQUEST_INVALID_HASH => ['statusCode' => 302, 'name' => 'Request Hash Invalid', 'error' => 'internal_error'], 34 | // TODO: should this one be a 500 because it’s an internal server error, or a 400 because the client_id was likely invalid? Is anyone ever going to notice, or care? 35 | self::HTTP_EXCEPTION_FETCHING_CLIENT_ID => ['statusCode' => 500, 'name' => 'Error Fetching Client App URL', 'explanation' => 'Fetching the client app (client_id) failed.'], 36 | self::INTERNAL_EXCEPTION_FETCHING_CLIENT_ID => ['statusCode' => 500, 'name' => 'Internal Error fetching client app URI', 'explanation' => 'Fetching the client app (client_id) failed due to an internal error.'], 37 | self::INVALID_REDIRECT_URI => ['statusCode' => 400, 'name' => 'Invalid Client App Redirect URI', 'explanation' => 'The client app redirect URI (redirect_uri) either was not a valid URI, did not sufficiently match client_id, or did not exactly match any redirect URIs parsed from fetching the client_id.'], 38 | self::INVALID_CLIENT_ID => ['statusCode' => 400, 'name' => 'Invalid Client Identifier URI', 'explanation' => 'The Client Identifier was not valid.'], 39 | self::INVALID_STATE => ['statusCode' => 302, 'name' => 'Invalid state Parameter', 'error' => 'invalid_request'], 40 | self::INVALID_CODE_CHALLENGE => ['statusCode' => 302, 'name' => 'Invalid code_challenge Parameter', 'error' => 'invalid_request'], 41 | self::INVALID_SCOPE => ['statusCode' => 302, 'name' => 'Invalid scope Parameter', 'error' => 'invalid_request'], 42 | self::INVALID_GRANT => ['statusCode' => 400, 'name' => 'The provided credentials were not valid.', 'error' => 'invalid_grant'], 43 | self::INVALID_REQUEST => ['statusCode' => 400, 'name' => 'Invalid Request', 'error' => 'invalid_request'], 44 | self::INVALID_REQUEST_REDIRECT => ['statusCode' => 302, 'name' => 'Invalid Request', 'error' => 'invalid_request'], 45 | ]; 46 | 47 | /** @var ServerRequestInterface $request */ 48 | protected $request; 49 | 50 | public static function create(int $code, ServerRequestInterface $request, ?Throwable $previous=null): self { 51 | // Only accept known codes. Default to 0 (generic internal error) on an unrecognised code. 52 | if (!in_array($code, array_keys(self::EXC_INFO))) { 53 | $code = 0; 54 | } 55 | $message = self::EXC_INFO[$code]['name']; 56 | $e = new self($message, $code, $previous); 57 | $e->request = $request; 58 | return $e; 59 | } 60 | 61 | public function getStatusCode() { 62 | return $this->getInfo()['statusCode'] ?? 500; 63 | } 64 | 65 | public function getExplanation() { 66 | return $this->getInfo()['explanation'] ?? 'An unknown error occured.'; 67 | } 68 | 69 | public function getInfo() { 70 | return self::EXC_INFO[$this->code] ?? self::EXC_INFO[self::INTERNAL_ERROR]; 71 | } 72 | 73 | /** 74 | * Trust Query Params 75 | * 76 | * Only useful on authorization form submission requests. If this returns false, 77 | * the client_id and/or request_uri have likely been tampered with, and the error 78 | * page SHOULD NOT offer the user a link to them. 79 | */ 80 | public function trustQueryParams() { 81 | return !in_array($this->code, [self::AUTHORIZATION_APPROVAL_REQUEST_INVALID_HASH, self::AUTHORIZATION_APPROVAL_REQUEST_MISSING_HASH]); 82 | } 83 | 84 | public function getRequest() { 85 | return $this->request; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Middleware/ClosureRequestHandler.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 17 | $this->args = array_slice(func_get_args(), 1); 18 | } 19 | 20 | public function handle(ServerRequestInterface $request): ResponseInterface { 21 | return call_user_func_array($this->callable, array_merge([$request], $this->args)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Middleware/DoubleSubmitCookieCsrfMiddleware.php: -------------------------------------------------------------------------------- 1 | attribute = $attribute ?? self::ATTRIBUTE; 74 | $this->ttl = $ttl ?? self::TTL; 75 | $this->tokenLength = $tokenLength ?? self::CSRF_TOKEN_LENGTH; 76 | 77 | if (!is_callable($errorResponse)) { 78 | if (!$errorResponse instanceof ResponseInterface) { 79 | if (!is_string($errorResponse)) { 80 | $errorResponse = self::DEFAULT_ERROR_RESPONSE_STRING; 81 | } 82 | $errorResponse = new Response(400, ['content-type' => 'text/plain'], $errorResponse); 83 | } 84 | $errorResponse = function (ServerRequestInterface $request) use ($errorResponse) { return $errorResponse; }; 85 | } 86 | $this->errorResponse = $errorResponse; 87 | 88 | if (!$logger instanceof LoggerInterface) { 89 | $logger = new NullLogger(); 90 | } 91 | $this->logger = $logger; 92 | } 93 | 94 | public function setLogger(LoggerInterface $logger): void { 95 | $this->logger = $logger; 96 | } 97 | 98 | public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface { 99 | // Generate a new CSRF token, add it to the request attributes, and as a cookie on the response. 100 | $csrfToken = generateRandomPrintableAsciiString($this->tokenLength); 101 | $request = $request->withAttribute($this->attribute, $csrfToken); 102 | 103 | // Add a pre-rendered CSRF form element to the request for convenience. 104 | $csrfFormElement = ''; 105 | $request = $request->withAttribute("{$this->attribute}FormElement", $csrfFormElement); 106 | 107 | if (!in_array(strtoupper($request->getMethod()), self::READ_METHODS) && !$this->isValid($request)) { 108 | // This request is a write method with invalid CSRF parameters. 109 | $response = call_user_func($this->errorResponse, $request); 110 | } else { 111 | $response = $handler->handle($request); 112 | } 113 | 114 | // Add the new CSRF cookie, restricting its scope to match the current request. 115 | $response = FigCookies\FigResponseCookies::set($response, FigCookies\SetCookie::create($this->attribute) 116 | ->withValue($csrfToken) 117 | ->withMaxAge($this->ttl) 118 | ->withSecure($request->getUri()->getScheme() == 'https') 119 | ->withDomain($request->getUri()->getHost()) 120 | ->withHttpOnly(true) 121 | ->withPath($this->cookiePath ?? $request->getUri()->getPath())); 122 | 123 | return $response; 124 | } 125 | 126 | protected function isValid(ServerRequestInterface $request) { 127 | if (array_key_exists($this->attribute, $request->getParsedBody() ?? [])) { 128 | if (array_key_exists($this->attribute, $request->getCookieParams() ?? [])) { 129 | // TODO: make sure CSRF token isn’t the empty string, possibly also check that it’s the same length 130 | // as defined in $this->tokenLength. 131 | return hash_equals($request->getParsedBody()[$this->attribute], $request->getCookieParams()[$this->attribute]); 132 | } 133 | } 134 | return false; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Middleware/NoOpMiddleware.php: -------------------------------------------------------------------------------- 1 | handle($request); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Middleware/ResponseRequestHandler.php: -------------------------------------------------------------------------------- 1 | response = $response; 15 | } 16 | 17 | public function handle(ServerRequestInterface $request): ResponseInterface { 18 | return $this->response; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Storage/Sqlite3Storage.php: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | IndieAuth • Error! 10 | 11 | 44 | 45 | 46 |
                                      47 |

                                      Error: getMessage()) ?>

                                      48 | 49 |

                                      getExplanation()) ?>

                                      50 | 51 | 53 | trustQueryParams() and !empty($request->getQueryParams()['client_id'])): ?> 54 |

                                      Return to app (getQueryParams()['client_id']) ?> ?>) 55 | 56 | 57 | 58 |

                                      59 | 60 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /templates/index.php: -------------------------------------------------------------------------------- 1 | createServerRequest('GET', '/'); 12 | 13 | if (empty($_GET['t'])) { 14 | ?> 15 |

                                      Templates:

                                      16 | 25 | 'Demo App', 33 | 'url' => 'https://client.example.com/', 34 | 'photo' => 'http://waterpigs.co.uk/taproot/logo.png', 35 | 'author' => 'https://waterpigs.co.uk' 36 | ]; 37 | break; 38 | case 'photo,full-author': 39 | $clientHApp = [ 40 | 'name' => 'Demo App', 41 | 'url' => 'https://client.example.com/', 42 | 'photo' => 'http://waterpigs.co.uk/taproot/logo.png', 43 | 'author' => [ 44 | 'name' => 'Barnaby Walters', 45 | 'photo' => 'https://waterpigs.co.uk/photo-2021-04-22-719w.jpg', 46 | 'url' => 'https://waterpigs.co.uk' 47 | ] 48 | ]; 49 | break; 50 | case 'photo': 51 | $clientHApp = [ 52 | 'name' => 'Demo App', 53 | 'url' => 'https://client.example.com/', 54 | 'photo' => 'http://waterpigs.co.uk/taproot/logo.png', 55 | ]; 56 | break; 57 | case 'name': 58 | $clientHApp = [ 59 | 'name' => 'Demo App', 60 | 'url' => 'https://client.example.com/' 61 | ]; 62 | break; 63 | default: 64 | $clientHApp = null; 65 | break; 66 | } 67 | 68 | $_profile = empty($_GET['profile']) ? null : $_GET['profile']; 69 | switch ($_profile) { 70 | case 'photo': 71 | $user = [ 72 | 'me' => 'https://me.example.com/', 73 | 'profile' => [ 74 | 'name' => 'Demo User', 75 | 'photo' => 'https://waterpigs.co.uk/photo-2021-04-22-719w.jpg' 76 | ] 77 | ]; 78 | break; 79 | case 'name': 80 | $user = [ 81 | 'me' => 'https://me.example.com/', 82 | 'profile' => [ 83 | 'name' => 'Demo User' 84 | ] 85 | ]; 86 | break; 87 | default: 88 | $user = ['me' => 'https://me.example.com/']; 89 | break; 90 | } 91 | 92 | $_scopes = empty($_GET['scopes']) ? null : $_GET['scopes']; 93 | switch ($_scopes) { 94 | case 'descriptions': 95 | $scopes = [ 96 | 'profile' => 'Allow the app to access to your profile data', 97 | 'create' => 'Allow the app to create new content on your site', 98 | 'update' => 'Allow the app to update content on your site' 99 | ]; 100 | break; 101 | case 'keys': 102 | $scopes = [ 103 | 'profile' => null, 104 | 'create' => null, 105 | 'update' => null 106 | ]; 107 | break; 108 | default: 109 | $scopes = []; 110 | break; 111 | } 112 | 113 | if (array_key_exists('exception', $_GET)) { 114 | $exception = new Exception('An example exception which might have occurred.'); 115 | } else { 116 | $exception = null; 117 | } 118 | 119 | $clientId = 'https://client.example.com/'; 120 | $formAction = ''; 121 | $csrfFormElement = ''; 122 | $clientRedirectUri = 'https://client.example.com/redirect'; 123 | 124 | include('default_authorization_page.html.php'); 125 | } elseif ($_GET['t'] == 'single_user_password_authentication_form.html') { 126 | $csrfFormElement = ''; 127 | $formAction = ''; 128 | include('single_user_password_authentication_form.html.php'); 129 | } elseif ($_GET['t'] == 'default_exception_response.html') { 130 | $exception = \Taproot\IndieAuth\IndieAuthException::create(\Taproot\IndieAuth\IndieAuthException::INTERNAL_ERROR, $request); 131 | include('default_exception_response.html.php'); 132 | } else { 133 | // Unknown template, redirect to template choosing page 134 | header('Location: /'); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /templates/single_user_password_authentication_form.html.php: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | IndieAuth • Log In 11 | 12 | 49 | 50 | 51 |
                                      52 | 53 | 54 |

                                      Log In

                                      55 | 56 |

                                      57 | 58 |

                                      59 | 60 |
                                      61 | 62 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/DefaultAuthorizationFormTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(1, $logCount); 22 | $this->assertEquals(LogLevel::WARNING, $logLevel); 23 | } 24 | } 25 | 26 | class MockLogger extends AbstractLogger { 27 | private $callback; 28 | 29 | public function __construct($callback) { 30 | $this->callback = $callback; 31 | } 32 | 33 | // Ignoring the signature mismatch error on this line for the moment to retain compat with PHP7. 34 | public function log($level, $message, array $context=[]): void { 35 | call_user_func($this->callback, $level, $message, $context); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/FilesystemJsonStorageTest.php: -------------------------------------------------------------------------------- 1 | isFile()) { 17 | unlink($fileInfo->getPathname()); 18 | } 19 | } 20 | } 21 | 22 | protected function tearDown(): void { 23 | // Clean tmp dir. 24 | foreach (new DirectoryIterator(TMP_DIR) as $fileInfo) { 25 | if ($fileInfo->isFile()) { 26 | unlink($fileInfo->getPathname()); 27 | } 28 | } 29 | } 30 | 31 | public function testCrud() { 32 | $s = new FilesystemJsonStorage(TMP_DIR, SECRET); 33 | 34 | $t1data = ['example' => 'data']; 35 | $t1path = $s->getPath('t1'); 36 | 37 | $this->assertTrue($s->put('t1', $t1data), "Saving t1 data failed"); 38 | 39 | $this->assertFileExists($t1path, "t1 was not stored to $t1path"); 40 | 41 | $this->assertEquals($t1data, $s->get('t1'), "The result of getting t1 did not match the saved data."); 42 | 43 | $s->delete('t1'); 44 | 45 | $this->assertFileDoesNotExist($t1path, "t1 was not successfully deleted."); 46 | 47 | $this->assertNull($s->get('t1'), "Getting a nonexistent key did not return null"); 48 | } 49 | 50 | public function testCleanUp() { 51 | $s = new FilesystemJsonStorage(TMP_DIR, SECRET); 52 | $s->put('t1', ['exp' => time() + 10]); 53 | $s->put('t2', ['exp' => time() - 10]); 54 | $s->deleteExpiredTokens(); 55 | $this->assertIsArray($s->get('t1'), 't1 was not expired and should not have been deleted.'); 56 | $this->assertNull($s->get('t2'), 't2 was not cleaned up after expiring.'); 57 | } 58 | 59 | public function testMigrateFromV0_1_0ToV0_2_0() { 60 | $s = new FilesystemJsonStorage(TMP_DIR, SECRET); 61 | 62 | $oldUnexchangedCode = [ 63 | 'me' => 'https://example.com', 64 | 'valid_until' => time() + 60 * 5 65 | ]; 66 | $newUnexchangedCode = array_merge($oldUnexchangedCode, [ 67 | 'code_exp' => $oldUnexchangedCode['valid_until'] 68 | ]); 69 | 70 | $oldExchangedCode = [ 71 | 'me' => 'https://example.com', 72 | 'exchanged_at' => time() + 60 * 1, 73 | 'valid_until' => time() + 60 * 5 74 | ]; 75 | $newExchangedCode = array_merge($oldExchangedCode, [ 76 | 'iat' => $oldExchangedCode['exchanged_at'], 77 | 'exp' => $oldExchangedCode['valid_until'] 78 | ]); 79 | 80 | $s->put('t1', $oldUnexchangedCode); 81 | $s->put('t2', $oldExchangedCode); 82 | 83 | $migrationFunctions = require_once __DIR__ . '/../bin/migrate.php'; 84 | 85 | $migrationFunctions['json_v0.1.0_v0.2.0'](TMP_DIR); 86 | 87 | $this->assertEquals($s->get('t1'), $newUnexchangedCode); 88 | $this->assertEquals($s->get('t2'), $newExchangedCode); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /tests/FunctionTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($len, strlen(hex2bin($rand))); 21 | } 22 | 23 | public function testBuildQueryString() { 24 | $testCases = [ 25 | 'key=value' => ['key' => 'value'], 26 | 'k1=v1&k2=v2' => ['k1' => 'v1', 'k2' => 'v2'] 27 | ]; 28 | 29 | foreach ($testCases as $expected => $params) { 30 | $this->assertEquals($expected, IA\buildQueryString($params)); 31 | } 32 | } 33 | 34 | public function testAppendQueryParams() { 35 | $testCases = [ 36 | 'https://example.com/?k=v' => ['https://example.com/', ['k' => 'v']], 37 | 'https://example.com/?k=v' => ['https://example.com/?', ['k' => 'v']], 38 | 'https://example.com/?k=v' => ['https://example.com/?k=v', []], 39 | 'https://example.com/?k=v&k2=v2' => ['https://example.com/?k=v', ['k2' => 'v2']] 40 | ]; 41 | 42 | foreach ($testCases as $expected => list($uri, $params)) { 43 | $this->assertEquals($expected, IA\appendQueryParams($uri, $params)); 44 | } 45 | } 46 | 47 | public function testHashAuthorizationRequestParametersReturnsNullWhenParameterIsMissing() { 48 | $req = (new ServerRequest('GET', 'https://example.com'))->withQueryParams([]); 49 | $hash = IA\hashAuthorizationRequestParameters($req, 'super secret'); 50 | $this->assertNull($hash); 51 | } 52 | 53 | public function testHashAuthorizationRequestParametersIgnoresExtraParameters() { 54 | $params = [ 55 | 'client_id' => '1', 56 | 'redirect_uri' => '1', 57 | 'code_challenge' => '1', 58 | 'code_challenge_method' => '1' 59 | ]; 60 | $req1 = (new ServerRequest('GET', 'https://example.com'))->withQueryParams($params); 61 | $req2 = (new ServerRequest('GET', 'https://example.com'))->withQueryParams(array_merge($params, [ 62 | 'an_additional_parameter' => 'an additional value!' 63 | ])); 64 | $this->assertEquals(IA\hashAuthorizationRequestParameters($req1, 'super secret'), IA\hashAuthorizationRequestParameters($req2, 'super secret')); 65 | } 66 | 67 | // Taken straight from https://indieauth.spec.indieweb.org/#user-profile-url-li-6 68 | public function testIsProfileUrl() { 69 | $testCases = [ 70 | 'https://example.com/' => true, 71 | 'https://example.com/username' => true, 72 | 'https://example.com/users?id=100' => true, 73 | 'example.com' => false, 74 | 'mailto:user@example.com' => false, 75 | 'https://example.com/foo/../bar' => false, 76 | 'https://example.com/#me' => false, 77 | 'https://user:pass@example.com/' => false, 78 | 'https://example.com:8443/' => false, 79 | 'https://172.28.92.51/' => false 80 | ]; 81 | 82 | foreach ($testCases as $url => $expected) { 83 | $this->assertEquals($expected, isProfileUrl($url), "$url was not correctly validated as $expected"); 84 | } 85 | } 86 | 87 | public function testIsClientIentifier() { 88 | $testCases = [ 89 | 'https://example.com/' => true, 90 | 'https://example.com/username' => true, 91 | 'https://example.com/users?id=100' => true, 92 | 'https://example.com:8443/' => true, 93 | 'https://127.0.0.1/' => true, 94 | 'https://[1::]/' => true, 95 | 'example.com' => false, 96 | 'mailto:user@example.com' => false, 97 | 'https://example.com/foo/../bar' => false, 98 | 'https://example.com/#me' => false, 99 | 'https://user:pass@example.com/' => false, 100 | 'https://172.28.92.51/' => false 101 | ]; 102 | 103 | foreach ($testCases as $url => $expected) { 104 | $this->assertEquals($expected, isClientIdentifier($url), "$url was not correctly validated as $expected"); 105 | } 106 | } 107 | 108 | public function testIsValidState() { 109 | $testCases = [ 110 | 'hisdfbusdgiueryb@#$%^&*(' => true, 111 | "\x19" => false 112 | ]; 113 | 114 | foreach ($testCases as $test => $expected) { 115 | $this->assertEquals($expected, isValidState($test), "$test was not correctly validated as $expected"); 116 | } 117 | } 118 | 119 | public function testIsValidScope() { 120 | $testCases = [ 121 | '!#[]~' => true, 122 | '!#[]~ scope1 another_scope moar_scopes!' => true, 123 | '"' => false, // ASCII 0x22 not permitted 124 | '\\' => false, // ASCII 0x5C not permitted 125 | ]; 126 | 127 | foreach ($testCases as $test => $expected) { 128 | $this->assertEquals($expected, isValidScope($test), "$test was not correctly validated as $expected"); 129 | } 130 | } 131 | 132 | // https://github.com/Taproot/indieauth/issues/13 133 | public function testIsValidCodeChallenge() { 134 | $testCases = [ 135 | generatePKCECodeChallenge('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~') => true, 136 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~' => true, 137 | 'has_bad_characters_in_*%#ü____' => false 138 | ]; 139 | 140 | foreach ($testCases as $test => $expected) { 141 | $this->assertEquals($expected, isValidCodeChallenge($test), "$test was not correctly validated as $expected"); 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /tests/SingleUserPasswordAuthenticationCallbackTest.php: -------------------------------------------------------------------------------- 1 | 'blah' 19 | ], password_hash('password', PASSWORD_DEFAULT)); 20 | $this->fail(); 21 | } catch (BadMethodCallException $e) { 22 | $this->assertEquals('The $user array MUST contain a “me” key, the value which must be the user’s canonical URL as a string.', $e->getMessage()); 23 | } 24 | } 25 | 26 | public function testThrowsExceptionIfSecretIsTooShort() { 27 | try { 28 | $c = new SingleUserPasswordAuthenticationCallback('not long enough', [ 29 | 'me' => 'blah' 30 | ], password_hash('password', PASSWORD_DEFAULT)); 31 | $this->fail(); 32 | } catch (BadMethodCallException $e) { 33 | $this->assertEquals('$secret must be a string with a minimum length of 64 characters.', $e->getMessage()); 34 | } 35 | } 36 | 37 | public function testThrowsExceptionIfHashedPasswordIsInvalid() { 38 | try { 39 | $c = new SingleUserPasswordAuthenticationCallback(SERVER_SECRET, [ 40 | 'me' => 'https://me.example.com/' 41 | ], 'definitely not a hashed password'); 42 | $this->fail(); 43 | } catch (BadMethodCallException $e) { 44 | $this->assertTrue(true); 45 | } 46 | } 47 | 48 | public function testShowsAuthenticationFormOnUnauthenticatedRequest() { 49 | $callback = new SingleUserPasswordAuthenticationCallback(SERVER_SECRET, [ 50 | 'me' => 'https://me.example.com/' 51 | ], password_hash('password', PASSWORD_DEFAULT)); 52 | 53 | $formAction = 'https://example.com/formaction'; 54 | 55 | $req = (new ServerRequest('GET', 'https://example.com/login'))->withAttribute(Server::DEFAULT_CSRF_KEY, 'csrf token'); 56 | $res = $callback($req, $formAction); 57 | 58 | $this->assertEquals(200, $res->getStatusCode()); 59 | $this->assertStringContainsString($formAction, (string) $res->getBody()); 60 | $this->assertEquals('no-store', $res->getHeaderLine('cache-control')); 61 | $this->assertEquals('no-cache', $res->getHeaderLine('pragma')); 62 | $this->assertStringContainsString("frame-ancestors 'none'", $res->getHeaderLine('content-security-policy')); 63 | $this->assertStringContainsString("DENY", $res->getHeaderLine('x-frame-options')); 64 | } 65 | 66 | public function testReturnsCookieRedirectOnAuthenticatedRequest() { 67 | $userData = [ 68 | 'me' => 'https://me.example.com', 69 | 'profile' => ['name' => 'Me'] 70 | ]; 71 | 72 | $password = 'my very secure password'; 73 | 74 | $callback = new SingleUserPasswordAuthenticationCallback(SERVER_SECRET, $userData, password_hash($password, PASSWORD_DEFAULT)); 75 | 76 | $req = (new ServerRequest('POST', 'https://example.com/login')) 77 | ->withAttribute(Server::DEFAULT_CSRF_KEY, 'csrf token') 78 | ->withParsedBody([ 79 | SingleUserPasswordAuthenticationCallback::PASSWORD_FORM_PARAMETER => $password 80 | ]); 81 | 82 | $res = $callback($req, 'form_action'); 83 | 84 | $this->assertEquals(302, $res->getStatusCode()); 85 | $this->assertEquals('form_action', $res->getHeaderLine('location')); 86 | $resCookies = FigCookies\SetCookies::fromResponse($res); 87 | $hashCookie = $resCookies->get(SingleUserPasswordAuthenticationCallback::LOGIN_HASH_COOKIE); 88 | $this->assertEquals(hash_hmac('SHA256', json_encode($userData), SERVER_SECRET), $hashCookie->getValue()); 89 | } 90 | 91 | public function testReturnsUserDataOnResponseWithValidHashCookie() { 92 | $userData = [ 93 | 'me' => 'https://me.example.com', 94 | 'profile' => ['name' => 'Me'] 95 | ]; 96 | 97 | $password = 'my very secure password'; 98 | 99 | $callback = new SingleUserPasswordAuthenticationCallback(SERVER_SECRET, $userData, password_hash($password, PASSWORD_DEFAULT)); 100 | 101 | $req = (new ServerRequest('POST', 'https://example.com/login')) 102 | ->withAttribute(Server::DEFAULT_CSRF_KEY, 'csrf token') 103 | ->withCookieParams([ 104 | SingleUserPasswordAuthenticationCallback::LOGIN_HASH_COOKIE => hash_hmac('SHA256', json_encode($userData), SERVER_SECRET) 105 | ]); 106 | 107 | $res = $callback($req, 'form_action'); 108 | 109 | $this->assertEquals($userData, $res); 110 | } 111 | 112 | public function testAcceptsCallableTemplate() { 113 | $expected = 'the expected response'; 114 | $callback = new SingleUserPasswordAuthenticationCallback(SERVER_SECRET, [ 115 | 'me' => 'https://me.example.com/' 116 | ], password_hash('password', PASSWORD_DEFAULT), function (array $context) use ($expected): string { 117 | return $expected; 118 | }); 119 | 120 | $formAction = 'https://example.com/formaction'; 121 | 122 | $req = (new ServerRequest('GET', 'https://example.com/login'))->withAttribute(Server::DEFAULT_CSRF_KEY, 'csrf token'); 123 | $res = $callback($req, $formAction); 124 | 125 | $this->assertEquals($expected, (string) $res->getBody()); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /tests/templates/authorization_form_json_response.json.php: -------------------------------------------------------------------------------- 1 | $formAction, 15 | 'clientHApp' => $clientHApp, 16 | 'exception' => is_null($exception) ? null : get_class($exception), 17 | 'user' => $user, 18 | 'scopes' => $scopes, 19 | 'clientId' => $clientId, 20 | 'clientRedirectUri' => $clientRedirectUri, 21 | 'csrfFormElement' => $csrfFormElement 22 | ]); 23 | ?> -------------------------------------------------------------------------------- /tests/templates/code_exception_response.txt.php: -------------------------------------------------------------------------------- 1 | getCode() ?> --------------------------------------------------------------------------------