Skip to content

Commit 1e98065

Browse files
Merge branch 'feat/api-key'
2 parents e1078fa + 58b71cb commit 1e98065

File tree

6 files changed

+110
-41
lines changed

6 files changed

+110
-41
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ Create a `.env` file in this directory that contains necessary environment varia
99
```
1010
USERNAME=<username>
1111
PASSWORD=<password>
12+
API_KEY=<apikey>
1213
1314
HOST=<host>
1415
PORT=<port>

authController.go

Lines changed: 40 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -14,28 +14,22 @@ import (
1414
type authController struct {
1515
jwtSecret string
1616
tokenDurationSeconds int
17-
username string
18-
password string
17+
authenticator authenticator
1918
}
2019

2120
// GET /auth/token
2221
// Requires basic auth
2322
func (a authController) getAuthToken(w http.ResponseWriter, r *http.Request) {
2423
reqUsername, reqPassword, _ := r.BasicAuth()
25-
if reqUsername != a.username {
26-
log.Printf("Invalid username: %s", reqUsername)
27-
w.WriteHeader(http.StatusForbidden)
28-
return
29-
}
30-
if reqPassword != a.password {
31-
log.Printf("Invalid password")
24+
valid, err := a.authenticator.authenticate(reqUsername, reqPassword)
25+
if !valid {
3226
w.WriteHeader(http.StatusForbidden)
3327
return
3428
}
3529

3630
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
3731
"subject": "go",
38-
"username": a.username,
32+
"username": reqUsername,
3933
"exp": time.Now().Unix() + int64(a.tokenDurationSeconds),
4034
})
4135
tokenString, err := token.SignedString([]byte(a.jwtSecret))
@@ -60,34 +54,50 @@ func (a authController) getAuthToken(w http.ResponseWriter, r *http.Request) {
6054
w.Write(httpJson)
6155
}
6256

63-
func tokenAuthMiddleware(jwtSecret string, next http.Handler) http.Handler {
57+
func authMiddleware(jwtSecret string, apiKey string, next http.Handler) http.Handler {
6458
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
65-
var tokenString string
59+
var bearerTokenString string
60+
var apiKeyString string
6661
for _, headerValue := range r.Header["Authorization"] {
6762
if strings.HasPrefix(headerValue, "Bearer ") {
68-
tokenString, _ = strings.CutPrefix(headerValue, "Bearer ")
63+
bearerTokenString, _ = strings.CutPrefix(headerValue, "Bearer ")
64+
}
65+
if strings.HasPrefix(headerValue, "ApiKey ") {
66+
apiKeyString, _ = strings.CutPrefix(headerValue, "ApiKey ")
6967
}
7068
}
7169

72-
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
73-
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
74-
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
70+
if bearerTokenString != "" {
71+
token, err := jwt.Parse(bearerTokenString, func(token *jwt.Token) (interface{}, error) {
72+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
73+
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
74+
}
75+
return []byte(jwtSecret), nil
76+
})
77+
if err != nil {
78+
log.Printf("JWT parsing failed: %s", err)
79+
w.WriteHeader(http.StatusUnauthorized)
80+
return
7581
}
76-
return []byte(jwtSecret), nil
77-
})
78-
if err != nil {
79-
log.Printf("JWT parsing failed: %s", err)
80-
w.WriteHeader(http.StatusUnauthorized)
81-
return
82-
}
8382

84-
claims, ok := token.Claims.(jwt.MapClaims)
85-
if !ok {
86-
log.Printf("JWT claims failed: %s", err)
87-
}
88-
exp, _ := claims.GetExpirationTime()
89-
if exp.Before(time.Now()) {
90-
log.Printf("JWT expired at: %s", exp)
83+
claims, ok := token.Claims.(jwt.MapClaims)
84+
if !ok {
85+
log.Printf("JWT claims failed: %s", err)
86+
}
87+
exp, _ := claims.GetExpirationTime()
88+
if exp.Before(time.Now()) {
89+
log.Printf("JWT expired at: %s", exp)
90+
w.WriteHeader(http.StatusUnauthorized)
91+
return
92+
}
93+
} else if apiKey != "" && apiKeyString != "" {
94+
if apiKeyString != apiKey {
95+
log.Printf("Invalid API key")
96+
w.WriteHeader(http.StatusUnauthorized)
97+
return
98+
}
99+
} else {
100+
log.Printf("Authorization scheme not supported")
91101
w.WriteHeader(http.StatusUnauthorized)
92102
return
93103
}

authenticator.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"log"
6+
)
7+
8+
// authenticator is an interface for validating usernames and passwords.
9+
type authenticator interface {
10+
// authenticate returns true if the username and password are valid.
11+
authenticate(username string, password string) (bool, error)
12+
}
13+
14+
// singleUserAuthenticator is a simple authenticator that only accepts a literal single username and password.
15+
// In real applications, these should be injected by environment variables.
16+
type singleUserAuthenticator struct {
17+
username string
18+
password string
19+
}
20+
21+
func (a singleUserAuthenticator) authenticate(username string, password string) (bool, error) {
22+
if username != a.username {
23+
log.Printf("Invalid username: %s", username)
24+
return false, fmt.Errorf("Invalid username: %s", username)
25+
}
26+
if password != a.password {
27+
log.Printf("Invalid password")
28+
return false, fmt.Errorf("Invalid password")
29+
}
30+
return true, nil
31+
}

main.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,13 +64,17 @@ func main() {
6464
go serveMetrics()
6565

6666
// Stores
67+
authenticator := singleUserAuthenticator{
68+
username: os.Getenv("USERNAME"),
69+
password: os.Getenv("PASSWORD"),
70+
}
6771
historyStore := newGormHistoryStore(db)
6872
recStore := newGormRecStore(db)
6973
currentStore := newInMemoryCurrentStore()
7074

7175
serverConfig := ServerConfig{
72-
username: os.Getenv("USERNAME"),
73-
password: os.Getenv("PASSWORD"),
76+
authenticator: authenticator,
77+
apiKey: os.Getenv("API_KEY"),
7478
jwtSecret: os.Getenv("JWT_SECRET"),
7579
tokenDurationSeconds: 60 * 60, // 1 hour
7680

server.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import (
88

99
type ServerConfig struct {
1010
// Auth
11-
username string
12-
password string
11+
authenticator authenticator
12+
apiKey string
1313
jwtSecret string
1414
tokenDurationSeconds int
1515

@@ -27,8 +27,7 @@ func NewServer(serverConfig ServerConfig) (http.Handler, error) {
2727
authController := authController{
2828
jwtSecret: serverConfig.jwtSecret,
2929
tokenDurationSeconds: serverConfig.tokenDurationSeconds,
30-
username: serverConfig.username,
31-
password: serverConfig.password,
30+
authenticator: serverConfig.authenticator,
3231
}
3332
hisController := hisController{store: serverConfig.historyStore}
3433
recController := recController{store: serverConfig.recStore}
@@ -51,9 +50,9 @@ func NewServer(serverConfig ServerConfig) (http.Handler, error) {
5150
handleFunc(tokenAuth, "DELETE /api/recs/{pointId}/history", hisController.deleteHis)
5251
handleFunc(tokenAuth, "GET /api/recs/{pointId}/current", currentController.getCurrent)
5352
handleFunc(tokenAuth, "POST /api/recs/{pointId}/current", currentController.postCurrent)
54-
server.Handle("/api/his/", tokenAuthMiddleware(serverConfig.jwtSecret, tokenAuth))
55-
server.Handle("/api/recs", tokenAuthMiddleware(serverConfig.jwtSecret, tokenAuth))
56-
server.Handle("/api/recs/", tokenAuthMiddleware(serverConfig.jwtSecret, tokenAuth))
53+
server.Handle("/api/his/", authMiddleware(serverConfig.jwtSecret, serverConfig.apiKey, tokenAuth))
54+
server.Handle("/api/recs", authMiddleware(serverConfig.jwtSecret, serverConfig.apiKey, tokenAuth))
55+
server.Handle("/api/recs/", authMiddleware(serverConfig.jwtSecret, serverConfig.apiKey, tokenAuth))
5756

5857
// Catch all others with public files. Not found fallback is app index for browser router.
5958
server.Handle("/app/", fileServerWithFallback(http.Dir("./public"), "./public/app/index.html"))

server_test.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,15 +33,19 @@ func (suite *ServerTestSuite) SetupTest() {
3333
err = db.AutoMigrate(&gormHis{}, &gormRec{})
3434
assert.Nil(suite.T(), err)
3535

36+
authenticator := singleUserAuthenticator{
37+
username: "test",
38+
password: "password",
39+
}
3640
historyStore := newGormHistoryStore(db)
3741
recStore := newGormRecStore(db)
3842
currentStore := newInMemoryCurrentStore()
3943

4044
server, err := NewServer(ServerConfig{
41-
username: "test",
42-
password: "password",
45+
authenticator: authenticator,
4346
jwtSecret: "aaa",
4447
tokenDurationSeconds: 60,
48+
apiKey: "valid",
4549

4650
historyStore: historyStore,
4751
recStore: recStore,
@@ -88,6 +92,26 @@ func (suite *ServerTestSuite) TestGetAuthTokenInvalidPassword() {
8892
assert.Equal(suite.T(), response.Code, http.StatusForbidden)
8993
}
9094

95+
func (suite *ServerTestSuite) TestApiKey() {
96+
request, _ := http.NewRequest(http.MethodGet, "/api/recs", nil)
97+
request.Header.Add("Authorization", fmt.Sprintf("ApiKey %s", "valid"))
98+
response := httptest.NewRecorder()
99+
100+
suite.server.ServeHTTP(response, request)
101+
102+
assert.Equal(suite.T(), http.StatusOK, response.Code)
103+
}
104+
105+
func (suite *ServerTestSuite) TestApiKeyInvalid() {
106+
request, _ := http.NewRequest(http.MethodGet, "/api/recs", nil)
107+
request.Header.Add("Authorization", fmt.Sprintf("ApiKey %s", "invalid"))
108+
response := httptest.NewRecorder()
109+
110+
suite.server.ServeHTTP(response, request)
111+
112+
assert.Equal(suite.T(), http.StatusUnauthorized, response.Code)
113+
}
114+
91115
func (suite *ServerTestSuite) TestGetHis() {
92116
// Insert data for 2 different points with varying timestamps
93117
pointId1 := uuid.New()

0 commit comments

Comments
 (0)