add a jwt tutorial + go client

This commit is contained in:
Gerasimos (Makis) Maropoulos 2020-11-04 21:12:13 +02:00
parent ed38047385
commit 579c3878f0
No known key found for this signature in database
GPG Key ID: 5DBE766BD26A54E7
22 changed files with 818 additions and 163 deletions

View File

@ -199,9 +199,11 @@
* [Basic Authentication](auth/basicauth/main.go)
* [CORS](auth/cors)
* JSON Web Tokens
* [Overview](auth/jwt/overview/main.go)
* [Basic](auth/jwt/basic/main.go)
* [Middleware](auth/jwt/midleware/main.go)
* [Blocklist](auth/jwt/blocklist/main.go)
* [Refresh Token](auth/jwt/refresh-token/main.go)
* [Tutorial](auth/jwt/tutorial)
* [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go)
* [OAUth2](auth/goth/main.go)
* [Manage Permissions](auth/permissions/main.go)

View File

@ -1,25 +0,0 @@
package main
import "github.com/kataras/iris/v12"
func loginView(ctx iris.Context) {
}
func login(ctx iris.Context) {
}
func logout(ctx iris.Context) {
ctx.Logout()
ctx.Redirect("/", iris.StatusTemporaryRedirect)
}
func createTodo(ctx iris.Context) {
}
func getTodo(ctx iris.Context) {
}

View File

@ -0,0 +1,142 @@
package api
import (
"fmt"
"os"
"time"
"myapp/domain/model"
"myapp/domain/repository"
"myapp/util"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
)
const defaultSecretKey = "sercrethatmaycontainch@r$32chars"
func getSecretKey() string {
secret := os.Getenv(util.AppName + "_SECRET")
if secret == "" {
return defaultSecretKey
}
return secret
}
// UserClaims represents the user token claims.
type UserClaims struct {
UserID string `json:"user_id"`
Roles []model.Role `json:"roles"`
}
// Validate implements the custom struct claims validator,
// this is totally optionally and maybe unnecessary but good to know how.
func (u *UserClaims) Validate() error {
if u.UserID == "" {
return fmt.Errorf("%w: %s", jwt.ErrMissingKey, "user_id")
}
return nil
}
// Verify allows only authorized clients.
func Verify() iris.Handler {
secret := getSecretKey()
verifier := jwt.NewVerifier(jwt.HS256, []byte(secret), jwt.Expected{Issuer: util.AppName})
verifier.Extractors = []jwt.TokenExtractor{jwt.FromHeader} // extract token only from Authorization: Bearer $token
return verifier.Verify(func() interface{} {
return new(UserClaims)
})
}
// AllowAdmin allows only authorized clients with "admin" access role.
// Should be registered after Verify.
func AllowAdmin(ctx iris.Context) {
if !IsAdmin(ctx) {
ctx.StopWithText(iris.StatusForbidden, "admin access required")
return
}
ctx.Next()
}
// SignIn accepts the user form data and returns a token to authorize a client.
func SignIn(repo repository.UserRepository) iris.Handler {
secret := getSecretKey()
signer := jwt.NewSigner(jwt.HS256, []byte(secret), 15*time.Minute)
return func(ctx iris.Context) {
/*
type LoginForm struct {
Username string `form:"username"`
Password string `form:"password"`
}
and ctx.ReadForm OR use the ctx.FormValue(s) method.
*/
var (
username = ctx.FormValue("username")
password = ctx.FormValue("password")
)
user, ok := repo.GetByUsernameAndPassword(username, password)
if !ok {
ctx.StopWithText(iris.StatusBadRequest, "wrong username or password")
return
}
claims := UserClaims{
UserID: user.ID,
Roles: user.Roles,
}
// Optionally, generate a JWT ID.
jti, err := util.GenerateUUID()
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
token, err := signer.Sign(claims, jwt.Claims{
ID: jti,
Issuer: util.AppName,
})
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
ctx.Write(token)
}
}
// SignOut invalidates a user from server-side using the jwt Blocklist.
// It's not used as we don't attach a blocklist, the user can be logged out from our client
// and we don't use access token so we don't actually need this in this example.
func SignOut(ctx iris.Context) {
ctx.Logout()
}
// GetClaims returns the current authorized client claims.
func GetClaims(ctx iris.Context) *UserClaims {
claims := jwt.Get(ctx).(*UserClaims)
return claims
}
// GetUserID returns the current authorized client's user id extracted from claims.
func GetUserID(ctx iris.Context) string {
return GetClaims(ctx).UserID
}
// IsAdmin reports whether the current client has admin access.
func IsAdmin(ctx iris.Context) bool {
for _, role := range GetClaims(ctx).Roles {
if role == model.Admin {
return true
}
}
return false
}

View File

@ -0,0 +1,119 @@
package api
import (
"errors"
"myapp/domain/repository"
"github.com/kataras/iris/v12"
)
// TodoRequest represents a Todo HTTP request.
type TodoRequest struct {
Title string `json:"title" form:"title" url:"title"`
Body string `json:"body" form:"body" url:"body"`
}
// CreateTodo handles the creation of a Todo entry.
func CreateTodo(repo repository.TodoRepository) iris.Handler {
return func(ctx iris.Context) {
var req TodoRequest
err := ctx.ReadBody(&req) // will bind the "req" to a JSON, form or url query request data.
if err != nil {
ctx.StopWithError(iris.StatusBadRequest, err)
return
}
userID := GetUserID(ctx)
todo, err := repo.Create(userID, req.Title, req.Body)
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
ctx.StatusCode(iris.StatusCreated)
ctx.JSON(todo)
}
}
// GetTodo lists all users todos.
// Parameter: {id}.
func GetTodo(repo repository.TodoRepository) iris.Handler {
return func(ctx iris.Context) {
id := ctx.Params().Get("id")
userID := GetUserID(ctx)
todo, err := repo.GetByID(id)
if err != nil {
code := iris.StatusInternalServerError
if errors.Is(err, repository.ErrNotFound) {
code = iris.StatusNotFound
}
ctx.StopWithError(code, err)
return
}
if !IsAdmin(ctx) { // admin can access any user's todos.
if todo.UserID != userID {
ctx.StopWithStatus(iris.StatusForbidden)
return
}
}
ctx.JSON(todo)
}
}
// ListTodos lists todos of the current user.
func ListTodos(repo repository.TodoRepository) iris.Handler {
return func(ctx iris.Context) {
userID := GetUserID(ctx)
todos, err := repo.GetAllByUser(userID)
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
// if len(todos) == 0 {
// ctx.StopWithError(iris.StatusNotFound, fmt.Errorf("no entries found"))
// return
// }
// Or let the client decide what to do on empty list.
ctx.JSON(todos)
}
}
// ListAllTodos lists all users todos.
// Access: admin.
// Middleware: AllowAdmin.
func ListAllTodos(repo repository.TodoRepository) iris.Handler {
return func(ctx iris.Context) {
todos, err := repo.GetAll()
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
ctx.JSON(todos)
}
}
/* Leave as exercise: use filtering instead...
// ListTodosByUser lists all todos by a specific user.
// Access: admin.
// Middleware: AllowAdmin.
// Parameter: {id}.
func ListTodosByUser(repo repository.TodoRepository) iris.Handler {
return func(ctx iris.Context) {
userID := ctx.Params().Get("id")
todos, err := repo.GetAllByUser(userID)
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
ctx.JSON(todos)
}
}
*/

View File

@ -0,0 +1,9 @@
package model
// Role represents a role.
type Role string
const (
// Admin represents the Admin access role.
Admin Role = "admin"
)

View File

@ -0,0 +1,10 @@
package model
// Todo represents the Todo model.
type Todo struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Title string `json:"title"`
Body string `json:"body"`
CreatedAt int64 `json:"created_at"` // unix seconds.
}

View File

@ -0,0 +1,9 @@
package model
// User represents our User model.
type User struct {
ID string `json:"id"`
Username string `json:"username"`
HashedPassword []byte `json:"-"`
Roles []Role `json:"roles"`
}

View File

@ -0,0 +1,45 @@
package repository
import (
"fmt"
"myapp/domain/model"
)
// GenerateSamples generates data samples.
func GenerateSamples(userRepo UserRepository, todoRepo TodoRepository) error {
// Create users.
for _, username := range []string{"vasiliki", "george", "kwstas"} {
// My grandmother.
// My young brother.
// My youngest brother.
password := fmt.Sprintf("%s_pass", username)
if _, err := userRepo.Create(username, password); err != nil {
return err
}
}
// Create a user with admin role.
if _, err := userRepo.Create("admin", "admin", model.Admin); err != nil {
return err
}
// Create two todos per user.
users, err := userRepo.GetAll()
if err != nil {
return err
}
for i, u := range users {
for j := 0; j < 2; j++ {
title := fmt.Sprintf("%s todo %d:%d title", u.Username, i, j)
body := fmt.Sprintf("%s todo %d:%d body", u.Username, i, j)
_, err := todoRepo.Create(u.ID, title, body)
if err != nil {
return err
}
}
}
return nil
}

View File

@ -0,0 +1,94 @@
package repository
import (
"errors"
"sync"
"myapp/domain/model"
"myapp/util"
)
// ErrNotFound indicates that an entry was not found.
// Usage: errors.Is(err, ErrNotFound)
var ErrNotFound = errors.New("not found")
// TodoRepository is responsible for Todo CRUD operations,
// however, for the sake of the example we only implement the Create and Read ones.
type TodoRepository interface {
Create(userID, title, body string) (model.Todo, error)
GetByID(id string) (model.Todo, error)
GetAll() ([]model.Todo, error)
GetAllByUser(userID string) ([]model.Todo, error)
}
var (
_ TodoRepository = (*memoryTodoRepository)(nil)
)
type memoryTodoRepository struct {
todos []model.Todo // map[string]model.Todo
mu sync.RWMutex
}
// NewMemoryTodoRepository returns the default in-memory todo repository.
func NewMemoryTodoRepository() TodoRepository {
r := new(memoryTodoRepository)
return r
}
func (r *memoryTodoRepository) Create(userID, title, body string) (model.Todo, error) {
id, err := util.GenerateUUID()
if err != nil {
return model.Todo{}, err
}
todo := model.Todo{
ID: id,
UserID: userID,
Title: title,
Body: body,
CreatedAt: util.Now().Unix(),
}
r.mu.Lock()
r.todos = append(r.todos, todo)
r.mu.Unlock()
return todo, nil
}
func (r *memoryTodoRepository) GetByID(id string) (model.Todo, error) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, todo := range r.todos {
if todo.ID == id {
return todo, nil
}
}
return model.Todo{}, ErrNotFound
}
func (r *memoryTodoRepository) GetAll() ([]model.Todo, error) {
r.mu.RLock()
tmp := make([]model.Todo, len(r.todos))
copy(tmp, r.todos)
r.mu.RUnlock()
return tmp, nil
}
func (r *memoryTodoRepository) GetAllByUser(userID string) ([]model.Todo, error) {
// initialize a slice, so we don't have "null" at empty response.
todos := make([]model.Todo, 0)
r.mu.RLock()
for _, todo := range r.todos {
if todo.UserID == userID {
todos = append(todos, todo)
}
}
r.mu.RUnlock()
return todos, nil
}

View File

@ -0,0 +1,82 @@
package repository
import (
"sync"
"myapp/domain/model"
"myapp/util"
)
// UserRepository is responsible for User CRUD operations,
// however, for the sake of the example we only implement the Read one.
type UserRepository interface {
Create(username, password string, roles ...model.Role) (model.User, error)
// GetByUsernameAndPassword should return a User based on the given input.
GetByUsernameAndPassword(username, password string) (model.User, bool)
GetAll() ([]model.User, error)
}
var (
_ UserRepository = (*memoryUserRepository)(nil)
)
type memoryUserRepository struct {
// Users represents a user database.
// For the sake of the tutorial we use a simple slice of users.
users []model.User
mu sync.RWMutex
}
// NewMemoryUserRepository returns the default in-memory user repository.
func NewMemoryUserRepository() UserRepository {
r := new(memoryUserRepository)
return r
}
func (r *memoryUserRepository) Create(username, password string, roles ...model.Role) (model.User, error) {
id, err := util.GenerateUUID()
if err != nil {
return model.User{}, err
}
hashedPassword, err := util.GeneratePassword(password)
if err != nil {
return model.User{}, err
}
user := model.User{
ID: id,
Username: username,
HashedPassword: hashedPassword,
Roles: roles,
}
r.mu.Lock()
r.users = append(r.users, user)
r.mu.Unlock()
return user, nil
}
// GetByUsernameAndPassword returns a user from the storage based on the given "username" and "password".
func (r *memoryUserRepository) GetByUsernameAndPassword(username, password string) (model.User, bool) {
for _, u := range r.users { // our example uses a static slice.
if u.Username == username {
// we compare the user input and the stored hashed password.
ok := util.ValidatePassword(password, u.HashedPassword)
if ok {
return u, true
}
}
}
return model.User{}, false
}
func (r *memoryUserRepository) GetAll() ([]model.User, error) {
r.mu.RLock()
tmp := make([]model.User, len(r.users))
copy(tmp, r.users)
r.mu.RUnlock()
return tmp, nil
}

View File

@ -0,0 +1,12 @@
# Go Client
```sh
$ go run .
```
```sh
2020/11/04 21:08:40 Access Token:
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoiYTAwYzI3ZDEtYjVhYS00NjU0LWFmMTYtYjExNzNkZTY1NjI5Iiwicm9sZXMiOlsiYWRtaW4iXSwiaWF0IjoxNjA0NTE2OTIwLCJleHAiOjE2MDQ1MTc4MjAsImp0aSI6IjYzNmVmMDc0LTE2MzktNGJhZi1hNGNiLTQ4ZDM4NGMxMzliYSIsImlzcyI6Im15YXBwIn0.T9B0zG0AHShO5JfQgrMQBlToH33KHgp8nLMPFpN6QmM"
2020/11/04 21:08:40 Todo Created:
model.Todo{ID:"cfa38d7a-c556-4301-ae1f-fb90f705071c", UserID:"a00c27d1-b5aa-4654-af16-b1173de65629", Title:"test todo title", Body:"test todo body contents", CreatedAt:1604516920}
```

View File

@ -0,0 +1,109 @@
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"strings"
)
// Client is the default http client instance used by the following methods.
var Client = http.DefaultClient
// RequestOption is a function which can be used to modify
// a request instance before Do.
type RequestOption func(*http.Request) error
// WithAccessToken sets the given "token" to the authorization request header.
func WithAccessToken(token []byte) RequestOption {
bearer := "Bearer " + string(token)
return func(req *http.Request) error {
req.Header.Add("Authorization", bearer)
return nil
}
}
// WithContentType sets the content-type request header.
func WithContentType(cType string) RequestOption {
return func(req *http.Request) error {
req.Header.Set("Content-Type", cType)
return nil
}
}
// WithContentLength sets the content-length request header.
func WithContentLength(length int) RequestOption {
return func(req *http.Request) error {
req.Header.Set("Content-Length", strconv.Itoa(length))
return nil
}
}
// Do fires a request to the server.
func Do(method, url string, body io.Reader, opts ...RequestOption) (*http.Response, error) {
req, err := http.NewRequest(method, url, body)
if err != nil {
return nil, err
}
for _, opt := range opts {
if err = opt(req); err != nil {
return nil, err
}
}
return Client.Do(req)
}
// JSON fires a request with "v" as client json data.
func JSON(method, url string, v interface{}, opts ...RequestOption) (*http.Response, error) {
buf := new(bytes.Buffer)
err := json.NewEncoder(buf).Encode(v)
if err != nil {
return nil, err
}
opts = append(opts, WithContentType("application/json; charset=utf-8"))
return Do(method, url, buf, opts...)
}
// Form fires a request with "formData" as client form data.
func Form(method, url string, formData url.Values, opts ...RequestOption) (*http.Response, error) {
encoded := formData.Encode()
body := strings.NewReader(encoded)
opts = append([]RequestOption{
WithContentType("application/x-www-form-urlencoded"),
WithContentLength(len(encoded)),
}, opts...)
return Do(method, url, body, opts...)
}
// BindResponse binds a response body to the "dest" pointer and closes the body.
func BindResponse(resp *http.Response, dest interface{}) error {
contentType := resp.Header.Get("Content-Type")
if idx := strings.IndexRune(contentType, ';'); idx > 0 {
contentType = contentType[0:idx]
}
switch contentType {
case "application/json":
defer resp.Body.Close()
return json.NewDecoder(resp.Body).Decode(dest)
default:
return fmt.Errorf("unsupported content type: %s", contentType)
}
}
// RawResponse simply returns the raw response body.
func RawResponse(resp *http.Response) ([]byte, error) {
defer resp.Body.Close()
return ioutil.ReadAll(resp.Body)
}

View File

@ -0,0 +1,69 @@
package main
import (
"fmt"
"log"
"net/http"
"net/url"
"myapp/api"
"myapp/domain/model"
)
const base = "http://localhost:8080"
func main() {
accessToken, err := authenticate("admin", "admin")
if err != nil {
log.Fatal(err)
}
log.Printf("Access Token:\n%q", accessToken)
todo, err := createTodo(accessToken, "test todo title", "test todo body contents")
if err != nil {
log.Fatal(err)
}
log.Printf("Todo Created:\n%#+v", todo)
}
func authenticate(username, password string) ([]byte, error) {
endpoint := base + "/signin"
data := make(url.Values)
data.Set("username", username)
data.Set("password", password)
resp, err := Form(http.MethodPost, endpoint, data)
if err != nil {
return nil, err
}
accessToken, err := RawResponse(resp)
return accessToken, err
}
func createTodo(accessToken []byte, title, body string) (model.Todo, error) {
var todo model.Todo
endpoint := base + "/todos"
req := api.TodoRequest{
Title: title,
Body: body,
}
resp, err := JSON(http.MethodPost, endpoint, req, WithAccessToken(accessToken))
if err != nil {
return todo, err
}
if resp.StatusCode != http.StatusCreated {
rawData, _ := RawResponse(resp)
return todo, fmt.Errorf("failed to create a todo: %s", string(rawData))
}
err = BindResponse(resp, &todo)
return todo, err
}

View File

@ -3,8 +3,9 @@ module myapp
go 1.15
require (
github.com/kataras/iris/v12 v12.2.0-alpha.0.20201031040657-23d4c411cadb
github.com/google/uuid v1.1.2
github.com/kataras/iris/v12 v12.2.0-alpha.0.20201031040657-23d4c411cadb
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
)
replace github.com/kataras/iris/v12 => ../../../../

View File

@ -1,89 +1,40 @@
package main
import (
"time"
"myapp/api"
"myapp/domain/repository"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/middleware/jwt"
"github.com/kataras/iris/v12/middleware/jwt/blocklist/redis"
// Optionally to set token identifier.
"github.com/google/uuid"
)
var (
signatureSharedKey = []byte("sercrethatmaycontainch@r32length")
signer = jwt.NewSigner(jwt.HS256, signatureSharedKey, 15*time.Minute)
verifier = jwt.NewVerifier(jwt.HS256, signatureSharedKey)
userRepository = repository.NewMemoryUserRepository()
todoRepository = repository.NewMemoryTodoRepository()
)
func main() {
if err := repository.GenerateSamples(userRepository, todoRepository); err != nil {
panic(err)
}
app := iris.New()
blocklist := redis.NewBlocklist()
verifier.Blocklist = blocklist
verifyMiddleware := verifier.Verify(func() interface{} {
return new(userClaims)
})
app.Post("/signin", api.SignIn(userRepository))
app.Get("/", loginView)
verify := api.Verify()
api := app.Party("/api")
{
api.Post("/login", login)
api.Post("/logout", verifyMiddleware, logout)
todosAPI := app.Party("/todos", verify)
todosAPI.Post("/", api.CreateTodo(todoRepository))
todosAPI.Get("/", api.ListTodos(todoRepository))
todosAPI.Get("/{id}", api.GetTodo(todoRepository))
todoAPI := api.Party("/todos", verifyMiddleware)
{
todoAPI.Post("/", createTodo)
todoAPI.Get("/", listTodos)
todoAPI.Get("/{id:uint64}", getTodo)
}
}
adminAPI := app.Party("/admin", verify, api.AllowAdmin)
adminAPI.Get("/todos", api.ListAllTodos(todoRepository))
protectedAPI := app.Party("/protected", verifyMiddleware)
protectedAPI.Get("/", protected)
protectedAPI.Get("/logout", logout)
// GET http://localhost:8080
// POST http://localhost:8080/api/login
// POST http://localhost:8080/api/logout
// POST http://localhost:8080/api/todos
// GET http://localhost:8080/api/todos
// GET http://localhost:8080/api/todos/{id}
// POST http://localhost:8080/signin (Form: username, password)
// GET http://localhost:8080/todos
// GET http://localhost:8080/todos/{id}
// POST http://localhost:8080/todos (JSON, Form or URL: title, body)
// GET http://localhost:8080/admin/todos
app.Listen(":8080")
}
func authenticate(ctx iris.Context) {
claims := userClaims{
Username: "kataras",
}
// Generate JWT ID.
random, err := uuid.NewRandom()
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
id := random.String()
// Set the ID with the jwt.ID.
token, err := signer.Sign(claims, jwt.ID(id))
if err != nil {
ctx.StopWithError(iris.StatusInternalServerError, err)
return
}
ctx.Write(token)
}
func protected(ctx iris.Context) {
claims := jwt.Get(ctx).(*userClaims)
// To the standard claims, e.g. the generated ID:
// jwt.GetVerifiedToken(ctx).StandardClaims.ID
ctx.WriteString(claims.Username)
}

View File

@ -1,63 +0,0 @@
package main
import "golang.org/x/crypto/bcrypt"
func init() {
generateSampleUsers()
}
// User represents our User model.
type User struct {
ID uint64 `json:"id"`
Username string `json:"username"`
HashedPassword []byte `json:"-"`
}
// Users represents a user database.
// For the sake of the tutorial we use a simple slice of users.
var Users []User
func generateSampleUsers() {
Users = []User{
{ID: 1, Username: "vasiliki", HashedPassword: mustGeneratePassword("vasiliki_pass")}, // my grandmother.
{ID: 2, Username: "kataras", HashedPassword: mustGeneratePassword("kataras_pass")}, // me.
{ID: 3, Username: "george", HashedPassword: mustGeneratePassword("george_pass")}, // my young brother.
{ID: 4, Username: "kwstas", HashedPassword: mustGeneratePassword("kwstas_pass")}, // my youngest brother.
}
}
func fetchUser(username, password string) (User, bool) {
for _, u := range Users { // our example uses a static slice.
if u.Username == username {
// we compare the user input and the stored hashed password.
ok := ValidatePassword(password, u.HashedPassword)
if ok {
return u, true
}
}
}
return User{}, false
}
// mustGeneratePassword same as GeneratePassword but panics on errors.
func mustGeneratePassword(userPassword string) []byte {
hashed, err := GeneratePassword(userPassword)
if err != nil {
panic(err)
}
return hashed
}
// GeneratePassword will generate a hashed password for us based on the
// user's input.
func GeneratePassword(userPassword string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost)
}
// ValidatePassword will check if passwords are matched.
func ValidatePassword(userPassword string, hashed []byte) bool {
err := bcrypt.CompareHashAndPassword(hashed, []byte(userPassword))
return err == nil
}

View File

@ -0,0 +1,7 @@
package util
// Constants for the application.
const (
Version = "0.0.1"
AppName = "myapp"
)

View File

@ -0,0 +1,7 @@
package util
import "time"
// Now is the default current time for the whole application.
// Can be modified for testing or custom timezone.
var Now = time.Now

View File

@ -0,0 +1,25 @@
package util
import "golang.org/x/crypto/bcrypt"
// MustGeneratePassword same as GeneratePassword but panics on errors.
func MustGeneratePassword(userPassword string) []byte {
hashed, err := GeneratePassword(userPassword)
if err != nil {
panic(err)
}
return hashed
}
// GeneratePassword will generate a hashed password for us based on the
// user's input.
func GeneratePassword(userPassword string) ([]byte, error) {
return bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost)
}
// ValidatePassword will check if passwords are matched.
func ValidatePassword(userPassword string, hashed []byte) bool {
err := bcrypt.CompareHashAndPassword(hashed, []byte(userPassword))
return err == nil
}

View File

@ -0,0 +1,23 @@
package util
import "github.com/google/uuid"
// MustGenerateUUID returns a new v4 UUID or panics.
func MustGenerateUUID() string {
id, err := GenerateUUID()
if err != nil {
panic(err)
}
return id
}
// GenerateUUID returns a new v4 UUID.
func GenerateUUID() (string, error) {
id, err := uuid.NewRandom()
if err != nil {
return "", err
}
return id.String(), nil
}

View File

@ -19,7 +19,8 @@ type Signer struct {
// MaxAge to set "exp" and "iat".
// Recommended value for access tokens: 15 minutes.
// Defaults to 0, no limit.
MaxAge time.Duration
MaxAge time.Duration
Options []SignOption
Encrypt func([]byte) ([]byte, error)
}
@ -33,11 +34,24 @@ type Signer struct {
// signer := NewSigner(HS256, secret, 15*time.Minute)
// token, err := signer.Sign(userClaims{Username: "kataras"})
func NewSigner(signatureAlg Alg, signatureKey interface{}, maxAge time.Duration) *Signer {
return &Signer{
if signatureAlg == HS256 {
// A tiny helper if the end-developer uses string instead of []byte for hmac keys.
if k, ok := signatureKey.(string); ok {
signatureKey = []byte(k)
}
}
s := &Signer{
Alg: signatureAlg,
Key: signatureKey,
MaxAge: maxAge,
}
if maxAge > 0 {
s.Options = []SignOption{MaxAge(maxAge)}
}
return s
}
// WithEncryption enables AES-GCM payload-only decryption.
@ -53,7 +67,13 @@ func (s *Signer) WithEncryption(key, additionalData []byte) *Signer {
// Sign generates a new token based on the given "claims" which is valid up to "s.MaxAge".
func (s *Signer) Sign(claims interface{}, opts ...SignOption) ([]byte, error) {
return SignEncrypted(s.Alg, s.Key, s.Encrypt, claims, append([]SignOption{MaxAge(s.MaxAge)}, opts...)...)
if len(opts) > 0 {
opts = append(opts, s.Options...)
} else {
opts = s.Options
}
return SignEncrypted(s.Alg, s.Key, s.Encrypt, claims, opts...)
}
// NewTokenPair accepts the access and refresh claims plus the life time duration for the refresh token

View File

@ -80,6 +80,13 @@ type Verifier struct {
// Get the context user:
// username, err := ctx.User().GetUsername()
func NewVerifier(signatureAlg Alg, signatureKey interface{}, validators ...TokenValidator) *Verifier {
if signatureAlg == HS256 {
// A tiny helper if the end-developer uses string instead of []byte for hmac keys.
if k, ok := signatureKey.(string); ok {
signatureKey = []byte(k)
}
}
return &Verifier{
Alg: signatureAlg,
Key: signatureKey,