mirror of
https://github.com/kataras/iris.git
synced 2025-12-18 10:27:06 +00:00
add a jwt tutorial + go client
This commit is contained in:
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)
|
||||
}
|
||||
}
|
||||
*/
|
||||
Reference in New Issue
Block a user