Whenever an app asks me to make a new account, I generally exit and uninstall the app. Sometimes it gets a pass if the sign up experience is streamlined. One of the requirements to have a streamlined sign up flow is to not have to create yet another email/username-password combo. And the way to get around having to create yet another email/username-password combo is to have a login with functionality.
So, how do you that code wise?
I will use Github as the provider example
The flow - Server side - Vert.x
There are 2 endpoints that need to be implemented server side. The first one redirects to the provider’s login page and the other is what the provider will redirect to after a succesfull login and where we get the user’s information.
I am using the Scribe library to make it easier to do the auth flow
First we need an OAuth Service object using our client ID and secret - which you get when you register for an oauth app with Github
1 2 3 4 5 6
privatefuncreateGithubOAuthService(): OAuth20Service { return ServiceBuilder("client-id") .apiSecret("secret") .callback("http://localhost:9090/api/github/callback") // this is where github will redirect the user to .build(GitHubApi.instance()) }
Once the object is created we can use it to get the redirect URL that will take the user to the Github login page
The frontend makes a request to /api/github/login and the backend will get the authorization url for github and redirect to there
The user then enters login information after which Github will redirect the browser back to our API to the endpoint specified, which is the second endpoint we need on the server side
privatefungithubApiCallbackHandler(routingContext: RoutingContext) { // Github will redirect to this endpoint with a query parameter `code` that has an auth token that we will use to get the user's data from github val accessToken = githubService.getAccessToken(routingContext.queryParam("code").first()) val request = OAuthRequest(Verb.GET, "https://api.github.com/user") // sign the request with the access token githubService.signRequest(accessToken, request) // make a request to github to get the user's information // the combination of provider and id can be used as a unique identifier to determine the user // then you can check your local datastore to see if this is a new user - sign up or returning user - login // in this example I will just add the user to an in memory map and issue a jwt token val response = githubService.execute(request) val jsonObject = JsonObject(response.body)
// create a JWT token for the user and add it as a cookie routingContext.response().addCookie(createCookie("api", createJwtForUser(jsonObject.getString("login"))))
// put the login name to the user auth data in a map that the frontend will fetch later userMap[jsonObject.getString("login")] = jsonObject // redirect back to the 'home' page of the frontend routingContext.redirect("/web") }
// auth middleware router.route().handler { authMiddleware(it) } // a /me route that returns self user data router.get("/me").handler { getSelfDetailsHandler(it) }
privatefunauthMiddleware(routingContext: RoutingContext) { // check if cookie exists val cookie = routingContext.request().getCookie("api") if (cookie == null) { // no cookie means unauthorized to call this endpoint - return 401 routingContext.response().setStatusCode(401).end() return } // cookie exists - now check if jwt is valid val verifier = JWT.require(jwtAlgorithm).withIssuer("api").build() val decodedJWTResult = runCatching { verifier.verify(cookie.value) } if (decodedJWTResult.isFailure) { // invalid jwt token return 401 routingContext.response().setStatusCode(401).end() return } // jwt token is valid - add the user's information to the context for downstream handlers to have access to routingContext.data()["user"] = decodedJWTResult.getOrThrow().subject routingContext.next() }
privatefungetSelfDetailsHandler(routingContext: RoutingContext) { // get the user's information from the map and send it as a json object routingContext.response() .end(userMap.getOrDefault(routingContext.data()["user"].toString(), JsonObject()).encodePrettily()) }
The flow - client side - Vue.js
Vue.js is built into static files in the dist folder, which can then be served via Vertx as well
1 2 3 4 5 6 7 8 9 10
val staticHandler = StaticHandler.create("github-login/dist") val assets = StaticHandler.create("github-login/dist/assets") // serves the favicon router.route("/favicon.ico").handler(staticHandler) // serves the assets router.route("/assets*").handler(assets) // serves the vue.js app router.route("/web").handler(staticHandler) // redirect / to the main vue js app router.route("/").handler { it.redirect("/web") }
The frontend is divided into 3 main ‘views’
Home view which is the landing page that everyone can see
Login view which is where the user can sign in via the login via github functionality
A protected About view which only logged in users can see and shows the user information received from Github
Vue router is used to define the routes and attach navigation guards
let isAuth = isAuthenticated(); // redirect to home if going to login page while already logged in if (isAuth && to.name == "login") { router.push("/home"); returntrue; }
// login and home pages are allowed for everyone if (to.name === "login" || to.name === "home") { returntrue; }
if (isAuth) { returntrue; }
// is going to a protected route - redirect to login page if (!isAuth && (to.name !== "login" || to.name !== "home")) { return { name: "login" }; }
The home view is a simple landing page that has a basic text message
1
<h1class="is-size-4">Welcome to auth example!</h1>
The login view will have a single button that calls the server side login endpoint - calling this endpoint redirects the vue js app to the github login page
1
<h4>Please <ahref="/api/github/login">login via github</a> to continue</h4>
The about view calls the protected /me route to grab the user information from the server side and show it
Keep it easy for the user - Let users login via facebook or google
Don’t make the sign up process difficult for your app. Make it so that it is a single button press that creates the user an account and takes them to the main app. Not only will it give you better conversion rates it will also help bypass many security pitfalls that can happen when trying to run your own auth implementation.