Commit | Line | Data |
714e2bdc |
1 | package Catalyst::Authentication::Realm::Progressive; |
2 | |
3 | use Carp; |
4 | use warnings; |
5 | use strict; |
6 | |
7 | use base 'Catalyst::Authentication::Realm'; |
8 | |
3a311407 |
9 | use Try::Tiny; |
10 | use Scalar::Util 'blessed'; |
11 | |
714e2bdc |
12 | =head1 NAME |
13 | |
14 | Catalyst::Authentication::Realm::Progressive - Authenticate against multiple realms |
15 | |
16 | =head1 SYNOPSIS |
17 | |
d02b2273 |
18 | This Realm allows an application to use a single authenticate() call during |
64db792b |
19 | which multiple realms are used and tried incrementally until one performs |
20 | a successful authentication is accomplished. |
714e2bdc |
21 | |
d02b2273 |
22 | A simple use case is a Temporary Password that looks and acts exactly as a |
23 | regular password. Without changing the authentication code, you can |
714e2bdc |
24 | authenticate against multiple realms. |
25 | |
d02b2273 |
26 | Another use might be to support a legacy website authentication system, trying |
27 | the current auth system first, and upon failure, attempting authentication against |
28 | the legacy system. |
29 | |
714e2bdc |
30 | =head2 EXAMPLE |
31 | |
32 | If your application has multiple realms to authenticate, such as a temporary |
33 | password realm and a normal realm, you can configure the progressive realm as |
34 | the default, and configure it to iteratively call the temporary realm and then |
35 | the normal realm. |
36 | |
37 | __PACKAGE__->config( |
38 | 'Plugin::Authentication' => { |
39 | default_realm => 'progressive', |
40 | realms => { |
41 | progressive => { |
42 | class => 'Progressive', |
43 | realms => [ 'temp', 'normal' ], |
44 | # Modify the authinfo passed into authenticate by merging |
45 | # these hashes into the realm's authenticate call: |
46 | authinfo_munge => { |
64db792b |
47 | normal => { 'type' => 'normal' }, |
48 | temp => { 'type' => 'temporary' }, |
714e2bdc |
49 | } |
50 | }, |
a55abc15 |
51 | normal => { |
714e2bdc |
52 | credential => { |
53 | class => 'Password', |
54 | password_field => 'secret', |
55 | password_type => 'hashed', |
56 | password_hash_type => 'SHA-1', |
57 | }, |
58 | store => { |
59 | class => 'DBIx::Class', |
055630f7 |
60 | user_model => 'Schema::Person::Identity', |
714e2bdc |
61 | id_field => 'id', |
62 | } |
63 | }, |
64 | temp => { |
65 | credential => { |
66 | class => 'Password', |
67 | password_field => 'secret', |
68 | password_type => 'hashed', |
69 | password_hash_type => 'SHA-1', |
70 | }, |
71 | store => { |
72 | class => 'DBIx::Class', |
055630f7 |
73 | user_model => 'Schema::Person::Identity', |
714e2bdc |
74 | id_field => 'id', |
75 | } |
76 | }, |
77 | } |
78 | } |
64db792b |
79 | ); |
714e2bdc |
80 | |
81 | Then, in your controller code, to attempt authentication against both realms |
82 | you just have to do a simple authenticate call: |
83 | |
84 | if ( $c->authenticate({ id => $username, password => $password }) ) { |
d02b2273 |
85 | if ( $c->user->type eq 'temporary' ) { |
714e2bdc |
86 | # Force user to change password |
87 | } |
88 | } |
89 | |
90 | =head1 CONFIGURATION |
91 | |
92 | =over |
93 | |
94 | =item realms |
95 | |
64db792b |
96 | An array reference consisting of each realm to attempt authentication against, |
714e2bdc |
97 | in the order listed. If the realm does not exist, calling authenticate will |
98 | die. |
99 | |
100 | =item authinfo_munge |
101 | |
102 | A hash reference keyed by realm names, with values being hash references to |
103 | merge into the authinfo call that is subsequently passed into the realm's |
104 | authenticate method. This is useful if your store uses the same class for each |
64db792b |
105 | realm, separated by some other token (in the L<EXAMPLE> authinfo_mungesection, |
714e2bdc |
106 | the 'realm' is a column on C<Schema::Person::Identity> that will be either |
107 | 'temp' or 'local', to ensure the query to fetch the user finds the right |
108 | Identity record for that realm. |
109 | |
110 | =back |
111 | |
112 | =head1 METHODS |
113 | |
3dae968a |
114 | =head2 new ($realmname, $config, $app) |
115 | |
116 | Constructs an instance of this realm. |
117 | |
714e2bdc |
118 | =head2 authenticate |
119 | |
120 | This method iteratively calls each realm listed in the C<realms> configuration |
121 | key. It returns after the first successful authentication call is done. |
122 | |
123 | =cut |
124 | |
125 | sub authenticate { |
126 | my ( $self, $c, $authinfo ) = @_; |
127 | my $realms = $self->config->{realms}; |
128 | carp "No realms to authenticate against, check configuration" |
3a311407 |
129 | unless $realms; |
714e2bdc |
130 | carp "Realms configuration must be an array reference" |
3a311407 |
131 | unless ref $realms eq 'ARRAY'; |
132 | |
133 | my $should_detach; |
134 | foreach my $realm_name (@$realms) { |
135 | my $realm = $c->get_auth_realm($realm_name); |
64db792b |
136 | carp "Unable to find realm: $realm_name, check configuration" |
3a311407 |
137 | unless $realm; |
138 | my $auth = {%$authinfo}; |
714e2bdc |
139 | $auth->{realm} ||= $realm->name; |
3a311407 |
140 | if ( my $info = $self->config->{authinfo_munge}->{ $realm->name } ) { |
141 | $auth = Catalyst::Utils::merge_hashes( $auth, $info ); |
714e2bdc |
142 | } |
3a311407 |
143 | |
144 | my $auth_obj; |
145 | try { |
146 | $auth_obj = $realm->authenticate( $c, $auth ); |
147 | } |
148 | catch { |
82134370 |
149 | if ( $_ eq $Catalyst::DETACH ) { |
3a311407 |
150 | $should_detach = 1; |
151 | } |
152 | else { |
153 | die $_; |
154 | } |
155 | }; |
156 | |
157 | if ($auth_obj) { |
158 | $c->set_authenticated( $auth_obj, $realm->name ); |
159 | return $auth_obj; |
714e2bdc |
160 | } |
161 | } |
3a311407 |
162 | |
163 | # detach if detach is thrown like from Credential::HTTP |
164 | $c->detach if $should_detach; |
165 | |
714e2bdc |
166 | return; |
167 | } |
168 | |
64db792b |
169 | ## we can not rely on inheriting new() because in this case we do not |
170 | ## load a credential or store, which is what new() sets up in the |
6657dfec |
171 | ## standard realm. So we have to create our realm object, set our name |
172 | ## and return $self in order to avoid nasty warnings. |
173 | |
174 | sub new { |
3a311407 |
175 | my ( $class, $realmname, $config, $app ) = @_; |
6657dfec |
176 | |
177 | my $self = { config => $config }; |
178 | bless $self, $class; |
64db792b |
179 | |
6657dfec |
180 | $self->name($realmname); |
181 | return $self; |
182 | } |
183 | |
714e2bdc |
184 | =head1 AUTHORS |
185 | |
186 | J. Shirley C<< <jshirley@cpan.org> >> |
187 | |
188 | Jay Kuri C<< <jayk@cpan.org> >> |
189 | |
3a311407 |
190 | =head1 CONTRIBUTORS |
191 | |
192 | Gavin Henry C<< <ghenry@surevoip.co.uk> >> |
193 | |
194 | Tomas Doran C<< <bobtfish@bobtfish.net> >> |
195 | |
714e2bdc |
196 | =head1 COPYRIGHT & LICENSE |
197 | |
3a311407 |
198 | Copyright (c) 2008-2011 the aforementioned authors. All rights reserved. This program |
714e2bdc |
199 | is free software; you can redistribute it and/or modify it under the same terms |
200 | as Perl itself. |
201 | |
202 | =cut |
203 | |
204 | 1; |