<?php

/*
 * This file is part of the AuthStack project.
 *
 * (c) Buckhill Ltd <development@buckhill.co.uk>
 *
 * This source file is subject to the Buckhill license that is bundled
 * with this source code in the file LICENSE.
 */

namespace AuthStack\SP\Validators;


use AuthStack\SP\Errors\ExpiredAssertionException;
use LightSaml\Credential\KeyHelper;
use LightSaml\Model\Protocol\Response;
use LightSaml\Credential\X509Certificate;
use LightSaml\Error\LightSamlSecurityException;
use AuthStack\SP\Errors\InvalidStatusException;
use AuthStack\SP\Errors\InvalidIssuerException;
use LightSaml\Model\XmlDSig\SignatureXmlReader;
use LightSaml\Model\XmlDSig\SignatureStringReader;
use AuthStack\SP\Errors\InvalidDestinationException;

/**
 * Class SAMLResponse
 * @package AuthStack\SP\Validators
 */
class SAMLResponse
{
    protected $idp;

    public function __construct($idp)
    {
        $this->idp = sha1($idp);
    }


    /**
     * @param Response $response
     *
     * @return $this
     * @throws InvalidDestinationException
     * @throws InvalidIssuerException
     * @throws InvalidStatusException
     * @throws LightSamlSecurityException
     *
     */
    public function validate(Response $response)
    {
        return $this
            ->validateStatus($response)
            ->validateIssuer($response)
            ->validateDestination($response)
            ->validateSignature($response)
            ->validateIssuedAtTime($response)
        ;
    }


    /**
     * @param Response $response
     *
     * @return $this
     * @throws InvalidStatusException
     */
    public function validateStatus(Response $response)
    {
        if($this->needsValidating('status'))
        {
            $status = $response->getStatus();

            if(!$status->isSuccess())
            {
                throw new InvalidStatusException(sprintf("Invalid status. Message: %s. ", $status->getStatusMessage()));
            }
        }

        return $this;
    }


    /**
     * @param Response $response
     *
     * @return $this
     * @throws InvalidIssuerException
     */
    public function validateIssuer(Response $response)
    {
        if($this->needsValidating('issuer'))
        {
            $got = $response->getIssuer()->getValue();
            $expected = \Buckhill\config(sprintf('AuthStack.IdP.%s.entityId', $this->idp));

            $result = strcmp($got, $expected) === 0;

            if(false === $result)
            {
                throw new InvalidIssuerException(sprintf("Invalid SAMLResponse issuer. Got %s, expected %s", $got, $expected));
            }
        }

        return $this;
    }


    /**
     * @param Response $response
     *
     * @return $this
     * @throws InvalidDestinationException
     */
    public function validateDestination(Response $response)
    {

        if($this->needsValidating('destination'))
        {
            $destination = $response->getDestination();
            $expected = sprintf("%s%s", \Buckhill\config('AuthStack.baseUrl'), \Buckhill\config('AuthStack.SP.AssertionConsumerService.location'));

            $result = strcmp($destination, $expected) === 0;

            if(false === $result)
            {
                throw new InvalidDestinationException(sprintf("Invalid destination in response. Expected %s, got: %s", $expected, $destination));
            }
        }

        return $this;
    }

    /**
     * @param Response $response
     *
     * @return $this
     * @throws LightSamlSecurityException
     */
    public function validateSignature(Response $response)
    {
        if($this->needsValidating('signature'))
        {
            $signature = $this->getSignatureFromResponse($response);
            $key = $this->getIdPPublicKey();

            $signature->validate($key);
        }

        return $this;
    }

    /**
     * @param Response $response
     * @return $this
     * @throws ExpiredAssertionException
     */
    protected function validateIssuedAtTime(Response $response)
    {
        if($this->needsValidating('lifetime'))
        {
            $timezone = new \DateTimeZone("UTC");

            $issued_at  = $response->getIssueInstantDateTime()->setTimezone($timezone);
            $upper      = new \DateTime(\Buckhill\config('AuthStack.SP.validate.upper', '+1 minute'), $timezone);
            $lower      = new \DateTime(\Buckhill\config('AuthStack.SP.validate.lower', '-1 minute'), $timezone);

            if($issued_at > $upper)
            {
                throw new ExpiredAssertionException(sprintf("Assertion valid in the future, check time synchronization between IdP and SP. Issued at: %s, checked against: %s",
                    $issued_at->format('c'),
                    $upper->format('c')
                ));
            }

            if($issued_at < $lower)
            {
                throw new ExpiredAssertionException(sprintf("Expired assertion. Issued at: %s, now: %s", $issued_at->format('c'), $lower->format('c')));
            }
        }

        return $this;
    }


    /**
     * @param Response $response
     *
     * @return SignatureXmlReader|SignatureStringReader
     */
    protected function getSignatureFromResponse(Response $response)
    {
        $signature = $response->getSignature();

        if(!($signature instanceof SignatureXmlReader) && !($signature instanceof SignatureStringReader))
        {
            throw new \LogicException("Unable to get signature from the SAML response - (Signature must be an instance of SignatureXmlReader or SignatureStringReader)");
        }

        return $signature;
    }


    /**
     * @return \RobRichards\XMLSecLibs\XMLSecurityKey
     */
    protected function getIdPPublicKey()
    {
        $key = KeyHelper::createPublicKey(
            (new X509Certificate)->setData(\Buckhill\config(sprintf('AuthStack.IdP.%s.x509', $this->idp)))
        );

        return $key;
    }

    protected function needsValidating($validationKey)
    {
        $idpSpecificValidate = \Buckhill\config(sprintf('AuthStack.IdP.%s.validate', $this->idp));

        if(!empty($idpSpecificValidate) && is_array($idpSpecificValidate) && array_key_exists($validationKey, $idpSpecificValidate))
        {
            return $idpSpecificValidate[$validationKey];
        }

        return \Buckhill\config(sprintf('AuthStack.SP.validate.%s', $validationKey), false);
    }
}
