1<?php
2
3/**
4 * Main class of OneLogin's PHP Toolkit
5 *
6 */
7class OneLogin_Saml2_Auth
8{
9    /**
10     * Settings data.
11     *
12     * @var OneLogin_Saml2_Settings
13     */
14    private $_settings;
15
16    /**
17     * User attributes data.
18     *
19     * @var array
20     */
21    private $_attributes = array();
22
23    /**
24     * User attributes data with FriendlyName index.
25     *
26     * @var array
27     */
28    private $_attributesWithFriendlyName = array();
29
30    /**
31     * NameID
32     *
33     * @var string
34     */
35    private $_nameid;
36
37    /**
38     * NameID Format
39     *
40     * @var string
41     */
42    private $_nameidFormat;
43
44
45    /**
46     * NameID NameQualifier
47     *
48     * @var string
49     */
50    private $_nameidNameQualifier;
51
52    /**
53     * NameID SP NameQualifier
54     *
55     * @var string
56     */
57    private $_nameidSPNameQualifier;
58
59    /**
60     * If user is authenticated.
61     *
62     * @var bool
63     */
64    private $_authenticated = false;
65
66
67    /**
68     * SessionIndex. When the user is logged, this stored it
69     * from the AuthnStatement of the SAML Response
70     *
71     * @var string
72     */
73    private $_sessionIndex;
74
75    /**
76     * SessionNotOnOrAfter. When the user is logged, this stored it
77     * from the AuthnStatement of the SAML Response
78     *
79     * @var int|null
80     */
81    private $_sessionExpiration;
82
83    /**
84     * The ID of the last message processed
85     *
86     * @var string
87     */
88    private $_lastMessageId;
89
90    /**
91     * The ID of the last assertion processed
92     *
93     * @var string
94     */
95    private $_lastAssertionId;
96
97    /**
98     * The NotOnOrAfter value of the valid SubjectConfirmationData
99     * node (if any) of the last assertion processed
100     *
101     * @var int
102     */
103    private $_lastAssertionNotOnOrAfter;
104
105    /**
106     * If any error.
107     *
108     * @var array
109     */
110    private $_errors = array();
111
112    /**
113     * Reason of the last error.
114     *
115     * @var string|null
116     */
117    private $_errorReason;
118
119    /**
120     * Last AuthNRequest ID or LogoutRequest ID generated by this Service Provider
121     *
122     * @var string
123     */
124    private $_lastRequestID;
125
126    /**
127     * The most recently-constructed/processed XML SAML request
128     * (AuthNRequest, LogoutRequest)
129     *
130     * @var string
131     */
132    private $_lastRequest;
133
134    /**
135     * The most recently-constructed/processed XML SAML response
136     * (SAMLResponse, LogoutResponse). If the SAMLResponse was
137     * encrypted, by default tries to return the decrypted XML
138     *
139     * @var string|\DomDocument|null
140     */
141    private $_lastResponse;
142
143    /**
144     * Initializes the SP SAML instance.
145     *
146     * @param array|object|null $oldSettings Setting data (You can provide a OneLogin_Saml_Settings, the settings object of the Saml folder implementation)
147     *
148     * @throws OneLogin_Saml2_Error
149     */
150    public function __construct($oldSettings = null)
151    {
152        $this->_settings = new OneLogin_Saml2_Settings($oldSettings);
153    }
154
155    /**
156     * Returns the settings info
157     *
158     * @return OneLogin_Saml2_Settings The settings data.
159     */
160    public function getSettings()
161    {
162        return $this->_settings;
163    }
164
165    /**
166     * Set the strict mode active/disable
167     *
168     * @param bool $value Strict parameter
169     *
170     * @throws OneLogin_Saml2_Error
171     */
172    public function setStrict($value)
173    {
174        if (!is_bool($value)) {
175            throw new OneLogin_Saml2_Error(
176                'Invalid value passed to setStrict()',
177                OneLogin_Saml2_Error::SETTINGS_INVALID_SYNTAX
178            );
179        }
180
181        $this->_settings->setStrict($value);
182    }
183
184    /**
185     * Set schemas path
186     *
187     * @param string $path
188     * @return $this
189     */
190    public function setSchemasPath($path)
191    {
192        $this->_paths['schemas'] = $path;
193    }
194
195    /**
196     * Process the SAML Response sent by the IdP.
197     *
198     * @param string|null $requestId The ID of the AuthNRequest sent by this SP to the IdP
199     *
200     * @throws OneLogin_Saml2_Error
201     * @throws OneLogin_Saml2_ValidationError
202     */
203    public function processResponse($requestId = null)
204    {
205        $this->_errors = array();
206        $this->_errorReason = null;
207        if (isset($_POST['SAMLResponse'])) {
208            // AuthnResponse -- HTTP_POST Binding
209            $response = new OneLogin_Saml2_Response($this->_settings, $_POST['SAMLResponse']);
210            $this->_lastResponse = $response->getXMLDocument();
211
212            if ($response->isValid($requestId)) {
213                $this->_attributes = $response->getAttributes();
214                $this->_attributesWithFriendlyName = $response->getAttributesWithFriendlyName();
215                $this->_nameid = $response->getNameId();
216                $this->_nameidFormat = $response->getNameIdFormat();
217                $this->_nameidNameQualifier = $response->getNameIdNameQualifier();
218                $this->_nameidSPNameQualifier = $response->getNameIdSPNameQualifier();
219                $this->_authenticated = true;
220                $this->_sessionIndex = $response->getSessionIndex();
221                $this->_sessionExpiration = $response->getSessionNotOnOrAfter();
222                $this->_lastMessageId = $response->getId();
223                $this->_lastAssertionId = $response->getAssertionId();
224                $this->_lastAssertionNotOnOrAfter = $response->getAssertionNotOnOrAfter();
225            } else {
226                $this->_errors[] = 'invalid_response';
227                $this->_errorReason = $response->getError();
228            }
229        } else {
230            $this->_errors[] = 'invalid_binding';
231            throw new OneLogin_Saml2_Error(
232                'SAML Response not found, Only supported HTTP_POST Binding',
233                OneLogin_Saml2_Error::SAML_RESPONSE_NOT_FOUND
234            );
235        }
236    }
237
238    /**
239     * Process the SAML Logout Response / Logout Request sent by the IdP.
240     *
241     * @param bool        $keepLocalSession             When false will destroy the local session, otherwise will keep it
242     * @param string|null $requestId                    The ID of the LogoutRequest sent by this SP to the IdP
243     * @param bool        $retrieveParametersFromServer True if we want to use parameters from $_SERVER to validate the signature
244     * @param callable    $cbDeleteSession              Callback to be executed to delete session
245     * @param bool        $stay                         True if we want to stay (returns the url string) False to redirect
246     *
247     * @return string|null
248     *
249     * @throws OneLogin_Saml2_Error
250     */
251    public function processSLO($keepLocalSession = false, $requestId = null, $retrieveParametersFromServer = false, $cbDeleteSession = null, $stay = false)
252    {
253        $this->_errors = array();
254        $this->_errorReason = null;
255        if (isset($_GET['SAMLResponse'])) {
256            $logoutResponse = new OneLogin_Saml2_LogoutResponse($this->_settings, $_GET['SAMLResponse']);
257            $this->_lastResponse = $logoutResponse->getXML();
258            if (!$logoutResponse->isValid($requestId, $retrieveParametersFromServer)) {
259                $this->_errors[] = 'invalid_logout_response';
260                $this->_errorReason = $logoutResponse->getError();
261            } else if ($logoutResponse->getStatus() !== OneLogin_Saml2_Constants::STATUS_SUCCESS) {
262                $this->_errors[] = 'logout_not_success';
263            } else {
264                $this->_lastMessageId = $logoutResponse->id;
265                if (!$keepLocalSession) {
266                    if ($cbDeleteSession === null) {
267                        OneLogin_Saml2_Utils::deleteLocalSession();
268                    } else {
269                        call_user_func($cbDeleteSession);
270                    }
271                }
272            }
273        } else if (isset($_GET['SAMLRequest'])) {
274            $logoutRequest = new OneLogin_Saml2_LogoutRequest($this->_settings, $_GET['SAMLRequest']);
275            $this->_lastRequest = $logoutRequest->getXML();
276            if (!$logoutRequest->isValid($retrieveParametersFromServer)) {
277                $this->_errors[] = 'invalid_logout_request';
278                $this->_errorReason = $logoutRequest->getError();
279            } else {
280                if (!$keepLocalSession) {
281                    if ($cbDeleteSession === null) {
282                        OneLogin_Saml2_Utils::deleteLocalSession();
283                    } else {
284                        call_user_func($cbDeleteSession);
285                    }
286                }
287                $inResponseTo = $logoutRequest->id;
288                $this->_lastMessageId = $logoutRequest->id;
289                $responseBuilder = new OneLogin_Saml2_LogoutResponse($this->_settings);
290                $responseBuilder->build($inResponseTo);
291                $this->_lastResponse = $responseBuilder->getXML();
292
293                $logoutResponse = $responseBuilder->getResponse();
294
295                $parameters = array('SAMLResponse' => $logoutResponse);
296                if (isset($_GET['RelayState'])) {
297                    $parameters['RelayState'] = $_GET['RelayState'];
298                }
299
300                $security = $this->_settings->getSecurityData();
301                if (isset($security['logoutResponseSigned']) && $security['logoutResponseSigned']) {
302                    $signature = $this->buildResponseSignature($logoutResponse, isset($parameters['RelayState'])? $parameters['RelayState']: null, $security['signatureAlgorithm']);
303                    $parameters['SigAlg'] = $security['signatureAlgorithm'];
304                    $parameters['Signature'] = $signature;
305                }
306
307                return $this->redirectTo($this->getSLOResponseUrl(), $parameters, $stay);
308            }
309        } else {
310            $this->_errors[] = 'invalid_binding';
311            throw new OneLogin_Saml2_Error(
312                'SAML LogoutRequest/LogoutResponse not found. Only supported HTTP_REDIRECT Binding',
313                OneLogin_Saml2_Error::SAML_LOGOUTMESSAGE_NOT_FOUND
314            );
315        }
316    }
317
318    /**
319     * Redirects the user to the url past by parameter
320     * or to the url that we defined in our SSO Request.
321     *
322     * @param string $url The target URL to redirect the user.
323     * @param array $parameters Extra parameters to be passed as part of the url
324     * @param bool $stay True if we want to stay (returns the url string) False to redirect
325     *
326     * @return string|null
327     *
328     * @throws OneLogin_Saml2_Error
329     */
330    public function redirectTo($url = '', $parameters = array(), $stay = false)
331    {
332        assert('is_string($url)');
333        assert('is_array($parameters)');
334
335        if (empty($url) && isset($_REQUEST['RelayState'])) {
336            $url = $_REQUEST['RelayState'];
337        }
338
339        return OneLogin_Saml2_Utils::redirect($url, $parameters, $stay);
340    }
341
342    /**
343     * Checks if the user is authenticated or not.
344     *
345     * @return bool  True if the user is authenticated
346     */
347    public function isAuthenticated()
348    {
349        return $this->_authenticated;
350    }
351
352    /**
353     * Returns the set of SAML attributes.
354     *
355     * @return array  Attributes of the user.
356     */
357    public function getAttributes()
358    {
359        return $this->_attributes;
360    }
361
362    /**
363     * Returns the set of SAML attributes indexed by FriendlyName
364     *
365     * @return array  Attributes of the user.
366     */
367    public function getAttributesWithFriendlyName()
368    {
369        return $this->_attributesWithFriendlyName;
370    }
371
372    /**
373     * Returns the nameID
374     *
375     * @return string  The nameID of the assertion
376     */
377    public function getNameId()
378    {
379        return $this->_nameid;
380    }
381
382    /**
383     * Returns the nameID Format
384     *
385     * @return string  The nameID Format of the assertion
386     */
387    public function getNameIdFormat()
388    {
389        return $this->_nameidFormat;
390    }
391
392    /**
393     * Returns the nameID NameQualifier
394     *
395     * @return string  The nameID NameQualifier of the assertion
396     */
397    public function getNameIdNameQualifier()
398    {
399        return $this->_nameidNameQualifier;
400    }
401
402    /**
403     * Returns the nameID SP NameQualifier
404     *
405     * @return string  The nameID SP NameQualifier of the assertion
406     */
407    public function getNameIdSPNameQualifier()
408    {
409        return $this->_nameidSPNameQualifier;
410    }
411
412    /**
413     * Returns the SessionIndex
414     *
415     * @return string|null  The SessionIndex of the assertion
416     */
417    public function getSessionIndex()
418    {
419        return $this->_sessionIndex;
420    }
421
422    /**
423     * Returns the SessionNotOnOrAfter
424     *
425     * @return int|null  The SessionNotOnOrAfter of the assertion
426     */
427    public function getSessionExpiration()
428    {
429        return $this->_sessionExpiration;
430    }
431
432    /**
433     * Returns if there were any error
434     *
435     * @return array  Errors
436     */
437    public function getErrors()
438    {
439        return $this->_errors;
440    }
441
442    /**
443     * Returns the reason for the last error
444     *
445     * @return string|null Error reason
446     */
447    public function getLastErrorReason()
448    {
449        return $this->_errorReason;
450    }
451
452    /**
453     * Returns the requested SAML attribute
454     *
455     * @param string $name The requested attribute of the user.
456     *
457     * @return array|null Requested SAML attribute ($name).
458     */
459    public function getAttribute($name)
460    {
461        assert('is_string($name)');
462
463        $value = null;
464        if (isset($this->_attributes[$name])) {
465            return $this->_attributes[$name];
466        }
467        return $value;
468    }
469
470    /**
471     * Returns the requested SAML attribute indexed by FriendlyName
472     *
473     * @param string $friendlyName The requested attribute of the user.
474     *
475     * @return array|null Requested SAML attribute ($friendlyName).
476     */
477    public function getAttributeWithFriendlyName($friendlyName)
478    {
479        assert('is_string($friendlyName)');
480
481        $value = null;
482        if (isset($this->_attributesWithFriendlyName[$friendlyName])) {
483            return $this->_attributesWithFriendlyName[$friendlyName];
484        }
485        return $value;
486    }
487
488    /**
489     * Initiates the SSO process.
490     *
491     * @param string|null $returnTo The target URL the user should be returned to after login.
492     * @param array $parameters Extra parameters to be added to the GET
493     * @param bool $forceAuthn When true the AuthNRequest will set the ForceAuthn='true'
494     * @param bool $isPassive When true the AuthNRequest will set the Ispassive='true'
495     * @param bool $stay True if we want to stay (returns the url string) False to redirect
496     * @param bool $setNameIdPolicy When true the AuthNRueqest will set a nameIdPolicy element
497     * @param string $nameIdValueReq Indicates to the IdP the subject that should be authenticated
498     *
499     * @return string|null If $stay is True, it return a string with the SLO URL + LogoutRequest + parameters
500     *
501     * @throws OneLogin_Saml2_Error
502     */
503    public function login($returnTo = null, $parameters = array(), $forceAuthn = false, $isPassive = false, $stay = false, $setNameIdPolicy = true, $nameIdValueReq = null)
504    {
505        assert('is_array($parameters)');
506
507        $authnRequest = new OneLogin_Saml2_AuthnRequest($this->_settings, $forceAuthn, $isPassive, $setNameIdPolicy, $nameIdValueReq);
508
509        $this->_lastRequest = $authnRequest->getXML();
510        $this->_lastRequestID = $authnRequest->getId();
511
512        $samlRequest = $authnRequest->getRequest();
513        $parameters['SAMLRequest'] = $samlRequest;
514
515        if (!empty($returnTo)) {
516            $parameters['RelayState'] = $returnTo;
517        } else {
518            $parameters['RelayState'] = OneLogin_Saml2_Utils::getSelfRoutedURLNoQuery();
519        }
520
521        $security = $this->_settings->getSecurityData();
522        if (isset($security['authnRequestsSigned']) && $security['authnRequestsSigned']) {
523            $signature = $this->buildRequestSignature($samlRequest, $parameters['RelayState'], $security['signatureAlgorithm']);
524            $parameters['SigAlg'] = $security['signatureAlgorithm'];
525            $parameters['Signature'] = $signature;
526        }
527        return $this->redirectTo($this->getSSOurl(), $parameters, $stay);
528    }
529
530    /**
531     * Initiates the SLO process.
532     *
533     * @param string|null $returnTo            The target URL the user should be returned to after logout.
534     * @param array       $parameters          Extra parameters to be added to the GET
535     * @param string|null $nameId              The NameID that will be set in the LogoutRequest.
536     * @param string|null $sessionIndex        The SessionIndex (taken from the SAML Response in the SSO process).
537     * @param bool        $stay                True if we want to stay (returns the url string) False to redirect
538     * @param string|null $nameIdFormat        The NameID Format will be set in the LogoutRequest.
539     * @param string|null $nameIdNameQualifier The NameID NameQualifier will be set in the LogoutRequest.
540     *
541     * @return string|null If $stay is True, it return a string with the SLO URL + LogoutRequest + parameters
542     *
543     * @throws OneLogin_Saml2_Error
544     */
545    public function logout($returnTo = null, $parameters = array(), $nameId = null, $sessionIndex = null, $stay = false, $nameIdFormat = null, $nameIdNameQualifier = null, $nameIdSPNameQualifier = null)
546    {
547        assert('is_array($parameters)');
548
549        $sloUrl = $this->getSLOurl();
550        if (empty($sloUrl)) {
551            throw new OneLogin_Saml2_Error(
552                'The IdP does not support Single Log Out',
553                OneLogin_Saml2_Error::SAML_SINGLE_LOGOUT_NOT_SUPPORTED
554            );
555        }
556
557        if (empty($nameId) && !empty($this->_nameid)) {
558            $nameId = $this->_nameid;
559        }
560        if (empty($nameIdFormat) && !empty($this->_nameidFormat)) {
561            $nameIdFormat = $this->_nameidFormat;
562        }
563
564        $logoutRequest = new OneLogin_Saml2_LogoutRequest($this->_settings, null, $nameId, $sessionIndex, $nameIdFormat, $nameIdNameQualifier, $nameIdSPNameQualifier);
565
566        $this->_lastRequest = $logoutRequest->getXML();
567        $this->_lastRequestID = $logoutRequest->id;
568
569        $samlRequest = $logoutRequest->getRequest();
570
571        $parameters['SAMLRequest'] = $samlRequest;
572        if (!empty($returnTo)) {
573            $parameters['RelayState'] = $returnTo;
574        } else {
575            $parameters['RelayState'] = OneLogin_Saml2_Utils::getSelfRoutedURLNoQuery();
576        }
577
578        $security = $this->_settings->getSecurityData();
579        if (isset($security['logoutRequestSigned']) && $security['logoutRequestSigned']) {
580            $signature = $this->buildRequestSignature($samlRequest, $parameters['RelayState'], $security['signatureAlgorithm']);
581            $parameters['SigAlg'] = $security['signatureAlgorithm'];
582            $parameters['Signature'] = $signature;
583        }
584
585        return $this->redirectTo($sloUrl, $parameters, $stay);
586    }
587
588    /**
589     * Gets the SSO url.
590     *
591     * @return string The url of the Single Sign On Service
592     */
593    public function getSSOurl()
594    {
595        $idpData = $this->_settings->getIdPData();
596        return $idpData['singleSignOnService']['url'];
597    }
598
599    /**
600     * Gets the SLO url.
601     *
602     * @return string|null The url of the Single Logout Service
603     */
604    public function getSLOurl()
605    {
606        $url = null;
607        $idpData = $this->_settings->getIdPData();
608        if (isset($idpData['singleLogoutService']) && isset($idpData['singleLogoutService']['url'])) {
609            $url = $idpData['singleLogoutService']['url'];
610        }
611        return $url;
612    }
613
614    /**
615     * Gets the SLO response url.
616     *
617     * @return string|null The response url of the Single Logout Service
618     */
619    public function getSLOResponseUrl()
620    {
621        $idpData = $this->_settings->getIdPData();
622        if (isset($idpData['singleLogoutService']) && isset($idpData['singleLogoutService']['responseUrl'])) {
623            return $idpData['singleLogoutService']['responseUrl'];
624        }
625        return $this->getSLOurl();
626    }
627
628    /**
629     * Gets the ID of the last AuthNRequest or LogoutRequest generated by the Service Provider.
630     *
631     * @return string The ID of the Request SAML message.
632     */
633    public function getLastRequestID()
634    {
635        return $this->_lastRequestID;
636    }
637
638    /**
639     * Generates the Signature for a SAML Request
640     *
641     * @param string $samlRequest    The SAML Request
642     * @param string $relayState     The RelayState
643     * @param string $signAlgorithm Signature algorithm method
644     *
645     * @return string A base64 encoded signature
646     *
647     * @throws OneLogin_Saml2_Error
648     */
649    public function buildRequestSignature($samlRequest, $relayState, $signAlgorithm = XMLSecurityKey::RSA_SHA1)
650    {
651        return $this->buildMessageSignature($samlRequest, $relayState, $signAlgorithm, "SAMLRequest");
652    }
653
654    /**
655     * Generates the Signature for a SAML Response
656     *
657     * @param string $samlResponse  The SAML Response
658     * @param string $relayState    The RelayState
659     * @param string $signAlgorithm Signature algorithm method
660     *
661     * @return string A base64 encoded signature
662     *
663     * @throws OneLogin_Saml2_Error
664     */
665    public function buildResponseSignature($samlResponse, $relayState, $signAlgorithm = XMLSecurityKey::RSA_SHA1)
666    {
667        return $this->buildMessageSignature($samlResponse, $relayState, $signAlgorithm, "SAMLResponse");
668    }
669
670    /**
671     * Generates the Signature for a SAML Response
672     *
673     * @param string $samlMessage   The SAML Response
674     * @param string $relayState    The RelayState
675     * @param string $signAlgorithm Signature algorithm method
676     * @param string $type          "SAMLRequest" or "SAMLResponse"
677     *
678     * @return string A base64 encoded signature
679     *
680     * @throws OneLogin_Saml2_Error
681     */
682    private function buildMessageSignature($samlMessage, $relayState, $signAlgorithm = XMLSecurityKey::RSA_SHA256, $type = "SAMLRequest")
683    {
684        $key = $this->_settings->getSPkey();
685        if (empty($key)) {
686            if ($type == "SAMLRequest") {
687                $errorMsg = "Trying to sign the SAML Request but can't load the SP private key";
688            } else {
689                $errorMsg = "Trying to sign the SAML Response but can't load the SP private key";
690            }
691
692            throw new OneLogin_Saml2_Error($errorMsg, OneLogin_Saml2_Error::PRIVATE_KEY_NOT_FOUND);
693        }
694
695        $objKey = new XMLSecurityKey($signAlgorithm, array('type' => 'private'));
696        $objKey->loadKey($key, false);
697
698        $security = $this->_settings->getSecurityData();
699        if ($security['lowercaseUrlencoding']) {
700            $msg = $type.'='.rawurlencode($samlMessage);
701            if (isset($relayState)) {
702                $msg .= '&RelayState='.rawurlencode($relayState);
703            }
704            $msg .= '&SigAlg=' . rawurlencode($signAlgorithm);
705        } else {
706            $msg = $type.'='.urlencode($samlMessage);
707            if (isset($relayState)) {
708                $msg .= '&RelayState='.urlencode($relayState);
709            }
710            $msg .= '&SigAlg=' . urlencode($signAlgorithm);
711        }
712        $signature = $objKey->signData($msg);
713        return base64_encode($signature);
714    }
715
716    /**
717     * @return string The ID of the last message processed
718     */
719    public function getLastMessageId()
720    {
721        return $this->_lastMessageId;
722    }
723
724    /**
725     * @return string The ID of the last assertion processed
726     */
727    public function getLastAssertionId()
728    {
729        return $this->_lastAssertionId;
730    }
731
732    /**
733     * @return int The NotOnOrAfter value of the valid
734     *         SubjectConfirmationData node (if any)
735     *         of the last assertion processed
736     */
737    public function getLastAssertionNotOnOrAfter()
738    {
739        return $this->_lastAssertionNotOnOrAfter;
740    }
741
742    /**
743     * Returns the most recently-constructed/processed
744     * XML SAML request (AuthNRequest, LogoutRequest)
745     *
746     * @return string The Request XML
747     */
748    public function getLastRequestXML()
749    {
750        return $this->_lastRequest;
751    }
752
753    /**
754     * Returns the most recently-constructed/processed
755     * XML SAML response (SAMLResponse, LogoutResponse).
756     * If the SAMLResponse was encrypted, by default tries
757     * to return the decrypted XML.
758     *
759     * @return string|null The Response XML
760     */
761    public function getLastResponseXML()
762    {
763        $response = null;
764        if (isset($this->_lastResponse)) {
765            if (is_string($this->_lastResponse)) {
766                $response = $this->_lastResponse;
767            } else {
768                $response = $this->_lastResponse->saveXML();
769            }
770        }
771
772        return $response;
773    }
774}
775