1 module hunt.framework.auth.Auth;
2 
3 import hunt.framework.auth.AuthOptions;
4 import hunt.framework.auth.AuthService;
5 import hunt.framework.auth.Claim;
6 import hunt.framework.auth.ClaimTypes;
7 import hunt.framework.auth.guard;
8 import hunt.framework.auth.Identity;
9 import hunt.framework.auth.JwtToken;
10 import hunt.framework.auth.JwtUtil;
11 import hunt.framework.auth.UserService;
12 import hunt.framework.auth.UserDetails;
13 import hunt.framework.http.Request;
14 // import hunt.framework.Simplify;
15 import hunt.framework.provider.ServiceProvider;
16 
17 import hunt.http.AuthenticationScheme;
18 import hunt.logging;
19 import hunt.shiro.Exceptions;
20 import hunt.shiro.authc.AuthenticationToken;
21 import hunt.util.TypeUtils;
22 
23 import hunt.jwt.JwtRegisteredClaimNames;
24 
25 import std.algorithm;
26 import std.array : split;
27 import std.base64;
28 import std.json;
29 import std.format;
30 import std.range;
31 import std.variant;
32 import core.time;
33 
34 private enum AuthState {
35     Auto,
36     Token,
37     SignIn,
38     SignOut
39 }
40 
41 
42 /**
43  * 
44  */
45 class Auth {
46     
47     private Identity _user;
48     private string _token;
49     private bool _remember = false;
50     private bool _isTokenRefreshed = false;
51     private bool _isLogout = false;
52     private AuthState _state = AuthState.Auto;
53     private string _guardName = DEFAULT_GURAD_NAME;
54     private Guard _guard;
55     private bool _isEnabled = false;
56 
57     private Request _request;
58     
59     this(Request request) {
60         _request = request;
61         _guardName = request.guardName();
62         AuthService authService = serviceContainer().resolve!AuthService();
63         
64         _guard = authService.guard(_guardName);
65         _isEnabled = isGuardAvailable();
66         _user = new Identity(_guardName, _isEnabled);
67 
68         version(HUNT_AUTH_DEBUG) {
69             if(_isEnabled) {
70                 warningf("path: %s, isAuthenticated: %s", request.path(), _user.isAuthenticated());
71             }
72         }
73     }
74 
75     bool isGuardAvailable() {
76         return _guard !is null;
77     }
78 
79     bool isEnabled() {
80         return _isEnabled;
81     }
82 
83     string tokenCookieName() {
84         return guard().tokenCookieName();
85     }
86 
87     // void autoDetect() {
88     //     if(_state != AuthState.Auto || !isEnabled()) 
89     //         return;
90 
91     //     version(HUNT_DEBUG) {
92     //         infof("Detecting the authentication state from %s", tokenCookieName());
93     //     }
94         
95     //     AuthenticationScheme scheme = guard().authScheme();
96     //     if(scheme == AuthenticationScheme.None)
97     //         scheme = AuthenticationScheme.Bearer;
98 
99     //     // Detect the auth type automatically
100     //     if(scheme == AuthenticationScheme.Bearer) {
101     //         _token = _request.bearerToken();
102     //     } else if(scheme == AuthenticationScheme.Basic) {
103     //         _token = _request.basicToken();
104     //     }
105 
106     //     if(_token.empty()) { // Detect the token from cookie
107     //         _token = request.cookie(tokenCookieName());
108     //     } 
109 
110     //     if(!_token.empty()) {
111     //         _user.authenticate(_token, scheme);
112     //     }
113 
114     //     _state = AuthState.Token;
115     // }
116 
117     Identity user() {
118         return _user;
119     }
120 
121     Guard guard() {
122         if(!isEnabled()) {
123             string msg = format("No guard avaliable for %s", _guardName);
124             throw new AuthenticationException(msg);
125         }
126         return _guard;
127     }
128 
129     Identity signIn(string name, string password, bool remember = false) {
130         _user.authenticate(name, password, remember);
131 
132         _remember = remember;
133         _state = AuthState.SignIn;
134 
135         if(!_user.isAuthenticated()) 
136             return _user;
137 
138         if(scheme == AuthenticationScheme.Bearer) {
139             UserDetails userDetails = _user.userDetails();
140             string salt = userDetails.salt;
141             // UserService userService = guard().userService();
142             // string salt = userService.getSalt(name, password);
143             
144             uint exp = guard().tokenExpiration; // config().auth.tokenExpiration;
145 
146             JSONValue claims;
147             claims["user_id"] = _user.id;
148 
149             Claim[] userClaims = _user.claims();
150 
151             foreach(Claim c; userClaims) {
152                 string claimName = toJwtClaimName(c.type());
153                 Variant value = c.value;
154                 if(TypeUtils.isIntegral(value.type))
155                     claims[claimName] = JSONValue(c.value.get!(long));
156                 else if(TypeUtils.isUsignedIntegral(value.type))
157                     claims[claimName] = JSONValue(c.value.get!(ulong));
158                 else if(TypeUtils.isFloatingPoint(value.type))
159                     claims[claimName] = JSONValue(c.value.get!(float));
160                 else 
161                     claims[claimName] = JSONValue(c.value.toString());
162             }
163 
164             _token = JwtUtil.sign(name, salt, exp.seconds, claims);
165             _isEnabled = true;
166         } else if(scheme == AuthenticationScheme.Basic) {
167             string str = name ~ ":" ~ password;
168             ubyte[] data = cast(ubyte[])str;
169             _token = cast(string)Base64.encode(data);
170             _isEnabled = true;
171         } else {
172             error("Unsupported AuthenticationScheme: %s", scheme);
173             _isEnabled = false;
174         }
175 
176         return _user;
177     }
178 
179     static string toJwtClaimName(string name) {
180         switch(name) {
181             case  ClaimTypes.Name: 
182                 return JwtRegisteredClaimNames.Sub;
183 
184             case  ClaimTypes.Nickname: 
185                 return JwtRegisteredClaimNames.Nickname;
186 
187             case  ClaimTypes.GivenName: 
188                 return JwtRegisteredClaimNames.GivenName;
189 
190             case  ClaimTypes.Surname: 
191                 return JwtRegisteredClaimNames.FamilyName;
192 
193             case  ClaimTypes.Email: 
194                 return JwtRegisteredClaimNames.Email;
195 
196             case  ClaimTypes.Gender: 
197                 return JwtRegisteredClaimNames.Gender;
198 
199             case  ClaimTypes.DateOfBirth: 
200                 return JwtRegisteredClaimNames.Birthdate;
201             
202             default:
203                 return name;
204         }
205     }
206 
207     /// Use token to login
208     Identity signIn() {
209         scope(success) {
210             _state = AuthState.Token;
211         }
212 
213         Guard g = guard();
214         
215         version(HUNT_DEBUG) infof("guard: %s, type: %s", g.name, typeid(g));
216 
217         AuthenticationToken token = g.getToken(_request);
218         _user.login(token);
219         _isEnabled = true;
220         return _user;
221     }
222 
223     void signOut() {
224         _state = AuthState.SignOut;
225         _token = null;
226         _remember = false;
227         _isLogout = true;
228         
229         if(scheme != AuthenticationScheme.Basic && scheme != AuthenticationScheme.Bearer) {
230             warningf("Unsupported authentication scheme: %s", scheme);
231         }
232 
233         if(_user.isAuthenticated()) {
234             _user.logout();
235         }
236     }
237 
238     string refreshToken(string salt) {
239         string username = _user.name();
240         if(!_user.isAuthenticated()) {
241             throw new AuthenticationException( format("The use is not authenticated: %s", _user.name()));
242         }
243 
244         if(scheme == AuthenticationScheme.Bearer) {
245             // UserService userService = serviceContainer().resolve!UserService();
246             // FIXME: Needing refactor or cleanup -@zhangxueping at 2020-07-17T11:10:18+08:00
247             // 
248             // string salt = userService.getSalt(username, "no password");
249             _token = JwtUtil.sign(username, salt);
250         } 
251         
252         _state = AuthState.Token;
253         _isTokenRefreshed = true;
254         return _token;
255     }
256 
257     // the token value for the "remember me" session.
258     string token() {
259         // autoDetect();
260         if(_token.empty) {
261             AuthenticationToken token = guard().getToken(_request);
262             if(token !is null)
263                 _token = token.getPrincipal();
264         }
265         return _token;
266     }
267   
268     AuthenticationScheme scheme() {
269         return guard().authScheme();
270     }
271 
272     bool canRememberMe() {
273         return _remember;
274     }
275 
276     bool isTokenRefreshed() {
277         return _isTokenRefreshed;
278     }
279 
280     bool isLogout() {
281         return _isLogout;
282     }
283 
284     void touchSession() {
285         _user.touchSession();
286     }
287 
288 }