Dancer2-Plugin-SPID

 view release on metacpan or  search on metacpan

CHANGES  view on Meta::CPAN

  Change: b96056c189625ae29ebf202f4be64f078f1f6607
  Author: Alessandro Ranellucci <alessandro@pintle.it>
  Date : 2018-08-06 21:22:33 +0000

    Apply some nice Bootstrap to the example app 

  Change: 740adaba378f7ce475bd2e934368821328828f89
  Author: Alessandro Ranellucci <alessandro@pintle.it>
  Date : 2018-08-06 21:06:58 +0000

    Added metadata_endpoint 

  Change: 7dfe9b4a6cd609cab24513ec2bae1fdc4b810897
  Author: Alessandro Ranellucci <alessandro@pintle.it>
  Date : 2018-08-06 21:02:10 +0000

    Update to the newest Net::SPID API 

  Change: 486cc691cd3a4125ea061532467baddafeed8608
  Author: Alessandro Ranellucci <alessandro@pintle.it>
  Date : 2018-08-06 20:27:12 +0000

example/app.pl  view on Meta::CPAN

        template 'user';
    } else {
        template 'index';
    }
};

# That's it. Seriously, that's all you need.
# Below you'll see how to customize the behavior further by configuring one or
# more hooks that the SPID plugin will call.

# This hook is called when the login endpoint is called and the AuthnRequest
# is about to be crafted.
hook 'plugin.SPID.before_login' => sub {
    # ...
};

# This hook is called after the SPID session was successfully initiated.
hook 'plugin.SPID.after_login' => sub {
    info "User " . spid_session->nameid . " logged in";
    
    # Here you might want to create the user in your local database or do more

example/app.pl  view on Meta::CPAN

    # or a dedicated log file.
    info "SPID Assertion: " . spid_session->assertion_xml;
};

# This hook is called after a failed SPID login.
hook 'plugin.SPID.after_failed_login' => sub {
    my $statuscode = shift;
    info "SPID login failed: $statuscode";
};

# This hook is called when the logout endpoint is called and the LogoutRequest
# is about to be crafted.
hook 'plugin.SPID.before_logout' => sub {
    debug "User " . spid_session->nameid . " is about to logout";
};

# This hook is called when a SPID session is terminated (this might be triggered
# also when user initiated logout from another Service Provider or directly
# within the Identity Provider, thus without calling our logout endpoint and
# the 'before_logout' hook).
hook 'plugin.SPID.after_logout' => sub {
    my $success = shift;  # 'success' or 'partial'
    debug "User " . spid_session->nameid . " logged out";
};

dance;

__END__

example/config.yml  view on Meta::CPAN

plugins:
  SPID:
    sp_entityid: "https://www.prova.it/"
    sp_key_file: "sp.key"
    sp_cert_file: "sp.pem"
    sp_assertionconsumerservice:
      - "http://localhost:3000/spid-sso"
    sp_singlelogoutservice:
      "http://localhost:3000/spid-slo": "HTTP-Redirect"
    idp_metadata_dir: "idp_metadata/"
    login_endpoint: "/spid-login"
    logout_endpoint: "/spid-logout"
    sso_endpoint: "/spid-sso"
    slo_endpoint: "/spid-slo"
    metadata_endpoint: "/spid-metadata.xml"

lib/Dancer2/Plugin/SPID.pm  view on Meta::CPAN

    
    # Load Identity Providers from their XML metadata.
    $spid->load_idp_metadata($self->config->{idp_metadata_dir});

    return $spid;
}

sub _build_spid_button {
    my ($self, %args) = @_;
    
    return $self->_spid->get_button($self->config->{login_endpoint} . '?idp=%s');
}

sub spid_session :PluginKeyword {
    my ($self) = @_;
    return $self->dsl->session('__spid_session');
}

sub BUILD {
    my ($self) = @_;
    
    # Check that we have all the required config options.
    foreach my $key (qw(sp_entityid sp_key_file sp_cert_file idp_metadata_dir
        login_endpoint logout_endpoint)) {
        croak "Missing required config option for SPID: '$key'"
            if !$self->config->{$key};
    }
    
    # Create a hook for populating the spid_* variables in templates.
    $self->app->add_hook(Dancer2::Core::Hook->new(
        name => 'before_template_render',
        code => sub {
            my $vars = shift;
            

lib/Dancer2/Plugin/SPID.pm  view on Meta::CPAN

                my $jwt = encode_jwt(
                    payload => {
                        idp         => $idp_id,
                        level       => ($args{level} || 1),
                        redirect    => ($args{redirect} || '/'),
                    },
                    alg => 'HS256',
                    key => $self->config->{jwt_secret} // $DEFAULT_JWT_SECRET,
                );
                sprintf '%s?t=%s',
                    $self->config->{login_endpoint},
                    $jwt;
            };
            
            $vars->{spid_button} = sub {
                my %args = %{$_[0]};
                $self->_spid->get_button($url_cb, %args);
            };
            
            $vars->{spid_login} = sub {
                my %args = %{$_[0]};
                $url_cb->($self->spid_session->idp_id, %args);
            };
            
            $vars->{spid_logout} = sub {
                my %args = %{$_[0]};
                
                sprintf '%s?redirect=%s',
                    $self->config->{logout_endpoint},
                    ($args{redirect} || '/');
            };
            
            $vars->{spid_session} = sub { $self->spid_session };
        }
    ));
    
    # Create a route for the metadata endpoint.
    if ($self->config->{metadata_endpoint}) {
        $self->app->add_route(
            method  => 'get',
            regexp  => $self->config->{metadata_endpoint},
            code    => sub {
                $self->dsl->content_type('application/xml');
                return $self->_spid->metadata;
            },
        );
    }
    
    # Create a route for the login endpoint.
    # This endpoint initiates SSO through the user-chosen Identity Provider.
    $self->app->add_route(
        method  => 'get',
        regexp  => $self->config->{login_endpoint},
        code    => sub {
            $self->execute_plugin_hook('before_login');
            
            my $jwt = decode_jwt(
                token   => $self->dsl->param('t'),
                key     => $self->config->{jwt_secret} // $DEFAULT_JWT_SECRET,
            );
            
            # Check that we have the mandatory 'idp' parameter and that it matches
            # an available Identity Provider.

lib/Dancer2/Plugin/SPID.pm  view on Meta::CPAN

            $self->dsl->session('__spid_authnreq_id' => $authnreq->ID);
            
            # Save the redirect destination to be used after successful login.
            $self->dsl->session('__spid_sso_redirect' => $jwt->{redirect} || '/');
    
            # Redirect user to the IdP using its HTTP-Redirect binding.
            $self->dsl->redirect($authnreq->redirect_url, 302);
        },
    );
    
    # Create a route for the SSO endpoint (AssertionConsumerService).
    # During SSO, the Identity Provider will redirect user to this URL POSTing
    # the resulting assertion.
    $self->app->add_route(
        method  => 'post',
        regexp  => $self->config->{sso_endpoint},
        code    => sub {
            # Parse and verify the incoming assertion. This may throw exceptions so we
            # enclose it in an eval {} block.
            my $response = eval {
                $self->_spid->parse_response(
                    $self->dsl->param('SAMLResponse'),
                    $self->dsl->session('__spid_authnreq_id'),  # Match the ID of our authentication request for increased security.
                );
            };
            

lib/Dancer2/Plugin/SPID.pm  view on Meta::CPAN

                $self->dsl->session('__spid_session' => undef);
                $self->execute_plugin_hook('after_failed_login');
            }
            
            # Regardless of the login result, redirect user to the saved destination
            $self->dsl->redirect($self->dsl->session('__spid_sso_redirect'));
            $self->dsl->session('__spid_sso_redirect' => undef);
        },
    );
    
    # Create a route for the logout endpoint.
    $self->app->add_route(
        method  => 'get',
        regexp  => $self->config->{logout_endpoint},
        code    => sub {
            # If we don't have an open SPID session, do nothing.
            return $self->dsl->redirect('/')
                if !$self->spid_session;
            
            $self->execute_plugin_hook('before_logout');
            
            # Craft the LogoutRequest.
            my $idp = $self->_spid->get_idp($self->spid_session->idp_id);
            my $logoutreq = $idp->logoutrequest(session => $self->spid_session);
            
            # Save the ID of the LogoutRequest so that we can check it in the response
            # in order to prevent forgery.
            $self->dsl->session('__spid_logoutreq_id' => $logoutreq->ID);
            
            # Redirect user to the Identity Provider for logout.
            $self->dsl->redirect($logoutreq->redirect_url, 302);
        },
    );
    
    # Create a route for the SingleLogoutService endpoint.
    # This endpoint exposes a SingleLogoutService for our Service Provider, using
    # a HTTP-POST or HTTP-Redirect binding (it does not support SOAP).
    # Identity Providers can direct both LogoutRequest and LogoutResponse messages
    # to this endpoint.
    $self->app->add_route(
        method  => 'post',
        regexp  => $self->config->{slo_endpoint},
        code    => sub {
            if ($self->dsl->param('SAMLResponse') && $self->dsl->session('__spid_logoutreq_id')) {
                my $logoutres = eval {
                    $self->_spid->parse_logoutresponse(
                        $self->dsl->param('SAMLResponse'),
                        $self->dsl->request->uri,
                        $self->dsl->session('__spid_logoutreq_id'),
                    )
                };
                

lib/Dancer2/Plugin/SPID.pm  view on Meta::CPAN

    plugins:
      SPID:
        sp_entityid: "https://www.prova.it/"
        sp_key_file: "sp.key"
        sp_cert_file: "sp.pem"
        sp_assertionconsumerservice:
          - "http://localhost:3000/spid-sso"
        sp_singlelogoutservice:
          "http://localhost:3000/spid-slo": "HTTP-Redirect"
        idp_metadata_dir: "idp_metadata/"
        login_endpoint: "/spid-login"
        logout_endpoint: "/spid-logout"
        sso_endpoint: "/spid-sso"
        slo_endpoint: "/spid-slo"

=over

=item I<sp_entityid>

(Required.) The entityID value for this Service Provider. According to SPID regulations, this should be a URI.

=item I<sp_key_file>

(Required.) The absolute or relative file path to our private key file.

=item I<sp_cert_file>

(Required.) The absolute or relative file path to our certificate file.

=item I<sp_assertionconsumerservice>

An arrayref with the URL(s) of our AssertionConsumerService endpoint(s). It is used for metadata generation and for validating the C<Destination> XML attribute of the incoming responses.

=item I<sp_singlelogoutservice>

A hashref with the URL(s) of our SingleLogoutService endpoint(s), along with the specification of the binding. It is used for metadata generation and for validating the C<Destination> XML attribute of the incoming responses.

=item I<sp_attributeconsumingservice>

(Optional.) An arrayref with the AttributeConsumingServices to list in metadata, each one described by a C<servicename> and a list of C<attributes>. This is optional as it's only used for metadata generation.

    sp_attributeconsumingservice:
      - servicename: "Service 1"
        attributes:
          - "fiscalNumber"
          - "name"
          - "familyName"
          - "dateOfBirth"

=item I<idp_metadatadir>

(Required.) The absolute or relative path to a directory containing metadata files for Identity Providers in XML format (their file names are expected to end in C<.xml>).

=item I<login_endpoint>

(Required.) The relative HTTP path we want to use for the SPID button login action. A route handler will be created for this path that generates an AuthnRequest and redirects the user to the chosen Identity Provider using the HTTP-Redirect binding.

=item I<logout_endpoint>

(Required.) The relative HTTP path we want to use for the logout action. A route handler will be created for this path that generates a LogoutRequest and redirects the user to the current Identity Provider using the HTTP-Redirect binding.

=item I<sso_endpoint>

(Required.) The relative HTTP path we want to expose as AssertionConsumerService. This must match the URL advertised in the Service Provider metadata.

=item I<slo_endpoint>

(Required.) The relative HTTP path we want to expose as SingleLogoutService. This must match the URL advertised in the Service Provider metadata.

=item I<metadata_endpoint>

(Optional.) The relative HTTP path we want to use for publishing our SP metadata. If omitted, no endpoint will be exposed.

=item I<jwt_secret>

(Optional.) The secret using for encoding relay state data.

=back

=head1 KEYWORDS

The following keywords are available.

lib/Dancer2/Plugin/SPID.pm  view on Meta::CPAN

This keyword will return the URL for initiating a Single Logout by directing the user to the current Identity Provider with a LogoutRequest. You must check whether the user has an active SPID session before using it. It accepts an optional C<redirect...

    [% IF spid_session %]
        <a href="[% spid_logout(redirect => '/') %]">Logout</a>
    [% END %]

=head1 HOOKS

=head2 before_login

This hook is called when the login endpoint is called (i.e. the SPID button is clicked or user visited the upgrade URL returned by L<spid_login>) and the AuthnRequest is about to be crafted.

    hook 'plugin.SPID.before_login' => sub {
        info "User is initiating SSO";
    };

=head2 after_login

This hook is called after the user returns to us after a successful SPID login.

    hook 'plugin.SPID.after_login' => sub {

lib/Dancer2/Plugin/SPID.pm  view on Meta::CPAN


This hook is called after the user returns to us after a failed SPID login. The SAML C<StatusCode> value is supplied as first argument. You can use this hook in order to display the correct error message according the error.

    hook 'plugin.SPID.after_failed_login' => sub {
        my $statuscode = shift;
        info "SPID login failed: $statuscode";
    };

=head2 before_logout

This hook is called when the logout endpoint is called and the LogoutRequest
is about to be crafted.

    hook 'plugin.SPID.before_logout' => sub {
        debug "User " . spid_session->nameid . " is about to logout";
    };

=head2 after_logout

This hook is called when a SPID session is terminated. Note that this might be triggered also when user initiated logout from another Service Provider or directly within the Identity Provider, thus without calling our logout endpoint and the L<before...
L<spid_session> will be cleared I<after> this hook is executed, so you can use it.

    hook 'plugin.SPID.after_logout' => sub {
        my $success = shift;  # 'success' or 'partial'
        debug "User " . spid_session->nameid . " logged out";
    };

=head1 AUTHOR

Alessandro Ranellucci <aar@cpan.org>

t/01_use.t  view on Meta::CPAN

use Dancer2;
use Test::More tests => 1;

BEGIN { # would usually be in config.yml
    set plugins => {
        SPID => {qw(
            sp_entityid         https://www.prova.it/
            sp_key_file         sp.key
            sp_cert_file        sp.pem
            idp_metadata_dir    idp_metadata/
            login_endpoint      /spid-login
            logout_endpoint     /spid-logout
            sso_endpoint        /spid-sso
            slo_endpoint        /spid-slo
        )},
    };
    use_ok('Dancer2::Plugin::SPID');
}

__END__



( run in 0.270 second using v1.01-cache-2.11-cpan-b61123c0432 )