Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
2 / 2
CRAP
100.00% covered (success)
100.00%
15 / 15
SingleUserPasswordAuthenticationCallback
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
2 / 2
6
100.00% covered (success)
100.00%
15 / 15
 __construct
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
9 / 9
 __invoke
100.00% covered (success)
100.00%
1 / 1
3
100.00% covered (success)
100.00%
6 / 6
1<?php declare(strict_types=1);
2
3namespace Taproot\IndieAuth\Callback;
4
5use BadMethodCallException;
6use Nyholm\Psr7\Response;
7use Psr\Http\Message\ServerRequestInterface;
8
9use function Taproot\IndieAuth\renderTemplate;
10
11/**
12 * Single User Password Authentication Callback
13 * 
14 * A simple example authentication callback which performs authentication itself rather
15 * than redirecting to an existing authentication flow.
16 * 
17 * In some cases, it may make sense for your IndieAuth server to be able to authenticate
18 * users itself, rather than redirecting them to an existing authentication flow. This
19 * implementation provides a simple single-user password authentication method intended
20 * for bootstrapping and testing purposes.
21 * 
22 * The sign-in form can be customised by making your own template and passing the path to
23 * the constructor.
24 * 
25 * Minimal usage:
26 * 
27 * ```php
28 * // One-off during app configuration:
29 * YOUR_HASHED_PASSWORD = password_hash('my super strong password', PASSWORD_DEFAULT);
30 * 
31 * // In your app:
32 * use Taproot\IndieAuth;
33 * $server = new IndieAuth\Server([
34 *   …
35 *   'authenticationHandler' => new IndieAuth\Callback\SingleUserPasswordAuthenticationCallback(
36 *     ['me' => 'https://me.example.com/'],
37 *     YOUR_HASHED_PASSWORD
38 *   )
39 *   …
40 * ]);
41 * ```
42 * 
43 * See documentation for `__construct()` for information about customising behaviour.
44 */
45class SingleUserPasswordAuthenticationCallback {
46    const PASSWORD_FORM_PARAMETER = 'taproot_indieauth_server_password';
47
48    public string $csrfKey;
49    public string $formTemplate;
50    protected array $user;
51    protected string $hashedPassword;
52    
53    /**
54     * Constructor
55     * 
56     * @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.
57     * @param string $hashedPassword The password used to authenticate as $user, hashed by `password_hash($pass, PASSWORD_DEFAULT)`
58     * @param string|null $formTemplate The path to a template used to render the sign-in form. Uses default if null.
59     * @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.
60     */
61    public function __construct(array $user, string $hashedPassword, ?string $formTemplate=null, ?string $csrfKey=null) {
62        if (!isset($user['me'])) {
63            throw new BadMethodCallException('The $user array MUST contain a “me” key, the value which must be the user’s canonical URL as a string.');
64        }
65        
66        if (is_null(password_get_info($hashedPassword)['algo'])) {
67            throw new BadMethodCallException('The provided $hashedPassword was not a valid hash created by the password_hash() function.');
68        }
69        $this->user = $user;
70        $this->hashedPassword = $hashedPassword;
71        $this->formTemplate = $formTemplate ?? __DIR__ . '/../../templates/single_user_password_authentication_form.html.php';
72        $this->csrfKey = $csrfKey ?? \Taproot\IndieAuth\Server::DEFAULT_CSRF_KEY;
73    }
74
75    public function __invoke(ServerRequestInterface $request, string $formAction, ?string $normalizedMeUrl=null) {
76        // If the request is a form submission with a matching password, return the corresponding
77        // user data.
78        if ($request->getMethod() == 'POST' && password_verify($request->getParsedBody()[self::PASSWORD_FORM_PARAMETER] ?? '', $this->hashedPassword)) {
79            return $this->user;
80        }
81
82        // Otherwise, return a response containing the password form.
83        return new Response(200, ['content-type' => 'text/html'], renderTemplate($this->formTemplate, [
84            'formAction' => $formAction,
85            'request' => $request,
86            'csrfFormElement' => '<input type="hidden" name="' . htmlentities($this->csrfKey) . '" value="' . htmlentities($request->getAttribute($this->csrfKey)) . '" />'
87        ]));
88    }
89}