1 module hunt.framework.auth.Identity;
2 
3 import hunt.framework.auth.UserDetails;
4 import hunt.framework.auth.Claim;
5 import hunt.framework.auth.ClaimTypes;
6 import hunt.framework.auth.JwtToken;
7 import hunt.framework.auth.principal;
8 import hunt.framework.config.AuthUserConfig;
9 
10 import hunt.http.AuthenticationScheme;
11 import hunt.logging;
12 import hunt.shiro;
13 
14 import std.algorithm;
15 import std.array;
16 import std.base64;
17 import std.string;
18 import std.variant;
19 
20 /**
21  * User Identity
22  */
23 class Identity {
24     private Subject _subject;
25     private string _guardName;
26     private bool _isValid = false;
27 
28     this(string guardName, bool isValid) {
29         _guardName = guardName;
30         _isValid = isValid;
31     }
32 
33     bool isValid() nothrow {
34         return _isValid;
35     }
36 
37     Subject subject() {
38         if(_isValid) {
39             if(_subject is null) {
40                 _subject = SecurityUtils.getSubject(_guardName);
41             }
42             return _subject;
43         } else {
44             string msg = format("This identity [%s] is invalid!", _guardName);
45             error(msg);
46             return null;
47             // throw new AuthorizationException(msg);
48         }
49     }
50 
51     UserDetails userDetails() nothrow {
52         UserDetails userDetails = null;
53         try {
54             Subject s = subject();
55             if(s is null) {
56                 UserDetails u = new UserDetails();
57                 u.name = "Wrong use name";
58                 return u;
59             }
60             userDetails = cast(UserDetails)s.getPrincipal();
61         } catch(Exception ex) {
62             warning(ex.msg);
63             version(HUNT_AUTH_DEBUG) warning(ex);
64         }
65         return userDetails;
66     }
67 
68     ulong id() nothrow {
69         UserDetails userDetails = userDetails();
70         if(userDetails !is null) {
71             return userDetails.id;
72         }
73         return 0;       
74     }
75 
76     string name() nothrow {
77         UserDetails userDetails = userDetails();
78         if(userDetails !is null) {
79             return userDetails.name;
80         }
81         return "";
82     }
83     
84     AuthenticationScheme authScheme() {
85         Variant var = claim(ClaimTypes.AuthScheme);
86         if(var == null) return AuthenticationScheme.None;
87         return cast(AuthenticationScheme)var.get!string();
88     }
89 
90     string fullName() {
91         return claimAs!(string)(ClaimTypes.FullName);
92     }
93 
94     Variant claim(string type) {
95         Variant v = Variant(null);
96         UserDetails userDetails = userDetails();
97         if(userDetails !is null) {
98             v = userDetails.claim(type);
99         }
100 
101         return v;
102     }
103     
104     T claimAs(T)(string type) {
105         Variant v = claim(type);
106         if(v == null || !v.hasValue()) {
107             return T.init;
108         }
109 
110         return v.get!T();
111     }
112 
113     Claim[] claims() {
114         Claim[] r;
115         
116         UserDetails userDetails = userDetails();
117         if(userDetails !is null) {
118             r = userDetails.claims();
119         }
120 
121         return r;
122     }
123 
124     void authenticate(string username, string password, bool remember = true, 
125             string tokenName = DEFAULT_AUTH_TOKEN_NAME) {
126         version(HUNT_DEBUG) {
127             infof("Authenticating for [%s]", username);
128         }
129 
130         Subject _subject = subject();
131         if(_subject is null) {
132             string msg = format("Failed to authenticate for %s", username);
133             errorf(msg);
134             throw new AuthenticationException(msg);
135             // return;
136         }
137 
138         version(HUNT_AUTH_DEBUG) { 
139             tracef("Checking the status at first: %s", _subject.isAuthenticated());
140         }
141 
142         if (_subject.isAuthenticated()) {
143             _subject.logout();
144         }
145 
146         UsernamePasswordToken token = new UsernamePasswordToken(username, password);
147         token.setRememberMe(remember);
148         token.name = tokenName;
149 
150         try {
151             _subject.login(token);
152         } catch (UnknownAccountException ex) {
153             info("There is no user with username of " ~ token.getPrincipal());
154         } catch (IncorrectCredentialsException ex) {
155             info("Password for account " ~ token.getPrincipal() ~ " was incorrect!");
156         } catch (LockedAccountException ex) {
157             info("The account for username " ~ token.getPrincipal()
158                     ~ " is locked.  " ~ "Please contact your administrator to unlock it.");
159         } catch (AuthenticationException ex) {
160             errorf("Authentication failed: ", ex.msg);
161             version(HUNT_DEBUG) error(ex);
162         } catch (Exception ex) {
163             errorf("Authentication failed: ", ex.msg);
164             version(HUNT_DEBUG) error(ex);
165         }
166     }
167 
168     void authenticate(string token, AuthenticationScheme scheme) {
169         version(HUNT_AUTH_DEBUG) {
170             infof("scheme: %s", scheme);
171         }
172 
173         if(scheme == AuthenticationScheme.Bearer) {
174             bearerLogin(token);
175         } else if(scheme == AuthenticationScheme.Basic) {
176             basicLogin(token);
177         } else {
178             warningf("Unknown AuthenticationScheme: %s", scheme);
179         }
180     }
181 
182     private void basicLogin(string tokenString) {
183         ubyte[] decoded = Base64.decode(tokenString);
184         string[] values = split(cast(string)decoded, ":");
185         if(values.length != 2) {
186             warningf("Wrong token: %s", values);
187             return;
188         }
189 
190         string username = values[0];
191         string password = values[1];
192         authenticate(username, password, true);
193     }
194 
195     private void bearerLogin(string tokenString) {
196         try {
197             JwtToken token = new JwtToken(tokenString);
198             subject().login(token);
199         } catch (AuthenticationException e) {
200             warning(e.msg);
201             version(HUNT_AUTH_DEBUG) warning(e);
202         } catch(Exception ex) {
203             warning(ex.msg);
204             version(HUNT_DEBUG) warning(ex);
205         }
206     }
207 
208     bool login(AuthenticationToken token) {
209         Subject sj = subject();
210         version(HUNT_AUTH_DEBUG) { 
211             tracef("Checking the status at first: %s", _subject.isAuthenticated());
212         }
213 
214         if (sj.isAuthenticated()) {
215             sj.logout();
216         }        
217         sj.logout();
218 
219         if(token is null) {
220             warning("The token is null");
221             return false;
222         }
223 
224 
225         try {
226             // FIXME: Needing refactor or cleanup -@zhangxueping at 2020-08-01T16:13:55+08:00
227             // Session with id [b82d265a-ec96-406b-854c-0a846e690aff] has expired
228             sj.login(token);
229         } catch (AuthenticationException e) {
230             version(HUNT_DEBUG) warning(e.msg);
231             version(HUNT_AUTH_DEBUG) warning(e);
232         } catch(Exception ex) {
233             version(HUNT_DEBUG) warning(ex.msg);
234             version(HUNT_AUTH_DEBUG) warning(ex);
235         }
236 
237         return sj.isAuthenticated();
238     }
239 
240     bool isAuthenticated() nothrow {
241         try {
242             Subject s = subject();
243             if(s is null)  return false;
244             return s.isAuthenticated();
245         } catch(Exception ex) {
246             warning(ex.msg);
247             version(HUNT_DEBUG) warning(ex);
248             return false;
249         }
250     }
251 
252     bool hasRole(string role) nothrow {
253         try {
254             Subject s = subject();
255             if(s is null)  return false;
256             return s.hasRole(role);
257         } catch(Exception ex) {
258             warning(ex.msg);
259             version(HUNT_DEBUG) warning(ex);
260             return false;
261         }
262     }
263     
264     bool hasAllRoles(string[] roles...) nothrow {
265         try {
266             Subject s = subject();
267             if(s is null)  return false;
268             return s.hasAllRoles(roles);
269         } catch(Exception ex) {
270             warning(ex.msg);
271             version(HUNT_DEBUG) warning(ex);
272             return false;
273         }
274     }
275 
276     bool isPermitted(string[] permissions...) nothrow {
277         
278         // Try to convert all the custom permissions to shiro's ones
279         try {
280             Subject s = subject();
281             if(s is null)  return false;
282 
283             string[] shiroPermissions = permissions.map!(p => p.strip().toShiroPermissions()).array;
284             bool[] resultSet = s.isPermitted(shiroPermissions);
285             foreach(bool r; resultSet ) {
286                 if(!r) return false;
287             }
288         } catch(Exception ex) {
289             warning(ex.msg);
290             version(HUNT_DEBUG) warning(ex);
291             return false;
292         }
293 
294         return true;
295     }
296 
297     void touchSession() nothrow {
298         try {
299 
300             Subject s = subject();
301             if(s is null)  return;
302 
303             Session session = s.getSession(false);
304             if (session !is null) {
305                 try {
306                     session.touch();
307                 } catch (Throwable t) {
308                     error("session.touch() method invocation has failed.  Unable to update " ~
309                             "the corresponding session's last access time based on the incoming request.");
310                     error(t.msg);
311                     version(HUNT_AUTH_DEBUG) warning(t);
312                 }
313             }
314         } catch(Exception ex) {
315             warning(ex.msg);
316             version(HUNT_DEBUG) warning(ex);
317         }
318     }
319 
320     /**
321      * It should be called from the Auth
322      */
323     void logout() nothrow {
324         try {
325             Subject s = subject();
326             if(s is null)  return;
327             s.logout();
328         } catch(Exception ex) {
329             warning(ex.msg);
330             version(HUNT_DEBUG) warning(ex);
331         }
332     }
333 
334     override string toString() {
335         return name(); 
336     }
337 }