diff --git a/examples/google-github-login/.gitignore b/examples/google-github-login/.gitignore new file mode 100644 index 0000000..b4ffaf3 --- /dev/null +++ b/examples/google-github-login/.gitignore @@ -0,0 +1 @@ +google-github-login-example diff --git a/examples/google-github-login/README.md b/examples/google-github-login/README.md new file mode 100644 index 0000000..8ec3f1f --- /dev/null +++ b/examples/google-github-login/README.md @@ -0,0 +1,13 @@ +# Google-Github Example + +If you want to try running this example, please register as a Github and Google developer and create apps. Be sure to use the correct redirect URIs (check `oauthimpl.d`). +Lastly, you need to export the environment credentials to the app as follows: + +```sh +export GITHUB_OAUTH_CLIENTID="..." +export GITHUB_OAUTH_CLIENTSECRET="..." + +export GOOGLE_OAUTH_CLIENTID="foo.apps.googleusercontent.com" +export GOOGLE_OAUTH_CLIENTSECRET="..." +export GOOGLE_OAUTH_PROJECTID="project123..." +``` diff --git a/examples/google-github-login/dub.json b/examples/google-github-login/dub.json new file mode 100644 index 0000000..47208a8 --- /dev/null +++ b/examples/google-github-login/dub.json @@ -0,0 +1,16 @@ +{ + "name" : "google-github-login-example", + "dependencies" : { + "oauth" : { + "path" : "..\/..\/" + }, + "vibe-d:http" : "*", + "vibe-d:core" : "*", + "vibe-d:data" : "*", + "vibe-d:web" : "*", + "vibe-d:mongodb" : "*" + }, + "versions" : [ + "VibeDefaultMain" + ] +} diff --git a/examples/google-github-login/source/app.d b/examples/google-github-login/source/app.d new file mode 100644 index 0000000..c00e90c --- /dev/null +++ b/examples/google-github-login/source/app.d @@ -0,0 +1,57 @@ +import vibe.http.router : URLRouter; +import vibe.http.server : HTTPServerSettings, listenHTTP, render; +import vibe.data.json : Json; +import vibe.http.session : MemorySessionStore; + +shared static this() +{ + import vibe.core.log; + setLogLevel(LogLevel.debug_); + + auto router = new URLRouter; + + router.get("/api/user/logout", (scope req, scope res) { + res.terminateSession(); + logDebug("user logged out"); + res.redirect("/"); + }); + + import oauthimpl : registerOAuth, isLoggedIn; + router.registerOAuth; + + // example route to dump the current session + with(router) { + get("/api/session", (req, res) { + if (req.session && req.session.isKeySet("user")) + res.writeJsonBody(req.session.get!Json("user")); + else + res.writeBody("Empty Session"); + }); + } + + // load and init a permanent user storage + import std.process : environment; + import vibe.db.mongo.mongo : connectMongoDB; + import users : UserController, users, User; + auto host = environment.get("APP_MONGO_URL", "mongodb://localhost"); + auto dbName = environment.get("APP_MONGO_DB", "hackback"); + auto db = connectMongoDB(host).getDatabase(dbName); + users = new UserController(db); + + import std.typecons : Nullable; + // A simple main page + with(router) { + get("/", (req, res) { + Nullable!User user; + if (req.session && req.session.isKeySet("user")) + user = req.session.get!User("user"); + + res.render!("index.dt", user); + }); + } + + auto settings = new HTTPServerSettings; + settings.port = 8080; + settings.sessionStore = new MemorySessionStore; + listenHTTP(settings, router); +} diff --git a/examples/google-github-login/source/oauthimpl.d b/examples/google-github-login/source/oauthimpl.d new file mode 100644 index 0000000..a174ec3 --- /dev/null +++ b/examples/google-github-login/source/oauthimpl.d @@ -0,0 +1,151 @@ +import oauth.settings : OAuthSettings; +import oauth.webapp : OAuthWebapp; + +import vibe.http.router : URLRouter; +import vibe.http.client : requestHTTP; +import vibe.http.server : HTTPServerRequest, HTTPServerResponse; +import vibe.data.json : Json; + +OAuthWebapp webapp; +immutable(OAuthSettings) googleOAuthSettings; +immutable(OAuthSettings) githubOAuthSettings; +string finalRedirectUri; + +/++ +Load OAuth configuration from environment variables. ++/ +auto loadFromEnvironment( + string providerName, + string envPrefix, + string redirectUri, +) +{ + import std.process : environment; + string clientId = environment[envPrefix ~ "_CLIENTID"]; + string clientSecret = environment[envPrefix ~ "_CLIENTSECRET"]; + + return new immutable(OAuthSettings)( + providerName, + clientId, + clientSecret, + redirectUri); +} + +shared static this() +{ + import oauth.provider.github; + import oauth.provider.google; + import std.process : environment; + + webapp = new OAuthWebapp; + + // oauth stuff + // TODO: make callback uri configureable + googleOAuthSettings = loadFromEnvironment("google", "GOOGLE_OAUTH", "http://localhost:8080/api/user/login/google"); + githubOAuthSettings = loadFromEnvironment("github", "GITHUB_OAUTH", "http://localhost:8080/api/user/login/github"); + finalRedirectUri = "/"; +} + +string[] googleScopes = ["https://www.googleapis.com/auth/userinfo.email", + "https://www.googleapis.com/auth/userinfo.profile"]; + +string[] githubScopes = ["user:email"]; + +import users : User, users; + +bool isLoggedIn(scope HTTPServerRequest req) @safe { + if (!req.session) + return false; + + if (req.session.isKeySet("user")) + return true; + + return false; +} + +void registerOAuth(scope URLRouter router) +{ + router.get("/api/user/login/error", (req, res) { + res.writeBody("An error happened"); + }); + + router.get("/api/user/login/google", (req, res) @safe { + // TODO: necessary? + if (isLoggedIn(req)) + { + return res.redirect(finalRedirectUri); + } + else if (webapp.login(req, res, googleOAuthSettings, null, googleScopes)) + { + // TODO: oauth.session is fetched from session store (was set in webapp.login) + auto session = webapp.oauthSession(req, googleOAuthSettings); + requestHTTP( + "https://www.googleapis.com/userinfo/v2/me", + (scope googleReq) { session.authorizeRequest(googleReq); }, + (scope googleRes) { + auto userInfo = googleRes.readJson(); + if ("error" !in userInfo) + { + User user = { + email: userInfo["email"].get!string, + name: userInfo["name"].get!string, + avatarUrl: userInfo["picture"].get!string, + googleId: userInfo["id"].get!string + }; + user = users.loginOrSignup!"googleId"(user); + req.session.set("user", user); + assert(isLoggedIn(req)); + return res.redirect(finalRedirectUri); + } + res.redirect("/api/user/login/error"); + }); + } + }); + + router.get("/api/user/login/github", (req, res) { + // TODO: necessary? + if (isLoggedIn(req)) + { + return res.redirect(finalRedirectUri); + } + else if (webapp.login(req, res, githubOAuthSettings, null, githubScopes)) + { + // TODO: oauth.session is fetched from session store (was set in webapp.login) + auto session = webapp.oauthSession(req, githubOAuthSettings); + requestHTTP( + "https://api.github.com/user", + delegate (scope githubReq) { + githubReq.headers["Accept"] = "application/vnd.github.v3+json"; + session.authorizeRequest(githubReq); + }, + delegate (scope githubRes) { + auto userInfo = githubRes.readJson(); + + // TODO: join requests! + requestHTTP( + "https://api.github.com/user/emails", + delegate (scope githubReq) { + githubReq.headers["Accept"] = "application/vnd.github.v3+json"; + session.authorizeRequest(githubReq); }, + delegate (scope emailRes) { + auto userEmail = emailRes.readJson(); + + import vibe.http.common : enforceBadRequest; + enforceBadRequest(userEmail.length >= 1, "At least one email expected"); + + User user = { + name: userInfo["name"].get!string, + email: userEmail[0]["email"].get!string, + avatarUrl: userInfo["avatar_url"].get!string, + githubId: userInfo["id"].get!long, + }; + user = users.loginOrSignup!"githubId"(user); + req.session.set("user", user); + + assert(isLoggedIn(req)); + res.redirect(finalRedirectUri); + }); + }); + } + }); +} diff --git a/examples/google-github-login/source/users.d b/examples/google-github-login/source/users.d new file mode 100644 index 0000000..34e6f2a --- /dev/null +++ b/examples/google-github-login/source/users.d @@ -0,0 +1,56 @@ +import vibe.data.bson : BsonObjectID; +import vibe.db.mongo.mongo; +import std.typecons : tuple; + +struct User +{ + import vibe.data.serialization : dbName = name; + @dbName("_id") BsonObjectID id; + string email; + string name; + long githubId; + string googleId; + string avatarUrl; +} + +// TLS instance +UserController users; + +class UserController +{ + MongoCollection m_users; + + this(MongoDatabase db) + { + m_users = db["users"]; + + m_users.ensureIndex([tuple("googleId", 1)], IndexFlags.unique | IndexFlags.sparse); + m_users.ensureIndex([tuple("githubId", 1)], IndexFlags.unique | IndexFlags.sparse); + } + + User loginOrSignup(string providerId)(User user) + { + auto u = m_users.findOne!User([providerId: mixin("user." ~ providerId)]); + if (!u.isNull) + { + // TODO: should we update attributes? + return u.get; + } + else + { + return addUser(user); + } + } + + User addUser(User user) + { + user.id = BsonObjectID.generate(); + m_users.insert(user); + return user; + } + + void updateToken(string id, string token) + { + m_users.update(["id": id], ["$set": token]); + } +} diff --git a/examples/google-github-login/views/index.dt b/examples/google-github-login/views/index.dt new file mode 100644 index 0000000..eb15da7 --- /dev/null +++ b/examples/google-github-login/views/index.dt @@ -0,0 +1,13 @@ +- import vibe.data.json : serializeToPrettyJson; +doctype html +html + head + title Google-Github authentication example + body + - if (user.isNull) + a(href="/api/user/login/github") Login with GitHub + br + a(href="/api/user/login/google") Login with Google + - else + a(href="/api/user/logout") Logout + div #{ user.get.serializeToPrettyJson}