1 module vibeauth.authenticators.EmberSimpleAuth;
2 
3 import vibe.inet.url;
4 import vibe.http.router;
5 import vibe.http.server;
6 import vibe.data.json;
7 
8 import vibeauth.authenticators.BaseAuth;
9 import vibeauth.router.responses;
10 import vibeauth.collections.usercollection;
11 import vibeauth.data.user;
12 
13 import std.datetime;
14 
15 /// Authentication using cookie storage for ember simple auth library.
16 /// http://ember-simple-auth.com/
17 class EmberSimpleAuth : BaseAuth {
18 
19   /// Instantiate the authenticator with an user collection
20   this(UserCollection userCollection) {
21     super(userCollection);
22   }
23 
24   ///
25   private AuthResult updateContext(HTTPServerRequest req, string bearer) {
26     User user;
27 
28     try {
29       user = collection.byToken(bearer);
30 
31       req.username = user.id;
32       req.context["email"] = user.email;
33     } catch(Exception) {
34       return AuthResult.invalidToken;
35     }
36 
37     return AuthResult.success;
38   }
39 
40   override {
41     /// Auth handler that will fail if a successfull auth was not performed.
42     /// This handler is usefull for routes that want to hide information to the
43     /// public.
44     void mandatoryAuth(HTTPServerRequest req, HTTPServerResponse res) {
45       super.mandatoryAuth(req, res);
46     }
47 
48     /// ditto
49     AuthResult mandatoryAuth(HTTPServerRequest req) {
50       if(!req.hasValidEmberSession) {
51         return AuthResult.unauthorized;
52       }
53 
54       Json data = req.sessionData;
55 
56       if(data.type != Json.Type.object || "authenticated" !in data || "access_token" !in data["authenticated"]) {
57         return AuthResult.unauthorized;
58       }
59 
60       string bearer = data["authenticated"]["access_token"].to!string;
61 
62       return updateContext(req, bearer);
63     }
64 
65     /// Auth handler that fails only if the auth fields are present and are not valid.
66     /// This handler is usefull when a route should return different data when the user is
67     /// logged in
68     void permisiveAuth(HTTPServerRequest req, HTTPServerResponse res) {
69       super.permisiveAuth(req, res);
70     }
71 
72     /// ditto
73     AuthResult permisiveAuth(HTTPServerRequest req) {
74       if("ember_simple_auth-session" !in req.cookies) {
75         return AuthResult.success;
76       }
77 
78       Json data = req.sessionData;
79 
80       if(data.type != Json.Type.object) {
81         return AuthResult.invalidToken;
82       }
83 
84       if("authenticated" in data && "access_token" !in data["authenticated"]) {
85         return AuthResult.success;
86       }
87 
88       string bearer = data["authenticated"]["access_token"].to!string;
89       return updateContext(req, bearer);
90     }
91 
92     ///
93     void respondUnauthorized(HTTPServerResponse res) {
94       vibeauth.router.responses.respondUnauthorized(res);
95     }
96 
97     ///
98     void respondInvalidToken(HTTPServerResponse res) {
99       vibeauth.router.responses.respondUnauthorized(res, "Invalid token.");
100     }
101   }
102 }
103 
104 version(unittest) {
105   import fluent.asserts;
106   import vibeauth.data.token;
107   import vibeauth.collections.usermemory;
108 
109   UserMemoryCollection collection;
110   User user;
111 
112   EmberSimpleAuth auth;
113   Token refreshToken;
114   Token bearerToken;
115 
116   auto testRouter(bool requireLogin = true) {
117     auto router = new URLRouter();
118 
119     collection = new UserMemoryCollection(["doStuff"]);
120     user = new User("user@gmail.com", "password");
121     user.firstName = "John";
122     user.lastName = "Doe";
123     user.username = "test";
124     user.id = 1;
125 
126     collection.add(user);
127 
128     bearerToken = collection.createToken("user@gmail.com", Clock.currTime + 3600.seconds, ["doStuff"], "Bearer");
129 
130     auth = new EmberSimpleAuth(collection);
131 
132     if(requireLogin) {
133       router.any("*", &auth.mandatoryAuth);
134     } else {
135       router.any("*", &auth.permisiveAuth);
136     }
137 
138     void handleRequest(HTTPServerRequest req, HTTPServerResponse res) {
139       res.statusCode = 200;
140       res.writeBody("Hello, World!");
141     }
142 
143     void showEmail(HTTPServerRequest req, HTTPServerResponse res) {
144       res.statusCode = 200;
145       res.writeBody(req.context["email"].get!string);
146     }
147 
148     router.get("/sites", &handleRequest);
149     router.get("/email", &showEmail);
150 
151     return router;
152   }
153 }
154 
155 /// with mandatory auth it should return 401 on missing cookie or useragent
156 unittest {
157   testRouter.request.get("/sites").expectStatusCode(401).end();
158 }
159 
160 /// with mandatory auth it should return 200 on valid credentials
161 unittest {
162   auto router = testRouter;
163 
164   router
165     .request.get("/sites")
166     .header("User-Agent", "something")
167     .header("Cookie", "ember_simple_auth-session=%7B%22authenticated%22%3A%7B%22access_token%22%3A%22" ~ bearerToken.name ~ "%22%7D%7D")
168     .expectStatusCode(200)
169     .end;
170 }
171 
172 /// with mandatory auth it should return 401 on invalid credentials
173 unittest {
174   auto router = testRouter;
175 
176   router
177     .request.get("/sites")
178     .header("User-Agent", "something")
179     .header("Cookie", "ember_simple_auth-session=%7B%22authenticated%22%3A%7B%22access_token%22%3A%22%22%7D%7D")
180     .expectStatusCode(401)
181     .end;
182 }
183 
184 /// with mandatory auth it should return 401 on missing access token
185 unittest {
186   auto router = testRouter;
187 
188   router
189     .request.get("/sites")
190     .header("User-Agent", "something")
191     .header("Cookie", "ember_simple_auth-session=%7B%22authenticated%22%3A%7B%7D%7D")
192     .expectStatusCode(401)
193     .end;
194 }
195 
196 /// with mandatory auth it should return 401 on missing authenticated data
197 unittest {
198   auto router = testRouter;
199 
200   router
201     .request.get("/sites")
202     .header("User-Agent", "something")
203     .header("Cookie", "ember_simple_auth-session=%7B%22authenticated%22%3A%7B%7D%7D")
204     .expectStatusCode(401)
205     .end;
206 }
207 
208 /// with mandatory auth it should return 401 on invalid json
209 unittest {
210   auto router = testRouter;
211 
212   router
213     .request.get("/sites")
214     .header("User-Agent", "something")
215     .header("Cookie", "ember_simple_auth-session=authenticated%22%3A%7B%7D%7D")
216     .expectStatusCode(401)
217     .end;
218 }
219 
220 /// with mandatory auth it should set the email on valid credentials
221 unittest {
222   testRouter
223     .request.get("/email")
224     .header("User-Agent", "something")
225     .header("Cookie", "ember_simple_auth-session=%7B%22authenticated%22%3A%7B%22access_token%22%3A%22" ~ bearerToken.name ~ "%22%7D%7D")
226     .expectStatusCode(200)
227     .end((Response response) => {
228       response.bodyString.should.equal("user@gmail.com");
229     });
230 }
231 
232 /// with permisive auth it should return 200 on missing cookie or useragent
233 unittest {
234   testRouter(false).request.get("/sites").expectStatusCode(200).end();
235 }
236 
237 /// with permisive auth it should return 401 on invalid json
238 unittest {
239   testRouter(false)
240     .request.get("/sites")
241     .header("User-Agent", "something")
242     .header("Cookie", "ember_simple_auth-session=authenticated%22%3A%7B%7D%7D")
243     .expectStatusCode(401)
244     .end;
245 }
246 
247 /// with permisive auth it should return 401 on invalid credentials
248 unittest {
249   testRouter(false)
250     .request.get("/sites")
251     .header("User-Agent", "something")
252     .header("Cookie", "ember_simple_auth-session=%7B%22authenticated%22%3A%7B%22access_token%22%3A%22%22%7D%7D")
253     .expectStatusCode(401)
254     .end;
255 }
256 
257 /// with permisive auth it should return 200 on missing user agent
258 unittest {
259   testRouter(false)
260     .request.get("/sites")
261     .header("Cookie", "ember_simple_auth-session=%7B%22authenticated%22%3A%7B%22access_token%22%3A%22" ~ bearerToken.name ~ "%22%7D%7D")
262     .expectStatusCode(200)
263     .end;
264 }
265 
266 /// with permisive auth it should return 200 on missing token
267 unittest {
268   testRouter(false)
269     .request.get("/sites")
270     .header("Cookie", "ember_simple_auth-session%3D%7B%22authenticated%22%3A%7B%7D%7D")
271     .expectStatusCode(200)
272     .end;
273 }
274 
275 /// with permisive auth it should return 200 on missing ember_simple_auth-session cookie
276 unittest {
277   testRouter(false)
278     .request.get("/sites")
279     .header("User-Agent", "something")
280     .expectStatusCode(200)
281     .end;
282 }
283 
284 /// with permisive auth it should return 200 on valid credentials
285 unittest {
286   testRouter(false)
287     .request.get("/sites")
288     .header("User-Agent", "something")
289     .header("Cookie", "ember_simple_auth-session=%7B%22authenticated%22%3A%7B%22access_token%22%3A%22" ~ bearerToken.name ~ "%22%7D%7D")
290     .expectStatusCode(200)
291     .end;
292 }
293 
294 /// with permisive auth it should return 200 on missing authenticated keys
295 unittest {
296   testRouter(false)
297     .request.get("/sites")
298     .header("User-Agent", "something")
299     .header("Cookie", "ember_simple_auth-session=%7B%22authenticated%22%3A%7B%7D%7D")
300     .expectStatusCode(200)
301     .end;
302 }
303 
304 /// with permisive auth it should set the email on valid credentials
305 unittest {
306   testRouter(false)
307     .request.get("/email")
308     .header("User-Agent", "something")
309     .header("Cookie", "ember_simple_auth-session=%7B%22authenticated%22%3A%7B%22access_token%22%3A%22" ~ bearerToken.name ~ "%22%7D%7D")
310     .expectStatusCode(200)
311     .end((Response response) => {
312       response.bodyString.should.equal("user@gmail.com");
313     });
314 }
315 
316 /// Checks if the request contains the `ember_simple_auth-session` cookie and the `User-Agent` header is set
317 bool hasValidEmberSession(HTTPServerRequest req) {
318   return "ember_simple_auth-session" in req.cookies && "User-Agent" in req.headers;
319 }
320 
321 /// Extract the ember auth session data
322 Json sessionData(HTTPServerRequest req) {
323   Json data = Json.emptyObject;
324 
325   try {
326     data = req.cookies["ember_simple_auth-session"].parseJsonString;
327   } catch(Exception) {
328     return Json();
329   }
330 
331   return data;
332 }