-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
22 changed files
with
1,221 additions
and
15 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
# Auth package | ||
|
||
|
||
Auth is a RESTFul Authentication and Authorization package for Golang HTTP apps. | ||
|
||
It handles the common tasks of registration, logging in, logging out, JWT token generation, and JWT token verification. | ||
|
||
## Usage | ||
import `auth` to your app, create `auth.Handler` and `auth.Middleware` based on requirements. | ||
```go | ||
package main | ||
|
||
import ( | ||
"log" | ||
"net/http" | ||
|
||
"github.com/rest-go/rest/pkg/auth" | ||
) | ||
|
||
func handle(w http.ResponseWriter, req *http.Request) { | ||
user := auth.GetUser(req) | ||
if user.IsAnonymous() { | ||
w.WriteHeader(http.StatusUnauthorized) | ||
} else { | ||
w.WriteHeader(http.StatusOK) | ||
} | ||
} | ||
|
||
func main() { | ||
dbURL := "sqlite://my.db" | ||
jwtSecret := "my secret" | ||
authHandler, err := auth.NewHandler(dbURL, []byte(jwtSecret)) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
http.Handle("/auth/", authHandler) | ||
|
||
middleware := auth.NewMiddleware([]byte(jwtSecret)) | ||
http.Handle("/", middleware(http.HandlerFunc(handle))) | ||
log.Fatal(http.ListenAndServe(":8000", nil)) //nolint:gosec | ||
} | ||
``` | ||
|
||
## Setup database | ||
|
||
Send a `POST` request to `/auth/setup` to set up database tables for users. This | ||
will also create an admin user account and return the username and password in | ||
the response. | ||
|
||
```bash | ||
$ curl -XPOST "localhost:8000/auth/setup" | ||
``` | ||
|
||
## Auth handler | ||
|
||
The `Auth` struct implements the `http.Hanlder` interface and provides the below endpoints for user management. | ||
|
||
1. Register | ||
|
||
```bash | ||
$ curl -XPOST "localhost:8000/auth/register" -d '{"username":"hello", "password": "world"}' | ||
``` | ||
|
||
2. Login | ||
|
||
```bash | ||
$ curl -XPOST "localhost:8000/auth/login" -d '{"username":"hello", "password": "world"}' | ||
``` | ||
|
||
3. Logout | ||
|
||
Currently, the authentication mechanism is based on JWT token only, logout is a no-op on the | ||
server side, and the client should clear the token by itself. | ||
|
||
```bash | ||
$ curl -XPOST "localhost:8000/auth/logout" | ||
``` | ||
|
||
## Auth middleware and `GetUser` | ||
|
||
Auth middleware will parse JWT token in the HTTP header, and when successful, | ||
set the user in the request context, the `GetUser` method can be used to get the | ||
user from the request. | ||
|
||
``` go | ||
user := auth.GetUser(req) | ||
``` | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package auth | ||
|
||
type Action int | ||
|
||
const ( | ||
ActionCreate Action = iota | ||
ActionRead | ||
ActionUpdate | ||
ActionDelete | ||
ActionReadMine // read with ?mine query, usually filter by user_id field | ||
) | ||
|
||
var actionToStr = map[Action]string{ | ||
ActionCreate: "create", | ||
ActionRead: "read", | ||
ActionUpdate: "update", | ||
ActionDelete: "delete", | ||
ActionReadMine: "read_mine", | ||
} | ||
|
||
func (a Action) String() string { | ||
return actionToStr[a] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
// package auth provide restful interface for authentication | ||
package auth | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
|
||
"github.com/golang-jwt/jwt/v4" | ||
"github.com/rest-go/rest/pkg/sql" | ||
) | ||
|
||
var primaryKeySQL = map[string]string{ | ||
"postgres": "BIGINT PRIMARY KEY GENERATED ALWAYS AS IDENTITY", | ||
"mysql": "BIGINT PRIMARY KEY AUTO_INCREMENT", | ||
"sqlite": "INTEGER PRIMARY KEY", | ||
} | ||
|
||
// GenJWTToken generate and return jwt token | ||
func GenJWTToken(secret []byte, data map[string]any) (string, error) { | ||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(data)) | ||
return token.SignedString(secret) | ||
} | ||
|
||
// ParseJWTToken parse tokenString and return data if token is valid | ||
func ParseJWTToken(secret []byte, tokenString string) (map[string]any, error) { | ||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { | ||
// Don't forget to validate the alg is what you expect: | ||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { | ||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"]) | ||
} | ||
return secret, nil | ||
}) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid { | ||
return map[string]any(claims), nil | ||
} | ||
|
||
return nil, errors.New("invalid token") | ||
} | ||
|
||
// Setup setup database tables and create an admin user account | ||
func Setup(db *sql.DB) (username, password string, err error) { | ||
if isSetupDone(db) { | ||
err = errors.New("setup is already done before") | ||
return | ||
} | ||
username, password, err = setupUsers(db) | ||
if err != nil { | ||
return | ||
} | ||
err = setupPolicies(db) | ||
return | ||
} | ||
|
||
func isSetupDone(db *sql.DB) bool { | ||
ctx, cancel := context.WithTimeout(context.Background(), sql.DefaultTimeout) | ||
defer cancel() | ||
_, err := db.ExecQuery(ctx, "SELECT 1 FROM auth_users") | ||
return err == nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
// package auth provide restful interface for authentication | ||
package auth | ||
|
||
import ( | ||
"os" | ||
"reflect" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/assert" | ||
|
||
"github.com/rest-go/rest/pkg/log" | ||
"github.com/rest-go/rest/pkg/sql" | ||
) | ||
|
||
func TestJWTToken(t *testing.T) { | ||
t.Run("happy path", func(t *testing.T) { | ||
data := map[string]any{ | ||
"a": "b", | ||
} | ||
token, err := GenJWTToken([]byte(testSecret), data) | ||
assert.Nil(t, err) | ||
|
||
parsedData, err := ParseJWTToken([]byte(testSecret), token) | ||
assert.Nil(t, err) | ||
assert.True(t, reflect.DeepEqual(data, parsedData)) | ||
}) | ||
|
||
t.Run("invalid token", func(t *testing.T) { | ||
data := map[string]any{ | ||
"a": "b", | ||
} | ||
token, err := GenJWTToken([]byte(testSecret), data) | ||
assert.Nil(t, err) | ||
|
||
parsedData, err := ParseJWTToken([]byte(testSecret), token[:len(token)-1]) | ||
assert.Nil(t, parsedData) | ||
assert.NotNil(t, err) | ||
t.Log(err) | ||
}) | ||
|
||
t.Run("expired token", func(t *testing.T) { | ||
data := map[string]any{ | ||
"a": "b", | ||
"exp": time.Now().Add(-24 * time.Hour).Unix(), | ||
} | ||
token, err := GenJWTToken([]byte(testSecret), data) | ||
assert.Nil(t, err) | ||
|
||
parsedData, err := ParseJWTToken([]byte(testSecret), token) | ||
assert.Nil(t, parsedData) | ||
assert.NotNil(t, err) | ||
t.Log(err) | ||
}) | ||
} | ||
|
||
func TestSetup(t *testing.T) { | ||
file, err := os.CreateTemp(".", "test-") | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
defer os.Remove(file.Name()) | ||
db, err := sql.Open("sqlite://" + file.Name()) | ||
assert.Nil(t, err) | ||
_, _, err = Setup(db) | ||
assert.Nil(t, err) | ||
|
||
// call Setup again will return an error | ||
_, _, err = Setup(db) | ||
assert.NotNil(t, err) | ||
t.Log(err) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
package main | ||
|
||
import ( | ||
"log" | ||
"net/http" | ||
|
||
"github.com/rest-go/rest/pkg/auth" | ||
) | ||
|
||
func handle(w http.ResponseWriter, req *http.Request) { | ||
user := auth.GetUser(req) | ||
if user.IsAnonymous() { | ||
w.WriteHeader(http.StatusUnauthorized) | ||
} else { | ||
w.WriteHeader(http.StatusOK) | ||
} | ||
} | ||
|
||
func main() { | ||
dbURL := "sqlite://my.db" | ||
jwtSecret := "my secret" | ||
authHandler, err := auth.NewHandler(dbURL, []byte(jwtSecret)) | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
middleware := auth.NewMiddleware([]byte(jwtSecret)) | ||
|
||
http.Handle("/auth/", authHandler) | ||
http.Handle("/", middleware(http.HandlerFunc(handle))) | ||
log.Fatal(http.ListenAndServe(":8000", nil)) //nolint:gosec | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
package main | ||
|
||
import ( | ||
"log" | ||
"net/http" | ||
|
||
"github.com/rest-go/rest/pkg/auth" | ||
) | ||
|
||
func handle(w http.ResponseWriter, req *http.Request) { | ||
user := auth.GetUser(req) | ||
if user.IsAnonymous() { | ||
w.WriteHeader(http.StatusUnauthorized) | ||
} else { | ||
w.WriteHeader(http.StatusOK) | ||
} | ||
} | ||
|
||
func main() { | ||
jwtSecret := "my secret" | ||
middleware := auth.NewMiddleware([]byte(jwtSecret)) | ||
|
||
http.Handle("/", middleware(http.HandlerFunc(handle))) | ||
log.Fatal(http.ListenAndServe(":8000", nil)) //nolint:gosec | ||
} |
Oops, something went wrong.