Methods

Source

LineCode
1
<?php
2
3
namespace AuthStack\SAMLServiceProvider\Model;
4
5
use Illuminate\Http\Request;
6
use LightSaml\{
7
    Binding\BindingFactory,
8
    Helper,
9
    Model\XmlDSig\Signature,
10
    Model\XmlDSig\SignatureStringReader,
11
    Model\XmlDSig\SignatureXmlReader,
12
    SamlConstants,
13
    Model\Assertion\Issuer,
14
    Model\Protocol\AuthnRequest,
15
    Model\Protocol\NameIDPolicy,
16
    Model\Protocol\Response,
17
    Binding\AbstractBinding,
18
    Binding\HttpPostBinding,
19
    Binding\HttpRedirectBinding,
20
    Context\Profile\MessageContext
21
};
22
use AuthStack\SAMLServiceProvider\Exceptions\SAMLException;
23
24
25
class AuthN
26
{
27
    /**
28
     * @var ServiceProvider
29
     */
30
    protected $sp;
31
32
    /**
33
     * @var IdentityProvider
34
     */
35
    protected $idp;
36
37
    /**
38
     * @var AuthnRequest
39
     */
40
    protected $authn;
41
42
    /**
43
     * @var Response
44
     */
45
    protected $authn_response;
46
47
    /**
48
     * AuthN constructor.
49
     * @param bool $create
50
     */
51
    public function __construct(bool $create = false)
52
    {
53
        $this->sp = new ServiceProvider();
54
        $this->idp = new IdentityProvider($this->sp->getIdPEntityId());
55
56
        if($create)
57
        {
58
            $this->create();
59
        }
60
    }
61
62
    /**
63
     * @throws SAMLException
64
     */
65
    public function send()
66
    {
67
        switch($this->idp->getSSOBinding())
68
        {
69
            case SamlConstants::BINDING_SAML2_HTTP_POST:
70
                $this->sendWith(new HttpPostBinding());
71
                break;
72
73
            case SamlConstants::BINDING_SAML2_HTTP_REDIRECT:
74
                $this->sendWith(new HttpRedirectBinding());
75
                break;
76
77
            default:
78
                throw new SAMLException(sprintf("Unsupported binding: ", $this->idp->getSSOBinding()), 500);
79
                break;
80
        }
81
    }
82
83
    /**
84
     * @param Request|null $request
85
     * @return $this
86
     */
87
    public function receive(Request $request = null)
88
    {
89
        $request = $request ?? Request::createFromGlobals();
90
91
        $factory = new BindingFactory();
92
        $context = new MessageContext();
93
94
        $binding = $factory->getBindingByRequest($request);
95
96
        $binding->receive($request, $context);
97
98
        $this->authn_response = $context->getMessage();
99
100
        return $this;
101
    }
102
103
    /**
104
     * @return AuthnRequest
105
     */
106
    public function getAuthnRequest(): AuthnRequest
107
    {
108
        return $this->authn;
109
    }
110
111
    /**
112
     * @return AuthNResponse
113
     */
114
    public function getAuthnResponse(): AuthNResponse
115
    {
116
        $response = $this->getLightSamlAuthnResponse();
117
118
        return new AuthNResponse($response, $this->sp);
119
    }
120
121
    /**
122
     * @return Response
123
     */
124
    public function getLightSamlAuthnResponse(): Response
125
    {
126
        return $this->authn_response;
127
    }
128
129
    /**
130
     * @throws SAMLException
131
     * @return bool
132
     */
133
    public function validateAuthnResponse(): bool
134
    {
135
        $response = $this->getLightSamlAuthnResponse();
136
137
        //--------------------------------------------------------------------------------------------------------------
138
        // Validate IdP's signature. This is the most important of all security aspects.
139
        //--------------------------------------------------------------------------------------------------------------
140
        if($this->sp->validateSignature())
141
        {
142
            /** @var  $signature SignatureXmlReader|SignatureStringReader */
143
            $signature = $response->getSignature();
144
            $pubkey = $this->idp->getSigningPublicKey();
145
146
            try
147
            {
148
                $signature->validate($pubkey);
149
            }
150
            catch(\Exception $e)
151
            {
152
                throw new SAMLException($e->getMessage(), 400);
153
            }
154
        }
155
156
        //--------------------------------------------------------------------------------------------------------------
157
        // Validate status. AuthN status should be marked as successful
158
        //--------------------------------------------------------------------------------------------------------------
159
        if($this->sp->validateStatus())
160
        {
161
            if(!$response->getStatus()->isSuccess())
162
            {
163
                throw new SAMLException(
164
                    sprintf("Invalid status. Message: %s", $response->getStatus()->getStatusMessage()),
165
                    400
166
                );
167
            }
168
        }
169
170
        //--------------------------------------------------------------------------------------------------------------
171
        // Check the issuer. EntityId in the response MUST be equal to EntityId in IdentityProvider::class
172
        //--------------------------------------------------------------------------------------------------------------
173
        if($this->sp->validateIssuer())
174
        {
175
            if(!(0 === strcmp($response->getIssuer()->getValue(), $this->idp->getEntityID())))
176
            {
177
                throw new SAMLException(
178
                    sprintf("Invalid issuer. Expected %s, received: %s", $this->idp->getEntityID(), $response->getIssuer()->getValue()),
179
                    400
180
                );
181
            }
182
        }
183
184
        //--------------------------------------------------------------------------------------------------------------
185
        // Check whether the destination matches
186
        //--------------------------------------------------------------------------------------------------------------
187
        if($this->sp->validateDestination())
188
        {
189
            if(!(0 === strcmp($response->getDestination(), $this->sp->getACSUrl())))
190
            {
191
                throw new SAMLException(
192
                    sprintf("Invalid destination. Expected %s, received: %s", $this->sp->getACSUrl(), $response->getDestination()),
193
                    400
194
                );
195
            }
196
        }
197
198
        //--------------------------------------------------------------------------------------------------------------
199
        // Check for the issued at time and compare it to allowed boundaries
200
        //--------------------------------------------------------------------------------------------------------------
201
        if($this->sp->validateLifetime())
202
        {
203
            $iat = $response->getIssueInstantDateTime()->setTimezone(new \DateTimeZone('UTC'));
204
            $upper = $this->sp->getUpperDT();
205
            $lower = $this->sp->getLowerDT();
206
207
            if($iat > $upper)
208
            {
209
                throw new SAMLException(
210
                    sprintf("Assertion valid in the future. Check the time synchronization between IdP and SP. Issued at: %s, checked against: %s",
211
                        $iat->format('c'), $upper->format('c')),
212
                    400);
213
            }
214
215
            if($iat < $lower)
216
            {
217
                throw new SAMLException("Assertion expired. Issued at: %s, now: %s", $iat->format('c'), $lower->format('c'));
218
            }
219
        }
220
221
        return true;
222
    }
223
224
    /**
225
     * @return SignatureXmlReader|SignatureStringReader
226
     * @throws SAMLException
227
     */
228
    protected function getAuthnResponseSignature(): Signature
229
    {
230
        $signature = $this->getAuthnResponse()->getSignature();
231
232
        if(!($signature instanceof Signature))
233
        {
234
            throw new SAMLException("AuthN Response signature required by configuration but not present in the response", 400);
235
        }
236
237
        return $signature;
238
    }
239
240
    /**
241
     * @return AuthN
242
     */
243
    protected function create(): self
244
    {
245
        $authn = new AuthnRequest();
246
247
        $id = Helper::generateID();
248
249
        $authn
250
            ->setNameIDPolicy(new NameIDPolicy($this->sp->getNameIdFormat()))
251
            ->setAssertionConsumerServiceURL($this->sp->getACSUrl())
252
            ->setProtocolBinding($this->sp->getACSBinding())
253
            ->setID($id)
254
            ->setIssueInstant(new \DateTime("now", new \DateTimeZone("UTC")))
255
            ->setDestination($this->idp->getSSOUrl())
256
            ->setIssuer(new Issuer($this->sp->getEntityId()))
257
            ;
258
259
        if($this->idp->wantsSignedReguests())
260
        {
261
            $authn->setSignature($this->sp->getSignatureWriter());
262
        }
263
264
        $this->authn = $authn;
265
266
        return $this;
267
    }
268
269
    /**
270
     * @param AbstractBinding $binding
271
     */
272
    protected  function sendWith(AbstractBinding $binding)
273
    {
274
        $request = $this->getAuthnRequest();
275
276
        $context = new MessageContext();
277
278
        $context->setMessage($request);
279
280
        $binding->send($context)->send();
281
    }
282
}