Skip to content

Commit 844f8d1

Browse files
authored
Merge pull request #151 from timlegge/idps
testapp: Show supported bindings for logout and configurable attributes by IdP
2 parents 8f284d8 + 3d291ff commit 844f8d1

File tree

7 files changed

+239
-40
lines changed

7 files changed

+239
-40
lines changed

lib/Net/SAML2.pm

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -102,25 +102,31 @@ Identity Providers (IdPs). It has been tested against:
102102
103103
=over
104104
105-
=item GSuite (Google)
105+
=item Auth0 (requires Net::SAML2 >=0.39)
106106
107107
=item Azure (Microsoft Office 365)
108108
109-
=item OneLogin
109+
=item GSuite (Google)
110110
111111
=item Jump
112112
113-
=item Mircosoft ADFS
114-
115113
=item Keycloak
116114
117-
=item Auth0 (requires Net::SAML2 >=0.39)
115+
=item Mircosoft ADFS (not recently tested)
116+
117+
=item Okta
118+
119+
=item OneLogin
118120
119121
=item PingIdentity
120122
121123
Version 0.54 and newer support EncryptedAssertions. No changes required to existing
122124
SP applications if EncryptedAssertions are not in use.
123125
126+
=item SAMLTEST.ID (requires Net::SAML2 >=0.63)
127+
128+
=item Shibboleth (requires Net::SAML2 >=0.63)
129+
124130
=back
125131
126132
=head1 MAJOR CAVEATS

xt/testapp/README.md

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ The testapp now supports a simplified automatic configuration for testing agains
5050
2. Download the metadata from your IdP and save it as IdPs/google/metadata.xml
5151
3. Download the cacert.pem from the IdP and save it as IdPs/google/cacert.pem
5252
4. Optionally create IdPs/google/config.yml for custom settings for the IdP (if the a custom config.yml does not exist it will refresh the settings from the default config.yml.
53+
4. Optionally create IdPs/google/mappings.yml for custom IdP attribute mappings. If a custom mappings.yml does not does not exist it will use the defaul mappings.
5354

5455
The index page will automatically list each configured Identity Provider as a link to initiate login against that IdP.
5556

@@ -66,6 +67,10 @@ IdPs/
6667
google/
6768
cacert.pem
6869
metadata.yml
70+
shibboleth
71+
cacert.pem
72+
metadata.yml
73+
mappings.yml (optional)
6974

7075
### Run lighttpd to deliver metadata.xml
7176

@@ -94,15 +99,21 @@ If there is an option to upload the metadata.xml that is probably your first ste
9499

95100
Saml2Test expects the Identity Provider to provide an assertion with the following values:
96101

97-
1. DN
98-
2. CN
99-
3. EmailAddress
100-
4. FirstName
101-
5. Address
102-
6. Phone
103-
7. EmployeeNumber
104-
105-
Note that DN and CN (and others) may not be available. That can be customized in views/user.tt if you want to choose other options. However the Identity Provider must provide the assertion attributes that match the expected names in views/user.tt.
102+
1. EmailAddress
103+
2. FirstName
104+
3. LastName
105+
4. Address
106+
5. PhoneNumber
107+
6. EmployeeNumber
108+
109+
If the Identity Provider does not provide assertion attributes that match the expected names above you can create a custom mapping in IdPs/idp_name/mappings.yml in the following format. The setting is the name the testapp expects and the value is the attribure name that the IdP provides in the Assertion.
110+
111+
EmailAddress: "urn:oid:0.9.2342.19200300.100.1.3"
112+
FirstName: "urn:oid:2.5.4.42"
113+
LastName: "urn:oid:2.5.4.4"
114+
Address: "urn:oid:2.5.4.9"
115+
PhoneNumber: "urn:oid:2.5.4.20"
116+
EmployeeNumber: "urn:oid:0.9.2342.19200300.100.1.1"
106117

107118
## Debugging
108119

xt/testapp/lib/Saml2Test.pm

Lines changed: 140 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ use URN::OASIS::SAML2 qw(:bindings :urn);
2121

2222
our $VERSION = '0.2';
2323

24-
get '/' => sub {
24+
sub load_idps {
2525
if ( ! -x './IdPs' ) {
2626
return "<html><pre>You must have a xt/testapp/IdPs directory</pre></html>";
2727
}
@@ -44,12 +44,23 @@ get '/' => sub {
4444
push @idps, \%tempidp;
4545
}
4646

47-
template 'index', { 'idps' => \@idps, 'sign_metadata' => config->{sign_metadata} };
47+
return @idps;
48+
}
49+
50+
get '/' => sub {
51+
my @idps = load_idps();
52+
53+
template 'index', {
54+
'idps' => \@idps,
55+
'sign_metadata' => config->{sign_metadata},
56+
(defined params->{logout}) ? ('logout' => params->{logout}) : (),
57+
};
4858
};
4959

5060
get '/login' => sub {
5161

5262
config->{cacert} = 'IdPs/' . params->{idp} . '/cacert.pem';
63+
config->{idp_name} = params->{idp};
5364
config->{idp} = 'http://localhost:8880/IdPs/' . params->{idp} . '/metadata.xml';
5465
if ( -f 'IdPs/' . params->{idp} . '/config.yml' ) {
5566
my $config_file = YAML::LoadFile('IdPs/' . params->{idp} . '/config.yml');
@@ -61,7 +72,6 @@ get '/login' => sub {
6172
for my $key (keys %$config_file) {
6273
config->{$key} = $config_file->{$key};
6374
}
64-
6575
}
6676
my $idp = _idp();
6777
my $sp = _sp();
@@ -71,6 +81,8 @@ get '/login' => sub {
7181
defined (config->{is_passive}) ? (is_passive => config->{is_passive}) : (),
7282
);
7383

84+
config->{slo_urls} = $idp->slo_urls();
85+
7486
my $authnreq = $sp->authn_request(
7587
$idp->sso_url('urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect'),
7688
$idp->format || '', # default format.
@@ -85,7 +97,7 @@ get '/login' => sub {
8597
};
8698

8799
get '/logout-local' => sub {
88-
redirect '/', 302;
100+
redirect '/?logout=local', 302;
89101
};
90102

91103
get '/logout-redirect' => sub {
@@ -144,7 +156,7 @@ get '/logout-soap' => sub {
144156
);
145157

146158
my $logoutreq = $sp->logout_request(
147-
$idp->entityid, params->{nameid}, $idp->format, params->{session},
159+
$slo_url, params->{nameid}, $idp->format || undef, params->{session},
148160
\%logout_params
149161
)->as_xml;
150162

@@ -164,39 +176,103 @@ get '/logout-soap' => sub {
164176

165177
my $res = $soap->request($logoutreq);
166178

167-
redirect '/', 302;
179+
if ($res) {
180+
my $logout = Net::SAML2::Protocol::LogoutResponse->new_from_xml(
181+
xml => $res
182+
);
183+
if ($logout->success) {
184+
print STDERR "\nLogout Success Status - $logout->{issuer}\n";
185+
}
186+
}
187+
else {
188+
return "<html><pre>Bad Logout Response</pre></html>";
189+
}
190+
191+
redirect '/?logout=SOAP', 302;
168192
return "Redirected\n";
169193
};
170194

171195
post '/consumer-post' => sub {
172196
my $post = Net::SAML2::Binding::POST->new(
173197
cacert => config->{cacert},
174198
);
199+
175200
my $ret = $post->handle_response(
176201
params->{SAMLResponse}
177202
);
178203

179204
if ($ret) {
205+
180206
my $assertion = Net::SAML2::Protocol::Assertion->new_from_xml(
181207
xml => decode_base64(params->{SAMLResponse}),
182208
key_file => config->{key},
183209
cacert => config->{cacert},
184210
);
185211

212+
if (! $assertion->valid(config->{issuer})) {
213+
return '<html><pre>Bad Assertion</pre></html>';
214+
}
215+
186216
my $name_qualifier = $assertion->nameid_name_qualifier();
187217
my $sp_name_qualifier = $assertion->nameid_sp_name_qualifier();
188218

219+
my $slo_urls = config->{slo_urls};
220+
221+
my $user_attributes = get_user_attributes($assertion);
222+
189223
template 'user', {
190-
assertion => $assertion,
224+
user_attributes => $user_attributes,
191225
(defined $name_qualifier ? (name_qualifier => $name_qualifier) : ()),
192226
(defined $sp_name_qualifier ? (sp_name_qualifier => $sp_name_qualifier) : ()),
227+
(defined $slo_urls ? (slo_urls => $slo_urls) : ()),
228+
message => 'Successful Login via POST',
193229
};
194230
}
195231
else {
196232
return "<html><pre>Bad Assertion</pre></html>";
197233
}
198234
};
199235

236+
sub get_attribute_map {
237+
238+
my $attribute_mappings;
239+
240+
my $file = 'IdPs/' . config->{idp_name} . '/mappings.yml';
241+
242+
if ( -e $file ) {
243+
$attribute_mappings = YAML::LoadFile($file);
244+
} else {
245+
246+
$attribute_mappings->{EmailAddress} = 'http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress';
247+
$attribute_mappings->{FirstName} = 'fname';
248+
$attribute_mappings->{LastName} = 'lname';
249+
$attribute_mappings->{Address} = 'Address';
250+
$attribute_mappings->{PhoneNumber} = 'PhoneNumber';
251+
$attribute_mappings->{EmployeeNumber} = 'EmployeeNumber';
252+
}
253+
254+
return $attribute_mappings;
255+
}
256+
257+
sub get_user_attributes {
258+
my $assertion = shift;
259+
260+
my %user;
261+
262+
my $map = get_attribute_map();
263+
$user{issuer} = $assertion->{issuer};
264+
$user{session} = $assertion->{session};
265+
$user{nameid} = $assertion->nameid();
266+
$user{EmailAddress} = $assertion->{attributes}{$map->{EmailAddress}}[0];
267+
$user{FirstName} = $assertion->{attributes}{$map->{FirstName}}[0];
268+
$user{LastName} = $assertion->{attributes}{$map->{LastName}}[0];
269+
$user{Address} = $assertion->{attributes}{$map->{Address}}[0];
270+
$user{PhoneNumber} = $assertion->{attributes}{$map->{PhoneNumber}}[0];
271+
$user{EmployeeNumber} = $assertion->{attributes}{$map->{EmployeeNumber}}[0];
272+
273+
return \%user;
274+
}
275+
200276
get '/consumer-artifact' => sub {
201277
my $idp = _idp();
202278
my $idp_cert = $idp->cert('signing');
@@ -228,13 +304,23 @@ get '/consumer-artifact' => sub {
228304
xml => $response
229305
);
230306

307+
if ( ! $assertion->valid(config->{issuer})) {
308+
return '<html><pre>Bad Assertion</pre></html>';
309+
}
310+
231311
my $name_qualifier = $assertion->nameid_name_qualifier();
232312
my $sp_name_qualifier = $assertion->nameid_sp_name_qualifier();
233313

314+
my $slo_urls = config->{slo_urls};
315+
316+
my $user_attributes = get_user_attributes($assertion);
317+
234318
template 'user', {
235-
assertion => $assertion,
319+
user_attributes => $user_attributes,
236320
($name_qualifier ? (name_qualifier => $name_qualifier) : ()),
237321
($sp_name_qualifier ? (sp_name_qualifier => $sp_name_qualifier) : ()),
322+
slo_urls => ($slo_urls ? $slo_urls : ()),
323+
message => 'Successful Login via SOAP',
238324
};
239325
}
240326
else {
@@ -255,14 +341,14 @@ get '/sls-redirect-response' => sub {
255341
my $logout = Net::SAML2::Protocol::LogoutResponse->new_from_xml(
256342
xml => $response
257343
);
258-
if ($logout->status eq 'urn:oasis:names:tc:SAML:2.0:status:Success') {
344+
if ($logout->success) {
259345
print STDERR "\nLogout Success Status - $logout->{issuer}\n";
260346
}
261347
}
262348
else {
263349
return "<html><pre>Bad Logout Response</pre></html>";
264350
}
265-
redirect $relaystate || '/', 302;
351+
redirect $relaystate || '/?logout=redirect', 302;
266352
return "Redirected\n";
267353
};
268354

@@ -281,15 +367,57 @@ post '/sls-post-response' => sub {
281367
my $logout = Net::SAML2::Protocol::LogoutResponse->new_from_xml(
282368
xml => decode_base64(params->{SAMLResponse})
283369
);
284-
if ($logout->status eq 'urn:oasis:names:tc:SAML:2.0:status:Success') {
370+
if ($logout->success) {
371+
print STDERR "\nLogout Success Status - $logout->{issuer}\n";
372+
}
373+
}
374+
else {
375+
return "<html><pre>Bad Logout Response</pre></html>";
376+
}
377+
378+
redirect "/?logout=POST", 302;
379+
return "Redirected\n";
380+
};
381+
382+
get '/sls-consumer-artifact' => sub {
383+
my $idp = _idp();
384+
my $idp_cert = $idp->cert('signing');
385+
my $art_url = $idp->art_url('urn:oasis:names:tc:SAML:2.0:bindings:SOAP');
386+
387+
my $artifact = params->{SAMLart};
388+
389+
my $sp = _sp();
390+
my $request = $sp->artifact_request($art_url, $artifact)->as_xml;
391+
392+
my $ua = LWP::UserAgent->new;
393+
394+
require LWP::Protocol::https;
395+
$ua->ssl_opts( (verify_hostname => config->{ssl_verify_hostname}));
396+
397+
my $soap = Net::SAML2::Binding::SOAP->new(
398+
ua => $ua,
399+
url => $art_url,
400+
key => config->{key},
401+
cert => config->{cert},
402+
idp_cert => $idp_cert,
403+
);
404+
405+
my $response = $soap->request($request);
406+
407+
if ($response) {
408+
my $logout = Net::SAML2::Protocol::LogoutResponse->new_from_xml(
409+
xml => $response,
410+
);
411+
412+
if ($logout->success) {
285413
print STDERR "\nLogout Success Status - $logout->{issuer}\n";
286414
}
287415
}
288416
else {
289417
return "<html><pre>Bad Logout Response</pre></html>";
290418
}
291419

292-
redirect '/', 302;
420+
redirect "/?logout=SOAP-ARTIFACT", 302;
293421
return "Redirected\n";
294422
};
295423

0 commit comments

Comments
 (0)