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

Commit f55297c

Browse files
author
Masayoshi Mizutani
committed
Enable Google OAuth and move API key to server-side
1 parent 32ebedb commit f55297c

File tree

8 files changed

+248
-141
lines changed

8 files changed

+248
-141
lines changed

api.go

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"net/http"
55
"net/http/httputil"
66
"net/url"
7+
"strings"
78

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

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

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

28-
proxy := &httputil.ReverseProxy{
29-
Transport: roundTripper(requestHandler),
30-
Director: func(req *http.Request) {
31-
req.URL.Host = url.Host
32-
req.URL.Scheme = url.Scheme
33-
req.URL.Path = url.Path + req.URL.Path
34-
},
35-
}
36-
3729
return func(c *gin.Context) {
38-
proxy.ServeHTTP(c.Writer, c.Request)
30+
userData, ok := c.Get("user")
31+
if !ok {
32+
c.JSON(http.StatusUnauthorized, gin.H{"msg": "Unauthenticated request"})
33+
return
34+
}
35+
36+
user := userData.(string)
37+
allowed, ok := authz.AllowTable[user]
38+
if !ok {
39+
c.JSON(http.StatusUnauthorized, gin.H{"msg": "Unauthorized user", "user": user})
40+
return
41+
}
42+
43+
tags := strings.Join(allowed, ",")
44+
if tags == "" {
45+
tags = "*"
46+
}
47+
48+
(&httputil.ReverseProxy{
49+
Transport: roundTripper(requestHandler),
50+
Director: func(req *http.Request) {
51+
req.URL.Host = url.Host
52+
req.URL.Scheme = url.Scheme
53+
req.URL.Path = url.Path + req.URL.Path
54+
req.Header.Set("x-api-key", apiKey)
55+
req.Header.Set("minerva-allowed-tags", tags)
56+
logger.WithField("header", req.Header).Info("API requet header")
57+
},
58+
}).ServeHTTP(c.Writer, c.Request)
59+
3960
}, nil
4061
}
4162

42-
func setupAPI(ssnMgr *sessionManager, endpoint string, r *gin.RouterGroup) error {
43-
proxy, err := reverseProxy(endpoint)
63+
func setupAPI(authz *authzService, apiKey, endpoint string, r *gin.RouterGroup) error {
64+
proxy, err := reverseProxy(authz, apiKey, endpoint)
4465
if err != nil {
4566
return err
4667
}

auth.go renamed to authn.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,16 @@ type sessionManager struct {
2626
}
2727

2828
func newSessionManager(jwtSecret string) *sessionManager {
29-
return &sessionManager{
30-
jwtSecret: []byte(jwtSecret),
29+
mgr := &sessionManager{}
30+
31+
if jwtSecret == "" {
32+
logger.Warn("jwt-secret is not set, then automatically generated")
33+
mgr.jwtSecret = []byte(genRandomSecret())
34+
} else {
35+
mgr.jwtSecret = []byte(jwtSecret)
3136
}
37+
38+
return mgr
3239
}
3340

3441
func (x *sessionManager) sign(user strixUser, c *gin.Context) error {

authz.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
8+
"github.com/pkg/errors"
9+
)
10+
11+
type authzService struct {
12+
AllowTable map[string][]string `json:"allow"`
13+
}
14+
15+
func newAuthzService(filePath string) (*authzService, error) {
16+
raw, err := ioutil.ReadFile(filePath)
17+
if err != nil {
18+
return nil, errors.Wrapf(err, "Fail to load authz file: %s", filePath)
19+
}
20+
21+
srv := authzService{
22+
AllowTable: make(map[string][]string),
23+
}
24+
if err := json.Unmarshal(raw, &srv); err != nil {
25+
return nil, errors.Wrapf(err, "Fail to parse authz file: %s", filePath)
26+
}
27+
28+
logger.WithField("authz", srv.AllowTable).Info("Read authorization table")
29+
return &srv, nil
30+
}
31+
32+
func (x *authzService) allowedTags(user string) ([]string, error) {
33+
allowed, ok := x.AllowTable[user]
34+
if !ok {
35+
return nil, fmt.Errorf("Not permitted")
36+
}
37+
38+
return allowed, nil
39+
}

logger.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/sirupsen/logrus"
7+
)
8+
9+
var logger = logrus.New()
10+
11+
var logLevelMap = map[string]logrus.Level{
12+
"trace": logrus.TraceLevel,
13+
"debug": logrus.DebugLevel,
14+
"info": logrus.InfoLevel,
15+
"warn": logrus.WarnLevel,
16+
"error": logrus.ErrorLevel,
17+
}
18+
19+
func setupLogger(logLevel string) error {
20+
level, ok := logLevelMap[logLevel]
21+
if !ok {
22+
return fmt.Errorf("Invalid log level: %s", logLevel)
23+
}
24+
logger.SetLevel(level)
25+
logger.SetFormatter(&logrus.JSONFormatter{})
26+
27+
return nil
28+
}

main.go

Lines changed: 22 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -6,95 +6,9 @@ import (
66
"os"
77
"time"
88

9-
"github.com/gin-contrib/static"
10-
"github.com/gin-gonic/gin"
11-
"github.com/sirupsen/logrus"
129
"github.com/urfave/cli"
13-
14-
"github.com/gin-contrib/sessions"
15-
"github.com/gin-contrib/sessions/cookie"
1610
)
1711

18-
var logger = logrus.New()
19-
20-
var logLevelMap = map[string]logrus.Level{
21-
"trace": logrus.TraceLevel,
22-
"debug": logrus.DebugLevel,
23-
"info": logrus.InfoLevel,
24-
"warn": logrus.WarnLevel,
25-
"error": logrus.ErrorLevel,
26-
}
27-
28-
type arguments struct {
29-
LogLevel string
30-
Endpoint string
31-
BindAddress string
32-
BindPort int
33-
StaticContents string
34-
35-
// Google OAuth options
36-
GoogleOAuthConfig string
37-
38-
// JWT
39-
JWTSecret string
40-
}
41-
42-
func runServer(args arguments) error {
43-
level, ok := logLevelMap[args.LogLevel]
44-
if !ok {
45-
return fmt.Errorf("Invalid log level: %s", args.LogLevel)
46-
}
47-
logger.SetLevel(level)
48-
logger.SetFormatter(&logrus.JSONFormatter{})
49-
logger.WithFields(logrus.Fields{
50-
"args": args,
51-
}).Info("Given options")
52-
53-
helloReply := os.Getenv("HELLO_REPLY")
54-
if helloReply == "" {
55-
helloReply = time.Now().String()
56-
}
57-
58-
r := gin.Default()
59-
store := cookie.NewStore([]byte("auth"))
60-
r.Use(sessions.Sessions("strix", store))
61-
r.Use(static.Serve("/", static.LocalFile(args.StaticContents, false)))
62-
/*
63-
r.LoadHTMLGlob(path.Join(args.TemplatePath, "*"))
64-
r.GET("/", func(c *gin.Context) {
65-
c.HTML(http.StatusOK, "index.html", gin.H{
66-
"googleOAuth": (args.GoogleOAuthConfig != ""),
67-
})
68-
})
69-
*/
70-
r.GET("/hello/revision", func(c *gin.Context) {
71-
c.String(200, helloReply)
72-
})
73-
74-
ssnMgr := newSessionManager(args.JWTSecret)
75-
76-
authGroup := r.Group("/auth")
77-
if err := setupAuth(ssnMgr, authGroup); err != nil {
78-
return err
79-
}
80-
if args.GoogleOAuthConfig != "" {
81-
if err := setupAuthGoogle(ssnMgr, args.GoogleOAuthConfig, authGroup); err != nil {
82-
return err
83-
}
84-
}
85-
86-
apiGroup := r.Group("/api/v1")
87-
if err := setupAPI(ssnMgr, args.Endpoint, apiGroup); err != nil {
88-
return err
89-
}
90-
91-
if err := r.Run(fmt.Sprintf("%s:%d", args.BindAddress, args.BindPort)); err != nil {
92-
return err
93-
}
94-
95-
return nil
96-
}
97-
9812
func genRandomSecret() string {
9913
const randomSecretLength = 32
10014
letters := "abcdefghijklmnopqrstuvwxyz" +
@@ -147,6 +61,11 @@ func main() {
14761
Usage: "Static contents path",
14862
Destination: &args.StaticContents,
14963
},
64+
cli.StringFlag{
65+
Name: "hello-reply, r", Value: time.Now().String(),
66+
Usage: "Reply message for /hello/revision",
67+
Destination: &args.HelloReply,
68+
},
15069

15170
cli.StringFlag{
15271
Name: "google-oauth-config, g",
@@ -155,9 +74,24 @@ func main() {
15574
},
15675
cli.StringFlag{
15776
Name: "jwt-secret, j",
158-
Value: genRandomSecret(),
15977
Usage: "JWT secret to sign and validate token",
160-
Destination: &args.GoogleOAuthConfig,
78+
Destination: &args.JWTSecret,
79+
},
80+
cli.StringFlag{
81+
Name: "api-key, k",
82+
Usage: "API Key of Minerva",
83+
Destination: &args.APIKey,
84+
},
85+
86+
cli.StringFlag{
87+
Name: "authz-path, z",
88+
Usage: "Authorization list json file path",
89+
Destination: &args.AuthzFilePath,
90+
},
91+
cli.StringFlag{
92+
Name: "secret-arn",
93+
Usage: "Secret ARN of SecretsManager",
94+
Destination: &args.SecretArn,
16195
},
16296
}
16397
app.ArgsUsage = "[endpoint]"

server.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
7+
"github.com/gin-contrib/sessions"
8+
"github.com/gin-contrib/sessions/cookie"
9+
"github.com/gin-contrib/static"
10+
"github.com/gin-gonic/gin"
11+
"github.com/sirupsen/logrus"
12+
)
13+
14+
type arguments struct {
15+
LogLevel string
16+
Endpoint string
17+
BindAddress string
18+
BindPort int
19+
StaticContents string
20+
HelloReply string
21+
APIKey string
22+
SecretArn string
23+
AuthzFilePath string
24+
25+
// Google OAuth options
26+
GoogleOAuthConfig string
27+
28+
// JWT
29+
JWTSecret string
30+
}
31+
32+
func runServer(args arguments) error {
33+
if err := setupLogger(args.LogLevel); err != nil {
34+
return err
35+
}
36+
37+
logger.WithFields(logrus.Fields{
38+
"args": args,
39+
}).Info("Given options")
40+
41+
r := gin.Default()
42+
store := cookie.NewStore([]byte("auth"))
43+
r.Use(sessions.Sessions("strix", store))
44+
r.Use(static.Serve("/", static.LocalFile(args.StaticContents, false)))
45+
46+
r.GET("/hello/revision", func(c *gin.Context) {
47+
c.String(200, args.HelloReply)
48+
})
49+
50+
// Setup session manager
51+
authz, err := newAuthzService(args.AuthzFilePath)
52+
if err != nil {
53+
return err
54+
}
55+
56+
ssnMgr := newSessionManager(args.JWTSecret)
57+
authCheck := func(c *gin.Context) {
58+
user, err := ssnMgr.validate(c)
59+
if err != nil {
60+
logger.WithError(err).Warn("Authentication Fail")
61+
c.JSON(http.StatusUnauthorized, gin.H{"msg": "Authentication failed"})
62+
} else {
63+
c.Set("user", user.UserID)
64+
c.Next()
65+
}
66+
}
67+
68+
// Auth route group
69+
authGroup := r.Group("/auth")
70+
if err := setupAuth(ssnMgr, authGroup); err != nil {
71+
return err
72+
}
73+
if args.GoogleOAuthConfig != "" {
74+
if err := setupAuthGoogle(ssnMgr, args.GoogleOAuthConfig, authGroup); err != nil {
75+
return err
76+
}
77+
}
78+
79+
// API route group
80+
apiGroup := r.Group("/api/v1")
81+
apiGroup.Use(authCheck)
82+
if err := setupAPI(authz, args.APIKey, args.Endpoint, apiGroup); err != nil {
83+
return err
84+
}
85+
86+
// Start server
87+
if err := r.Run(fmt.Sprintf("%s:%d", args.BindAddress, args.BindPort)); err != nil {
88+
return err
89+
}
90+
91+
return nil
92+
}

src/js/header.vue

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@
77
</h1>
88
</div>
99
</nav>
10-
<div class="msgbox login">
11-
<a href="/auth/google">Google</a>
12-
</div>
1310
</div>
1411
</template>
1512

0 commit comments

Comments
 (0)