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.ConsoleLogger;
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         version(HUNT_DEBUG) {
123             if(!isEnabled()) {
124                 string msg = format("No guard avaliable for %s", _guardName);
125                 throw new AuthenticationException(msg);
126             }
127         }
128         return _guard;
129     }
130 
131     Identity signIn(string name, string password, bool remember = false) {
132         _user.authenticate(name, password, remember);
133 
134         _remember = remember;
135         _state = AuthState.SignIn;
136 
137         if(!_user.isAuthenticated()) 
138             return _user;
139 
140         if(scheme == AuthenticationScheme.Bearer) {
141             UserDetails userDetails = _user.userDetails();
142             string salt = userDetails.salt;
143             // UserService userService = guard().userService();
144             // string salt = userService.getSalt(name, password);
145             
146             uint exp = guard().tokenExpiration; // config().auth.tokenExpiration;
147 
148             JSONValue claims;
149             claims["user_id"] = _user.id;
150 
151             Claim[] userClaims = _user.claims();
152 
153             foreach(Claim c; userClaims) {
154                 string claimName = toJwtClaimName(c.type());
155                 Variant value = c.value;
156                 if(TypeUtils.isIntegral(value.type))
157                     claims[claimName] = JSONValue(c.value.get!(long));
158                 else if(TypeUtils.isUsignedIntegral(value.type))
159                     claims[claimName] = JSONValue(c.value.get!(ulong));
160                 else if(TypeUtils.isFloatingPoint(value.type))
161                     claims[claimName] = JSONValue(c.value.get!(float));
162                 else 
163                     claims[claimName] = JSONValue(c.value.toString());
164             }
165 
166             _token = JwtUtil.sign(name, salt, exp.seconds, claims);
167             _isEnabled = true;
168         } else if(scheme == AuthenticationScheme.Basic) {
169             string str = name ~ ":" ~ password;
170             ubyte[] data = cast(ubyte[])str;
171             _token = cast(string)Base64.encode(data);
172             _isEnabled = true;
173         } else {
174             error("Unsupported AuthenticationScheme: %s", scheme);
175             _isEnabled = false;
176         }
177 
178         return _user;
179     }
180 
181     static string toJwtClaimName(string name) {
182         switch(name) {
183             case  ClaimTypes.Name: 
184                 return JwtRegisteredClaimNames.Sub;
185 
186             case  ClaimTypes.Nickname: 
187                 return JwtRegisteredClaimNames.Nickname;
188 
189             case  ClaimTypes.GivenName: 
190                 return JwtRegisteredClaimNames.GivenName;
191 
192             case  ClaimTypes.Surname: 
193                 return JwtRegisteredClaimNames.FamilyName;
194 
195             case  ClaimTypes.Email: 
196                 return JwtRegisteredClaimNames.Email;
197 
198             case  ClaimTypes.Gender: 
199                 return JwtRegisteredClaimNames.Gender;
200 
201             case  ClaimTypes.DateOfBirth: 
202                 return JwtRegisteredClaimNames.Birthdate;
203             
204             default:
205                 return name;
206         }
207     }
208 
209     /// Use token to login
210     Identity signIn() {
211         scope(success) {
212             _state = AuthState.Token;
213         }
214 
215         Guard g = guard();
216         
217         version(HUNT_DEBUG) infof("guard: %s, type: %s", g.name, typeid(g));
218 
219         AuthenticationToken token = g.getToken(_request);
220         _user.login(token);
221         _isEnabled = true;
222         return _user;
223     }
224 
225     void signOut() {
226         _state = AuthState.SignOut;
227         _token = null;
228         _remember = false;
229         _isLogout = true;
230         
231         if(scheme != AuthenticationScheme.Basic && scheme != AuthenticationScheme.Bearer) {
232             warningf("Unsupported authentication scheme: %s", scheme);
233         }
234 
235         if(_user.isAuthenticated()) {
236             _user.logout();
237         }
238     }
239 
240     string refreshToken(string salt) {
241         string username = _user.name();
242         if(!_user.isAuthenticated()) {
243             throw new AuthenticationException( format("The use is not authenticated: %s", _user.name()));
244         }
245 
246         if(scheme == AuthenticationScheme.Bearer) {
247             // UserService userService = serviceContainer().resolve!UserService();
248             // FIXME: Needing refactor or cleanup -@zhangxueping at 2020-07-17T11:10:18+08:00
249             // 
250             // string salt = userService.getSalt(username, "no password");
251             _token = JwtUtil.sign(username, salt);
252         } 
253         
254         _state = AuthState.Token;
255         _isTokenRefreshed = true;
256         return _token;
257     }
258 
259     // the token value for the "remember me" session.
260     string token() {
261         // autoDetect();
262         if(_token.empty) {
263             AuthenticationToken token = guard().getToken(_request);
264             if(token !is null)
265                 _token = token.getPrincipal();
266         }
267         return _token;
268     }
269   
270     AuthenticationScheme scheme() {
271         return guard().authScheme();
272     }
273 
274     bool canRememberMe() {
275         return _remember;
276     }
277 
278     bool isTokenRefreshed() {
279         return _isTokenRefreshed;
280     }
281 
282     bool isLogout() {
283         return _isLogout;
284     }
285 
286     void touchSession() {
287         _user.touchSession();
288     }
289 
290 }