22
33namespace MessageBird ;
44
5+ use Firebase \JWT \JWT ;
6+ use Firebase \JWT \SignatureInvalidException ;
7+ use MessageBird \Exceptions \ValidationException ;
58use MessageBird \Objects \SignedRequest ;
69
710/**
8- * Class RequestValidator
11+ * Class RequestValidator validates request signature signed by MessageBird services.
912 *
1013 * @package MessageBird
14+ * @see https://developers.messagebird.com/docs/verify-http-requests
1115 */
1216class RequestValidator
1317{
1418 const BODY_HASH_ALGO = 'sha256 ' ;
1519 const HMAC_HASH_ALGO = 'sha256 ' ;
20+ const ALLOWED_ALGOS = array ('HS256 ' , 'HS384 ' , 'HS512 ' );
1621
1722 /**
1823 * The key with which requests will be signed by MessageBird.
@@ -21,21 +26,36 @@ class RequestValidator
2126 */
2227 private $ signingKey ;
2328
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+
2441 /**
2542 * RequestValidator constructor.
2643 *
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.
2846 */
29- public function __construct ($ signingKey )
47+ public function __construct (string $ signingKey, bool $ skipURLValidation = false )
3048 {
3149 $ this ->signingKey = $ signingKey ;
50+ $ this ->skipURLValidation = $ skipURLValidation ;
3251 }
3352
3453 /**
3554 * Verify that the signed request was submitted from MessageBird using the known key.
3655 *
3756 * @param SignedRequest $request
3857 * @return bool
58+ * @deprecated Use {@link RequestValidator::validateSignature()} instead.
3959 */
4060 public function verify (SignedRequest $ request )
4161 {
@@ -47,6 +67,9 @@ public function verify(SignedRequest $request)
4767 return \hash_equals ($ expectedSignature , $ calculatedSignature );
4868 }
4969
70+ /**
71+ * @deprecated Use {@link RequestValidator::validateSignature()} instead.
72+ */
5073 private function buildPayloadFromRequest (SignedRequest $ request ): string
5174 {
5275 $ parts = [];
@@ -71,9 +94,80 @@ private function buildPayloadFromRequest(SignedRequest $request): string
7194 * @param SignedRequest $request The signed request object.
7295 * @param int $offset The maximum number of seconds that is allowed to consider the request recent
7396 * @return bool
97+ * @deprecated Use {@link RequestValidator::validateSignature()} instead.
7498 */
7599 public function isRecent (SignedRequest $ request , $ offset = 10 )
76100 {
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 );
78172 }
79173}
0 commit comments