-
Notifications
You must be signed in to change notification settings - Fork 1
/
AuthFactory.php
166 lines (158 loc) · 6.67 KB
/
AuthFactory.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
<?php
declare(strict_types=1);
namespace Dakujem\Middleware\Factory;
use Dakujem\Middleware\GenericMiddleware;
use Dakujem\Middleware\TokenManipulators as Man;
use Dakujem\Middleware\TokenMiddleware;
use LogicException;
use Psr\Http\Message\ResponseFactoryInterface as ResponseFactory;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface as Handler;
use Psr\Log\LoggerInterface as Logger;
/**
* AuthFactory - convenience middleware factory.
*
* @author Andrej Rypak <xrypak@gmail.com>
*/
class AuthFactory
{
/** @var callable|null fn(string):callable */
protected $decoderFactory;
protected ?ResponseFactory $responseFactory;
public function __construct(
?callable $decoderFactory,
?ResponseFactory $responseFactory
) {
$this->decoderFactory = $decoderFactory;
$this->responseFactory = $responseFactory;
}
/**
* Creates a preconfigured instance of TokenMiddleware.
*
* By default, the MW extracts tokens from `Authorization` header or `token` cookie,
* then decodes them using Firebase JWT decoder,
* and finally writes the decoded tokens to the `token` attribute of the request.
*
* @param string|null $tokenAttribute the decoded token will appear in this attribute; defaults to `token`
* @param string|null $headerName a header to look for a Bearer token; header detection not used when `null`
* @param string|null $cookieName a cookie to look for a token; cookie detection not used when `null`
* @param string|null $errorAttribute an error message will appear here; defaults to `{$tokenAttribute}.error`
* @param Logger|null $logger
* @return TokenMiddleware
*/
public function decodeTokens(
?string $tokenAttribute = null,
?string $headerName = Man::HEADER_NAME,
?string $cookieName = Man::COOKIE_NAME,
?string $errorAttribute = null,
?Logger $logger = null
): MiddlewareInterface {
if ($this->decoderFactory === null) {
throw new LogicException('Decoder factory not provided.');
}
$extractors = [];
if ($headerName !== null) {
$extractors[] = Man::headerExtractor($headerName);
}
if ($cookieName !== null) {
$extractors[] = Man::cookieExtractor($cookieName);
}
if ($extractors === []) {
throw new LogicException('No extractors. Using the token middleware without extractors is pointless.');
}
return new TokenMiddleware(
($this->decoderFactory)(),
$extractors,
Man::attributeInjector(
$tokenAttribute ?? Man::TOKEN_ATTRIBUTE_NAME,
$errorAttribute ?? (($tokenAttribute ?? Man::TOKEN_ATTRIBUTE_NAME) . Man::ERROR_ATTRIBUTE_SUFFIX)
),
$logger
);
}
/**
* Create an instance of middleware that asserts the presence of decoded tokens
* in a request attribute of choice.
*
* The MW will check the token Request attribute for presence of a decoded token,
* and return a 401 Response when it is not present.
* Error messages are read from the error Request attribute.
*
* @param string|null $tokenAttribute name of the token-containing attribute, defaults to `token`
* @param string|null $errorAttribute name of the error-containing attribute, defaults to `{$tokenAttribute}.error`
* @return GenericMiddleware
*/
public function assertTokens(
?string $tokenAttribute = null,
?string $errorAttribute = null
): MiddlewareInterface {
return $this->inspectTokens(
fn($token, callable $next) => $next(),
$tokenAttribute,
$errorAttribute
);
}
/**
* Create an instance of middleware that inspects tokens present in an attribute
* and calls the next middleware for valid tokens
* or returns an error response for invalid tokens and when tokens are missing.
*
* The "inspector" receives a decoded token
* and decides whether to invoke the next middleware
* or return an error response.
*
* An example "inspector":
* ```
* function (object $token, callable $next, callable $withError): Response {
* if ($token->sub === 42) { // Implement your inspection logic here.
* return $next(); // Invoke the next middleware for valid tokens
* } // or
* return $withError('Bad token.'); // return an error response for invalid ones.
* }
* ```
* Note:
* $withError returns a fresh 401 Response upon invocation, but you need not use it.
* When invoked with an argument, it encodes it as JSON and sets the Content-type header.
*
* @param callable $inspector fn(Token,callable Next,callable WithError):Response
* @param string|null $tokenAttribute name of the token-containing attribute, defaults to `token`
* @param string|null $errorAttribute name of the error-containing attribute, defaults to `{$tokenAttribute}.error`
* @return GenericMiddleware
*/
public function inspectTokens(
callable $inspector, // fn(Token,callable,callable):Response
?string $tokenAttribute = null,
?string $errorAttribute = null
): MiddlewareInterface {
if ($this->responseFactory === null) {
throw new LogicException('Response factory not provided.');
}
return Man::callableToMiddleware(function (
Request $request,
Handler $next
) use (
$inspector,
$tokenAttribute,
$errorAttribute
): Response {
$response = fn() => $this->responseFactory->createResponse(401); // HTTP status 401 (Unauthorized)
$withError = fn($error = null) => $error !== null ? Man::writeJsonError($response(), $error) : $response();
$token = $request->getAttribute($tokenAttribute ?? Man::TOKEN_ATTRIBUTE_NAME);
if (is_object($token)) { // asserts that the token is an object
return $inspector(
$token,
fn() => $next->handle($request),
$withError
);
}
// When the token is `null`, read the error message attribute
return $withError(
$request->getAttribute(
$errorAttribute ?? (($tokenAttribute ?? Man::TOKEN_ATTRIBUTE_NAME) . Man::ERROR_ATTRIBUTE_SUFFIX)
) ?? 'No valid token found.'
);
});
}
}