aerialship / SamlSPBundle

SAML 2.0 Symfony SP Bundle - new version available at
http://www.lightsaml.com/SP-Bundle/
MIT License
63 stars 43 forks source link

BUG: Request not set #45

Closed INSEAD-asim closed 8 years ago

INSEAD-asim commented 10 years ago

First of all, Big Thanks for such a great effort.

I am facing an issue while logging out. It shows following stack trace

[1] RuntimeException: Request not set
at n/a
    in D:\www\Symfony2-ADFS\vendor\aerialship\saml-sp-bundle\src\AerialShip\SamlSPBundle\Config\SpEntityDescriptorBuilder.php line 155

at AerialShip\SamlSPBundle\Config\SpEntityDescriptorBuilder->buildPath('/saml/sp/logout')
    in D:\www\Symfony2-ADFS\vendor\aerialship\saml-sp-bundle\src\AerialShip\SamlSPBundle\Config\SpEntityDescriptorBuilder.php line 120

at AerialShip\SamlSPBundle\Config\SpEntityDescriptorBuilder->build()
    in D:\www\Symfony2-ADFS\vendor\aerialship\saml-sp-bundle\src\AerialShip\SamlSPBundle\Config\SpEntityDescriptorBuilder.php line 99

at AerialShip\SamlSPBundle\Config\SpEntityDescriptorBuilder->getEntityDescriptor()
    in D:\www\Symfony2-ADFS\vendor\aerialship\saml-sp-bundle\src\AerialShip\SamlSPBundle\Bridge\LogoutReceiveRequest.php line 80

at AerialShip\SamlSPBundle\Bridge\LogoutReceiveRequest->manage(object(Request))
    in D:\www\Symfony2-ADFS\vendor\aerialship\saml-sp-bundle\src\AerialShip\SamlSPBundle\RelyingParty\RelyingPartyCollection.php line 41

at AerialShip\SamlSPBundle\RelyingParty\RelyingPartyCollection->manage(object(Request))
    in D:\www\Symfony2-ADFS\vendor\aerialship\saml-sp-bundle\src\AerialShip\SamlSPBundle\RelyingParty\RelyingPartyCollection.php line 41

at AerialShip\SamlSPBundle\RelyingParty\RelyingPartyCollection->manage(object(Request))
    in D:\www\Symfony2-ADFS\vendor\aerialship\saml-sp-bundle\src\AerialShip\SamlSPBundle\Security\Http\Firewall\SamlSpAuthenticationListener.php line 64

at AerialShip\SamlSPBundle\Security\Http\Firewall\SamlSpAuthenticationListener->attemptAuthentication(object(Request))
    in D:\www\Symfony2-ADFS\vendor\symfony\symfony\src\Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener.php line 144

at Symfony\Component\Security\Http\Firewall\AbstractAuthenticationListener->handle(object(GetResponseEvent))
    in D:\www\Symfony2-ADFS\app\cache\dev\classes.php line 2340

at Symfony\Component\Security\Http\Firewall->onKernelRequest(object(GetResponseEvent))
    in  line 

at call_user_func(array(object(Firewall), 'onKernelRequest'), object(GetResponseEvent))
    in D:\www\Symfony2-ADFS\vendor\symfony\symfony\src\Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher.php line 450

at Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher->Symfony\Component\HttpKernel\Debug\{closure}(object(GetResponseEvent))
    in  line 

at call_user_func(object(Closure), object(GetResponseEvent))
    in D:\www\Symfony2-ADFS\app\cache\dev\classes.php line 1655

at Symfony\Component\EventDispatcher\EventDispatcher->doDispatch(array(object(Closure), object(Closure), object(Closure), object(Closure), object(Closure), object(Closure), object(Closure), object(Closure), object(Closure)), 'kernel.request', object(GetResponseEvent))
    in D:\www\Symfony2-ADFS\app\cache\dev\classes.php line 1588

at Symfony\Component\EventDispatcher\EventDispatcher->dispatch('kernel.request', object(GetResponseEvent))
    in D:\www\Symfony2-ADFS\app\cache\dev\classes.php line 1752

at Symfony\Component\EventDispatcher\ContainerAwareEventDispatcher->dispatch('kernel.request', object(GetResponseEvent))
    in D:\www\Symfony2-ADFS\vendor\symfony\symfony\src\Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher.php line 139

at Symfony\Component\HttpKernel\Debug\TraceableEventDispatcher->dispatch('kernel.request', object(GetResponseEvent))
    in D:\www\Symfony2-ADFS\app\bootstrap.php.cache line 2755

at Symfony\Component\HttpKernel\HttpKernel->handleRaw(object(Request), '1')
    in D:\www\Symfony2-ADFS\app\bootstrap.php.cache line 2740

at Symfony\Component\HttpKernel\HttpKernel->handle(object(Request), '1', true)
    in D:\www\Symfony2-ADFS\app\bootstrap.php.cache line 2870

at Symfony\Component\HttpKernel\DependencyInjection\ContainerAwareHttpKernel->handle(object(Request), '1', true)
    in D:\www\Symfony2-ADFS\app\bootstrap.php.cache line 2171

at Symfony\Component\HttpKernel\Kernel->handle(object(Request))
    in D:\www\Symfony2-ADFS\web\app_dev.php line 29

I try to set request at aerialship\saml-sp-bundle\src\AerialShip\SamlSPBundle\Bridge\LogoutReceiveRequest.php at line 74 as:

    $serviceInfo->getSpProvider()->setRequest($request);

but it shows xml response as under and not forward to logout (I replaced the urls with dummy one).

<?xml version="1.0"?>
<LogoutResponse xmlns="urn:oasis:names:tc:SAML:2.0:protocol" ID="_bf45bd9a2a970ab4ac0b5cdeced0b2f6f2accef1cb" Version="2.0" IssueInstant="2014-07-15T08:58:36Z" Destination="https://adfs.example.com/adfs/ls/" InResponseTo="_81507cc8-d702-4689-ac49-bbedf237d816">
    <saml:Issuer xmlns:saml="urn:oasis:names:tc:SAML:2.0:assertion">https://secured.example.com/</saml:Issuer>
    <samlp:Status xmlns:samlp="urn:oasis:names:tc:SAML:2.0:protocol">
        <samlp:StatusCode Value="urn:oasis:names:tc:SAML:2.0:status:Success"/>
    </samlp:Status>
</LogoutResponse>

can you help if I am missing something? Thanks

i3or1s commented 10 years ago

Hi, Can you provide config.yml and security.yml

INSEAD-asim commented 10 years ago

Thanks for so fast. Please find them as below: :)

// /app/config/security.yml
security:
    encoders:
        Acme\ADFS\Security\User\MyCustomUserProvider: plaintext

    role_hierarchy:
#       ROLE_ADMIN:       ROLE_USER
#       ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]

    providers:
        saml_user_provider:
            id: my.custom.service # implements UserManagerInterface

    firewalls:
        saml:
            pattern: ^/
            anonymous: false
            aerial_ship_saml_sp:
                local_logout_path: /logout
                provider: saml_user_provider
                create_user_if_not_exists: false
                services:
                    somename:
                        idp:
                            file: "@AcmeADFSBundle/Resources/MetaData.xml"
                        sp:
                            config:
                                entity_id: https://secured.example.edu/
            logout:
                path: /logout
                target: /
                invalidate_session: true
        dev:
            pattern:  ^/(_(profiler|wdt)|css|images|js)/
            security: false

    access_control:
        - { path: ^/saml/sp, roles: IS_AUTHENTICATED_ANONYMOUSLY }
#       - { path: ^/saml/sp/login, roles: IS_AUTHENTICATED_ANONYMOUSLY }
#       - { path: ^/saml/sp/acs, roles: IS_AUTHENTICATED_ANONYMOUSLY }
        - { path: ^/demo/secured/hello, roles: ROLE_ADMIN }
        - { path: ^/, roles: ROLE_USER }
#       - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY, requires_channel: https }

// /app/config/config.yml
imports:
    - { resource: parameters.yml }
    - { resource: security.yml }

framework:
    #esi:             ~
    #translator:      { fallback: %locale% }
    secret:          %secret%
    router:
        resource: "%kernel.root_dir%/config/routing.yml"
        strict_requirements: ~
    form:            ~
    csrf_protection: ~
    validation:      { enable_annotations: true }
    templating:
        engines: ['twig']
        #assets_version: SomeVersionScheme
    default_locale:  "%locale%"
    trusted_proxies: ~
    session:         ~
    fragments:       ~

# Twig Configuration
twig:
    debug:            %kernel.debug%
    strict_variables: %kernel.debug%

# Assetic Configuration
assetic:
    debug:          %kernel.debug%
    use_controller: false
    bundles:        [ ]
    #java: /usr/bin/java
    filters:
        cssrewrite: ~
        #closure:
        #    jar: %kernel.root_dir%/Resources/java/compiler.jar
        #yui_css:
        #    jar: %kernel.root_dir%/Resources/java/yuicompressor-2.4.7.jar

# Doctrine Configuration
doctrine:
    dbal:
        driver:   %database_driver%
        host:     %database_host%
        port:     %database_port%
        dbname:   %database_name%
        user:     %database_user%
        password: %database_password%
        charset:  UTF8
        # if using pdo_sqlite as your database driver, add the path in parameters.yml
        # e.g. database_path: %kernel.root_dir%/data/data.db3
        # path:     %database_path%

    orm:
        auto_generate_proxy_classes: %kernel.debug%
        auto_mapping: true

# Swiftmailer Configuration
swiftmailer:
    transport: %mailer_transport%
    host:      %mailer_host%
    username:  %mailer_user%
    password:  %mailer_password%
    spool:     { type: memory }

aerial_ship_saml_sp:
    driver: orm
    sso_state_entity_class: Acme\ADFSBundle\Entity\SSOState
INSEAD-asim commented 10 years ago

Also, let me know if I need to send claims from ADFS server in any specific SHA hash. Our server support SHA-1 and SHA-256 only.

i3or1s commented 10 years ago

Judging by the method and Configuration Reference Your configuration is missing base_url

# if different then url being used in request
# used for construction of assertion consumer and logout urls in SP entity descriptor
base_url: https://100.200.100.200/
    /**
     * @param string $path
     * @return string
     * @throws \RuntimeException
     */
    protected function buildPath($path)
    {
        if (isset($this->config['base_url']) && $this->config['base_url']) {
            return $this->config['base_url'] . $path;
        } else {
            if (!$this->request) {
                throw new \RuntimeException('Request not set');
            }

            return $this->httpUtils->generateUri($this->request, $path);
        }
    }
i3or1s commented 10 years ago

Since method should be compatible with request also ill look into the creation to see why it is failing to set the Request

INSEAD-asim commented 10 years ago

Thanks @i3or1s :) It is not showing error any more after base_url but it is also not invalidating session. Whenever I navigate to /logout, it redirects to IdP and then comes back to SP but I am still login with valid rule. Am I still missing something?

INSEAD-asim commented 10 years ago

I have one more issue... application stuck in loop when UsernameNotFoundException thrown. Below is the code of user provider and associated class

// Acme/ADFSBundle/Security/User/MyCustomUserProvider
<?php
namespace Acme\ADFSBundle\Security\User;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\Exception\UsernameNotFoundException;
use Symfony\Component\Security\Core\Exception\UnsupportedUserException;
use AerialShip\SamlSPBundle\Security\Core\User\UserManagerInterface;
use AerialShip\SamlSPBundle\Bridge\SamlSpInfo;

class MyCustomUserProvider implements UserManagerInterface
{

    public function loadUserByUsername($username)
    {
        if ($username == '0846037') {
            $roles = array('ROLE_USER');

            return new MyCustomUser($username, $roles);
        }

        throw new UsernameNotFoundException(
            sprintf('Username "%s" does not exist.', $username)
        );
    }

    public function loadUserBySamlInfo(SamlSpInfo $samlInfo)
    {
        $username = $samlInfo->getNameID()->getValue();
        return $this->loadUserByUsername($username);
    }

    public function createUserFromSamlInfo(SamlSpInfo $samlInfo)
    {
        throw new UsernameNotFoundException(
            sprintf('Username "%s" does not exist.', $samlInfo)
        );
    }

    public function refreshUser(UserInterface $user)
    {
        if (!$user instanceof MyCustomUser) {
            throw new UnsupportedUserException(
                sprintf('Instances of "%s" are not supported.', get_class($user))
            );
        }

        return $this->loadUserByUsername($user->getUsername());
    }

    public function supportsClass($class)
    {
        return $class === 'Acme\ADFSBundle\Security\User\MyCustomUser';
    }
} 

// Acme/ADFSBundle/Security/User/MyCustomUser
<?php
namespace Acme\ADFSBundle\Security\User;

use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Security\Core\User\EquatableInterface;

class MyCustomUser implements UserInterface, EquatableInterface
{
    private $username;
    private $roles;

    public function __construct($username, array $roles)
    {
        $this->username = $username;
        $this->roles = $roles;
    }

    public function getRoles()
    {
        return $this->roles;
    }

    public function getPassword()
    {
        return '';
    }

    public function getSalt()
    {
        return '';
    }

    public function getUsername()
    {
        return $this->username;
    }

    public function eraseCredentials()
    {
    }

    public function isEqualTo(UserInterface $user)
    {
        if (!$user instanceof MyCustomUser) {
            return false;
        }

        if ($this->username !== $user->getUsername()) {
            return false;
        }

        if ($this->roles !== $user->getRoles()) {
            return false;
        }

        return true;
    }
} 
i3or1s commented 10 years ago

Hi,

When loging out you should first visit saml logout route then code will redirect you to regular /logout

As for the loop i would need to check rest of the code, but from what i see i think your problem might be in the refreshUser(UserInterface $user) since this method is calling loadUserByUsername($user->getUsername()) and loadUserByUsername is throwing an exception that you did not handle.

I would not call loadUserByUsername at all since you already have a user.

You can check out http://symfony.com/doc/current/cookbook/security/entity_provider.html might give you some insight.

INSEAD-asim commented 10 years ago

Is saml logout route is /saml/sp/logout or one provided by my IdP? If I call IdP logout url, it comes back and show me XML file I sent you earlier :(

I will try to fix the things you mentioned. But when I didn't implemented loadUserByUsername, it is not loading user in role in loadUserBySamlInfo i.e. if I just throw error it is again in infinite loop. Can you tell me what would be the both functions in this case? These are the only issue I am stuck. Rest I found your bundle very useful.

INSEAD-asim commented 10 years ago

Hi @i3or1s, I am just back from my vacations and started looking into this again. I follow your guide as per directions to see if I miss anything. I am only facing two issues i.e. not logging out and site stuck in loop for failed authorization. I call the saml logout (IdP logout) page and it redirects me back to SP but it shows XML page. If I try to load site again, it shows error at /saml/sp/failure and next reloads auto-login me at site. If I don't implement refreshUser() or loadUserByUsername() function, my site is not working as Symfony try to reload user information at each new request. Can you help if I am still missing something? I can also try to look into the code and fix if think there is some bug. Thanks

i3or1s commented 10 years ago

What did you register in IDP as logout route?

INSEAD-asim commented 10 years ago

It seems I figure the fix for site stuck in loop for failed authorization. I am returning $user as it is in refreshUser(), authorizing user with empty roles in loadUserByUsername() and authorizing user with valid roles in loadUserBySamlInfo(). It shows 403 Access Denied in case of failed authorization and no more loop. I will perform some more test to see if it would be valid.

Regarding logout route, my IdP has my SP logout route i.e. /saml/sp/logout. I am calling my IdP logout i.e. https://adfs.example.org/adfs/ls/?wa=wsignout1.0

i3or1s commented 10 years ago

Ok that is good :smile: :+1:

INSEAD-asim commented 10 years ago

Thanks. But still I am facing issue with logout :(

i3or1s commented 10 years ago

Can you achieve logout on your application (without trying to do it through IDP)? I think in your configuration setup is missing logout_path: /saml/sp/logout

INSEAD-asim commented 10 years ago

The route in IdP is auto defined by metadata provided by your bundle /saml/sp/FederationMetadata.xml. I am unable to logout without IdP as it also redirects to IdP and it again come to SP and shows same XML page. I try adding logout_path in security.yml but still same. Do I need to setup something special in IdP for NameID or any other special parameter?

i3or1s commented 10 years ago

Logout saml/sp/logout should invalidate SSOState and send to logout which is handled by symfony. After everything is cleaned out it will redirect you to route which is behind firewall and will redirect you to login again on IDP. I am not sure how you end up getting xml view (which is valid).

Looks to me that issue is in configuration. I am not sure what exactly is happening since i cant see the app it self running and debug.

I read again through entire config and from what i see all of the routes are behind ROLE_USER

    access_control:
        - { path: ^/, roles: ROLE_USER }

I suggest debugging entire process and pinpointing moment when issue occurs. It would be helpful to describe entire debug process and what happens up until the error.

INSEAD-asim commented 10 years ago

One issue I found that in config, I set anonymous: false. I change it to anonymous: true. Now, it is showing the same XML at logout but if I call my SP url again, it shows "SSO session has expired" along with trace and SamlSpToken. I think the issue is on IdP as well as it is not invalidating my cookie and calling my SP again re-logins me without asking login information. I need to check this with our IdP expert may be they have insight.

It would be good if I can sort out this XML showing issue. I think it should either go to target or success_handler configured in security.yml. But it is not going anywhere and showing XML instead. Do you think that we need to use invalidate_session: true ?

INSEAD-asim commented 10 years ago

Hi @i3or1s, I try to connect strings. Here are some findings.

In factory \vendor\aerialship\saml-sp-bundle\src\AerialShip\SamlSPBundle\DependencyInjection\Security\Factory\SamlSpFactory.php, we have a function as:

protected function createRelyingPartyLogout(ContainerBuilder $container, $id)
{
    $this->createRelyingPartyLogoutSendRequest($container, $id);
    $this->createRelyingPartyLogoutReceiveResponse($container, $id);
    $this->createRelyingPartyLogoutReceiveRequest($container, $id);

    $service = new DefinitionDecorator('aerial_ship_saml_sp.relying_party.logout');
    $container->setDefinition('aerial_ship_saml_sp.relying_party.logout.'.$id, $service);

    $service->addMethodCall('append', array(new Reference('aerial_ship_saml_sp.relying_party.logout.receive_response.'.$id)));
    $service->addMethodCall('append', array(new Reference('aerial_ship_saml_sp.relying_party.logout.receive_request.'.$id)));
    // must come after receive response
    $service->addMethodCall('append', array(new Reference('aerial_ship_saml_sp.relying_party.logout.send_request.'.$id)));
    $service->addMethodCall('append', array(new Reference('aerial_ship_saml_sp.relying_party.logout.fallback')));
}

I think these functions are called in listener \vendor\aerialship\saml-sp-bundle\src\AerialShip\SamlSPBundle\Security\Http\Firewall\SamlSpAuthenticationListener.php in function attemptAuthentication(). Since the LogoutReceiveRequest->manage() is returning response as XML in \vendor\aerialship\saml-sp-bundle\src\AerialShip\SamlSPBundle\Bridge\LogoutReceiveRequest.php, it is shown in the browser and rest of the functions i.e. send_request and fallback not executed. Can you please help if I am missing something in configuration? To me, I look everything in configuration but it seems correct.

I also tried to remove logout URLs /saml/sp/logout from IdP. It then throws an error while logout but interestingly when I visit my SP url, it asks for authentication at IdP.

Sorry, I am bothering you much but in-fact I really find this bundle useful and I like to use it in our application. Thanks for all your work.

INSEAD-asim commented 10 years ago

@i3or1s, Can you share the documentation of IdP you used as reference to build this bundle? I am using this as a reference to understand the flow. http://support.ca.com/cadocs/0/CA%20SiteMinder%20r12%20SP2-ENU/Bookshelf_Files/HTML/252747.html

INSEAD-asim commented 10 years ago

@i3or1s, I try to study IdP and SP behavior with help of documentations. My understanding it that when IdP send request with query string of parameters SAMLRequest, the SP should output redirect request back to IdP. In our case, it is showing XML. Can you shed light on this?

i3or1s commented 10 years ago

Hi @INSEAD-asim i was on vacation so i could not respond sooner. I suggest we continue this over the email if you are all ok with it?

mvanmeerbeck commented 9 years ago

Hi @i3or1s @INSEAD-asim, I have the same problems than you: It shows the xml (LogoutResponse) when i try to logout from another sp

Did you guys find a solution ?

Thanks!

Great work by the way.

INSEAD-asim commented 9 years ago

Hi @mvanmeerbeck, I able to find another fork by @rodrigodirknsj. There was signing issue which I resolved. You can check my fork at https://github.com/INSEAD-asim/SamlSPBundle.git

BTW, I also implemented to download and cache IdP MetaData.xml. :)

i3or1s commented 9 years ago

Issue is that logout request it not processed when initiated through browser. You can find this line in the 'Bridge/LogoutReceiveRequest.php' method 'manage'. As you see it is returning Resonse should be redirect to idp.

This will be processed in version 2.

Checkout also this link https://wiki.shibboleth.net/confluence/display/SHIB2/SLOIssues it has the reason why this is yet not processed.

tmilos commented 8 years ago

New version of LightSAML core is available in https://github.com/lightSAML/lightSAML and it's released, with SLO implemented in https://github.com/lightSAML/lightSAML-Logout which is in alpha version.

INSEAD-asim commented 8 years ago

Great @tmilos ! It means that we will be moving to the new library soon. I will surely test it.