Skip to content
This repository has been archived by the owner on Feb 18, 2025. It is now read-only.

Commit

Permalink
Enable Google OAuth and move API key to server-side
Browse files Browse the repository at this point in the history
Masayoshi Mizutani committed Feb 10, 2020
1 parent 32ebedb commit f55297c
Showing 8 changed files with 248 additions and 141 deletions.
47 changes: 34 additions & 13 deletions api.go
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import (
"net/http"
"net/http/httputil"
"net/url"
"strings"

"github.com/gin-gonic/gin"
"github.com/pkg/errors"
@@ -13,7 +14,7 @@ type roundTripper func(*http.Request) (*http.Response, error)

func (f roundTripper) RoundTrip(req *http.Request) (*http.Response, error) { return f(req) }

func reverseProxy(target string) (gin.HandlerFunc, error) {
func reverseProxy(authz *authzService, apiKey, target string) (gin.HandlerFunc, error) {
logger.WithField("target", target).Info("proxy")
url, err := url.Parse(target)
if err != nil {
@@ -25,22 +26,42 @@ func reverseProxy(target string) (gin.HandlerFunc, error) {
return http.DefaultTransport.RoundTrip(req)
}

proxy := &httputil.ReverseProxy{
Transport: roundTripper(requestHandler),
Director: func(req *http.Request) {
req.URL.Host = url.Host
req.URL.Scheme = url.Scheme
req.URL.Path = url.Path + req.URL.Path
},
}

return func(c *gin.Context) {
proxy.ServeHTTP(c.Writer, c.Request)
userData, ok := c.Get("user")
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"msg": "Unauthenticated request"})
return
}

user := userData.(string)
allowed, ok := authz.AllowTable[user]
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized user", "user": user})
return
}

tags := strings.Join(allowed, ",")
if tags == "" {
tags = "*"
}

(&httputil.ReverseProxy{
Transport: roundTripper(requestHandler),
Director: func(req *http.Request) {
req.URL.Host = url.Host
req.URL.Scheme = url.Scheme
req.URL.Path = url.Path + req.URL.Path
req.Header.Set("x-api-key", apiKey)
req.Header.Set("minerva-allowed-tags", tags)
logger.WithField("header", req.Header).Info("API requet header")
},
}).ServeHTTP(c.Writer, c.Request)

}, nil
}

func setupAPI(ssnMgr *sessionManager, endpoint string, r *gin.RouterGroup) error {
proxy, err := reverseProxy(endpoint)
func setupAPI(authz *authzService, apiKey, endpoint string, r *gin.RouterGroup) error {
proxy, err := reverseProxy(authz, apiKey, endpoint)
if err != nil {
return err
}
11 changes: 9 additions & 2 deletions auth.go → authn.go
Original file line number Diff line number Diff line change
@@ -26,9 +26,16 @@ type sessionManager struct {
}

func newSessionManager(jwtSecret string) *sessionManager {
return &sessionManager{
jwtSecret: []byte(jwtSecret),
mgr := &sessionManager{}

if jwtSecret == "" {
logger.Warn("jwt-secret is not set, then automatically generated")
mgr.jwtSecret = []byte(genRandomSecret())
} else {
mgr.jwtSecret = []byte(jwtSecret)
}

return mgr
}

func (x *sessionManager) sign(user strixUser, c *gin.Context) error {
39 changes: 39 additions & 0 deletions authz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"encoding/json"
"fmt"
"io/ioutil"

"github.com/pkg/errors"
)

type authzService struct {
AllowTable map[string][]string `json:"allow"`
}

func newAuthzService(filePath string) (*authzService, error) {
raw, err := ioutil.ReadFile(filePath)
if err != nil {
return nil, errors.Wrapf(err, "Fail to load authz file: %s", filePath)
}

srv := authzService{
AllowTable: make(map[string][]string),
}
if err := json.Unmarshal(raw, &srv); err != nil {
return nil, errors.Wrapf(err, "Fail to parse authz file: %s", filePath)
}

logger.WithField("authz", srv.AllowTable).Info("Read authorization table")
return &srv, nil
}

func (x *authzService) allowedTags(user string) ([]string, error) {
allowed, ok := x.AllowTable[user]
if !ok {
return nil, fmt.Errorf("Not permitted")
}

return allowed, nil
}
28 changes: 28 additions & 0 deletions logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package main

import (
"fmt"

"github.com/sirupsen/logrus"
)

var logger = logrus.New()

var logLevelMap = map[string]logrus.Level{
"trace": logrus.TraceLevel,
"debug": logrus.DebugLevel,
"info": logrus.InfoLevel,
"warn": logrus.WarnLevel,
"error": logrus.ErrorLevel,
}

func setupLogger(logLevel string) error {
level, ok := logLevelMap[logLevel]
if !ok {
return fmt.Errorf("Invalid log level: %s", logLevel)
}
logger.SetLevel(level)
logger.SetFormatter(&logrus.JSONFormatter{})

return nil
}
110 changes: 22 additions & 88 deletions main.go
Original file line number Diff line number Diff line change
@@ -6,95 +6,9 @@ import (
"os"
"time"

"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"

"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
)

var logger = logrus.New()

var logLevelMap = map[string]logrus.Level{
"trace": logrus.TraceLevel,
"debug": logrus.DebugLevel,
"info": logrus.InfoLevel,
"warn": logrus.WarnLevel,
"error": logrus.ErrorLevel,
}

type arguments struct {
LogLevel string
Endpoint string
BindAddress string
BindPort int
StaticContents string

// Google OAuth options
GoogleOAuthConfig string

// JWT
JWTSecret string
}

func runServer(args arguments) error {
level, ok := logLevelMap[args.LogLevel]
if !ok {
return fmt.Errorf("Invalid log level: %s", args.LogLevel)
}
logger.SetLevel(level)
logger.SetFormatter(&logrus.JSONFormatter{})
logger.WithFields(logrus.Fields{
"args": args,
}).Info("Given options")

helloReply := os.Getenv("HELLO_REPLY")
if helloReply == "" {
helloReply = time.Now().String()
}

r := gin.Default()
store := cookie.NewStore([]byte("auth"))
r.Use(sessions.Sessions("strix", store))
r.Use(static.Serve("/", static.LocalFile(args.StaticContents, false)))
/*
r.LoadHTMLGlob(path.Join(args.TemplatePath, "*"))
r.GET("/", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.html", gin.H{
"googleOAuth": (args.GoogleOAuthConfig != ""),
})
})
*/
r.GET("/hello/revision", func(c *gin.Context) {
c.String(200, helloReply)
})

ssnMgr := newSessionManager(args.JWTSecret)

authGroup := r.Group("/auth")
if err := setupAuth(ssnMgr, authGroup); err != nil {
return err
}
if args.GoogleOAuthConfig != "" {
if err := setupAuthGoogle(ssnMgr, args.GoogleOAuthConfig, authGroup); err != nil {
return err
}
}

apiGroup := r.Group("/api/v1")
if err := setupAPI(ssnMgr, args.Endpoint, apiGroup); err != nil {
return err
}

if err := r.Run(fmt.Sprintf("%s:%d", args.BindAddress, args.BindPort)); err != nil {
return err
}

return nil
}

func genRandomSecret() string {
const randomSecretLength = 32
letters := "abcdefghijklmnopqrstuvwxyz" +
@@ -147,6 +61,11 @@ func main() {
Usage: "Static contents path",
Destination: &args.StaticContents,
},
cli.StringFlag{
Name: "hello-reply, r", Value: time.Now().String(),
Usage: "Reply message for /hello/revision",
Destination: &args.HelloReply,
},

cli.StringFlag{
Name: "google-oauth-config, g",
@@ -155,9 +74,24 @@ func main() {
},
cli.StringFlag{
Name: "jwt-secret, j",
Value: genRandomSecret(),
Usage: "JWT secret to sign and validate token",
Destination: &args.GoogleOAuthConfig,
Destination: &args.JWTSecret,
},
cli.StringFlag{
Name: "api-key, k",
Usage: "API Key of Minerva",
Destination: &args.APIKey,
},

cli.StringFlag{
Name: "authz-path, z",
Usage: "Authorization list json file path",
Destination: &args.AuthzFilePath,
},
cli.StringFlag{
Name: "secret-arn",
Usage: "Secret ARN of SecretsManager",
Destination: &args.SecretArn,
},
}
app.ArgsUsage = "[endpoint]"
92 changes: 92 additions & 0 deletions server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
package main

import (
"fmt"
"net/http"

"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-contrib/static"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)

type arguments struct {
LogLevel string
Endpoint string
BindAddress string
BindPort int
StaticContents string
HelloReply string
APIKey string
SecretArn string
AuthzFilePath string

// Google OAuth options
GoogleOAuthConfig string

// JWT
JWTSecret string
}

func runServer(args arguments) error {
if err := setupLogger(args.LogLevel); err != nil {
return err
}

logger.WithFields(logrus.Fields{
"args": args,
}).Info("Given options")

r := gin.Default()
store := cookie.NewStore([]byte("auth"))
r.Use(sessions.Sessions("strix", store))
r.Use(static.Serve("/", static.LocalFile(args.StaticContents, false)))

r.GET("/hello/revision", func(c *gin.Context) {
c.String(200, args.HelloReply)
})

// Setup session manager
authz, err := newAuthzService(args.AuthzFilePath)
if err != nil {
return err
}

ssnMgr := newSessionManager(args.JWTSecret)
authCheck := func(c *gin.Context) {
user, err := ssnMgr.validate(c)
if err != nil {
logger.WithError(err).Warn("Authentication Fail")
c.JSON(http.StatusUnauthorized, gin.H{"msg": "Authentication failed"})
} else {
c.Set("user", user.UserID)
c.Next()
}
}

// Auth route group
authGroup := r.Group("/auth")
if err := setupAuth(ssnMgr, authGroup); err != nil {
return err
}
if args.GoogleOAuthConfig != "" {
if err := setupAuthGoogle(ssnMgr, args.GoogleOAuthConfig, authGroup); err != nil {
return err
}
}

// API route group
apiGroup := r.Group("/api/v1")
apiGroup.Use(authCheck)
if err := setupAPI(authz, args.APIKey, args.Endpoint, apiGroup); err != nil {
return err
}

// Start server
if err := r.Run(fmt.Sprintf("%s:%d", args.BindAddress, args.BindPort)); err != nil {
return err
}

return nil
}
3 changes: 0 additions & 3 deletions src/js/header.vue
Original file line number Diff line number Diff line change
@@ -7,9 +7,6 @@
</h1>
</div>
</nav>
<div class="msgbox login">
<a href="/auth/google">Google</a>
</div>
</div>
</template>

59 changes: 24 additions & 35 deletions src/js/query.vue
Original file line number Diff line number Diff line change
@@ -42,29 +42,9 @@
<button class="send_query thin2" v-on:click="submitQuery">Query</button>
</div>
<div class="columns">
<button
class="secondary thin2"
v-on:click="showApiKey"
v-if="!showApiKeyForm"
>Change API Key</button>
</div>
</div>

<div class="row" v-if="showApiKeyForm">
<div class="columns" style="text-align: right;">
<h4>API Key</h4>
</div>
<div class="columns">
<input type="text" v-model="apiKey" />
</div>
<div class="columns">
<button class="highlight thin" v-on:click="saveApiKey">Save</button>
</div>
</div>

<div class="row" v-if="searchStatus !== null">
<div class="columns query-status">
<div class="alert-box radius">{{ searchStatus }}</div>
<button class="secondary thin2" v-if="!authenticated">
<a href="/auth/google">Google Login</a>
</button>
</div>
</div>

@@ -86,10 +66,6 @@ import querystring from "querystring";
import SHA1 from "crypto-js/sha1";
import escapeHTML from "escape-html";
var httpClient = axios.create({
headers: { "x-api-key": localStorage.getItem("apiKey") }
});
const nowDatetime = new Date();
const utcDatetime = new Date(
nowDatetime.getUTCFullYear(),
@@ -103,22 +79,30 @@ const utcDatetime = new Date(
const appData = {
query: "",
queryTerms: [],
searchStatus: null,
queryID: null,
apiKey: localStorage.getItem("apiKey"),
showApiKeyForm: localStorage.getItem("apiKey") === null,
metadata: {},
timeSpan: 3600,
timeBegin: strftime("%Y-%m-%dT%H:%M", utcDatetime),
timeEnd: strftime("%Y-%m-%dT%H:%M", utcDatetime),
errorMessage: null,
spanMode: "relative"
spanMode: "relative",
authenticated: false
};
export default {
data() {
return appData;
},
mounted() {
axios
.get("/auth")
.then(resp => {
appData.authenticated = true;
})
.catch(err => {
console.log("auth NG", err);
});
},
methods: {
saveApiKey: saveApiKey,
editApiKey: editApiKey,
@@ -144,8 +128,13 @@ function showApiKey() {
appData.showApiKeyForm = true;
}
function showError(errMsg) {
appData.errorMessage = errMsg;
function showError(err) {
console.log(err);
if (err.response) {
console.log("error response: ", err.response);
}
appData.errorMessage = err;
}
function clearError() {
@@ -218,9 +207,9 @@ function submitQuery(ev) {
console.log("body =>", body);
const router = this.$router;
httpClient
axios
.post(`/api/v1/search`, body)
.then(function(response) {
.then(response => {
console.log(response);
router.push("/search/" + response.data.query_id);
})

0 comments on commit f55297c

Please sign in to comment.