mirror of
https://github.com/kataras/iris.git
synced 2025-01-23 02:31:04 +01:00
add a jwt tutorial + go client
This commit is contained in:
parent
ed38047385
commit
579c3878f0
|
@ -199,9 +199,11 @@
|
||||||
* [Basic Authentication](auth/basicauth/main.go)
|
* [Basic Authentication](auth/basicauth/main.go)
|
||||||
* [CORS](auth/cors)
|
* [CORS](auth/cors)
|
||||||
* JSON Web Tokens
|
* JSON Web Tokens
|
||||||
* [Overview](auth/jwt/overview/main.go)
|
|
||||||
* [Basic](auth/jwt/basic/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)
|
* [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)
|
* [JWT (community edition)](https://github.com/iris-contrib/middleware/tree/v12/jwt/_example/main.go)
|
||||||
* [OAUth2](auth/goth/main.go)
|
* [OAUth2](auth/goth/main.go)
|
||||||
* [Manage Permissions](auth/permissions/main.go)
|
* [Manage Permissions](auth/permissions/main.go)
|
||||||
|
|
|
@ -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) {
|
|
||||||
|
|
||||||
}
|
|
142
_examples/auth/jwt/tutorial/api/auth.go
Normal file
142
_examples/auth/jwt/tutorial/api/auth.go
Normal 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
|
||||||
|
}
|
119
_examples/auth/jwt/tutorial/api/todo.go
Normal file
119
_examples/auth/jwt/tutorial/api/todo.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
*/
|
9
_examples/auth/jwt/tutorial/domain/model/role.go
Normal file
9
_examples/auth/jwt/tutorial/domain/model/role.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package model
|
||||||
|
|
||||||
|
// Role represents a role.
|
||||||
|
type Role string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Admin represents the Admin access role.
|
||||||
|
Admin Role = "admin"
|
||||||
|
)
|
10
_examples/auth/jwt/tutorial/domain/model/todo.go
Normal file
10
_examples/auth/jwt/tutorial/domain/model/todo.go
Normal 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.
|
||||||
|
}
|
9
_examples/auth/jwt/tutorial/domain/model/user.go
Normal file
9
_examples/auth/jwt/tutorial/domain/model/user.go
Normal 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"`
|
||||||
|
}
|
45
_examples/auth/jwt/tutorial/domain/repository/samples.go
Normal file
45
_examples/auth/jwt/tutorial/domain/repository/samples.go
Normal 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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
12
_examples/auth/jwt/tutorial/go-client/README.md
Normal file
12
_examples/auth/jwt/tutorial/go-client/README.md
Normal 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}
|
||||||
|
```
|
109
_examples/auth/jwt/tutorial/go-client/client.go
Normal file
109
_examples/auth/jwt/tutorial/go-client/client.go
Normal 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)
|
||||||
|
}
|
69
_examples/auth/jwt/tutorial/go-client/main.go
Normal file
69
_examples/auth/jwt/tutorial/go-client/main.go
Normal 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
|
||||||
|
}
|
|
@ -3,8 +3,9 @@ module myapp
|
||||||
go 1.15
|
go 1.15
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/kataras/iris/v12 v12.2.0-alpha.0.20201031040657-23d4c411cadb
|
|
||||||
github.com/google/uuid v1.1.2
|
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 => ../../../../
|
replace github.com/kataras/iris/v12 => ../../../../
|
||||||
|
|
|
@ -1,89 +1,40 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
"myapp/api"
|
||||||
|
"myapp/domain/repository"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12"
|
"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 (
|
var (
|
||||||
signatureSharedKey = []byte("sercrethatmaycontainch@r32length")
|
userRepository = repository.NewMemoryUserRepository()
|
||||||
|
todoRepository = repository.NewMemoryTodoRepository()
|
||||||
signer = jwt.NewSigner(jwt.HS256, signatureSharedKey, 15*time.Minute)
|
|
||||||
verifier = jwt.NewVerifier(jwt.HS256, signatureSharedKey)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
if err := repository.GenerateSamples(userRepository, todoRepository); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
|
||||||
blocklist := redis.NewBlocklist()
|
app.Post("/signin", api.SignIn(userRepository))
|
||||||
verifier.Blocklist = blocklist
|
|
||||||
verifyMiddleware := verifier.Verify(func() interface{} {
|
|
||||||
return new(userClaims)
|
|
||||||
})
|
|
||||||
|
|
||||||
app.Get("/", loginView)
|
verify := api.Verify()
|
||||||
|
|
||||||
api := app.Party("/api")
|
todosAPI := app.Party("/todos", verify)
|
||||||
{
|
todosAPI.Post("/", api.CreateTodo(todoRepository))
|
||||||
api.Post("/login", login)
|
todosAPI.Get("/", api.ListTodos(todoRepository))
|
||||||
api.Post("/logout", verifyMiddleware, logout)
|
todosAPI.Get("/{id}", api.GetTodo(todoRepository))
|
||||||
|
|
||||||
todoAPI := api.Party("/todos", verifyMiddleware)
|
adminAPI := app.Party("/admin", verify, api.AllowAdmin)
|
||||||
{
|
adminAPI.Get("/todos", api.ListAllTodos(todoRepository))
|
||||||
todoAPI.Post("/", createTodo)
|
|
||||||
todoAPI.Get("/", listTodos)
|
|
||||||
todoAPI.Get("/{id:uint64}", getTodo)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protectedAPI := app.Party("/protected", verifyMiddleware)
|
// POST http://localhost:8080/signin (Form: username, password)
|
||||||
protectedAPI.Get("/", protected)
|
// GET http://localhost:8080/todos
|
||||||
protectedAPI.Get("/logout", logout)
|
// GET http://localhost:8080/todos/{id}
|
||||||
|
// POST http://localhost:8080/todos (JSON, Form or URL: title, body)
|
||||||
// GET http://localhost:8080
|
// GET http://localhost:8080/admin/todos
|
||||||
// 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}
|
|
||||||
app.Listen(":8080")
|
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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -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
|
|
||||||
}
|
|
7
_examples/auth/jwt/tutorial/util/app.go
Normal file
7
_examples/auth/jwt/tutorial/util/app.go
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
package util
|
||||||
|
|
||||||
|
// Constants for the application.
|
||||||
|
const (
|
||||||
|
Version = "0.0.1"
|
||||||
|
AppName = "myapp"
|
||||||
|
)
|
7
_examples/auth/jwt/tutorial/util/clock.go
Normal file
7
_examples/auth/jwt/tutorial/util/clock.go
Normal 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
|
25
_examples/auth/jwt/tutorial/util/password.go
Normal file
25
_examples/auth/jwt/tutorial/util/password.go
Normal 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
|
||||||
|
}
|
23
_examples/auth/jwt/tutorial/util/uuid.go
Normal file
23
_examples/auth/jwt/tutorial/util/uuid.go
Normal 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
|
||||||
|
}
|
|
@ -19,7 +19,8 @@ type Signer struct {
|
||||||
// MaxAge to set "exp" and "iat".
|
// MaxAge to set "exp" and "iat".
|
||||||
// Recommended value for access tokens: 15 minutes.
|
// Recommended value for access tokens: 15 minutes.
|
||||||
// Defaults to 0, no limit.
|
// Defaults to 0, no limit.
|
||||||
MaxAge time.Duration
|
MaxAge time.Duration
|
||||||
|
Options []SignOption
|
||||||
|
|
||||||
Encrypt func([]byte) ([]byte, error)
|
Encrypt func([]byte) ([]byte, error)
|
||||||
}
|
}
|
||||||
|
@ -33,11 +34,24 @@ type Signer struct {
|
||||||
// signer := NewSigner(HS256, secret, 15*time.Minute)
|
// signer := NewSigner(HS256, secret, 15*time.Minute)
|
||||||
// token, err := signer.Sign(userClaims{Username: "kataras"})
|
// token, err := signer.Sign(userClaims{Username: "kataras"})
|
||||||
func NewSigner(signatureAlg Alg, signatureKey interface{}, maxAge time.Duration) *Signer {
|
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,
|
Alg: signatureAlg,
|
||||||
Key: signatureKey,
|
Key: signatureKey,
|
||||||
MaxAge: maxAge,
|
MaxAge: maxAge,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if maxAge > 0 {
|
||||||
|
s.Options = []SignOption{MaxAge(maxAge)}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithEncryption enables AES-GCM payload-only decryption.
|
// 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".
|
// 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) {
|
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
|
// NewTokenPair accepts the access and refresh claims plus the life time duration for the refresh token
|
||||||
|
|
|
@ -80,6 +80,13 @@ type Verifier struct {
|
||||||
// Get the context user:
|
// Get the context user:
|
||||||
// username, err := ctx.User().GetUsername()
|
// username, err := ctx.User().GetUsername()
|
||||||
func NewVerifier(signatureAlg Alg, signatureKey interface{}, validators ...TokenValidator) *Verifier {
|
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{
|
return &Verifier{
|
||||||
Alg: signatureAlg,
|
Alg: signatureAlg,
|
||||||
Key: signatureKey,
|
Key: signatureKey,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user