mirror of
https://github.com/kataras/iris.git
synced 2026-05-11 16:43:48 +00:00
Merge pull request #1662 from kataras/jwt-new-features
New JWT Middleware features and more
This commit is contained in:
@@ -168,8 +168,9 @@
|
||||
* [Bind Form](request-body/read-form/main.go)
|
||||
* [Checkboxes](request-body/read-form/checkboxes/main.go)
|
||||
* [Bind Query](request-body/read-query/main.go)
|
||||
* [Bind Headers](request-body/read-headers/main.go)
|
||||
* [Bind Params](request-body/read-params/main.go)
|
||||
* [Bind URL](request-body/read-url/main.go)
|
||||
* [Bind Headers](request-body/read-headers/main.go)
|
||||
* [Bind Body](request-body/read-body/main.go)
|
||||
* [Bind Custom per type](request-body/read-custom-per-type/main.go)
|
||||
* [Bind Custom via Unmarshaler](request-body/read-custom-via-unmarshaler/main.go)
|
||||
@@ -197,8 +198,12 @@
|
||||
* Authentication, Authorization & Bot Detection
|
||||
* [Basic Authentication](auth/basicauth/main.go)
|
||||
* [CORS](auth/cors)
|
||||
* [JWT](auth/jwt/main.go)
|
||||
* JSON Web Tokens
|
||||
* [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)
|
||||
@@ -218,6 +223,7 @@
|
||||
* [Badger](sessions/database/badger/main.go)
|
||||
* [BoltDB](sessions/database/boltdb/main.go)
|
||||
* [Redis](sessions/database/redis/main.go)
|
||||
* [View Data](sessions/viewdata)
|
||||
* Websocket
|
||||
* [Gorilla FileWatch (3rd-party)](websocket/gorilla-filewatch/main.go)
|
||||
* [Basic](websocket/basic)
|
||||
|
||||
@@ -56,7 +56,9 @@ func h(ctx iris.Context) {
|
||||
// makes sure for that, otherwise this handler will not be executed.
|
||||
// OR:
|
||||
user := ctx.User()
|
||||
ctx.Writef("%s %s:%s", ctx.Path(), user.GetUsername(), user.GetPassword())
|
||||
username, _ := user.GetUsername()
|
||||
password, _ := user.GetPassword
|
||||
ctx.Writef("%s %s:%s", ctx.Path(), username, password)
|
||||
}
|
||||
|
||||
func logout(ctx iris.Context) {
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
# Generate RSA
|
||||
|
||||
```sh
|
||||
$ openssl genrsa -des3 -out private_rsa.pem 2048
|
||||
```
|
||||
|
||||
```go
|
||||
b, err := ioutil.ReadFile("./private_rsa.pem")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
key := jwt.MustParseRSAPrivateKey(b, []byte("pass"))
|
||||
```
|
||||
|
||||
OR
|
||||
|
||||
```go
|
||||
import "crypto/rand"
|
||||
import "crypto/rsa"
|
||||
|
||||
key, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
```
|
||||
|
||||
# Generate Ed25519
|
||||
|
||||
```sh
|
||||
$ openssl genpkey -algorithm Ed25519 -out private_ed25519.pem
|
||||
$ openssl req -x509 -key private_ed25519.pem -out cert_ed25519.pem -days 365
|
||||
```
|
||||
78
_examples/auth/jwt/basic/main.go
Normal file
78
_examples/auth/jwt/basic/main.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/jwt"
|
||||
)
|
||||
|
||||
/*
|
||||
Learn how to use any JWT 3rd-party package with Iris.
|
||||
In this example we use the kataras/jwt one.
|
||||
|
||||
Install with:
|
||||
go get -u github.com/kataras/jwt
|
||||
|
||||
Documentation:
|
||||
https://github.com/kataras/jwt#table-of-contents
|
||||
*/
|
||||
|
||||
// Replace with your own key and keep them secret.
|
||||
// The "signatureSharedKey" is used for the HMAC(HS256) signature algorithm.
|
||||
var signatureSharedKey = []byte("sercrethatmaycontainch@r32length")
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
|
||||
app.Get("/", generateToken)
|
||||
app.Get("/protected", protected)
|
||||
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
type fooClaims struct {
|
||||
Foo string `json:"foo"`
|
||||
}
|
||||
|
||||
func generateToken(ctx iris.Context) {
|
||||
claims := fooClaims{
|
||||
Foo: "bar",
|
||||
}
|
||||
|
||||
// Sign and generate compact form token.
|
||||
token, err := jwt.Sign(jwt.HS256, signatureSharedKey, claims, jwt.MaxAge(10*time.Minute))
|
||||
if err != nil {
|
||||
ctx.StopWithStatus(iris.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := string(token) // or jwt.BytesToString
|
||||
ctx.HTML(`Token: ` + tokenString + `<br/><br/>
|
||||
<a href="/protected?token=` + tokenString + `">/protected?token=` + tokenString + `</a>`)
|
||||
}
|
||||
|
||||
func protected(ctx iris.Context) {
|
||||
// Extract the token, e.g. cookie, Authorization: Bearer $token
|
||||
// or URL query.
|
||||
token := ctx.URLParam("token")
|
||||
// Verify the token.
|
||||
verifiedToken, err := jwt.Verify(jwt.HS256, signatureSharedKey, []byte(token))
|
||||
if err != nil {
|
||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Writef("This is an authenticated request.\n\n")
|
||||
|
||||
// Decode the custom claims.
|
||||
var claims fooClaims
|
||||
verifiedToken.Claims(&claims)
|
||||
|
||||
// Just an example on how you can retrieve all the standard claims (set by jwt.MaxAge, "exp").
|
||||
standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims
|
||||
expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
|
||||
timeLeft := standardClaims.Timeleft()
|
||||
|
||||
ctx.Writef("foo=%s\nexpires at: %s\ntime left: %s\n", claims.Foo, expiresAtString, timeLeft)
|
||||
}
|
||||
101
_examples/auth/jwt/blocklist/main.go
Normal file
101
_examples/auth/jwt/blocklist/main.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"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)
|
||||
)
|
||||
|
||||
type userClaims struct {
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
|
||||
// IMPORTANT
|
||||
//
|
||||
// To use the in-memory blocklist just:
|
||||
// verifier.WithDefaultBlocklist()
|
||||
// To use a persistence blocklist, e.g. redis,
|
||||
// start your redis-server and:
|
||||
blocklist := redis.NewBlocklist()
|
||||
// To configure single client or a cluster one:
|
||||
// blocklist.ClientOptions.Addr = "127.0.0.1:6379"
|
||||
// blocklist.ClusterOptions.Addrs = []string{...}
|
||||
// To set a prefix for jwt ids:
|
||||
// blocklist.Prefix = "myapp-"
|
||||
//
|
||||
// To manually connect and check its error before continue:
|
||||
// err := blocklist.Connect()
|
||||
// By default the verifier will try to connect, if failed then it will throw http error.
|
||||
//
|
||||
// And then register it:
|
||||
verifier.Blocklist = blocklist
|
||||
verifyMiddleware := verifier.Verify(func() interface{} {
|
||||
return new(userClaims)
|
||||
})
|
||||
|
||||
app.Get("/", authenticate)
|
||||
|
||||
protectedAPI := app.Party("/protected", verifyMiddleware)
|
||||
protectedAPI.Get("/", protected)
|
||||
protectedAPI.Get("/logout", logout)
|
||||
|
||||
// http://localhost:8080
|
||||
// http://localhost:8080/protected?token=$token
|
||||
// http://localhost:8080/logout?token=$token
|
||||
// http://localhost:8080/protected?token=$token (401)
|
||||
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)
|
||||
}
|
||||
|
||||
func logout(ctx iris.Context) {
|
||||
ctx.Logout()
|
||||
|
||||
ctx.Redirect("/", iris.StatusTemporaryRedirect)
|
||||
}
|
||||
@@ -1,159 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/middleware/jwt"
|
||||
)
|
||||
|
||||
// UserClaims a custom claims structure. You can just use jwt.Claims too.
|
||||
type UserClaims struct {
|
||||
jwt.Claims
|
||||
Username string
|
||||
}
|
||||
|
||||
func main() {
|
||||
// Get keys from system's environment variables
|
||||
// JWT_SECRET (for signing and verification) and JWT_SECRET_ENC(for encryption and decryption),
|
||||
// or defaults to "secret" and "itsa16bytesecret" respectfully.
|
||||
//
|
||||
// Use the `jwt.New` instead for more flexibility, if necessary.
|
||||
j := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
|
||||
// By default it extracts the token from url parameter "token={token}"
|
||||
// and the Authorization Bearer {token} header.
|
||||
// You can also take token from JSON body:
|
||||
// j.Extractors = append(j.Extractors, jwt.FromJSON)
|
||||
|
||||
app := iris.New()
|
||||
app.Logger().SetLevel("debug")
|
||||
|
||||
app.Get("/authenticate", func(ctx iris.Context) {
|
||||
standardClaims := jwt.Claims{Issuer: "an-issuer", Audience: jwt.Audience{"an-audience"}}
|
||||
// NOTE: if custom claims then the `j.Expiry(claims)` (or jwt.Expiry(duration, claims))
|
||||
// MUST be called in order to set the expiration time.
|
||||
customClaims := UserClaims{
|
||||
Claims: j.Expiry(standardClaims),
|
||||
Username: "kataras",
|
||||
}
|
||||
|
||||
j.WriteToken(ctx, customClaims)
|
||||
})
|
||||
|
||||
userRouter := app.Party("/user")
|
||||
{
|
||||
// userRouter.Use(j.Verify)
|
||||
// userRouter.Get("/", func(ctx iris.Context) {
|
||||
// var claims UserClaims
|
||||
// if err := jwt.ReadClaims(ctx, &claims); err != nil {
|
||||
// // Validation-only errors, the rest are already
|
||||
// // checked on `j.Verify` middleware.
|
||||
// ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// ctx.Writef("Claims: %#+v\n", claims)
|
||||
// })
|
||||
//
|
||||
// OR:
|
||||
userRouter.Get("/", func(ctx iris.Context) {
|
||||
var claims UserClaims
|
||||
if err := j.VerifyToken(ctx, &claims); err != nil {
|
||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Writef("Username: %s\nExpires at: %s\n", claims.Username, claims.Expiry.Time())
|
||||
})
|
||||
}
|
||||
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
/*
|
||||
func default_RSA_Example() {
|
||||
j := jwt.RSA(15*time.Minute)
|
||||
}
|
||||
|
||||
Same as:
|
||||
|
||||
func load_File_Or_Generate_RSA_Example() {
|
||||
signKey, err := jwt.LoadRSA("jwt_sign.key", 2048)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
j, err := jwt.New(15*time.Minute, jwt.RS256, signKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
encKey, err := jwt.LoadRSA("jwt_enc.key", 2048)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = j.WithEncryption(jwt.A128CBCHS256, jwt.RSA15, encKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
func hmac_Example() {
|
||||
// hmac
|
||||
key := []byte("secret")
|
||||
j, err := jwt.New(15*time.Minute, jwt.HS256, key)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// OPTIONAL encryption:
|
||||
encryptionKey := []byte("itsa16bytesecret")
|
||||
err = j.WithEncryption(jwt.A128GCM, jwt.DIRECT, encryptionKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
func load_From_File_With_Password_Example() {
|
||||
b, err := ioutil.ReadFile("./rsa_password_protected.key")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
signKey,err := jwt.ParseRSAPrivateKey(b, []byte("pass"))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
j, err := jwt.New(15*time.Minute, jwt.RS256, signKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/*
|
||||
func generate_RSA_Example() {
|
||||
signKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
encryptionKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
j, err := jwt.New(15*time.Minute, jwt.RS512, signKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = j.WithEncryption(jwt.A128CBCHS256, jwt.RSA15, encryptionKey)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
*/
|
||||
91
_examples/auth/jwt/middleware/main.go
Normal file
91
_examples/auth/jwt/middleware/main.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/middleware/jwt"
|
||||
)
|
||||
|
||||
var (
|
||||
sigKey = []byte("signature_hmac_secret_shared_key")
|
||||
encKey = []byte("GCM_AES_256_secret_shared_key_32")
|
||||
)
|
||||
|
||||
type fooClaims struct {
|
||||
Foo string `json:"foo"`
|
||||
}
|
||||
|
||||
/*
|
||||
In this example you will learn the essentials
|
||||
of the Iris builtin JWT middleware based on the github.com/kataras/jwt package.
|
||||
*/
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
|
||||
signer := jwt.NewSigner(jwt.HS256, sigKey, 10*time.Minute)
|
||||
// Enable payload encryption with:
|
||||
// signer.WithEncryption(encKey, nil)
|
||||
app.Get("/", generateToken(signer))
|
||||
|
||||
verifier := jwt.NewVerifier(jwt.HS256, sigKey)
|
||||
// Enable server-side token block feature (even before its expiration time):
|
||||
verifier.WithDefaultBlocklist()
|
||||
// Enable payload decryption with:
|
||||
// verifier.WithDecryption(encKey, nil)
|
||||
verifyMiddleware := verifier.Verify(func() interface{} {
|
||||
return new(fooClaims)
|
||||
})
|
||||
|
||||
protectedAPI := app.Party("/protected")
|
||||
// Register the verify middleware to allow access only to authorized clients.
|
||||
protectedAPI.Use(verifyMiddleware)
|
||||
// ^ or UseRouter(verifyMiddleware) to disallow unauthorized http error handlers too.
|
||||
|
||||
protectedAPI.Get("/", protected)
|
||||
// Invalidate the token through server-side, even if it's not expired yet.
|
||||
protectedAPI.Get("/logout", logout)
|
||||
|
||||
// http://localhost:8080
|
||||
// http://localhost:8080/protected?token=$token (or Authorization: Bearer $token)
|
||||
// http://localhost:8080/protected/logout?token=$token
|
||||
// http://localhost:8080/protected?token=$token (401)
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func generateToken(signer *jwt.Signer) iris.Handler {
|
||||
return func(ctx iris.Context) {
|
||||
claims := fooClaims{Foo: "bar"}
|
||||
|
||||
token, err := signer.Sign(claims)
|
||||
if err != nil {
|
||||
ctx.StopWithStatus(iris.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Write(token)
|
||||
}
|
||||
}
|
||||
|
||||
func protected(ctx iris.Context) {
|
||||
// Get the verified and decoded claims.
|
||||
claims := jwt.Get(ctx).(*fooClaims)
|
||||
|
||||
// Optionally, get token information if you want to work with them.
|
||||
// Just an example on how you can retrieve all the standard claims (set by signer's max age, "exp").
|
||||
standardClaims := jwt.GetVerifiedToken(ctx).StandardClaims
|
||||
expiresAtString := standardClaims.ExpiresAt().Format(ctx.Application().ConfigurationReadOnly().GetTimeFormat())
|
||||
timeLeft := standardClaims.Timeleft()
|
||||
|
||||
ctx.Writef("foo=%s\nexpires at: %s\ntime left: %s\n", claims.Foo, expiresAtString, timeLeft)
|
||||
}
|
||||
|
||||
func logout(ctx iris.Context) {
|
||||
err := ctx.Logout()
|
||||
if err != nil {
|
||||
ctx.WriteString(err.Error())
|
||||
} else {
|
||||
ctx.Writef("token invalidated, a new token is required to access the protected API")
|
||||
}
|
||||
}
|
||||
@@ -1,139 +1,183 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/middleware/jwt"
|
||||
)
|
||||
|
||||
// UserClaims a custom claims structure. You can just use jwt.Claims too.
|
||||
const (
|
||||
accessTokenMaxAge = 10 * time.Minute
|
||||
refreshTokenMaxAge = time.Hour
|
||||
)
|
||||
|
||||
var (
|
||||
privateKey, publicKey = jwt.MustLoadRSA("rsa_private_key.pem", "rsa_public_key.pem")
|
||||
|
||||
signer = jwt.NewSigner(jwt.RS256, privateKey, accessTokenMaxAge)
|
||||
verifier = jwt.NewVerifier(jwt.RS256, publicKey)
|
||||
)
|
||||
|
||||
// UserClaims a custom access claims structure.
|
||||
type UserClaims struct {
|
||||
jwt.Claims
|
||||
Username string
|
||||
ID string `json:"user_id"`
|
||||
// Do: `json:"username,required"` to have this field required
|
||||
// or see the Validate method below instead.
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
// TokenPair holds the access token and refresh token response.
|
||||
type TokenPair struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
// GetID implements the partial context user's ID interface.
|
||||
// Note that if claims were a map then the claims value converted to UserClaims
|
||||
// and no need to implement any method.
|
||||
//
|
||||
// This is useful when multiple auth methods are used (e.g. basic auth, jwt)
|
||||
// but they all share a couple of methods.
|
||||
func (u *UserClaims) GetID() string {
|
||||
return u.ID
|
||||
}
|
||||
|
||||
// GetUsername implements the partial context user's Username interface.
|
||||
func (u *UserClaims) GetUsername() string {
|
||||
return u.Username
|
||||
}
|
||||
|
||||
// Validate completes the middleware's custom ClaimsValidator.
|
||||
// It will not accept a token which its claims missing the username field
|
||||
// (useful to not accept refresh tokens generated by the same algorithm).
|
||||
func (u *UserClaims) Validate() error {
|
||||
if u.Username == "" {
|
||||
return fmt.Errorf("username field is missing")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// For refresh token, we will just use the jwt.Claims
|
||||
// structure which contains the standard JWT fields.
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
app.OnErrorCode(iris.StatusUnauthorized, handleUnauthorized)
|
||||
|
||||
// Access token, short-live.
|
||||
accessJWT := jwt.HMAC(15*time.Minute, "secret", "itsa16bytesecret")
|
||||
// Refresh token, long-live. Important: Give different secret keys(!)
|
||||
refreshJWT := jwt.HMAC(1*time.Hour, "other secret", "other16bytesecre")
|
||||
// On refresh token, we extract it only from a request body
|
||||
// of JSON, e.g. {"refresh_token": $token }.
|
||||
// You can also do it manually in the handler level though.
|
||||
refreshJWT.Extractors = []jwt.TokenExtractor{
|
||||
jwt.FromJSON("refresh_token"),
|
||||
app.Get("/authenticate", generateTokenPair)
|
||||
app.Get("/refresh", refreshToken)
|
||||
|
||||
protectedAPI := app.Party("/protected")
|
||||
{
|
||||
verifyMiddleware := verifier.Verify(func() interface{} {
|
||||
return new(UserClaims)
|
||||
})
|
||||
|
||||
protectedAPI.Use(verifyMiddleware)
|
||||
|
||||
protectedAPI.Get("/", func(ctx iris.Context) {
|
||||
// Access the claims through: jwt.Get:
|
||||
// claims := jwt.Get(ctx).(*UserClaims)
|
||||
// ctx.Writef("Username: %s\n", claims.Username)
|
||||
//
|
||||
// OR through context's user (if at least one method was implement by our UserClaims):
|
||||
user := ctx.User()
|
||||
id, _ := user.GetID()
|
||||
username, _ := user.GetUsername()
|
||||
ctx.Writef("ID: %s\nUsername: %s\n", id, username)
|
||||
})
|
||||
}
|
||||
|
||||
// Generate access and refresh tokens and send to the client.
|
||||
app.Get("/authenticate", func(ctx iris.Context) {
|
||||
tokenPair, err := generateTokenPair(accessJWT, refreshJWT)
|
||||
if err != nil {
|
||||
ctx.StopWithStatus(iris.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(tokenPair)
|
||||
})
|
||||
|
||||
app.Get("/refresh", func(ctx iris.Context) {
|
||||
// Manual (if jwt.FromJSON missing):
|
||||
// var payload = struct {
|
||||
// RefreshToken string `json:"refresh_token"`
|
||||
// }{}
|
||||
//
|
||||
// err := ctx.ReadJSON(&payload)
|
||||
// if err != nil {
|
||||
// ctx.StatusCode(iris.StatusBadRequest)
|
||||
// return
|
||||
// }
|
||||
//
|
||||
// j.VerifyTokenString(ctx, payload.RefreshToken, &claims)
|
||||
|
||||
var claims jwt.Claims
|
||||
if err := refreshJWT.VerifyToken(ctx, &claims); err != nil {
|
||||
ctx.Application().Logger().Warnf("verify refresh token: %v", err)
|
||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
userID := claims.Subject
|
||||
if userID == "" {
|
||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Simulate a database call against our jwt subject.
|
||||
if userID != "53afcf05-38a3-43c3-82af-8bbbe0e4a149" {
|
||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// All OK, re-generate the new pair and send to client.
|
||||
tokenPair, err := generateTokenPair(accessJWT, refreshJWT)
|
||||
if err != nil {
|
||||
ctx.StopWithStatus(iris.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.JSON(tokenPair)
|
||||
})
|
||||
|
||||
app.Get("/", func(ctx iris.Context) {
|
||||
var claims UserClaims
|
||||
if err := accessJWT.VerifyToken(ctx, &claims); err != nil {
|
||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Writef("Username: %s\nExpires at: %s\n", claims.Username, claims.Expiry.Time())
|
||||
})
|
||||
|
||||
// http://localhost:8080 (401)
|
||||
// http://localhost:8080/protected (401)
|
||||
// http://localhost:8080/authenticate (200) (response JSON {access_token, refresh_token})
|
||||
// http://localhost:8080?token={access_token} (200)
|
||||
// http://localhost:8080?token={refresh_token} (401)
|
||||
// http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
|
||||
// http://localhost:8080/protected?token={access_token} (200)
|
||||
// http://localhost:8080/protected?token={refresh_token} (401)
|
||||
// http://localhost:8080/refresh?refresh_token={refresh_token}
|
||||
// OR http://localhost:8080/refresh (request JSON{refresh_token = {refresh_token}}) (200) (response JSON {access_token, refresh_token})
|
||||
// http://localhost:8080/refresh?refresh_token={access_token} (401)
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func generateTokenPair(accessJWT, refreshJWT *jwt.JWT) (TokenPair, error) {
|
||||
standardClaims := jwt.Claims{Issuer: "an-issuer", Audience: jwt.Audience{"an-audience"}}
|
||||
func generateTokenPair(ctx iris.Context) {
|
||||
// Simulate a user...
|
||||
userID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
|
||||
|
||||
customClaims := UserClaims{
|
||||
Claims: accessJWT.Expiry(standardClaims),
|
||||
// Map the current user with the refresh token,
|
||||
// so we make sure, on refresh route, that this refresh token owns
|
||||
// to that user before re-generate.
|
||||
refreshClaims := jwt.Claims{Subject: userID}
|
||||
|
||||
accessClaims := UserClaims{
|
||||
ID: userID,
|
||||
Username: "kataras",
|
||||
}
|
||||
|
||||
accessToken, err := accessJWT.Token(customClaims)
|
||||
// Generates a Token Pair, long-live for refresh tokens, e.g. 1 hour.
|
||||
// First argument is the access claims,
|
||||
// second argument is the refresh claims,
|
||||
// third argument is the refresh max age.
|
||||
tokenPair, err := signer.NewTokenPair(accessClaims, refreshClaims, refreshTokenMaxAge)
|
||||
if err != nil {
|
||||
return TokenPair{}, err
|
||||
ctx.Application().Logger().Errorf("token pair: %v", err)
|
||||
ctx.StopWithStatus(iris.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// At refresh tokens you don't need any custom claims.
|
||||
refreshClaims := refreshJWT.Expiry(jwt.Claims{
|
||||
ID: "refresh_kataras",
|
||||
// For example, the User ID,
|
||||
// this is necessary to check against the database
|
||||
// if the user still exist or has credentials to access our page.
|
||||
Subject: "53afcf05-38a3-43c3-82af-8bbbe0e4a149",
|
||||
})
|
||||
|
||||
refreshToken, err := refreshJWT.Token(refreshClaims)
|
||||
if err != nil {
|
||||
return TokenPair{}, err
|
||||
}
|
||||
|
||||
return TokenPair{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
}, nil
|
||||
// Send the generated token pair to the client.
|
||||
// The tokenPair looks like: {"access_token": $token, "refresh_token": $token}
|
||||
ctx.JSON(tokenPair)
|
||||
}
|
||||
|
||||
// There are various methods of refresh token, depending on the application requirements.
|
||||
// In this example we will accept a refresh token only, we will verify only a refresh token
|
||||
// and we re-generate a whole new pair. An alternative would be to accept a token pair
|
||||
// of both access and refresh tokens, verify the refresh, verify the access with a Leeway time
|
||||
// and check if its going to expire soon, then generate a single access token.
|
||||
func refreshToken(ctx iris.Context) {
|
||||
// Assuming you have access to the current user, e.g. sessions.
|
||||
//
|
||||
// Simulate a database call against our jwt subject
|
||||
// to make sure that this refresh token is a pair generated by this user.
|
||||
// * Note: You can remove the ExpectSubject and do this validation later on by yourself.
|
||||
currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
|
||||
|
||||
// Get the refresh token from ?refresh_token=$token OR
|
||||
// the request body's JSON{"refresh_token": "$token"}.
|
||||
refreshToken := []byte(ctx.URLParam("refresh_token"))
|
||||
if len(refreshToken) == 0 {
|
||||
// You can read the whole body with ctx.GetBody/ReadBody too.
|
||||
var tokenPair jwt.TokenPair
|
||||
if err := ctx.ReadJSON(&tokenPair); err != nil {
|
||||
ctx.StopWithError(iris.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
refreshToken = tokenPair.RefreshToken
|
||||
}
|
||||
|
||||
// Verify the refresh token, which its subject MUST match the "currentUserID".
|
||||
_, err := verifier.VerifyToken(refreshToken, jwt.Expected{Subject: currentUserID})
|
||||
if err != nil {
|
||||
ctx.Application().Logger().Errorf("verify refresh token: %v", err)
|
||||
ctx.StatusCode(iris.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
/* Custom validation checks can be performed after Verify calls too:
|
||||
currentUserID := "53afcf05-38a3-43c3-82af-8bbbe0e4a149"
|
||||
userID := verifiedToken.StandardClaims.Subject
|
||||
if userID != currentUserID {
|
||||
ctx.StopWithStatus(iris.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
*/
|
||||
|
||||
// All OK, re-generate the new pair and send to client,
|
||||
// we could only generate an access token as well.
|
||||
generateTokenPair(ctx)
|
||||
}
|
||||
|
||||
func handleUnauthorized(ctx iris.Context) {
|
||||
if err := ctx.GetErr(); err != nil {
|
||||
ctx.Application().Logger().Errorf("unauthorized: %v", err)
|
||||
}
|
||||
|
||||
ctx.WriteString("Unauthorized")
|
||||
}
|
||||
|
||||
27
_examples/auth/jwt/refresh-token/rsa_private_key.pem
Normal file
27
_examples/auth/jwt/refresh-token/rsa_private_key.pem
Normal file
@@ -0,0 +1,27 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEowIBAAKCAQEArwO0q8WbBvrplz3lTQjsWu66HC7M3mVAjmjLq8Wj/ipqVtiJ
|
||||
MrUL9t/0q9PNO/KX9u+HayFNYM4TnYkXVZX3M5E31W8fPPy74D/XpqFwrwT7bAEw
|
||||
pT51JJyxkoBAyOh08lmR2EYvpGF7qErra7qbkk4LGFbhoFCXdMLXguT4rPymkzFH
|
||||
dQrmGYOBS+v9imSuJddCZpXyv6Ko7AKB4mhzg4RC5RJZO5GEHVUrSMHxZB0syF8c
|
||||
U+28iL8A7SlGKTNZPZiHmCQVRqA6WlllL/YV/t6p24kaNZBUp9JGbAzOeKuVUv2u
|
||||
vfNKwB/aBwnFKauM9I6RmC4bnI1nGHjETlNNWwIDAQABAoIBAHBPKHmybTGlgpET
|
||||
nzo4J7SSzcuYHM/6mdrJVSn9wqcwAN2KR0DK/cqHHTPGz0VRAEPuojAVRtqAZAYM
|
||||
G3VIr0HgRrwoextf9BCL549+uhkWUWGVwenIktPT2f/xXaGPyrxazkTDhX8vL3Nn
|
||||
4HtZXMweWPBdkJyYGxlKj5Hn7czTpG3VKpvpHeFlY4caF+FT2as1jcQ1MjPnGslH
|
||||
Ss+sYPBp/70w2T114Z4wlR4OryI1LeuFeje9obrn0HAmJd0ZKYM21awp/YWJ/y8J
|
||||
wIH6XQ4AGR9iTRhuffK1XRM/Iec3K/YhOn4PtKdT7OsIujAKY7A9WcqSFif+/E1g
|
||||
jom3eMECgYEAw5Zdqt2uZ19FuDlDTW4Kw8Z2NyXgWp33LkAXG1mJw7bqDhfPeB1c
|
||||
xTPs4i4RubGuDusygxZ3GgJAO7tLGzNQfWNoi03mM7Q/BJGkA9VZr+U28zsSRQOQ
|
||||
+J9xNsdgUMP1js7X/NNM2bxTC8zy9wEsWr9JwNo1C7uHTE9WXAumBI8CgYEA5RKV
|
||||
niSbyko36W3Vi0ZnGBrRhy0Eiq85V2mhWzHN+txcv+8aISow2wioTUzrpR0aVZ4j
|
||||
v9+siJENlALVzdUFihy0lPxHqLJT746Cixz95WRTLkdHeNllV0DMfOph2x3j1Hjd
|
||||
3PgTv+jqb6npY0/2Vb2pp4t/zVikGaObsAalSHUCgYBne8B1bjMfqI3n6gxNBIMX
|
||||
kILtrNGmwFuPEgPnyZkVf0sZR8nSwJ5cDJwyE7P3LyZr6E9igllj3nsD35Xef2j/
|
||||
3r/qrL2275BEJ5bDHHgGk91eFgwVjcx/b0TkedrhAL2E4LXwpA/OSFEcNkT7IZjJ
|
||||
Ltqj+hAE9CSi4HtN2i/tywKBgBotKn28zzSpkIQTMgDNVcCSZ/kbctZqOZI8lty1
|
||||
70TIY6znJMQ/bv/ImHrk3FSs47J+9LTbWXrtoHCWdlokCpMCvrv7rDCh2Cea0F4X
|
||||
PQg2k67JJGix5vu2guePXQlN/Bfui+PRUWhvtEJ4VxwrKgoYN0fXEA6mH3JymLrf
|
||||
t4l1AoGBALk4o9swGjw7MnByYJmOidlJ0p9Wj1BWWJJYoYX2VfjIuvZj6BNxkEb0
|
||||
aVmYRC+40e9L1rOyrlyaO/TiQaIPE4ljVs/AmMKGz8sIcVfwdyERH3nDrXxvlAav
|
||||
lSvfKoYM3J+5c63CDuU45gztpmavNerzCczqYTLOEMx1eCLHOQlx
|
||||
-----END PRIVATE KEY-----
|
||||
9
_examples/auth/jwt/refresh-token/rsa_public_key.pem
Normal file
9
_examples/auth/jwt/refresh-token/rsa_public_key.pem
Normal file
@@ -0,0 +1,9 @@
|
||||
-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArwO0q8WbBvrplz3lTQjs
|
||||
Wu66HC7M3mVAjmjLq8Wj/ipqVtiJMrUL9t/0q9PNO/KX9u+HayFNYM4TnYkXVZX3
|
||||
M5E31W8fPPy74D/XpqFwrwT7bAEwpT51JJyxkoBAyOh08lmR2EYvpGF7qErra7qb
|
||||
kk4LGFbhoFCXdMLXguT4rPymkzFHdQrmGYOBS+v9imSuJddCZpXyv6Ko7AKB4mhz
|
||||
g4RC5RJZO5GEHVUrSMHxZB0syF8cU+28iL8A7SlGKTNZPZiHmCQVRqA6WlllL/YV
|
||||
/t6p24kaNZBUp9JGbAzOeKuVUv2uvfNKwB/aBwnFKauM9I6RmC4bnI1nGHjETlNN
|
||||
WwIDAQAB
|
||||
-----END PUBLIC KEY-----
|
||||
@@ -1,30 +0,0 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
Proc-Type: 4,ENCRYPTED
|
||||
DEK-Info: DES-EDE3-CBC,6B0BC214C94124FE
|
||||
|
||||
lAM48DEM/GdCDimr9Vhi+fSHLgduDb0l2BA4uhILgNby51jxY/4X3IqM6f3ImKX7
|
||||
cEd9OBug+pwIugB0UW0L0f5Pd59Ovpiaz3xLci1/19ehYnMqsuP3YAnJm40hT5VP
|
||||
p0gWRiR415PJ0fPeeJPFx5IsqvkTJ30LWZHUZX4EkdcL5L8PrVbmthGDbLh+OcMc
|
||||
LzoP8eTglzlZF03nyvAol6+p2eZtvOJLu8nWG25q17kyBx6kEiCsWFcUBTX9G7sH
|
||||
CM3naByDijqZXE/XXtmTMLSRRnlk7Q5WLxClroHlUP9y8BQFMo2TW4Z+vNjHUkc1
|
||||
77ghabX1704bAlIE8LLZJKrm/C5+VKyV6117SVG/2bc4036Y5rStXpANbk1j4K0x
|
||||
ADvpRhuTpifaogdvJP+8eXBdl841MQMRzWuZHp6UNYYQegoV9C+KHyJx4UPjZyzd
|
||||
gblZmKgU+BsX3mV6MLhJtd6dheLZtpBsAlSstJxzmgwqz9faONYEGeItXO+NnxbA
|
||||
mxAp/mI+Fz2jfgYlWjwkyPPzD4k/ZMMzB4XLkKKs9XaxUtTomiDkuYZfnABhxt73
|
||||
xBy40V1rb/NyeW80pk1zEHM6Iy/48ETSp9U3k9sSOXjMhYbPXgxDtimV8w0qGFAo
|
||||
2Tif7ZuaiuC38rOkoHK9C6vy2Dp8lQZ+QBnUKLeFsyhq9CaqSdnyUTMj3oEZXXf+
|
||||
TqqeO+PTtl7JaNfGRq6/aMZqxACHkyVUvYvjZzx07CJ2fr+OtNqxallM6Oc/o9NZ
|
||||
5u7lpgrYaKM/b67q0d2X/AoxR5zrZuM8eam3acD1PwHFQKbJWuFNmjWtnlZNuR3X
|
||||
fZEmxIKwDlup8TxFcqbbZtPHuQA2mTMTqfRkf8oPSO+N6NNaUpb0ignYyA7Eu5GT
|
||||
b02d/oNLETMikxUxntMSH7GhuOpfJyELz8krYTttbJ+a93h4wBeYW2+LyAr/cRLB
|
||||
mbtKLtaN7f3FaOSnu8e0+zlJ7xglHPXqblRL9q6ZDM5UJtJD4rA7LPZHk/0Y1Kb6
|
||||
hBh1qMDu0r3IV4X7MDacvxw7aa7D8TyXJiFSvxykVhds+ndjIe51Ics5908+lev3
|
||||
nwE69PLMwyqe2vvE2oDwao4XJuBLCHjcv/VagRSz/XQGMbZqb3L6unyd3UPl8JjP
|
||||
ovipNwM4rFnE54uiUUeki7TZGDYO72vQcSaLrmbeAWc2m202+rqLz0WMm6HpPmCv
|
||||
IgexpX2MnIeHJ3+BlEjA2u+S6xNSD7qHGk2pb7DD8nRvUdSHAHeaQbrkEfEhhR2Q
|
||||
Dw5gdw1JyQ0UKBl5ndn/1Ub2Asl016lZjpqHyMIVS4tFixACDsihEYMmq/zQmTj4
|
||||
8oBZTU+fycN/KiGKZBsqxIwgYIeMz/GfvoyN5m57l6fwEZALVpveI1pP4fiZB/Z8
|
||||
xLKa5JK6L10lAD1YHWc1dPhamf9Sb3JwN2CFtGvjOJ/YjAZu3jJoxi40DtRkE3Rh
|
||||
HI8Cbx1OORzoo0kO0vy42rz5qunYyVmEzPKtOj+YjVEhVJ85yJZ9bTZtuyqMv8mH
|
||||
cnwEeIFK8cmm9asbVzQGDwN/UGB4cO3LrMX1RYk4GRttTGlp0729BbmZmu00RnD/
|
||||
-----END RSA PRIVATE KEY-----
|
||||
62
_examples/auth/jwt/tutorial/README.md
Normal file
62
_examples/auth/jwt/tutorial/README.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Iris JWT Tutorial
|
||||
|
||||
This example show how to use JWT with domain-driven design pattern with Iris. There is also a simple Go client which describes how you can use Go to authorize a user and use the server's API.
|
||||
|
||||
## Run the server
|
||||
|
||||
```sh
|
||||
$ go run main.go
|
||||
```
|
||||
|
||||
## Authenticate, get the token
|
||||
|
||||
```sh
|
||||
$ curl --location --request POST 'http://localhost:8080/signin' \
|
||||
--header 'Content-Type: application/x-www-form-urlencoded' \
|
||||
--data-urlencode 'username=admin' \
|
||||
--data-urlencode 'password=admin'
|
||||
|
||||
> $token
|
||||
```
|
||||
|
||||
## Get all TODOs for this User
|
||||
|
||||
```sh
|
||||
$ curl --location --request GET 'http://localhost:8080/todos' \
|
||||
--header 'Authorization: Bearer $token'
|
||||
|
||||
> $todos
|
||||
```
|
||||
|
||||
## Get a specific User's TODO
|
||||
|
||||
```sh
|
||||
$ curl --location --request GET 'http://localhost:8080/todos/$id' \
|
||||
--header 'Authorization: Bearer $token'
|
||||
|
||||
> $todo
|
||||
```
|
||||
|
||||
## Get all TODOs for all Users (admin role)
|
||||
|
||||
```sh
|
||||
$ curl --location --request GET 'http://localhost:8080/admin/todos' \
|
||||
--header 'Authorization: Bearer $token'
|
||||
|
||||
> $todos
|
||||
```
|
||||
|
||||
## Create a new TODO
|
||||
|
||||
```sh
|
||||
$ curl --location --request POST 'http://localhost:8080/todos' \
|
||||
--header 'Authorization: Bearer $token' \
|
||||
--header 'Content-Type: application/json' \
|
||||
--data-raw '{
|
||||
"title": "test titlte",
|
||||
"body": "test body"
|
||||
}'
|
||||
|
||||
> Status Created
|
||||
> $todo
|
||||
```
|
||||
140
_examples/auth/jwt/tutorial/api/auth.go
Normal file
140
_examples/auth/jwt/tutorial/api/auth.go
Normal file
@@ -0,0 +1,140 @@
|
||||
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.
|
||||
func SignOut(ctx iris.Context) {
|
||||
ctx.Logout() // this is automatically binded to a function which invalidates the current request token by the JWT Verifier above.
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
11
_examples/auth/jwt/tutorial/go.mod
Normal file
11
_examples/auth/jwt/tutorial/go.mod
Normal file
@@ -0,0 +1,11 @@
|
||||
module myapp
|
||||
|
||||
go 1.15
|
||||
|
||||
require (
|
||||
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 => ../../../../
|
||||
40
_examples/auth/jwt/tutorial/main.go
Normal file
40
_examples/auth/jwt/tutorial/main.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"myapp/api"
|
||||
"myapp/domain/repository"
|
||||
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
var (
|
||||
userRepository = repository.NewMemoryUserRepository()
|
||||
todoRepository = repository.NewMemoryTodoRepository()
|
||||
)
|
||||
|
||||
func main() {
|
||||
if err := repository.GenerateSamples(userRepository, todoRepository); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
app := iris.New()
|
||||
|
||||
app.Post("/signin", api.SignIn(userRepository))
|
||||
|
||||
verify := api.Verify()
|
||||
|
||||
todosAPI := app.Party("/todos", verify)
|
||||
todosAPI.Post("/", api.CreateTodo(todoRepository))
|
||||
todosAPI.Get("/", api.ListTodos(todoRepository))
|
||||
todosAPI.Get("/{id}", api.GetTodo(todoRepository))
|
||||
|
||||
adminAPI := app.Party("/admin", verify, api.AllowAdmin)
|
||||
adminAPI.Get("/todos", api.ListAllTodos(todoRepository))
|
||||
|
||||
// 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")
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -2,4 +2,6 @@ module github.com/kataras/iris/_examples/dependency-injection/jwt/contrib
|
||||
|
||||
go 1.15
|
||||
|
||||
require github.com/iris-contrib/middleware/jwt v0.0.0-20200810001613-32cf668f999f
|
||||
require (
|
||||
github.com/iris-contrib/middleware/jwt v0.0.0-20201017024110-39b50ffeb885
|
||||
)
|
||||
|
||||
@@ -11,37 +11,57 @@ func main() {
|
||||
app := iris.New()
|
||||
app.ConfigureContainer(register)
|
||||
|
||||
// http://localhost:8080/authenticate
|
||||
// http://localhost:8080/restricted (Header: Authorization = Bearer $token)
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func register(api *iris.APIContainer) {
|
||||
j := jwt.HMAC(15*time.Minute, "secret", "secretforencrypt")
|
||||
var secret = []byte("secret")
|
||||
|
||||
func register(api *iris.APIContainer) {
|
||||
api.RegisterDependency(func(ctx iris.Context) (claims userClaims) {
|
||||
if err := j.VerifyToken(ctx, &claims); err != nil {
|
||||
/* Using the middleware:
|
||||
if ctx.Proceed(verify) {
|
||||
// ^ the "verify" middleware will stop the execution if it's failed to verify the request.
|
||||
// Map the input parameter of "restricted" function with the claims.
|
||||
return jwt.Get(ctx).(*userClaims)
|
||||
}*/
|
||||
token := jwt.FromHeader(ctx)
|
||||
if token == "" {
|
||||
ctx.StopWithError(iris.StatusUnauthorized, jwt.ErrMissing)
|
||||
return
|
||||
}
|
||||
|
||||
verifiedToken, err := jwt.Verify(jwt.HS256, secret, []byte(token))
|
||||
if err != nil {
|
||||
ctx.StopWithError(iris.StatusUnauthorized, err)
|
||||
return
|
||||
}
|
||||
|
||||
verifiedToken.Claims(&claims)
|
||||
return
|
||||
})
|
||||
|
||||
api.Get("/authenticate", writeToken(j))
|
||||
api.Get("/authenticate", writeToken)
|
||||
api.Get("/restricted", restrictedPage)
|
||||
}
|
||||
|
||||
type userClaims struct {
|
||||
jwt.Claims
|
||||
Username string
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func writeToken(j *jwt.JWT) iris.Handler {
|
||||
return func(ctx iris.Context) {
|
||||
j.WriteToken(ctx, userClaims{
|
||||
Claims: j.Expiry(jwt.Claims{Issuer: "an-issuer"}),
|
||||
Username: "kataras",
|
||||
})
|
||||
func writeToken(ctx iris.Context) {
|
||||
claims := userClaims{
|
||||
Username: "kataras",
|
||||
}
|
||||
|
||||
token, err := jwt.Sign(jwt.HS256, secret, claims, jwt.MaxAge(1*time.Minute))
|
||||
if err != nil {
|
||||
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Write(token)
|
||||
}
|
||||
|
||||
func restrictedPage(claims userClaims) string {
|
||||
|
||||
@@ -29,7 +29,10 @@ func makeAccessLog() *accesslog.AccessLog {
|
||||
ac.PanicLog = accesslog.LogHandler
|
||||
|
||||
// Set Custom Formatter:
|
||||
ac.SetFormatter(&accesslog.JSON{})
|
||||
ac.SetFormatter(&accesslog.JSON{
|
||||
Indent: " ",
|
||||
HumanTime: true,
|
||||
})
|
||||
// ac.SetFormatter(&accesslog.CSV{})
|
||||
// ac.SetFormatter(&accesslog.Template{Text: "{{.Code}}"})
|
||||
|
||||
|
||||
38
_examples/request-body/read-url/main.go
Normal file
38
_examples/request-body/read-url/main.go
Normal file
@@ -0,0 +1,38 @@
|
||||
// package main contains an example on how to use the ReadURL,
|
||||
// same way you can do the ReadQuery, ReadParams, ReadJSON, ReadProtobuf and e.t.c.
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/v12"
|
||||
)
|
||||
|
||||
type myURL struct {
|
||||
Name string `url:"name"` // or `param:"name"`
|
||||
Age int `url:"age"` // >> >>
|
||||
Tail []string `url:"tail"` // >> >>
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := newApp()
|
||||
|
||||
// http://localhost:8080/iris/web/framework?name=kataras&age=27
|
||||
// myURL: main.myURL{Name:"kataras", Age:27, Tail:[]string{"iris", "web", "framework"}}
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func newApp() *iris.Application {
|
||||
app := iris.New()
|
||||
|
||||
app.Get("/{tail:path}", func(ctx iris.Context) {
|
||||
var u myURL
|
||||
// ReadURL is a shortcut of ReadParams + ReadQuery.
|
||||
if err := ctx.ReadURL(&u); err != nil {
|
||||
ctx.StopWithError(iris.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Writef("myURL: %#v", u)
|
||||
})
|
||||
|
||||
return app
|
||||
}
|
||||
16
_examples/request-body/read-url/main_test.go
Normal file
16
_examples/request-body/read-url/main_test.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/kataras/iris/v12/httptest"
|
||||
)
|
||||
|
||||
func TestReadURL(t *testing.T) {
|
||||
app := newApp()
|
||||
|
||||
e := httptest.New(t, app)
|
||||
|
||||
expectedBody := `myURL: main.myURL{Name:"kataras", Age:27, Tail:[]string{"iris", "web", "framework"}}`
|
||||
e.GET("/iris/web/framework").WithQuery("name", "kataras").WithQuery("age", 27).Expect().Status(httptest.StatusOK).Body().Equal(expectedBody)
|
||||
}
|
||||
@@ -38,9 +38,9 @@ func NewApp(sess *sessions.Sessions) *iris.Application {
|
||||
session := sessions.Get(ctx)
|
||||
isNew := session.IsNew()
|
||||
|
||||
session.Set("name", "iris")
|
||||
session.Set("username", "iris")
|
||||
|
||||
ctx.Writef("All ok session set to: %s [isNew=%t]", session.GetString("name"), isNew)
|
||||
ctx.Writef("All ok session set to: %s [isNew=%t]", session.GetString("username"), isNew)
|
||||
})
|
||||
|
||||
app.Get("/get", func(ctx iris.Context) {
|
||||
@@ -48,9 +48,9 @@ func NewApp(sess *sessions.Sessions) *iris.Application {
|
||||
|
||||
// get a specific value, as string,
|
||||
// if not found then it returns just an empty string.
|
||||
name := session.GetString("name")
|
||||
name := session.GetString("username")
|
||||
|
||||
ctx.Writef("The name on the /set was: %s", name)
|
||||
ctx.Writef("The username on the /set was: %s", name)
|
||||
})
|
||||
|
||||
app.Get("/set-struct", func(ctx iris.Context) {
|
||||
|
||||
42
_examples/sessions/viewdata/main.go
Normal file
42
_examples/sessions/viewdata/main.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/kataras/iris/v12"
|
||||
"github.com/kataras/iris/v12/sessions"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := iris.New()
|
||||
app.RegisterView(iris.HTML("./views", ".html"))
|
||||
|
||||
sess := sessions.New(sessions.Config{Cookie: "session_cookie", AllowReclaim: true})
|
||||
app.Use(sess.Handler())
|
||||
// ^ use app.UseRouter instead to access sessions on HTTP errors too.
|
||||
|
||||
// Register our custom middleware, after the sessions middleware.
|
||||
app.Use(setSessionViewData)
|
||||
|
||||
app.Get("/", index)
|
||||
app.Listen(":8080")
|
||||
}
|
||||
|
||||
func setSessionViewData(ctx iris.Context) {
|
||||
session := sessions.Get(ctx)
|
||||
ctx.ViewData("session", session)
|
||||
ctx.Next()
|
||||
}
|
||||
|
||||
func index(ctx iris.Context) {
|
||||
session := sessions.Get(ctx)
|
||||
session.Set("username", "kataras")
|
||||
ctx.View("index")
|
||||
/* OR without middleware:
|
||||
ctx.View("index", iris.Map{
|
||||
"session": session,
|
||||
// {{.session.Get "username"}}
|
||||
// OR to pass only the 'username':
|
||||
// "username": session.Get("username"),
|
||||
// {{.username}}
|
||||
})
|
||||
*/
|
||||
}
|
||||
11
_examples/sessions/viewdata/views/index.html
Normal file
11
_examples/sessions/viewdata/views/index.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sessions View Data</title>
|
||||
</head>
|
||||
<body>
|
||||
Hello {{.session.Get "username"}}
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user