Skip to content

Commit 34ee97a

Browse files
committed
feat: auth package
1 parent 6987737 commit 34ee97a

File tree

22 files changed

+1221
-15
lines changed

22 files changed

+1221
-15
lines changed

examples/auth/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import (
44
"log"
55
"net/http"
66

7-
"github.com/rest-go/auth"
7+
"github.com/rest-go/rest/pkg/auth"
88
"github.com/rest-go/rest/pkg/server"
99
)
1010

go.mod

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ go 1.18
44

55
require (
66
github.com/go-sql-driver/mysql v1.7.0
7+
github.com/golang-jwt/jwt/v4 v4.4.3
78
github.com/jackc/pgconn v1.13.0
89
github.com/jackc/pgx/v5 v5.2.0
9-
github.com/rest-go/auth v0.1.1
1010
github.com/stretchr/testify v1.8.1
11+
golang.org/x/crypto v0.5.0
1112
gopkg.in/yaml.v3 v3.0.1
1213
modernc.org/sqlite v1.20.0
1314
)
1415

1516
require (
1617
github.com/davecgh/go-spew v1.1.1 // indirect
17-
github.com/golang-jwt/jwt/v4 v4.4.3 // indirect
1818
github.com/google/uuid v1.3.0 // indirect
1919
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
2020
github.com/jackc/pgio v1.0.0 // indirect
@@ -27,7 +27,6 @@ require (
2727
github.com/pmezard/go-difflib v1.0.0 // indirect
2828
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
2929
github.com/rogpeppe/go-internal v1.6.1 // indirect
30-
golang.org/x/crypto v0.5.0 // indirect
3130
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
3231
golang.org/x/sys v0.4.0 // indirect
3332
golang.org/x/text v0.6.0 // indirect

go.sum

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -76,16 +76,12 @@ github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd
7676
github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s=
7777
github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ=
7878
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
79-
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
79+
github.com/mattn/go-sqlite3 v1.14.15 h1:vfoHhTN1af61xCRSWzFIWzx2YskyMTwHLrExkBOjvxI=
8080
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
8181
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
8282
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
8383
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
8484
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
85-
github.com/rest-go/auth v0.1.0 h1:wsJsKilaJUdl4jvYKD00kOOaoGJ0469FMpz35WenZh8=
86-
github.com/rest-go/auth v0.1.0/go.mod h1:i9RnkB56gcydO2lOMVfRxBlJ6GKV4xY0rJW4itIk3cw=
87-
github.com/rest-go/auth v0.1.1 h1:dTs4RHRkrM2WpvpRvT2Ni24YaQ6iYdQMLIIGh2Uk/V8=
88-
github.com/rest-go/auth v0.1.1/go.mod h1:yLuzwqpfapKIXU6jACopEZBpKJL61GcQ7SnXghvTIAY=
8985
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
9086
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
9187
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import (
99
"strconv"
1010
"time"
1111

12-
"github.com/rest-go/auth"
12+
"github.com/rest-go/rest/pkg/auth"
1313

1414
"github.com/rest-go/rest/pkg/log"
1515
"github.com/rest-go/rest/pkg/server"

pkg/auth/README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
# Auth package
2+
3+
4+
Auth is a RESTFul Authentication and Authorization package for Golang HTTP apps.
5+
6+
It handles the common tasks of registration, logging in, logging out, JWT token generation, and JWT token verification.
7+
8+
## Usage
9+
import `auth` to your app, create `auth.Handler` and `auth.Middleware` based on requirements.
10+
```go
11+
package main
12+
13+
import (
14+
"log"
15+
"net/http"
16+
17+
"github.com/rest-go/rest/pkg/auth"
18+
)
19+
20+
func handle(w http.ResponseWriter, req *http.Request) {
21+
user := auth.GetUser(req)
22+
if user.IsAnonymous() {
23+
w.WriteHeader(http.StatusUnauthorized)
24+
} else {
25+
w.WriteHeader(http.StatusOK)
26+
}
27+
}
28+
29+
func main() {
30+
dbURL := "sqlite://my.db"
31+
jwtSecret := "my secret"
32+
authHandler, err := auth.NewHandler(dbURL, []byte(jwtSecret))
33+
if err != nil {
34+
log.Fatal(err)
35+
}
36+
http.Handle("/auth/", authHandler)
37+
38+
middleware := auth.NewMiddleware([]byte(jwtSecret))
39+
http.Handle("/", middleware(http.HandlerFunc(handle)))
40+
log.Fatal(http.ListenAndServe(":8000", nil)) //nolint:gosec
41+
}
42+
```
43+
44+
## Setup database
45+
46+
Send a `POST` request to `/auth/setup` to set up database tables for users. This
47+
will also create an admin user account and return the username and password in
48+
the response.
49+
50+
```bash
51+
$ curl -XPOST "localhost:8000/auth/setup"
52+
```
53+
54+
## Auth handler
55+
56+
The `Auth` struct implements the `http.Hanlder` interface and provides the below endpoints for user management.
57+
58+
1. Register
59+
60+
```bash
61+
$ curl -XPOST "localhost:8000/auth/register" -d '{"username":"hello", "password": "world"}'
62+
```
63+
64+
2. Login
65+
66+
```bash
67+
$ curl -XPOST "localhost:8000/auth/login" -d '{"username":"hello", "password": "world"}'
68+
```
69+
70+
3. Logout
71+
72+
Currently, the authentication mechanism is based on JWT token only, logout is a no-op on the
73+
server side, and the client should clear the token by itself.
74+
75+
```bash
76+
$ curl -XPOST "localhost:8000/auth/logout"
77+
```
78+
79+
## Auth middleware and `GetUser`
80+
81+
Auth middleware will parse JWT token in the HTTP header, and when successful,
82+
set the user in the request context, the `GetUser` method can be used to get the
83+
user from the request.
84+
85+
``` go
86+
user := auth.GetUser(req)
87+
```
88+

pkg/auth/action.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package auth
2+
3+
type Action int
4+
5+
const (
6+
ActionCreate Action = iota
7+
ActionRead
8+
ActionUpdate
9+
ActionDelete
10+
ActionReadMine // read with ?mine query, usually filter by user_id field
11+
)
12+
13+
var actionToStr = map[Action]string{
14+
ActionCreate: "create",
15+
ActionRead: "read",
16+
ActionUpdate: "update",
17+
ActionDelete: "delete",
18+
ActionReadMine: "read_mine",
19+
}
20+
21+
func (a Action) String() string {
22+
return actionToStr[a]
23+
}

pkg/auth/auth.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
// package auth provide restful interface for authentication
2+
package auth
3+
4+
import (
5+
"context"
6+
"errors"
7+
"fmt"
8+
9+
"github.com/golang-jwt/jwt/v4"
10+
"github.com/rest-go/rest/pkg/sql"
11+
)
12+
13+
var primaryKeySQL = map[string]string{
14+
"postgres": "BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY",
15+
"mysql": "BIGINT PRIMARY KEY AUTO_INCREMENT",
16+
"sqlite": "INTEGER PRIMARY KEY",
17+
}
18+
19+
// GenJWTToken generate and return jwt token
20+
func GenJWTToken(secret []byte, data map[string]any) (string, error) {
21+
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(data))
22+
return token.SignedString(secret)
23+
}
24+
25+
// ParseJWTToken parse tokenString and return data if token is valid
26+
func ParseJWTToken(secret []byte, tokenString string) (map[string]any, error) {
27+
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
28+
// Don't forget to validate the alg is what you expect:
29+
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
30+
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
31+
}
32+
return secret, nil
33+
})
34+
if err != nil {
35+
return nil, err
36+
}
37+
38+
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
39+
return map[string]any(claims), nil
40+
}
41+
42+
return nil, errors.New("invalid token")
43+
}
44+
45+
// Setup setup database tables and create an admin user account
46+
func Setup(db *sql.DB) (username, password string, err error) {
47+
if isSetupDone(db) {
48+
err = errors.New("setup is already done before")
49+
return
50+
}
51+
username, password, err = setupUsers(db)
52+
if err != nil {
53+
return
54+
}
55+
err = setupPolicies(db)
56+
return
57+
}
58+
59+
func isSetupDone(db *sql.DB) bool {
60+
ctx, cancel := context.WithTimeout(context.Background(), sql.DefaultTimeout)
61+
defer cancel()
62+
_, err := db.ExecQuery(ctx, "SELECT 1 FROM auth_users")
63+
return err == nil
64+
}

pkg/auth/auth_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// package auth provide restful interface for authentication
2+
package auth
3+
4+
import (
5+
"os"
6+
"reflect"
7+
"testing"
8+
"time"
9+
10+
"github.com/stretchr/testify/assert"
11+
12+
"github.com/rest-go/rest/pkg/log"
13+
"github.com/rest-go/rest/pkg/sql"
14+
)
15+
16+
func TestJWTToken(t *testing.T) {
17+
t.Run("happy path", func(t *testing.T) {
18+
data := map[string]any{
19+
"a": "b",
20+
}
21+
token, err := GenJWTToken([]byte(testSecret), data)
22+
assert.Nil(t, err)
23+
24+
parsedData, err := ParseJWTToken([]byte(testSecret), token)
25+
assert.Nil(t, err)
26+
assert.True(t, reflect.DeepEqual(data, parsedData))
27+
})
28+
29+
t.Run("invalid token", func(t *testing.T) {
30+
data := map[string]any{
31+
"a": "b",
32+
}
33+
token, err := GenJWTToken([]byte(testSecret), data)
34+
assert.Nil(t, err)
35+
36+
parsedData, err := ParseJWTToken([]byte(testSecret), token[:len(token)-1])
37+
assert.Nil(t, parsedData)
38+
assert.NotNil(t, err)
39+
t.Log(err)
40+
})
41+
42+
t.Run("expired token", func(t *testing.T) {
43+
data := map[string]any{
44+
"a": "b",
45+
"exp": time.Now().Add(-24 * time.Hour).Unix(),
46+
}
47+
token, err := GenJWTToken([]byte(testSecret), data)
48+
assert.Nil(t, err)
49+
50+
parsedData, err := ParseJWTToken([]byte(testSecret), token)
51+
assert.Nil(t, parsedData)
52+
assert.NotNil(t, err)
53+
t.Log(err)
54+
})
55+
}
56+
57+
func TestSetup(t *testing.T) {
58+
file, err := os.CreateTemp(".", "test-")
59+
if err != nil {
60+
log.Fatal(err)
61+
}
62+
defer os.Remove(file.Name())
63+
db, err := sql.Open("sqlite://" + file.Name())
64+
assert.Nil(t, err)
65+
_, _, err = Setup(db)
66+
assert.Nil(t, err)
67+
68+
// call Setup again will return an error
69+
_, _, err = Setup(db)
70+
assert.NotNil(t, err)
71+
t.Log(err)
72+
}

pkg/auth/examples/handler/main.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+
"log"
5+
"net/http"
6+
7+
"github.com/rest-go/rest/pkg/auth"
8+
)
9+
10+
func handle(w http.ResponseWriter, req *http.Request) {
11+
user := auth.GetUser(req)
12+
if user.IsAnonymous() {
13+
w.WriteHeader(http.StatusUnauthorized)
14+
} else {
15+
w.WriteHeader(http.StatusOK)
16+
}
17+
}
18+
19+
func main() {
20+
dbURL := "sqlite://my.db"
21+
jwtSecret := "my secret"
22+
authHandler, err := auth.NewHandler(dbURL, []byte(jwtSecret))
23+
if err != nil {
24+
log.Fatal(err)
25+
}
26+
middleware := auth.NewMiddleware([]byte(jwtSecret))
27+
28+
http.Handle("/auth/", authHandler)
29+
http.Handle("/", middleware(http.HandlerFunc(handle)))
30+
log.Fatal(http.ListenAndServe(":8000", nil)) //nolint:gosec
31+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package main
2+
3+
import (
4+
"log"
5+
"net/http"
6+
7+
"github.com/rest-go/rest/pkg/auth"
8+
)
9+
10+
func handle(w http.ResponseWriter, req *http.Request) {
11+
user := auth.GetUser(req)
12+
if user.IsAnonymous() {
13+
w.WriteHeader(http.StatusUnauthorized)
14+
} else {
15+
w.WriteHeader(http.StatusOK)
16+
}
17+
}
18+
19+
func main() {
20+
jwtSecret := "my secret"
21+
middleware := auth.NewMiddleware([]byte(jwtSecret))
22+
23+
http.Handle("/", middleware(http.HandlerFunc(handle)))
24+
log.Fatal(http.ListenAndServe(":8000", nil)) //nolint:gosec
25+
}

0 commit comments

Comments
 (0)