2
2
3
3
namespace MessageBird ;
4
4
5
+ use Firebase \JWT \JWT ;
6
+ use Firebase \JWT \SignatureInvalidException ;
7
+ use MessageBird \Exceptions \ValidationException ;
5
8
use MessageBird \Objects \SignedRequest ;
6
9
7
10
/**
8
- * Class RequestValidator
11
+ * Class RequestValidator validates request signature signed by MessageBird services.
9
12
*
10
13
* @package MessageBird
14
+ * @see https://developers.messagebird.com/docs/verify-http-requests
11
15
*/
12
16
class RequestValidator
13
17
{
14
18
const BODY_HASH_ALGO = 'sha256 ' ;
15
19
const HMAC_HASH_ALGO = 'sha256 ' ;
20
+ const ALLOWED_ALGOS = array ('HS256 ' , 'HS384 ' , 'HS512 ' );
16
21
17
22
/**
18
23
* The key with which requests will be signed by MessageBird.
@@ -21,21 +26,36 @@ class RequestValidator
21
26
*/
22
27
private $ signingKey ;
23
28
29
+ /**
30
+ * This field instructs Validator to not validate url_hash claim.
31
+ * It is recommended to not skip URL validation to ensure high security.
32
+ * but the ability to skip URL validation is necessary in some cases, e.g.
33
+ * your service is behind proxy or when you want to validate it yourself.
34
+ * Note that when true, no query parameters should be trusted.
35
+ * Defaults to false.
36
+ *
37
+ * @var bool
38
+ */
39
+ private $ skipURLValidation ;
40
+
24
41
/**
25
42
* RequestValidator constructor.
26
43
*
27
- * @param string $signingKey
44
+ * @param string $signingKey customer signature key. Can be retrieved through <a href="https://dashboard.messagebird.com/developers/settings">Developer Settings</a>. This is NOT your API key.
45
+ * @param bool $skipURLValidation whether url_hash claim validation should be skipped. Note that when true, no query parameters should be trusted.
28
46
*/
29
- public function __construct ($ signingKey )
47
+ public function __construct (string $ signingKey, bool $ skipURLValidation = false )
30
48
{
31
49
$ this ->signingKey = $ signingKey ;
50
+ $ this ->skipURLValidation = $ skipURLValidation ;
32
51
}
33
52
34
53
/**
35
54
* Verify that the signed request was submitted from MessageBird using the known key.
36
55
*
37
56
* @param SignedRequest $request
38
57
* @return bool
58
+ * @deprecated Use {@link RequestValidator::validateSignature()} instead.
39
59
*/
40
60
public function verify (SignedRequest $ request )
41
61
{
@@ -47,6 +67,9 @@ public function verify(SignedRequest $request)
47
67
return \hash_equals ($ expectedSignature , $ calculatedSignature );
48
68
}
49
69
70
+ /**
71
+ * @deprecated Use {@link RequestValidator::validateSignature()} instead.
72
+ */
50
73
private function buildPayloadFromRequest (SignedRequest $ request ): string
51
74
{
52
75
$ parts = [];
@@ -71,9 +94,80 @@ private function buildPayloadFromRequest(SignedRequest $request): string
71
94
* @param SignedRequest $request The signed request object.
72
95
* @param int $offset The maximum number of seconds that is allowed to consider the request recent
73
96
* @return bool
97
+ * @deprecated Use {@link RequestValidator::validateSignature()} instead.
74
98
*/
75
99
public function isRecent (SignedRequest $ request , $ offset = 10 )
76
100
{
77
- return (\time () - (int ) $ request ->requestTimestamp ) < $ offset ;
101
+ return (\time () - (int )$ request ->requestTimestamp ) < $ offset ;
102
+ }
103
+
104
+ /**
105
+ * Validate JWT signature.
106
+ * This JWT is signed with a MessageBird account unique secret key, ensuring the request is from MessageBird and a specific account.
107
+ * The JWT contains the following claims:
108
+ * - "url_hash" - the raw URL hashed with SHA256 ensuring the URL wasn't altered.
109
+ * - "payload_hash" - the raw payload hashed with SHA256 ensuring the payload wasn't altered.
110
+ * - "jti" - a unique token ID to implement an optional non-replay check (NOT validated by default).
111
+ * - "nbf" - the not before timestamp.
112
+ * - "exp" - the expiration timestamp is ensuring that a request isn't captured and used at a later time.
113
+ * - "iss" - the issuer name, always MessageBird.
114
+ *
115
+ * @param string $signature the actual signature taken from request header "MessageBird-Signature-JWT".
116
+ * @param string $url the raw url including the protocol, hostname and query string, {@code https://example.com/?example=42}.
117
+ * @param string $body the raw request body.
118
+ * @return object JWT token payload
119
+ * @throws ValidationException if signature validation fails.
120
+ *
121
+ * @see https://developers.messagebird.com/docs/verify-http-requests
122
+ */
123
+ public function validateSignature (string $ signature , string $ url , string $ body )
124
+ {
125
+ if (empty ($ signature )) {
126
+ throw new ValidationException ("Signature cannot be empty. " );
127
+ }
128
+ if (!$ this ->skipURLValidation && empty ($ url )) {
129
+ throw new ValidationException ("URL cannot be empty " );
130
+ }
131
+
132
+ JWT ::$ leeway = 1 ;
133
+ try {
134
+ $ decoded = JWT ::decode ($ signature , $ this ->signingKey , self ::ALLOWED_ALGOS );
135
+ } catch (\InvalidArgumentException | \UnexpectedValueException | SignatureInvalidException $ e ) {
136
+ throw new ValidationException ($ e ->getMessage (), $ e ->getCode (), $ e );
137
+ }
138
+
139
+ if (empty ($ decoded ->iss ) || $ decoded ->iss !== 'MessageBird ' ) {
140
+ throw new ValidationException ('invalid jwt: claim iss has wrong value ' );
141
+ }
142
+
143
+ if (!$ this ->skipURLValidation && !hash_equals (hash (self ::HMAC_HASH_ALGO , $ url ), $ decoded ->url_hash )) {
144
+ throw new ValidationException ('invalid jwt: claim url_hash is invalid ' );
145
+ }
146
+
147
+ switch (true ) {
148
+ case empty ($ body ) && !empty ($ decoded ->payload_hash ):
149
+ throw new ValidationException ('invalid jwt: claim payload_hash is set but actual payload is missing ' );
150
+ case !empty ($ body ) && empty ($ decoded ->payload_hash ):
151
+ throw new ValidationException ('invalid jwt: claim payload_hash is not set but payload is present ' );
152
+ case !empty ($ body ) && !hash_equals (hash (self ::HMAC_HASH_ALGO , $ body ), $ decoded ->payload_hash ):
153
+ throw new ValidationException ('invalid jwt: claim payload_hash is invalid ' );
154
+ }
155
+
156
+ return $ decoded ;
157
+ }
158
+
159
+ /**
160
+ * Validate request signature from PHP globals.
161
+ *
162
+ * @return object JWT token payload
163
+ * @throws ValidationException if signature validation fails.
164
+ */
165
+ public function validateRequestFromGlobals ()
166
+ {
167
+ $ signature = $ _SERVER ['MessageBird-Signature-JWT ' ] ?? null ;
168
+ $ url = (isset ($ _SERVER ['HTTPS ' ]) && $ _SERVER ['HTTPS ' ] === 'on ' ? "https " : "http " ) . ":// $ _SERVER [HTTP_HOST ]$ _SERVER [REQUEST_URI ]" ;
169
+ $ body = file_get_contents ('php://input ' );
170
+
171
+ return $ this ->validateSignature ($ signature , $ url , $ body );
78
172
}
79
173
}
0 commit comments