1 package Catalyst::Authentication::Realm::Adaptor;
7 extends 'Catalyst::Authentication::Realm';
11 Catalyst::Authentication::Realm::Adaptor - Adjust parameters of authentication processes on the fly
19 ## goes in catagits@jules.scsys.co.uk:Catalyst-Authentication-Realm-Adaptor.git
21 our $VERSION = '0.02';
24 my ( $self, $c, $authinfo ) = @_;
28 if (exists($self->config->{'credential_adaptor'})) {
30 if ($self->config->{'credential_adaptor'}{'method'} eq 'merge_hash') {
32 $newauthinfo = _munge_hash($authinfo, $self->config->{'credential_adaptor'}{'merge_hash'}, $authinfo);
34 } elsif ($self->config->{'credential_adaptor'}{'method'} eq 'new_hash') {
36 $newauthinfo = _munge_hash({}, $self->config->{'credential_adaptor'}{'new_hash'}, $authinfo);
38 } elsif ($self->config->{'credential_adaptor'}{'method'} eq 'action') {
40 my $controller = $c->controller($self->config->{'credential_adaptor'}{'controller'});
42 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s credential_adaptor tried to use a controller that doesn't exist: " .
43 $self->config->{'credential_adaptor'}{'controller'});
46 my $action = $controller->action_for($self->config->{'credential_adaptor'}{'action'});
48 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s credential_adaptor tried to use an action that doesn't exist: " .
49 $self->config->{'credential_adaptor'}{'controller'} . "->" .
50 $self->config->{'credential_adaptor'}{'action'});
52 $newauthinfo = $c->forward($action, $self->name, $authinfo, $self->config->{'credential_adaptor'});
54 } elsif ($self->config->{'credential_adaptor'}{'method'} eq 'code' ) {
56 if (ref($self->config->{'credential_adaptor'}{'code'}) eq 'CODE') {
57 my $sub = $self->config->{'credential_adaptor'}{'code'};
58 $newauthinfo = $sub->($self->name, $authinfo, $self->config->{'credential_adaptor'});
60 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s credential_adaptor is configured to use a code ref that doesn't exist");
63 return $self->SUPER::authenticate($c, $newauthinfo);
65 return $self->SUPER::authenticate($c, $authinfo);
70 my ( $self, $authinfo, $c ) = @_;
74 if (exists($self->config->{'store_adaptor'})) {
76 if ($self->config->{'store_adaptor'}{'method'} eq 'merge_hash') {
78 $newauthinfo = _munge_hash($authinfo, $self->config->{'store_adaptor'}{'merge_hash'}, $authinfo);
80 } elsif ($self->config->{'store_adaptor'}{'method'} eq 'new_hash') {
82 $newauthinfo = _munge_hash({}, $self->config->{'store_adaptor'}{'new_hash'}, $authinfo);
84 } elsif ($self->config->{'store_adaptor'}{'method'} eq 'action') {
86 my $controller = $c->controller($self->config->{'store_adaptor'}{'controller'});
88 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s store_adaptor tried to use a controller that doesn't exist: " .
89 $self->config->{'store_adaptor'}{'controller'});
92 my $action = $controller->action_for($self->config->{'store_adaptor'}{'action'});
94 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s store_adaptor tried to use an action that doesn't exist: " .
95 $self->config->{'store_adaptor'}{'controller'} . "->" .
96 $self->config->{'store_adaptor'}{'action'});
98 $newauthinfo = $c->forward($action, $self->name, $authinfo, $self->config->{'store_adaptor'});
100 } elsif ($self->config->{'store_adaptor'}{'method'} eq 'code' ) {
102 if (ref($self->config->{'store_adaptor'}{'code'}) eq 'CODE') {
103 my $sub = $self->config->{'store_adaptor'}{'code'};
104 $newauthinfo = $sub->($self->name, $authinfo, $self->config->{'store_adaptor'});
106 Catalyst::Exception->throw(__PACKAGE__ . " realm: " . $self->name . "'s store_adaptor is configured to use a code ref that doesn't exist");
109 return $self->SUPER::find_user($newauthinfo, $c);
111 return $self->SUPER::find_user($authinfo, $c);
116 my ($sourcehash, $modhash, $referencehash) = @_;
118 my $resulthash = { %{$sourcehash} };
120 foreach my $key (keys %{$modhash}) {
121 if (ref($modhash->{$key}) eq 'HASH') {
122 if (ref($sourcehash->{$key}) eq 'HASH') {
123 $resulthash->{$key} = _munge_hash($sourcehash->{$key}, $modhash->{$key}, $referencehash)
125 $resulthash->{$key} = _munge_hash({}, $modhash->{$key}, $referencehash);
128 if (ref($modhash->{$key} eq 'ARRAY') && ref($sourcehash->{$key}) eq 'ARRAY') {
129 push @{$resulthash->{$key}}, _munge_value($modhash->{$key}, $referencehash)
131 $resulthash->{$key} = _munge_value($modhash->{$key}, $referencehash);
132 if (ref($resulthash->{$key}) eq 'SCALAR' && ${$resulthash->{$key}} eq '-') {
133 ## Scalar reference to a string '-' means delete the element from the source array.
134 delete($resulthash->{$key});
142 my ($modvalue, $referencehash) = @_;
145 if ($modvalue =~ m/^([+-])\((.*)\)$/) {
149 if ($action eq '+') {
150 ## action = string '-' means delete the element from the source array.
151 ## otherwise it means copy it from a field in the original hash with nesting
152 ## indicated via '.' - IE similar to Template Toolkit handling of nested hashes
153 my @hashpath = split /\./, $keypath;
154 my $val = $referencehash;
155 foreach my $subkey (@hashpath) {
156 if (ref($val) eq 'HASH') {
157 $val = $val->{$subkey};
158 } elsif (ref($val) eq 'ARRAY') {
159 $val = $val->[$subkey];
161 ## failed to find that key in the hash / array
168 ## delete the value... so we return a scalar ref to '-'
171 } elsif (ref($modvalue) eq 'ARRAY') {
173 foreach my $row (0..$#{$modvalue}) {
174 if (defined($modvalue->[$row])) {
175 my $val = _munge_value($modvalue->[$row], $referencehash);
176 ## this is the first time I've ever wanted to use unless
177 ## to make things clearer
178 unless (ref($val) eq 'SCALAR' && ${$val} eq '-') {
179 $newvalue->[$row] = $val;
184 $newvalue = $modvalue;
192 The Catalyst::Authentication::Realm::Adaptor allows for modification of
193 authentication parameters within the catalyst application. It's basically a
194 filter used to adjust authentication parameters globally within the
195 application or to adjust user retrieval parameters provided by the credential
196 in order to be compatible with a different store. It provides for better
197 control over interaction between credentials and stores. This is particularly
198 useful when working with external authentication such as OpenID or OAuth.
201 'Plugin::Authentication' => {
206 password_field => 'secret',
207 password_type => 'hashed',
208 password_hash_type => 'SHA-1',
211 class => 'DBIx::Class',
212 user_class => 'Schema::Person',
215 method => 'merge_hash',
217 status => [ 'temporary', 'active' ]
226 The above example ensures that no matter how $c->authenticate() is called
227 within your application, the key 'status' is added to the authentication hash.
228 This allows you to, among other things, set parameters that should always be
229 applied to your authentication process or modify the parameters to better
230 connect a credential and a store that were not built to work together. In the
231 above example, we are making sure that the user search is restricted to those
232 with a status of either 'temporary' or 'active.'
234 This realm works by intercepting the original authentication information
235 between the time C<< $c->authenticate($authinfo) >> is called and the time the
236 realm's C<< $realm->authenticate($c,$authinfo) >> method is called, allowing for
237 the $authinfo parameter to be modified or replaced as your application
238 requires. It can also operate after the call to the credential's
239 C<authenticate()> method but before the call to the store's C<find_user>
242 If you don't know what the above means, you probably do not need this module.
246 The configuration for this module goes within your realm configuration alongside your
247 credential and store options.
249 This module can operate in two points during authentication processing.
250 The first is prior the realm's C<authenticate> call (immediately after the call to
251 C<< $c->authenticate() >>.) To operate here, your filter options should go in a hash
252 under the key C<credential_adaptor>.
254 The second point is after the call to credential's C<authenticate> method but
255 immediately before the call to the user store's C<find_user> method. To operate
256 prior to C<find_user>, your filter options should go in a hash under the key
259 The filtering options for both points are the same, and both the C<store_adaptor> and
260 C<credential_adaptor> can be used simultaneously in a single realm.
264 There are four ways to configure your filters. You specify which one you want by setting
265 the C<method> configuration option to one of the following: C<merge_hash>, C<new_hash>,
266 C<code>, or C<action>. You then provide the additional information based on which method
267 you have chosen. The different options are described below.
273 credential_adaptor => {
274 method => 'merge_hash',
276 status => [ 'temporary', 'active' ]
280 This causes the original authinfo hash to be merged with a hash provided by
281 the realm configuration under the key C<merge_hash> key. This is a deep merge
282 and in the case of a conflict, the hash specified by merge_hash takes
283 precedence over what was passed into the authenticate or find_user call. The
284 method of merging is described in detail in the L<HASH MERGING> section below.
289 method => 'new_hash',
291 username => '+(user)', # this sets username to the value of $originalhash{user}
292 user_source => 'openid'
296 This causes the original authinfo hash to be set aside and replaced with a new hash provided under the
297 C<new_hash> key. The new hash can grab portions of the original hash. This can be used to remap the authinfo
298 into a new format. See the L<HASH MERGING> section for information on how to do this.
305 my ($realmname, $original_authinfo, $hashref_to_config ) = @_;
306 my $newauthinfo = {};
312 The C<code> method allows for more complex filtering by executing code
313 provided as a subroutine reference in the C<code> key. The realm name,
314 original auth info and the portion of the config specific to this filter are
315 passed as arguments to the provided subroutine. In the above example, it would
316 be the entire store_adaptor hash. If you were using a code ref in a
317 credential_adaptor, you'd get the credential_adapter config instead.
321 credential_adaptor => {
323 controller => 'UserProcessing',
324 action => 'FilterCredentials'
327 The C<action> method causes the adaptor to delegate filtering to a Catalyst
328 action. This is similar to the code ref above, except that instead of simply
329 calling the routine, the action specified is called via C<<$c->forward>>. The
330 arguments passed to the action are the same as the code method as well,
331 namely the realm name, the original authinfo hash and the config for the adaptor.
337 The hash merging mechanism in Catalyst::Authentication::Realm::Adaptor is not
338 a simple merge of two hashes. It has some niceties which allow for both
339 re-mapping of existing keys, and a mechanism for removing keys from the
340 original hash. When using the 'merge_hash' method above, the keys from the
341 original hash and the keys for the merge hash are simply combined with the
342 merge_hash taking precedence in the case of a key conflict. If there are
343 sub-hashes they are merged as well.
345 If both the source and merge hash contain an array for a given hash-key, the
346 values in the merge array are appended to the original array. Note that hashes
347 within arrays will not be merged, and will instead simply be copied.
349 Simple values are left intact, and in the case of a key existing in both
350 hashes, the value from the merge_hash takes precedence. Note that in the case
351 of a key conflict where the values are of different types, the value from the
352 merge_hash will be used and no attempt is made to merge or otherwise convert
355 =head2 Advanced merging
357 Whether you are using C<merge_hash> or C<new_hash> as the method, you have access
358 to the values from the original authinfo hash. In your new or merged hash, you
359 can use values from anywhere within the original hash. You do this by setting
360 the value for the key you want to set to a special string indicating the key
361 path in the original hash. The string is formatted as follows:
362 C<<'+(key1.key2.key3)'>> This will grab the hash associated with key1, retrieve the hash
363 associated with key2, and finally obtain the value associated with key3. This is easier to
364 show than to explain:
370 haircolor => 'black',
371 favoritenumbers => [ 17, 42, 19 ]
377 # would result in a value of 'black'
378 haircolor => '+(user.details.haircolor)',
380 # bestnumber would be 42.
381 bestnumber => '+(user.details.favoritenumbers.1)'
384 Given the example above, the value for the userage key would be 27, (obtained
385 via C<<'+(user.details.age)'>>) and the value for bestnumber would be 42. Note
386 that you can traverse both hashes and arrays using this method. This can be
387 quite useful when you need the values that were passed in, but you need to put
388 them under different keys.
390 When using the C<merge_hash> method, you sometimes may want to remove an item
391 from the original hash. You can do this by providing a key in your merge_hash
392 at the same point, but setting it's value to '-()'. This will remove the key
393 entirely from the resultant hash. This works better than simply setting the
394 value to undef in some cases.
396 =head1 NOTES and CAVEATS
398 The authentication system for Catalyst is quite flexible. In most cases this
399 module is not needed. Evidence of this fact is that the Catalyst auth system
400 was substantially unchanged for 2+ years prior to this modules first release.
401 If you are looking at this module, then there is a good chance your problem would
402 be better solved by adjusting your credential or store directly.
404 That said, there are some areas where this module can be particularly useful.
405 For example, this module allows for global application of additional arguments
406 to authinfo for a certain realm via your config. It also allows for preliminary
407 testing of alternate configs before you adjust every C<< $c->authenticate() >> call
408 within your application.
410 It is also useful when combined with the various external authentication
411 modules available, such as OpenID, OAuth or Facebook. These modules expect to
412 store their user information in the Hash provided by the Minimal user store.
413 Often, however, you want to store user information locally in a database or
414 other storage mechanism. Doing this lies somewhere between difficult and
415 impossible normally. With the Adapter realm, you can massage the authinfo hash
416 between the credential's verification and the creation of the local user, and
417 instead use the information returned to look up a user instead.
419 Using the external auth mechanisms and the C<action> method, you can actually
420 trigger an action to create a user record on the fly when the user has
421 authenticated via an external method. These are just some of the possibilities
422 that Adaptor provides that would otherwise be very difficult to accomplish,
423 even with Catalyst's flexible authentication system.
425 With all of that said, caution is warranted when using this module. It modifies
426 the behavior of the application in ways that are not obvious and can therefore
427 lead to extremely hard to track-down bugs. This is especially true when using
428 the C<action> filter method. When a developer calls C<< $c->authenticate() >>
429 they are not expecting any actions to be called before it returns.
431 If you use the C<action> method, I strongly recommend that you use it only as a
432 filter routine and do not do other catalyst dispatch related activities (such as
433 further forwards, detach's or redirects). Also note that it is B<EXTREMELY
434 DANGEROUS> to call authentication routines from within a filter action. It is
435 extremely easy to accidentally create an infinite recursion bug which can crash
436 your Application. In short - B<DON'T DO IT>.
440 Jay Kuri, C<< <jayk at cpan.org> >>
444 Please report any bugs or feature requests to C<bug-catalyst-authentication-realm-adaptor at rt.cpan.org>, or through
445 the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Catalyst-Authentication-Realm-Adaptor>. I will be notified, and then you'll
446 automatically be notified of progress on your bug as I make changes.
451 You can find documentation for this module with the perldoc command.
453 perldoc Catalyst::Authentication::Realm::Adaptor
455 You can also look for information at:
461 L<http://search.cpan.org/dist/Catalyst-Authentication-Realm-Adaptor/>
463 =item * Catalyzed.org Wiki
465 L<http://wiki.catalyzed.org/cpan-modules/Catalyst-Authentication-Realm-Adaptor>
470 =head1 ACKNOWLEDGEMENTS
473 =head1 COPYRIGHT & LICENSE
475 Copyright 2009 Jay Kuri, all rights reserved.
477 This program is free software; you can redistribute it and/or modify it
478 under the same terms as Perl itself.
483 1; # End of Catalyst::Authentication::Realm::Adaptor