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