Support third party login for example GitHub (#222)

Fix #14

### Motivation
This pr is used to support Github authentication. Next, I will add support for user authorization.

### Modifications

* Support authentication by Github
* Add user table
* Add a third-party login page

### Verifying this change

 Add unit test
diff --git a/build.gradle b/build.gradle
index 8c6803e..960354d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -97,6 +97,7 @@
     compile group: 'org.springframework.boot', name: 'spring-boot-devtools', version: springBootVersion
     compile group: 'org.springframework.cloud', name: 'spring-cloud-starter-netflix-zuul', version: springBootVersion
     compile group: 'org.mybatis.spring.boot', name: 'mybatis-spring-boot-starter', version: springMybatisVersion
+    compile group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf', version: springBootVersion
     compile group: 'org.postgresql', name: 'postgresql', version: postgresqlVersion
     compile group: 'org.herddb', name: 'herddb-jdbc', version: herddbVersion
     compile group: 'javax.validation', name: 'validation-api', version: javaxValidationVersion
diff --git a/front-end/src/api/socialsignin.js b/front-end/src/api/socialsignin.js
new file mode 100644
index 0000000..fee4336
--- /dev/null
+++ b/front-end/src/api/socialsignin.js
@@ -0,0 +1,23 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import request from '@/utils/request'
+
+const BASE_URL = '/pulsar-manager/third-party-login'
+
+export function getGithubLoginHost() {
+  return request({
+    url: BASE_URL + `/github/login`,
+    method: 'get'
+  })
+}
diff --git a/front-end/src/icons/svg/github.svg b/front-end/src/icons/svg/github.svg
new file mode 100644
index 0000000..3899712
--- /dev/null
+++ b/front-end/src/icons/svg/github.svg
@@ -0,0 +1 @@
+<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub icon</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
\ No newline at end of file
diff --git a/front-end/src/views/login/index.vue b/front-end/src/views/login/index.vue
index 758714f..3d95039 100644
--- a/front-end/src/views/login/index.vue
+++ b/front-end/src/views/login/index.vue
@@ -53,6 +53,9 @@
       </el-form-item>
 
       <el-button :loading="loading" type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="handleLogin">{{ $t('login.logIn') }}</el-button>
+      <el-button class="thirdparty-button" type="primary" @click="showDialog=true">
+        Or connect with
+      </el-button>
     </el-form>
 
     <el-dialog :title="$t('login.thirdparty')" :visible.sync="showDialog" append-to-body>
@@ -119,6 +122,9 @@
   destroyed() {
     // window.removeEventListener('hashchange', this.afterQRScan)
   },
+  mounted() {
+    window.addEventListener('message', this.handleMessage)
+  },
   methods: {
     showPwd() {
       if (this.passwordType === 'password') {
@@ -127,6 +133,12 @@
         this.passwordType = 'password'
       }
     },
+    handleMessage(event) {
+      const data = event.data
+      if (data.hasOwnProperty('name') && data.hasOwnProperty('accessToken')) {
+        // to do set token, track task https://github.com/apache/pulsar-manager/issues/14
+      }
+    },
     handleLogin() {
       this.$refs.loginForm.validate(valid => {
         if (valid) {
@@ -276,7 +288,12 @@
   .thirdparty-button {
     position: absolute;
     right: 35px;
-    bottom: 28px;
+    bottom: 1px;
+  }
+  @media only screen and (max-width: 470px) {
+    .thirdparty-button {
+      display: none;
+    }
   }
 }
 </style>
diff --git a/front-end/src/views/login/socialsignin.vue b/front-end/src/views/login/socialsignin.vue
index d9bb8d7..51d1735 100644
--- a/front-end/src/views/login/socialsignin.vue
+++ b/front-end/src/views/login/socialsignin.vue
@@ -15,36 +15,33 @@
 -->
 <template>
   <div class="social-signup-container">
-    <div class="sign-btn" @click="wechatHandleClick('wechat')">
-      <span class="wx-svg-container"><svg-icon icon-class="wechat" class="icon"/></span> 微信
-    </div>
-    <div class="sign-btn" @click="tencentHandleClick('tencent')">
-      <span class="qq-svg-container"><svg-icon icon-class="qq" class="icon"/></span> QQ
+    <div class="sign-btn" @click="githubHandleClick('github')">
+      <span class="github-container"><svg-icon icon-class="github" class="icon"/></span> GitHub
     </div>
   </div>
 </template>
 
 <script>
-// import openWindow from '@/utils/openWindow'
+import openWindow from '@/utils/openWindow'
+import { getGithubLoginHost } from '@/api/socialsignin'
 
 export default {
   name: 'SocialSignin',
   methods: {
-    wechatHandleClick(thirdpart) {
-      alert('ok')
-      // this.$store.commit('SET_AUTH_TYPE', thirdpart)
-      // const appid = 'xxxxx'
-      // const redirect_uri = encodeURIComponent('xxx/redirect?redirect=' + window.location.origin + '/auth-redirect')
-      // const url = 'https://open.weixin.qq.com/connect/qrconnect?appid=' + appid + '&redirect_uri=' + redirect_uri + '&response_type=code&scope=snsapi_login#wechat_redirect'
-      // openWindow(url, thirdpart, 540, 540)
-    },
-    tencentHandleClick(thirdpart) {
-      alert('ok')
-      // this.$store.commit('SET_AUTH_TYPE', thirdpart)
-      // const client_id = 'xxxxx'
-      // const redirect_uri = encodeURIComponent('xxx/redirect?redirect=' + window.location.origin + '/auth-redirect')
-      // const url = 'https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=' + client_id + '&redirect_uri=' + redirect_uri
-      // openWindow(url, thirdpart, 540, 540)
+    githubHandleClick(thirdpart) {
+      getGithubLoginHost().then(response => {
+        if (!response.data) return
+        if (response.data.message === 'success') {
+          openWindow(decodeURIComponent(response.data.url), thirdpart, 540, 540)
+        } else {
+          this.$notify({
+            title: 'failed',
+            message: response.data.message,
+            type: 'error',
+            duration: 3000
+          })
+        }
+      })
     }
   }
 }
@@ -62,8 +59,7 @@
       font-size: 24px;
       margin-top: 8px;
     }
-    .wx-svg-container,
-    .qq-svg-container {
+    .github-container {
       display: inline-block;
       width: 40px;
       height: 40px;
@@ -74,7 +70,7 @@
       margin-bottom: 20px;
       margin-right: 5px;
     }
-    .wx-svg-container {
+    .github-container {
       background-color: #8ada53;
     }
     .qq-svg-container {
diff --git a/src/README.md b/src/README.md
index b18eaec..858f52d 100644
--- a/src/README.md
+++ b/src/README.md
@@ -101,3 +101,28 @@
 docker run -it -p 9527:9527 -e REDIRECT_HOST=http://192.168.55.182 -e REDIRECT_PORT=9527 -e DRIVER_CLASS_NAME=org.postgresql.Driver -e URL='jdbc:postgresql://127.0.0.1:5432/pulsar_manager' -e USERNAME=pulsar -e PASSWORD=pulsar -e LOG_LEVEL=DEBUG -e JWT_TOKEN=$JWT_TOKEN -e PRIVATE_KEY=$PRIVATE_KEY -e PUBLIC_KEY=$PUBLIC_KEY -v $PWD:/data -v $PWD/secret-key-path:/pulsar-manager/secret-key-path apachepulsar/pulsar-manager:v0.1.0 /bin/sh
 ```
 
+### Enable Github Login
+
+#### Third party login options
+
+```
+# default empty, current options github
+third.party.login.option=
+```
+
+#### Github login configuration
+
+```
+# The client ID you received from GitHub when you registered https://github.com/settings/applications/new.
+github.client.id=your-client-id
+# The client secret you received from GitHub for your GitHub App.
+github.client.secret=your-client-secret
+github.oauth.host=https://github.com/login/oauth/access_token
+github.user.info=https://api.github.com/user
+github.login.host=https://github.com/login/oauth/authorize
+github.redirect.host=http://localhost:9527
+
+# Expiration time of token for third party platform, unit second.
+# 60 * 60 * 24 * 7
+user.access.token.expire=604800
+```
diff --git a/src/main/java/org/apache/pulsar/manager/controller/ThirdPartyLoginCallbackController.java b/src/main/java/org/apache/pulsar/manager/controller/ThirdPartyLoginCallbackController.java
new file mode 100644
index 0000000..f9f175d
--- /dev/null
+++ b/src/main/java/org/apache/pulsar/manager/controller/ThirdPartyLoginCallbackController.java
@@ -0,0 +1,113 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pulsar.manager.controller;
+
+import com.google.common.collect.Maps;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import io.swagger.annotations.ApiResponse;
+import io.swagger.annotations.ApiResponses;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.pulsar.manager.entity.UserInfoEntity;
+import org.apache.pulsar.manager.service.ThirdPartyLoginService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.ResponseEntity;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+
+/**
+ * Callback function of third party platform login.
+ */
+@Slf4j
+@Controller
+@RequestMapping(value = "/pulsar-manager/third-party-login")
+@Api(description = "Calling the request below this class does not require authentication because " +
+        "the user has not logged in yet.")
+@Validated
+public class ThirdPartyLoginCallbackController {
+
+    @Value("${github.client.id}")
+    private String githubClientId;
+
+    @Value("${github.login.host}")
+    private String githubLoginHost;
+
+    @Value("${github.redirect.host}")
+    private String githubRedirectHost;
+
+    @Autowired
+    private ThirdPartyLoginService thirdPartyLoginService;
+
+    @ApiOperation(value = "When use pass github authentication, Github platform will carry code parameter to call " +
+            "back this address actively. At this time, we can request token and get user information through " +
+            "this code." +
+            "Reference document: https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/")
+    @ApiResponses({
+            @ApiResponse(code = 200, message = "ok"),
+            @ApiResponse(code = 404, message = "Not found"),
+            @ApiResponse(code = 500, message = "Internal server error")
+    })
+    @RequestMapping(value = "/callback/github")
+    public String githubCallbackIndex(Model model, @RequestParam() String code) {
+        Map<String, String> parameters = Maps.newHashMap();
+        parameters.put("code", code);
+        String accessToken = thirdPartyLoginService.getAuthToken(parameters);
+        Map<String, String> authenticationMap = Maps.newHashMap();
+        authenticationMap.put("access_token", accessToken);
+        UserInfoEntity userInfoEntity = thirdPartyLoginService.getUserInfo(authenticationMap);
+        if (userInfoEntity == null) {
+            model.addAttribute("messages", "Authentication failed, please check carefully");
+            model.addAttribute("flag", false);
+            return "index";
+        }
+        model.addAttribute("message", "Authentication successful, logging in");
+        model.addAttribute("flag", true);
+        model.addAttribute("userInfo", userInfoEntity);
+        return "index";
+    }
+
+    @ApiOperation(value = "Github's third-party authorized login address, HTTP GET request, needs to carry " +
+            "client_id and redirect_host parameters. Parameter client_id and redirect_host needs to be applied " +
+            "from github platform https://developer.github.com/apps/building-oauth-apps/creating-an-oauth-app/.")
+    @ApiResponses({
+            @ApiResponse(code = 200, message = "ok"),
+            @ApiResponse(code = 404, message = "Not found"),
+            @ApiResponse(code = 500, message = "Internal server error")
+    })
+    @RequestMapping(value = "/github/login", method = RequestMethod.GET)
+    public @ResponseBody ResponseEntity<Map<String, Object>> getGithubLoginUrl() {
+        Map<String, Object> result = Maps.newHashMap();
+        String url = githubLoginHost + "?client_id=" + githubClientId +
+                "&redirect_host=" + githubRedirectHost + "/pulsar-manager/third-party-login/callback/github";
+        try {
+            result.put("url", URLEncoder.encode(url, StandardCharsets.UTF_8.toString()));
+            result.put("message", "success");
+        } catch (UnsupportedEncodingException e) {
+            log.error("Url encoding failed, please check: [{}]", url);
+            result.put("message", "Url encoding failed, please check:: " + url);
+        }
+        return ResponseEntity.ok(result);
+    }
+}
diff --git a/src/main/java/org/apache/pulsar/manager/dao/UsersRepositoryImpl.java b/src/main/java/org/apache/pulsar/manager/dao/UsersRepositoryImpl.java
new file mode 100644
index 0000000..410c5f1
--- /dev/null
+++ b/src/main/java/org/apache/pulsar/manager/dao/UsersRepositoryImpl.java
@@ -0,0 +1,62 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pulsar.manager.dao;
+
+import com.github.pagehelper.Page;
+import com.github.pagehelper.PageHelper;
+import org.apache.pulsar.manager.entity.UserInfoEntity;
+import org.apache.pulsar.manager.entity.UsersRepository;
+import org.apache.pulsar.manager.mapper.UsersMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public class UsersRepositoryImpl implements UsersRepository {
+
+    private final UsersMapper usersMapper;
+
+    @Autowired
+    public UsersRepositoryImpl(UsersMapper usersMapper) {
+        this.usersMapper = usersMapper;
+    }
+
+    @Override
+    public long save(UserInfoEntity userInfoEntity) {
+        long userId = this.usersMapper.save(userInfoEntity);
+        return userId;
+    }
+
+    @Override
+    public Optional<UserInfoEntity> findByUserName(String name) {
+        return Optional.ofNullable(this.usersMapper.findByUserName(name));
+    }
+
+    @Override
+    public Page<UserInfoEntity> findUsersList(Integer pageNum, Integer pageSize) {
+        PageHelper.startPage(pageNum, pageSize);
+        return this.usersMapper.findUsersList();
+    }
+
+    @Override
+    public void update(UserInfoEntity userInfoEntity) {
+        this.usersMapper.update(userInfoEntity);
+    }
+
+    @Override
+    public void delete(String name) {
+        this.usersMapper.delete(name);
+    }
+}
diff --git a/src/main/java/org/apache/pulsar/manager/entity/GithubAuthEntity.java b/src/main/java/org/apache/pulsar/manager/entity/GithubAuthEntity.java
new file mode 100644
index 0000000..986ce05
--- /dev/null
+++ b/src/main/java/org/apache/pulsar/manager/entity/GithubAuthEntity.java
@@ -0,0 +1,36 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pulsar.manager.entity;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * Github auth information entity.
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+public class GithubAuthEntity {
+
+    @SerializedName("access_token")
+    private String accessToken;
+
+    @SerializedName("token_type")
+    private String token_type;
+
+    private String scope;
+}
diff --git a/src/main/java/org/apache/pulsar/manager/entity/GithubUserInfoEntity.java b/src/main/java/org/apache/pulsar/manager/entity/GithubUserInfoEntity.java
new file mode 100644
index 0000000..0409454
--- /dev/null
+++ b/src/main/java/org/apache/pulsar/manager/entity/GithubUserInfoEntity.java
@@ -0,0 +1,108 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pulsar.manager.entity;
+
+import com.google.gson.annotations.SerializedName;
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * Github user information entity.
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+@Data
+public class GithubUserInfoEntity {
+    private String login;
+    private Long id;
+
+    @SerializedName("node_id")
+    private String nodeId;
+
+    @SerializedName("avatar_url")
+    private String avatarUrl;
+
+    @SerializedName("gravatar_id")
+    private String gravatarId;
+
+    private String url;
+
+    @SerializedName("html_url")
+    private String htmlUrl;
+
+    @SerializedName("followers_url")
+    private String followersUrl;
+
+    @SerializedName("following_url")
+    private String followingUrl;
+
+    @SerializedName("gists_url")
+    private String gistsUrl;
+
+    @SerializedName("starred_url")
+    private String starredUrl;
+
+    @SerializedName("subscriptions_url")
+    private String subscriptionsUrl;
+
+    @SerializedName("organizations_url")
+    private String organizationsUrl;
+
+    @SerializedName("repos_url")
+    private String reposUrl;
+
+    @SerializedName("events_url")
+    private String eventsUrl;
+
+    @SerializedName("received_events_url")
+    private String receivedEventsUrl;
+
+    private String type;
+
+    @SerializedName("site_admin")
+    private String siteAdmin;
+
+    private String name;
+
+    private String company;
+
+    private String blog;
+
+    private String location;
+
+    private String email;
+
+    private String hireable;
+
+    private String bio;
+
+    @SerializedName("public_repos")
+    private Integer publicRepos;
+
+    @SerializedName("public_gists")
+    private Integer publicGists;
+
+    private Long followers;
+
+    private Long following;
+
+    @SerializedName("created_at")
+    private String createdAt;
+
+    @SerializedName("updated_at")
+    private String updatedAt;
+}
diff --git a/src/main/java/org/apache/pulsar/manager/entity/UserInfoEntity.java b/src/main/java/org/apache/pulsar/manager/entity/UserInfoEntity.java
new file mode 100644
index 0000000..bff8d54
--- /dev/null
+++ b/src/main/java/org/apache/pulsar/manager/entity/UserInfoEntity.java
@@ -0,0 +1,38 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pulsar.manager.entity;
+
+import lombok.Data;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+import lombok.Setter;
+
+/**
+ * Pulsar Manager platform entity.
+ */
+@Getter
+@Setter
+@NoArgsConstructor
+@Data
+public class UserInfoEntity {
+    private long userId;
+    private String name;
+    private String description;
+    private String location;
+    private String company;
+    private String phoneNumber;
+    private String email;
+    private String accessToken;
+    private long expire;
+}
diff --git a/src/main/java/org/apache/pulsar/manager/entity/UsersRepository.java b/src/main/java/org/apache/pulsar/manager/entity/UsersRepository.java
new file mode 100644
index 0000000..fa467bb
--- /dev/null
+++ b/src/main/java/org/apache/pulsar/manager/entity/UsersRepository.java
@@ -0,0 +1,58 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pulsar.manager.entity;
+
+import com.github.pagehelper.Page;
+import org.springframework.stereotype.Repository;
+
+import java.util.Optional;
+
+@Repository
+public interface UsersRepository  {
+
+    /**
+     * Create a user.
+     * @param userInfoEntity
+     * @return user id
+     */
+    long save(UserInfoEntity userInfoEntity);
+
+    /**
+     * Get a user information by user name.
+     * @param name The user name
+     * @return UserInfoEntity
+     */
+    Optional<UserInfoEntity> findByUserName(String name);
+
+
+    /**
+     * Get user list, support paging.
+     * @param pageNum Get the data on which page.
+     * @param pageSize The number of data per page
+     * @return A list of UserInfoEntity.
+     */
+    Page<UserInfoEntity> findUsersList(Integer pageNum, Integer pageSize);
+
+    /**
+     * Update a user information.
+     * @param userInfoEntity UserInfoEntity
+     */
+    void update(UserInfoEntity userInfoEntity);
+
+    /**
+     * Delete a user by username.
+     * @param name username
+     */
+    void delete(String name);
+}
diff --git a/src/main/java/org/apache/pulsar/manager/interceptor/WebAppConfigurer.java b/src/main/java/org/apache/pulsar/manager/interceptor/WebAppConfigurer.java
index 818e294..c79f4b8 100644
--- a/src/main/java/org/apache/pulsar/manager/interceptor/WebAppConfigurer.java
+++ b/src/main/java/org/apache/pulsar/manager/interceptor/WebAppConfigurer.java
@@ -27,6 +27,8 @@
 
     @Override
     public void addInterceptors(InterceptorRegistry registry) {
-        registry.addInterceptor(adminHandlerInterceptor).addPathPatterns("/**").excludePathPatterns("/pulsar-manager/login");
+        registry.addInterceptor(adminHandlerInterceptor).addPathPatterns("/**")
+                .excludePathPatterns("/pulsar-manager/login")
+                .excludePathPatterns("/pulsar-manager/third-party-login/**");
     }
 }
diff --git a/src/main/java/org/apache/pulsar/manager/mapper/UsersMapper.java b/src/main/java/org/apache/pulsar/manager/mapper/UsersMapper.java
new file mode 100644
index 0000000..d792c12
--- /dev/null
+++ b/src/main/java/org/apache/pulsar/manager/mapper/UsersMapper.java
@@ -0,0 +1,54 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pulsar.manager.mapper;
+
+import com.github.pagehelper.Page;
+import org.apache.ibatis.annotations.Delete;
+import org.apache.ibatis.annotations.Insert;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Options;
+import org.apache.ibatis.annotations.Select;
+import org.apache.ibatis.annotations.Update;
+import org.apache.pulsar.manager.entity.UserInfoEntity;
+
+@Mapper
+public interface UsersMapper {
+
+    @Insert("INSERT INTO users (access_token, name, description, email, phone_number" +
+            ", location, company, expire)" +
+            "VALUES (#{accessToken}, #{name}, #{description}, #{email}, #{phoneNumber}" +
+            ", #{location}, #{company}, #{expire})")
+    @Options(useGeneratedKeys=true, keyProperty="userId", keyColumn="user_id")
+    long save(UserInfoEntity userInfoEntity);
+
+    @Select("SELECT access_token AS accessToken, user_id AS userId, name, description, email," +
+            "phone_number AS phoneNumber, location, company, expire " +
+            "FROM users " +
+            "WHERE name = #{name}")
+    UserInfoEntity findByUserName(String name);
+
+    @Select("SELECT access_token AS accessToken, user_id AS userId, name, description, email," +
+            "phone_number AS phoneNumber, location, company, expire " +
+            "FROM users")
+    Page<UserInfoEntity> findUsersList();
+
+    @Update("UPDATE users " +
+            "SET access_token = #{accessToken}, description = #{description}, email = #{email}," +
+            "phone_number = #{phoneNumber}, location = #{location}, company = #{company}, expire=#{expire} " +
+            "WHERE name = #{name}")
+    void update(UserInfoEntity userInfoEntity);
+
+    @Delete("DELETE FROM users WHERE name=#{name}")
+    void delete(String name);
+}
diff --git a/src/main/java/org/apache/pulsar/manager/service/ThirdPartyLoginService.java b/src/main/java/org/apache/pulsar/manager/service/ThirdPartyLoginService.java
new file mode 100644
index 0000000..9f44bd7
--- /dev/null
+++ b/src/main/java/org/apache/pulsar/manager/service/ThirdPartyLoginService.java
@@ -0,0 +1,37 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pulsar.manager.service;
+
+import org.apache.pulsar.manager.entity.UserInfoEntity;
+
+import java.util.Map;
+
+public interface ThirdPartyLoginService {
+
+    /**
+     * Obtaining an authentication token from a third-party platform.
+     * @param parameters For a kv type map, different third-party platforms may need to pass different parameters,
+     *                  which are passed according to the actual situation and analyzed in their implementation classes.
+     * @return String format access token information
+     */
+    String getAuthToken(Map<String, String> parameters);
+
+    /**
+     * Acquiring user information according to an authentication token.
+     * @param authenticationMap For a kv type map, different third-party platforms need different parameters,
+     *                          which are passed through the map structure.
+     * @return UserInfoEntity
+     */
+    UserInfoEntity getUserInfo(Map<String, String> authenticationMap);
+}
diff --git a/src/main/java/org/apache/pulsar/manager/service/impl/GithubLoginServiceImpl.java b/src/main/java/org/apache/pulsar/manager/service/impl/GithubLoginServiceImpl.java
new file mode 100644
index 0000000..4a2eacb
--- /dev/null
+++ b/src/main/java/org/apache/pulsar/manager/service/impl/GithubLoginServiceImpl.java
@@ -0,0 +1,112 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pulsar.manager.service.impl;
+
+import com.google.common.collect.Maps;
+import com.google.gson.Gson;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.pulsar.manager.entity.GithubAuthEntity;
+import org.apache.pulsar.manager.entity.GithubUserInfoEntity;
+import org.apache.pulsar.manager.entity.UserInfoEntity;
+import org.apache.pulsar.manager.service.ThirdPartyLoginService;
+import org.apache.pulsar.manager.utils.HttpUtil;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Map;
+
+/**
+ * Github login for get user information.
+ */
+@Slf4j
+@Service
+public class GithubLoginServiceImpl implements ThirdPartyLoginService {
+
+    @Value("${github.client.id}")
+    private String githubClientId;
+
+    @Value("${github.client.secret}")
+    private String githubClientSecret;
+
+    @Value("${github.oauth.host}")
+    private String githubAuthHost;
+
+    @Value("${github.user.info}")
+    private String githubUserInfo;
+
+    /**
+     * Get user access token from github.
+     * @param parameters For get code to github.
+     *          GitHub redirects back to local site with a temporary code in a code parameter as well as the state
+     *          you provided in the previous step in a state parameter.The temporary code will expire after 10 minutes.
+     * @return access_token of string type
+     */
+    public String getAuthToken(Map<String, String> parameters) {
+        Map<String, String> header = Maps.newHashMap();
+        header.put("Content-Type", "application/json");
+        header.put("Accept", "application/json");
+        Map<String, Object> body = Maps.newHashMap();
+        body.put("client_id", githubClientId);
+        body.put("client_secret", githubClientSecret);
+        if (!parameters.containsKey("code")) {
+            log.error("Parameter does not contain code field, which is illegal.");
+            return null;
+        }
+        body.put("code", parameters.get("code"));
+        Gson gson = new Gson();
+        try {
+            // result example: access_token=your-token&token_type=bearer
+            String result = HttpUtil.doPost(githubAuthHost, header, gson.toJson(body));
+            GithubAuthEntity githubAuthEntity = gson.fromJson(result, GithubAuthEntity.class);
+            log.info("Success get access token from github");
+            return githubAuthEntity.getAccessToken();
+        } catch (UnsupportedEncodingException e) {
+            log.error("Failed get access token from github, error stack: {}", e.getCause());
+            return null;
+        }
+    }
+
+    /**
+     * Get user information from github by access token.
+     * @param authenticationMap Authentication mark requesting user information.
+     * @return UserInfoEntity
+     */
+    public UserInfoEntity getUserInfo(Map<String, String> authenticationMap) {
+        Map<String, String> header = Maps.newHashMap();
+        header.put("Content-Type", "application/json");
+        if (!authenticationMap.containsKey("access_token")) {
+            log.error("The authenticationMap does not contain access_token field, which is illegal.");
+            return null;
+        }
+        header.put("Authorization", "token " + authenticationMap.get("access_token"));
+        String result = HttpUtil.doGet(githubUserInfo, header);
+        Gson gson = new Gson();
+        GithubUserInfoEntity githubUserInfoEntity = gson.fromJson(result, GithubUserInfoEntity.class);
+        if (githubUserInfoEntity == null) {
+            log.error("Get user information from github failed.");
+            return null;
+        }
+        UserInfoEntity userInfoEntity = new UserInfoEntity();
+        userInfoEntity.setCompany(githubUserInfoEntity.getCompany());
+        // User 'login' field of github as platform name, because name field of github often empty.
+        // Github's name field is more like an alias.
+        userInfoEntity.setName(githubUserInfoEntity.getLogin());
+        userInfoEntity.setDescription(githubUserInfoEntity.getBio());
+        userInfoEntity.setEmail(githubUserInfoEntity.getEmail());
+        userInfoEntity.setLocation(githubUserInfoEntity.getLocation());
+        userInfoEntity.setAccessToken(authenticationMap.get("access_token"));
+        return userInfoEntity;
+    }
+}
diff --git a/src/main/java/org/apache/pulsar/manager/utils/HttpUtil.java b/src/main/java/org/apache/pulsar/manager/utils/HttpUtil.java
index f08b7d4..cf0ba45 100644
--- a/src/main/java/org/apache/pulsar/manager/utils/HttpUtil.java
+++ b/src/main/java/org/apache/pulsar/manager/utils/HttpUtil.java
@@ -17,6 +17,7 @@
 import org.apache.http.client.config.RequestConfig;
 import org.apache.http.client.methods.CloseableHttpResponse;
 import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
 import org.apache.http.client.methods.HttpPut;
 import org.apache.http.client.methods.HttpUriRequest;
 import org.apache.http.entity.StringEntity;
@@ -59,7 +60,23 @@
         return httpRequest(request, header);
     }
 
-    public static String doPut(String url, Map<String, String> header, String body) throws UnsupportedEncodingException {
+    /**
+     * HTTP post method.
+     * @param url Destination host
+     * @param header Header information
+     * @param body Body information
+     * @return HTTP response information
+     * @throws UnsupportedEncodingException
+     */
+    public static String doPost(String url, Map<String, String> header, String body)
+            throws UnsupportedEncodingException {
+        HttpPost request = new HttpPost(url);
+        request.setEntity(new StringEntity(body));
+        return httpRequest(request, header);
+    }
+
+    public static String doPut(String url, Map<String, String> header, String body)
+            throws UnsupportedEncodingException {
         HttpPut request = new HttpPut(url);
         request.setEntity(new StringEntity(body));
         return httpRequest(request, header);
diff --git a/src/main/resources/META-INF/sql/herddb-schema.sql b/src/main/resources/META-INF/sql/herddb-schema.sql
index 61d7469..28fc09a 100644
--- a/src/main/resources/META-INF/sql/herddb-schema.sql
+++ b/src/main/resources/META-INF/sql/herddb-schema.sql
@@ -113,3 +113,15 @@
   description varchar(128),
   token varchar(1024)
 );
+
+CREATE TABLE IF NOT EXISTS users (
+  user_id BIGINT PRIMARY KEY AUTO_INCREMENT,
+  access_token varchar(256) NOT NULL,
+  name varchar(256) NOT NULL,
+  description varchar(128),
+  email varchar(256),
+  phone_number varchar(48),
+  location varchar(256),
+  company varchar(256),
+  expire LONG NOT NULL
+);
diff --git a/src/main/resources/META-INF/sql/mysql-schema.sql b/src/main/resources/META-INF/sql/mysql-schema.sql
index ba22fbb..4607a31 100644
--- a/src/main/resources/META-INF/sql/mysql-schema.sql
+++ b/src/main/resources/META-INF/sql/mysql-schema.sql
@@ -122,4 +122,17 @@
   description varchar(128),
   token varchar(1024),
   UNIQUE (role)
+)ENGINE=InnoDB CHARACTER SET utf8;
+
+CREATE TABLE IF NOT EXISTS users (
+  user_id BIGINT PRIMARY KEY AUTO_INCREMENT,
+  access_token varchar(256) NOT NULL,
+  name varchar(256) NOT NULL,
+  description varchar(128),
+  email varchar(256),
+  phone_number varchar(48),
+  location varchar(256),
+  company varchar(256),
+  expire LONG NOT NULL,
+  UNIQUE (name)
 )ENGINE=InnoDB CHARACTER SET utf8;
\ No newline at end of file
diff --git a/src/main/resources/META-INF/sql/postgresql-schema.sql b/src/main/resources/META-INF/sql/postgresql-schema.sql
index 4f4ccf4..bf55254 100644
--- a/src/main/resources/META-INF/sql/postgresql-schema.sql
+++ b/src/main/resources/META-INF/sql/postgresql-schema.sql
@@ -122,4 +122,17 @@
   description varchar(128),
   token varchar(1024) NOT NUll,
   UNIQUE (role)
+);
+
+CREATE TABLE IF NOT EXISTS users (
+  user_id BIGSERIAL PRIMARY KEY AUTO_INCREMENT,
+  access_token varchar(256) NOT NULL,
+  name varchar(256) NOT NULL,
+  description varchar(128),
+  email varchar(256),
+  phone_number varchar(48),
+  location varchar(256),
+  company varchar(256),
+  expire BIGINT NOT NULL,
+  UNIQUE (name)
 );
\ No newline at end of file
diff --git a/src/main/resources/META-INF/sql/sqlite-schema.sql b/src/main/resources/META-INF/sql/sqlite-schema.sql
index 7e6aa09..4ae82be 100644
--- a/src/main/resources/META-INF/sql/sqlite-schema.sql
+++ b/src/main/resources/META-INF/sql/sqlite-schema.sql
@@ -120,4 +120,16 @@
   UNIQUE (role)
 );
 
+CREATE TABLE IF NOT EXISTS users (
+  user_id integer PRIMARY KEY AUTOINCREMENT,
+  access_token varchar(256) NOT NULL,
+  name varchar(256) NOT NULL,
+  description varchar(128),
+  email varchar(256),
+  phone_number varchar(48),
+  location varchar(256),
+  company varchar(256),
+  expire integer NOT NUll,
+  UNIQUE (name)
+);
 
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index dbace83..9552e75 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -20,7 +20,7 @@
 logging.file=pulsar-manager.log
 
 # DEBUG print execute sql
-logging.level.org.apache=DEBUG
+logging.level.org.apache=INFO
 
 mybatis.type-aliases-package=org.apache.pulsar.manager
 
@@ -105,3 +105,25 @@
 
 # cluster data reload
 cluster.cache.reload.interval.ms=60000
+
+# Third party login options
+third.party.login.option=
+
+# Github login configuration
+github.client.id=your-client-id
+github.client.secret=your-client-secret
+github.oauth.host=https://github.com/login/oauth/access_token
+github.user.info=https://api.github.com/user
+github.login.host=https://github.com/login/oauth/authorize
+github.redirect.host=http://localhost:9527
+
+user.access.token.expire=604800
+
+# thymeleaf configuration for third login.
+spring.thymeleaf.cache=false
+spring.thymeleaf.prefix=classpath:/templates/
+spring.thymeleaf.check-template-location=true
+spring.thymeleaf.suffix=.html
+spring.thymeleaf.encoding=UTF-8
+spring.thymeleaf.servlet.content-type=text/html
+spring.thymeleaf.mode=HTML5
diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html
new file mode 100644
index 0000000..9cd4011
--- /dev/null
+++ b/src/main/resources/templates/index.html
@@ -0,0 +1,40 @@
+<!--
+
+    Licensed under the Apache License, Version 2.0 (the "License");
+    you may not use this file except in compliance with the License.
+    You may obtain a copy of the License at
+
+        http://www.apache.org/licenses/LICENSE-2.0
+
+    Unless required by applicable law or agreed to in writing, software
+    distributed under the License is distributed on an "AS IS" BASIS,
+    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+    See the License for the specific language governing permissions and
+    limitations under the License.
+
+-->
+<!DOCTYPE html>
+<html lang="en" xmlns:th="http://www.thymeleaf.org">
+<head>
+    <meta charset="utf-8">
+    <title>Pulsar Manager</title>
+    <meta name="viewport" content="width=device-width, initial-scale=1">
+    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
+</head>
+<body>
+<div>
+    <p th:utext="${message}"></p>
+</div>
+</body>
+<script th:inline="javascript">
+    window.onload = function() {
+        var flag = [[${flag}]]
+        if (flag) {
+            var userInfo = [[${userInfo}]]
+            var targetOrigin = window.opener;
+            targetOrigin.postMessage(userInfo, 'http://' + window.location.host);
+            window.close();
+        }
+    }
+</script>
+</html>
\ No newline at end of file
diff --git a/src/test/java/org/apache/pulsar/manager/dao/UsersRepositoryImplTest.java b/src/test/java/org/apache/pulsar/manager/dao/UsersRepositoryImplTest.java
new file mode 100644
index 0000000..ac08a7a
--- /dev/null
+++ b/src/test/java/org/apache/pulsar/manager/dao/UsersRepositoryImplTest.java
@@ -0,0 +1,100 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pulsar.manager.dao;
+
+import com.github.pagehelper.Page;
+import org.apache.pulsar.manager.PulsarManagerApplication;
+import org.apache.pulsar.manager.entity.UserInfoEntity;
+import org.apache.pulsar.manager.entity.UsersRepository;
+import org.apache.pulsar.manager.profiles.HerdDBTestProfile;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.util.Optional;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest(
+        classes = {
+                PulsarManagerApplication.class,
+                HerdDBTestProfile.class
+        }
+)
+@ActiveProfiles("test")
+public class UsersRepositoryImplTest {
+
+    @Autowired
+    private UsersRepository usersRepository;
+
+    private void initUser(UserInfoEntity userInfoEntity) {
+        userInfoEntity.setAccessToken("test-access-token");
+        userInfoEntity.setEmail("test@apache.org");
+        userInfoEntity.setLocation("bj");
+        userInfoEntity.setDescription("test-description");
+        userInfoEntity.setName("test-user");
+        userInfoEntity.setExpire(157900045678l);
+        userInfoEntity.setPhoneNumber("1356789023456");
+    }
+
+    private void validateUser(UserInfoEntity user) {
+        Assert.assertEquals(user.getName(), "test-user");
+        Assert.assertEquals(user.getExpire(), 157900045678l);
+        Assert.assertEquals(user.getPhoneNumber(), "1356789023456");
+        Assert.assertEquals(user.getDescription(), "test-description");
+        Assert.assertEquals(user.getLocation(), "bj");
+        Assert.assertEquals(user.getEmail(), "test@apache.org");
+        Assert.assertEquals(user.getAccessToken(), "test-access-token");
+    }
+
+    @Test
+    public void getUsersListTest() {
+        UserInfoEntity userInfoEntity = new UserInfoEntity();
+        initUser(userInfoEntity);
+
+        usersRepository.save(userInfoEntity);
+
+        Page<UserInfoEntity> userInfoEntities = usersRepository.findUsersList(1, 10);
+        userInfoEntities.count(true);
+        userInfoEntities.getResult().forEach((user) -> {
+            validateUser(user);
+            usersRepository.delete(user.getName());
+        });
+    }
+
+    @Test
+    public void getAndUpdateUserTest() {
+        UserInfoEntity userInfoEntity = new UserInfoEntity();
+        initUser(userInfoEntity);
+        usersRepository.save(userInfoEntity);
+        Optional<UserInfoEntity> userInfoEntityOptional = usersRepository.findByUserName(userInfoEntity.getName());
+        UserInfoEntity getUserInfoEntity = userInfoEntityOptional.get();
+        validateUser(getUserInfoEntity);
+        userInfoEntity.setPhoneNumber("1356789023456");
+        userInfoEntity.setEmail("test2@apache.org");
+        usersRepository.update(userInfoEntity);
+
+        userInfoEntityOptional = usersRepository.findByUserName(userInfoEntity.getName());
+        UserInfoEntity updateUserInfoEntity = userInfoEntityOptional.get();
+        Assert.assertEquals(updateUserInfoEntity.getPhoneNumber(), "1356789023456");
+        Assert.assertEquals(updateUserInfoEntity.getEmail(), "test2@apache.org");
+
+        usersRepository.delete(updateUserInfoEntity.getName());
+        userInfoEntityOptional = usersRepository.findByUserName(userInfoEntity.getName());
+        Assert.assertFalse(userInfoEntityOptional.isPresent());
+    }
+}
diff --git a/src/test/java/org/apache/pulsar/manager/service/GithubLoginServiceImplTest.java b/src/test/java/org/apache/pulsar/manager/service/GithubLoginServiceImplTest.java
new file mode 100644
index 0000000..4dadd54
--- /dev/null
+++ b/src/test/java/org/apache/pulsar/manager/service/GithubLoginServiceImplTest.java
@@ -0,0 +1,129 @@
+/**
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pulsar.manager.service;
+
+import com.google.common.collect.Maps;
+import com.google.gson.Gson;
+import org.apache.pulsar.manager.PulsarManagerApplication;
+import org.apache.pulsar.manager.entity.UserInfoEntity;
+import org.apache.pulsar.manager.profiles.HerdDBTestProfile;
+import org.apache.pulsar.manager.utils.HttpUtil;
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.powermock.api.mockito.PowerMockito;
+import org.powermock.core.classloader.annotations.PowerMockIgnore;
+import org.powermock.core.classloader.annotations.PrepareForTest;
+import org.powermock.modules.junit4.PowerMockRunner;
+import org.powermock.modules.junit4.PowerMockRunnerDelegate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Map;
+
+@RunWith(PowerMockRunner.class)
+@PowerMockRunnerDelegate(SpringRunner.class)
+@PowerMockIgnore( {"javax.*", "sun.*", "com.sun.*", "org.xml.*", "org.w3c.*"})
+@PrepareForTest(HttpUtil.class)
+@SpringBootTest(
+        classes = {
+                PulsarManagerApplication.class,
+                HerdDBTestProfile.class
+        }
+)
+@ActiveProfiles("test")
+public class GithubLoginServiceImplTest {
+
+    @Autowired
+    private ThirdPartyLoginService thirdPartyLoginService;
+
+    @Value("${github.client.id}")
+    private String githubClientId;
+
+    @Value("${github.client.secret}")
+    private String githubClientSecret;
+
+    @Value("${github.oauth.host}")
+    private String githubAuthHost;
+
+    @Value("${github.user.info}")
+    private String githubUserInfo;
+
+    @Test
+    public void getAuthTokenTest() throws UnsupportedEncodingException {
+        PowerMockito.mockStatic(HttpUtil.class);
+        Map<String, String> header = Maps.newHashMap();
+        header.put("Content-Type", "application/json");
+        header.put("Accept", "application/json");
+        Map<String, String> parameters = Maps.newHashMap();
+
+        // Test no code
+        String noCodeResult = thirdPartyLoginService.getAuthToken(parameters);
+        Assert.assertNull(noCodeResult);
+
+        // Test with code
+        parameters.put("code", "test-code");
+        Gson gson = new Gson();
+        Map<String, String> body = Maps.newHashMap();
+        body.put("code", parameters.get("code"));
+        body.put("client_id", githubClientId);
+        body.put("client_secret", githubClientSecret);
+        PowerMockito.when(HttpUtil.doPost(githubAuthHost, header, gson.toJson(body)))
+                .thenReturn("{" +
+                    "\"access_token\": \"e72e16c7e42f292c6912e7710c838347ae178b4a\"," +
+                    "\"scope\": \"repo,gist\"," +
+                    "\"token_type\": \"bearer\"" +
+                "}");
+        String withCodeResult = thirdPartyLoginService.getAuthToken(parameters);
+        Assert.assertEquals(withCodeResult, "e72e16c7e42f292c6912e7710c838347ae178b4a");
+    }
+
+    @Test
+    public void getUserInfoTest() {
+
+        Map<String, String> authenticationMap = Maps.newHashMap();
+        UserInfoEntity noTokenUserInfoEntity = thirdPartyLoginService.getUserInfo(authenticationMap);
+
+        Assert.assertEquals(noTokenUserInfoEntity, null);
+
+        authenticationMap.put("access_token", "test-user-token");
+        PowerMockito.mockStatic(HttpUtil.class);
+        Map<String, String> header = Maps.newHashMap();
+        header.put("Content-Type", "application/json");
+        header.put("Authorization", "token test-user-token");
+        PowerMockito.when(HttpUtil.doGet(githubUserInfo, header))
+                .thenReturn(null);
+        UserInfoEntity withTokenNullUserInfoEntity = thirdPartyLoginService.getUserInfo(authenticationMap);
+        Assert.assertNull(withTokenNullUserInfoEntity);
+        PowerMockito.when(HttpUtil.doGet(githubUserInfo, header))
+                .thenReturn("{\n" +
+                        "\t\"login\": \"test1\",\n" +
+                        "\t\"company\": bj,\n" +
+                        "\t\"location\": \"nw\",\n" +
+                        "\t\"email\": \"test@apache.org\",\n" +
+                        "\t\"bio\": \"this is description\"" +
+                        "}");
+        UserInfoEntity withTokenUserInfoEntity = thirdPartyLoginService.getUserInfo(authenticationMap);
+        Assert.assertEquals(withTokenUserInfoEntity.getEmail(), "test@apache.org");
+        Assert.assertEquals(withTokenUserInfoEntity.getName(), "test1");
+        Assert.assertEquals(withTokenUserInfoEntity.getCompany(), "bj");
+        Assert.assertEquals(withTokenUserInfoEntity.getDescription(), "this is description");
+        Assert.assertEquals(withTokenUserInfoEntity.getLocation(), "nw");
+        Assert.assertEquals(withTokenUserInfoEntity.getAccessToken(), "test-user-token");
+    }
+}