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 }