1 /*
2  * Licensed to the Apache Software Foundation (ASF) under one
3  * or more contributor license agreements.  See the NOTICE file
4  * distributed with this work for additional information
5  * regarding copyright ownership.  The ASF licenses this file
6  * to you under the Apache License, Version 2.0 (the
7  * "License"); you may not use this file except in compliance
8  * with the License.  You may obtain a copy of the License at
9  *
10  *     http://www.apache.org/licenses/LICENSE-2.0
11  *
12  * Unless required by applicable law or agreed to in writing,
13  * software distributed under the License is distributed on an
14  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15  * KIND, either express or implied.  See the License for the
16  * specific language governing permissions and limitations
17  * under the License.
18  */
19 module hunt.shiro.authc.AbstractAuthenticator;
20 
21 import hunt.shiro.authc.Authenticator;
22 import hunt.shiro.authc.AuthenticationInfo;
23 import hunt.shiro.authc.AuthenticationListener;
24 import hunt.shiro.authc.AuthenticationToken;
25 import hunt.shiro.authc.LogoutAware;
26 
27 import hunt.shiro.Exceptions;
28 import hunt.shiro.subject.PrincipalCollection;
29 
30 import hunt.Exceptions;
31 import hunt.logging.ConsoleLogger;
32 import hunt.collection;
33 
34 
35 /**
36  * Superclass for almost all {@link Authenticator} implementations that performs the common work around authentication
37  * attempts.
38  * <p/>
39  * This class delegates the actual authentication attempt to subclasses but supports notification for
40  * successful and failed logins as well as logouts. Notification is sent to one or more registered
41  * {@link AuthenticationListener AuthenticationListener}s to allow for custom processing logic
42  * when these conditions occur.
43  * <p/>
44  * In most cases, the only thing a subclass needs to do (via its {@link #doAuthenticate} implementation)
45  * is perform the actual principal/credential verification process for the submitted {@code AuthenticationToken}.
46  *
47  */
48 abstract class AbstractAuthenticator : Authenticator, LogoutAware {
49 
50     /*-------------------------------------------
51     |             C O N S T A N T S             |
52     ============================================*/
53     /**
54      * Private class log instance.
55      */
56 
57 
58     /*-------------------------------------------
59     |    I N S T A N C E   V A R I A B L E S    |
60     ============================================*/
61     /**
62      * Any registered listeners that wish to know about things during the authentication process.
63      */
64     private Collection!(AuthenticationListener) listeners;
65 
66     /*-------------------------------------------
67     |         C O N S T R U C T O R S           |
68     ============================================*/
69 
70     /**
71      * Default no-argument constructor. Ensures the internal
72      * {@link AuthenticationListener AuthenticationListener} collection is a non-null {@code ArrayList}.
73      */
74      this() {
75         listeners = new ArrayList!(AuthenticationListener)();
76     }
77 
78     /*--------------------------------------------
79     |  A C C E S S O R S / M O D I F I E R S    |
80     ============================================*/
81 
82     /**
83      * Sets the {@link AuthenticationListener AuthenticationListener}s that should be notified during authentication
84      * attempts.
85      *
86      * @param listeners one or more {@code AuthenticationListener}s that should be notified due to an
87      *                  authentication attempt.
88      */
89     //@SuppressWarnings({"UnusedDeclaration"})
90      void setAuthenticationListeners(Collection!(AuthenticationListener) listeners) {
91         if (listeners is null) {
92             this.listeners = new ArrayList!(AuthenticationListener)();
93         } else {
94             this.listeners = listeners;
95         }
96     }
97 
98     /**
99      * Returns the {@link AuthenticationListener AuthenticationListener}s that should be notified during authentication
100      * attempts.
101      *
102      * @return the {@link AuthenticationListener AuthenticationListener}s that should be notified during authentication
103      *         attempts.
104      */
105     //@SuppressWarnings({"UnusedDeclaration"})
106      Collection!(AuthenticationListener) getAuthenticationListeners() {
107         return this.listeners;
108     }
109 
110     /*-------------------------------------------
111     |               M E T H O D S               |
112     ============================================*/
113 
114     /**
115      * Notifies any registered {@link AuthenticationListener AuthenticationListener}s that
116      * authentication was successful for the specified {@code token} which resulted in the specified
117      * {@code info}.  This implementation merely iterates over the internal {@code listeners} collection and
118      * calls {@link AuthenticationListener#onSuccess(AuthenticationToken, AuthenticationInfo) onSuccess}
119      * for each.
120      *
121      * @param token the submitted {@code AuthenticationToken} that resulted in a successful authentication.
122      * @param info  the returned {@code AuthenticationInfo} resulting from the successful authentication.
123      */
124     protected void notifySuccess(AuthenticationToken token, AuthenticationInfo info) {
125         foreach(AuthenticationListener listener ; this.listeners) {
126             listener.onSuccess(token, info);
127         }
128     }
129 
130     /**
131      * Notifies any registered {@link AuthenticationListener AuthenticationListener}s that
132      * authentication failed for the
133      * specified {@code token} which resulted in the specified {@code ae} exception.  This implementation merely
134      * iterates over the internal {@code listeners} collection and calls
135      * {@link AuthenticationListener#onFailure(AuthenticationToken, AuthenticationException) onFailure}
136      * for each.
137      *
138      * @param token the submitted {@code AuthenticationToken} that resulted in a failed authentication.
139      * @param ae    the resulting {@code AuthenticationException} that caused the authentication to fail.
140      */
141     protected void notifyFailure(AuthenticationToken token, AuthenticationException ae) {
142         foreach(AuthenticationListener listener ; this.listeners) {
143             listener.onFailure(token, ae);
144         }
145     }
146 
147     /**
148      * Notifies any registered {@link AuthenticationListener AuthenticationListener}s that a
149      * {@code Subject} has logged-out.  This implementation merely
150      * iterates over the internal {@code listeners} collection and calls
151      * {@link AuthenticationListener#onLogout(hunt.shiro.subject.PrincipalCollection) onLogout}
152      * for each.
153      *
154      * @param principals the identifying principals of the {@code Subject}/account logging out.
155      */
156     protected void notifyLogout(PrincipalCollection principals) {
157         foreach(AuthenticationListener listener ; this.listeners) {
158             listener.onLogout(principals);
159         }
160     }
161 
162     /**
163      * This implementation merely calls
164      * {@link #notifyLogout(hunt.shiro.subject.PrincipalCollection) notifyLogout} to allow any registered listeners
165      * to react to the logout.
166      *
167      * @param principals the identifying principals of the {@code Subject}/account logging out.
168      */
169      void onLogout(PrincipalCollection principals) {
170         notifyLogout(principals);
171     }
172 
173     /**
174      * Implementation of the {@link Authenticator} interface that functions in the following manner:
175      * <ol>
176      * <li>Calls template {@link #doAuthenticate doAuthenticate} method for subclass execution of the actual
177      * authentication behavior.</li>
178      * <li>If an {@code AuthenticationException} is thrown during {@code doAuthenticate},
179      * {@link #notifyFailure(AuthenticationToken, AuthenticationException) notify} any registered
180      * {@link AuthenticationListener AuthenticationListener}s of the exception and then propagate the exception
181      * for the caller to handle.</li>
182      * <li>If no exception is thrown (indicating a successful login),
183      * {@link #notifySuccess(AuthenticationToken, AuthenticationInfo) notify} any registered
184      * {@link AuthenticationListener AuthenticationListener}s of the successful attempt.</li>
185      * <li>Return the {@code AuthenticationInfo}</li>
186      * </ol>
187      *
188      * @param token the submitted token representing the subject's (user's) login principals and credentials.
189      * @return the AuthenticationInfo referencing the authenticated user's account data.
190      * @throws AuthenticationException if there is any problem during the authentication process - see the
191      *                                 interface's JavaDoc for a more detailed explanation.
192      */
193      final AuthenticationInfo authenticate(AuthenticationToken token){
194 
195         if (token is null) {
196             throw new IllegalArgumentException("Method argument (authentication token) cannot be null.");
197         }
198 
199         version(HUNT_SHIRO_DEBUG) tracef("Authentication attempt received for token [%s]", token);
200 
201         AuthenticationInfo info;
202         try {
203             info = doAuthenticate(token);
204             if (info is null) {
205                 string msg = "No account information found for authentication token [" ~ 
206                         (cast(Object)token).toString() ~ "] by this " ~
207                         "Authenticator instance.  Please check that it is configured correctly.";
208                 throw new AuthenticationException(msg);
209             }
210         } catch (Throwable t) {
211             warning(t.msg);
212             version(HUNT_DEBUG) warning(t);
213             AuthenticationException ae = null;
214             auto tCast = cast(AuthenticationException)t;
215             if (tCast !is null) {
216                 ae = tCast;
217             }
218             if (ae is null) {
219                 //Exception thrown was not an expected AuthenticationException.  Therefore it is probably a little more
220                 //severe or unexpected.  So, wrap in an AuthenticationException, log to warn, and propagate:
221                 string msg = "Authentication failed for token submission [" ~ 
222                         (cast(Object)token).toString() ~ "].  Possible unexpected " ~
223                         "error? (Typical or expected login exceptions should extend from AuthenticationException).";
224                 ae = new AuthenticationException(msg, t);
225             }
226             try {
227                 notifyFailure(token, ae);
228             } catch (Throwable t2) {
229                 version(HUNT_DEBUG) {
230                     string msg = "Unable to send notification for failed authentication attempt - listener error?.  " ~
231                             "Please check your AuthenticationListener implementation(s).  Logging sending exception " ~
232                             "and propagating original AuthenticationException instead...";
233                     warning(msg, t2);
234                 }
235             }
236 
237 
238             throw ae;
239         }
240 
241         version(HUNT_SHIRO_DEBUG) {
242             infof("Authentication successful for token [%s].  Returned account [%s]", 
243                 token, info);
244         }
245 
246         notifySuccess(token, info);
247 
248         return info;
249     }
250 
251     /**
252      * Template design pattern hook for subclasses to implement specific authentication behavior.
253      * <p/>
254      * Common behavior for most authentication attempts is encapsulated in the
255      * {@link #authenticate} method and that method invokes this one for custom behavior.
256      * <p/>
257      * <b>N.B.</b> Subclasses <em>should</em> throw some kind of
258      * {@code AuthenticationException} if there is a problem during
259      * authentication instead of returning {@code null}.  A {@code null} return value indicates
260      * a configuration or programming error, since {@code AuthenticationException}s should
261      * indicate any expected problem (such as an unknown account or username, or invalid password, etc).
262      *
263      * @param token the authentication token encapsulating the user's login information.
264      * @return an {@code AuthenticationInfo} object encapsulating the user's account information
265      *         important to Shiro.
266      * @throws AuthenticationException if there is a problem logging in the user.
267      */
268     protected abstract AuthenticationInfo doAuthenticate(AuthenticationToken token);
269 
270 
271 }