Login with Github in Vert.x and Vue.js

Introduction

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
private fun createGithubOAuthService(): 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

1
2
3
4
5
router.route("/api/github/login").handler { apiGithubLoginHandler(it) }

private fun apiGithubLoginHandler(routingContext: RoutingContext) {
routingContext.redirect(githubService.getAuthorizationUrl())
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// keep an inmemory map of github login name to github user data - to show later on the frontend
private val userMap = hashMapOf<String, JsonObject>()

router.route("/api/github/callback").handler { githubApiCallbackHandler(it) }

private fun githubApiCallbackHandler(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")
}

private fun createCookie(name: String, data: String): Cookie {
return Cookie.cookie(name, data)
.setPath("/")
.setSameSite(CookieSameSite.STRICT)
.setSecure(true)
.setMaxAge(86400 * 7)
}

private fun createJwtForUser(userId: String): String {
val jwtAlgorithm = Algorithm.HMAC256("super-secret-key")
return JWT.create().withSubject(userId)
.withExpiresAt(Date.from(Instant.now().plus(7, ChronoUnit.DAYS)))
.withIssuer("api")
.withIssuedAt(Instant.now())
.sign(jwtAlgorithm)
}

That is pretty much it on the server side to get Login with Github working

Then we can attach a protected route and an auth middleware that checks for valid jwt tokens

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// auth middleware
router.route().handler { authMiddleware(it) }
// a /me route that returns self user data
router.get("/me").handler { getSelfDetailsHandler(it) }

private fun authMiddleware(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()
}

private fun getSelfDetailsHandler(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’

  1. Home view which is the landing page that everyone can see
  2. Login view which is where the user can sign in via the login via github functionality
  3. 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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
path: "/home",
name: "home",
component: HomeView,
},
{
path: "/about",
name: "about",
component: AboutView
},
{
path: "/login",
name: "login",
component: Login,
},
],
});

// navigation guards
router.beforeEach((to, from) => {
if (to.path == "/") {
router.push("/home");
return true;
}

let isAuth = isAuthenticated();
// redirect to home if going to login page while already logged in
if (isAuth && to.name == "login") {
router.push("/home");
return true;
}

// login and home pages are allowed for everyone
if (to.name === "login" || to.name === "home") {
return true;
}

if (isAuth) {
return true;
}

// is going to a protected route - redirect to login page
if (!isAuth && (to.name !== "login" || to.name !== "home")) {
return { name: "login" };
}

return false;
});

function isAuthenticated() {
return $cookies.get("api") != null;
}

The main App.vue file will contain the routing to the various pages via Vue Router

1
2
3
4
5
6
7
<div class="navbar-menu">
<div class="navbar-end">
<RouterLink class="navbar-item" to="/home">Home</RouterLink>
<RouterLink v-if="!isAuthenticated()" class="navbar-item" to="/login">Login</RouterLink>
<RouterLink v-if="isAuthenticated()" class="navbar-item" to="/about">About</RouterLink>
</div>
</div>

The home view is a simple landing page that has a basic text message

1
<h1 class="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 <a href="/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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<script>
export default {
data() {
return {
me: "",
};
},
mounted() {
fetch("/me")
.then((response) => response.json())
.then((j) => (this.me = j));
},
};
</script>

<template>
<div class="container">
<h1 class="is-size-4">About page!</h1>
<h1 class="is-size-4">data is {{ me }}</h1>
</div>
</template>

Conclusion

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.