File Coverage

blib/lib/Dancer/Plugin/Auth/CAS.pm
Criterion Covered Total %
statement 22 24 91.6
branch n/a
condition n/a
subroutine 8 8 100.0
pod n/a
total 30 32 93.7


line stmt bran cond sub pod time code
1             package Dancer::Plugin::Auth::CAS;
2             {
3             $Dancer::Plugin::Auth::CAS::VERSION = '1.128';
4             }
5              
6             =head1 NAME
7              
8             Dancer::Plugin::Auth::CAS - CAS sso authentication for Dancer
9              
10             =cut
11              
12 1     1   537 use warnings;
  1         1  
  1         34  
13 1     1   4 use strict;
  1         1  
  1         25  
14              
15 1     1   596 use Dancer ':syntax';
  1         206899  
  1         5  
16 1     1   793 use Dancer::Plugin;
  1         1115  
  1         63  
17 1     1   6 use Dancer::Response;
  1         2  
  1         17  
18 1     1   4 use Dancer::Exception ':all';
  1         1  
  1         95  
19 1     1   5 use HTTP::Headers;
  1         1  
  1         17  
20 1     1   224 use Authen::CAS::Client;
  0            
  0            
21             use Scalar::Util 'blessed';
22              
23             our $VERSION;
24              
25             register_exception('InvalidConfig', message_pattern => "Invalid or missing configuration: %s");
26             register_exception('CasError', message_pattern => "Unable to auth with CAS backend: %s");
27              
28             my $settings = plugin_setting;
29              
30             sub _auth_cas {
31             my (%options) = @_;
32              
33             my $base_url = $settings->{cas_url} // raise( InvalidConfig => "cas_url is unset" );
34             my $cas_version = $settings->{cas_version} || raise( InvalidConfig => "cas_version is unset");
35             my $cas_user_map = $options{cas_user_map} || $settings->{cas_user_map} || 'cas_user';
36             my $cas_denied_url = $options{cas_denied_path} || $settings->{cas_denied_path} || '/denied';
37              
38             my $ssl_verify_hostname = $settings->{ssl_verify_hostname};
39             $ENV{"PERL_LWP_SSL_VERIFY_HOSTNAME"} = defined( $ssl_verify_hostname ) ? $ssl_verify_hostname : 1;
40              
41             # check supported versions
42             unless( grep(/$cas_version/, qw( 2.0 1.0 )) ) {
43             raise( InvalidConfig => "cas_version '$cas_version' not supported");
44             }
45              
46             my $mapping = $settings->{cas_attr_map} || {};
47              
48             my $ticket = $options{ticket};
49             my $params = request->params;
50             unless( $ticket ) {
51             my $tickets = $params->{ticket};
52             # For the case when application also uses 'ticket' parameters
53             # we only remove the real cas service ticket
54             if( ref($tickets) eq "ARRAY" ) {
55             while( my ($index, $value) = each @$tickets ) {
56             # The 'ST-' is specified in CAS-protocol
57             if( $value =~ m/^ST\-/ ) {
58             $ticket = delete $tickets->[$index];
59             }
60             }
61             } else {
62             $ticket = delete $params->{ticket};
63             }
64             }
65             my $service = uri_for( request->path_info, $params );
66              
67             my $cas = Authen::CAS::Client->new( $base_url );
68              
69             my $user = session($cas_user_map);
70              
71             unless( $user ) {
72              
73             my $response = Dancer::Response->new( status => 302 );
74             my $redirect_url;
75              
76             if( $ticket) {
77             debug "Trying to validate via CAS '$cas_version' with ticket=$ticket";
78            
79             my $r;
80             if( $cas_version eq "1.0" ) {
81             $r = $cas->validate( $service, $ticket );
82             }
83             elsif( $cas_version eq "2.0" ) {
84             $r = $cas->service_validate( $service, $ticket );
85             }
86             else {
87             raise( InvalidConfig => "cas_version '$cas_version' not supported");
88             }
89              
90             if( $r->is_success ) {
91              
92             # Redirect to given path
93             info "Authenticated as: ".$r->user;
94             if( $cas_version eq "1.0" ) {
95             session $cas_user_map => $r->user;
96             } else {
97             session $cas_user_map => _map_attributes( $r->doc, $mapping );
98             }
99             $redirect_url = $service;
100              
101             } elsif( $r->is_failure ) {
102              
103             # Redirect to denied
104             debug "Failed to authenticate: ".$r->code." / ".$r->message;
105             $redirect_url = uri_for( $cas_denied_url );
106              
107             } else {
108              
109             # Raise hard error, backend has errors
110             error "Unable to authenticate: ".$r->error;
111             raise( CasError => $r->error );
112             }
113              
114             } else {
115             # Has no ticket, needs one
116             debug "Redirecting to CAS: ".$cas->login_url( $service );
117             $redirect_url = $cas->login_url( $service );
118             }
119              
120             # General redir response
121             $response->header( Location => $redirect_url );
122             halt( $response );
123             }
124            
125             }
126              
127             sub _map_attributes {
128             my ( $doc, $mapping ) = @_;
129              
130             my $attrs = {};
131              
132             my $result = $doc->find( '/cas:serviceResponse/cas:authenticationSuccess' );
133             if( $result ) {
134             my $node = $result->get_node(1);
135              
136             # extra all attributes
137             my @attributes = $node->findnodes( "./cas:attributes/*" );
138             foreach my $a (@attributes) {
139             my $name = (split(/:/, $a->nodeName, 2))[1];
140             my $val = $a->textContent;
141              
142             my $mapped_name = $mapping->{ $name } // $name;
143             $attrs->{ $mapped_name } = $val;
144             }
145            
146             }
147             debug "Mapped attributes: ".to_dumper( $attrs );
148             return $attrs;
149             }
150              
151              
152             register auth_cas => \&_auth_cas;
153             register_plugin;
154              
155             1; # End of Dancer::Plugin::Auth::CAS
156             __END__