mirror of
https://github.com/kataras/iris.git
synced 2025-02-13 12:36:20 +01:00
Merge pull request #1677 from kataras/new-basicauth-features
New Basic Authentication Features
This commit is contained in:
commit
288646a31c
|
@ -30,7 +30,7 @@ The codebase for Dependency Injection, Internationalization and localization and
|
||||||
|
|
||||||
- Add `iris.DirOptions.SPA bool` field to allow [Single Page Applications](https://github.com/kataras/iris/tree/master/_examples/file-server/single-page-application/basic/main.go) under a file server.
|
- Add `iris.DirOptions.SPA bool` field to allow [Single Page Applications](https://github.com/kataras/iris/tree/master/_examples/file-server/single-page-application/basic/main.go) under a file server.
|
||||||
- A generic User interface, see the `Context.SetUser/User` methods in the New Context Methods section for more. In-short, the basicauth middleware's stored user can now be retrieved through `Context.User()` which provides more information than the native `ctx.Request().BasicAuth()` method one. Third-party authentication middleware creators can benefit of these two methods, plus the Logout below.
|
- A generic User interface, see the `Context.SetUser/User` methods in the New Context Methods section for more. In-short, the basicauth middleware's stored user can now be retrieved through `Context.User()` which provides more information than the native `ctx.Request().BasicAuth()` method one. Third-party authentication middleware creators can benefit of these two methods, plus the Logout below.
|
||||||
- A `Context.Logout` method is added, can be used to invalidate [basicauth](https://github.com/kataras/iris/blob/master/_examples/auth/basicauth/main.go) or [jwt](https://github.com/kataras/iris/blob/master/_examples/auth/jwt/blocklist/main.go) client credentials.
|
- A `Context.Logout` method is added, can be used to invalidate [basicauth](https://github.com/kataras/iris/blob/master/_examples/auth/basicauth/basic/main.go) or [jwt](https://github.com/kataras/iris/blob/master/_examples/auth/jwt/blocklist/main.go) client credentials.
|
||||||
- Add the ability to [share functions](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-funcs) between handlers chain and add an [example](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-services) on sharing Go structures (aka services).
|
- Add the ability to [share functions](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-funcs) between handlers chain and add an [example](https://github.com/kataras/iris/tree/master/_examples/routing/writing-a-middleware/share-services) on sharing Go structures (aka services).
|
||||||
|
|
||||||
- Add the new `Party.UseOnce` method to the `*Route`
|
- Add the new `Party.UseOnce` method to the `*Route`
|
||||||
|
|
|
@ -196,7 +196,11 @@
|
||||||
* [Ttemplates and Functions](i18n/template)
|
* [Ttemplates and Functions](i18n/template)
|
||||||
* [Pluralization and Variables](i18n/plurals)
|
* [Pluralization and Variables](i18n/plurals)
|
||||||
* Authentication, Authorization & Bot Detection
|
* Authentication, Authorization & Bot Detection
|
||||||
* [Basic Authentication](auth/basicauth/main.go)
|
* Basic Authentication
|
||||||
|
* [Basic](auth/basicauth/basic)
|
||||||
|
* [Load from a slice of Users](auth/basicauth/users_list)
|
||||||
|
* [Load from a file & encrypted passwords](auth/basicauth/users_file_bcrypt)
|
||||||
|
* [Fetch & validate a User from a Database (MySQL)](auth/basicauth/database)
|
||||||
* [CORS](auth/cors)
|
* [CORS](auth/cors)
|
||||||
* JSON Web Tokens
|
* JSON Web Tokens
|
||||||
* [Basic](auth/jwt/basic/main.go)
|
* [Basic](auth/jwt/basic/main.go)
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12"
|
"github.com/kataras/iris/v12"
|
||||||
"github.com/kataras/iris/v12/middleware/basicauth"
|
"github.com/kataras/iris/v12/middleware/basicauth"
|
||||||
)
|
)
|
||||||
|
@ -10,33 +8,48 @@ import (
|
||||||
func newApp() *iris.Application {
|
func newApp() *iris.Application {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
|
||||||
authConfig := basicauth.Config{
|
/*
|
||||||
Users: map[string]string{"myusername": "mypassword", "mySecondusername": "mySecondpassword"},
|
opts := basicauth.Options{
|
||||||
Realm: "Authorization Required", // defaults to "Authorization Required"
|
Realm: "Authorization Required",
|
||||||
Expires: time.Duration(30) * time.Minute,
|
MaxAge: 30 * time.Minute,
|
||||||
|
GC: basicauth.GC{
|
||||||
|
Every: 2 * time.Hour,
|
||||||
|
},
|
||||||
|
Allow: basicauth.AllowUsers(map[string]string{
|
||||||
|
"myusername": "mypassword",
|
||||||
|
"mySecondusername": "mySecondpassword",
|
||||||
|
}),
|
||||||
|
MaxTries: 2,
|
||||||
}
|
}
|
||||||
|
auth := basicauth.New(opts)
|
||||||
|
|
||||||
authentication := basicauth.New(authConfig)
|
OR simply:
|
||||||
|
*/
|
||||||
|
|
||||||
// to global app.Use(authentication) (or app.UseGlobal before the .Run)
|
auth := basicauth.Default(map[string]string{
|
||||||
|
"myusername": "mypassword",
|
||||||
|
"mySecondusername": "mySecondpassword",
|
||||||
|
})
|
||||||
|
|
||||||
|
// to global app.Use(auth) (or app.UseGlobal before the .Run)
|
||||||
// to routes
|
// to routes
|
||||||
/*
|
/*
|
||||||
app.Get("/mysecret", authentication, h)
|
app.Get("/mysecret", auth, h)
|
||||||
*/
|
*/
|
||||||
|
|
||||||
app.Get("/", func(ctx iris.Context) { ctx.Redirect("/admin") })
|
app.Get("/", func(ctx iris.Context) { ctx.Redirect("/admin") })
|
||||||
|
|
||||||
// to party
|
// to party
|
||||||
|
|
||||||
needAuth := app.Party("/admin", authentication)
|
needAuth := app.Party("/admin", auth)
|
||||||
{
|
{
|
||||||
//http://localhost:8080/admin
|
//http://localhost:8080/admin
|
||||||
needAuth.Get("/", h)
|
needAuth.Get("/", handler)
|
||||||
// http://localhost:8080/admin/profile
|
// http://localhost:8080/admin/profile
|
||||||
needAuth.Get("/profile", h)
|
needAuth.Get("/profile", handler)
|
||||||
|
|
||||||
// http://localhost:8080/admin/settings
|
// http://localhost:8080/admin/settings
|
||||||
needAuth.Get("/settings", h)
|
needAuth.Get("/settings", handler)
|
||||||
|
|
||||||
needAuth.Get("/logout", logout)
|
needAuth.Get("/logout", logout)
|
||||||
}
|
}
|
||||||
|
@ -50,12 +63,13 @@ func main() {
|
||||||
app.Listen(":8080")
|
app.Listen(":8080")
|
||||||
}
|
}
|
||||||
|
|
||||||
func h(ctx iris.Context) {
|
func handler(ctx iris.Context) {
|
||||||
// username, password, _ := ctx.Request().BasicAuth()
|
// username, password, _ := ctx.Request().BasicAuth()
|
||||||
// third parameter it will be always true because the middleware
|
// third parameter it will be always true because the middleware
|
||||||
// makes sure for that, otherwise this handler will not be executed.
|
// makes sure for that, otherwise this handler will not be executed.
|
||||||
// OR:
|
// OR:
|
||||||
user := ctx.User()
|
user := ctx.User()
|
||||||
|
// OR ctx.User().GetRaw() to get the underline value.
|
||||||
username, _ := user.GetUsername()
|
username, _ := user.GetUsername()
|
||||||
password, _ := user.GetPassword()
|
password, _ := user.GetPassword()
|
||||||
ctx.Writef("%s %s:%s", ctx.Path(), username, password)
|
ctx.Writef("%s %s:%s", ctx.Path(), username, password)
|
18
_examples/auth/basicauth/database/Dockerfile
Normal file
18
_examples/auth/basicauth/database/Dockerfile
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# docker build -t myapp .
|
||||||
|
# docker run --rm -it -p 8080:8080 myapp:latest
|
||||||
|
FROM golang:latest AS builder
|
||||||
|
RUN apt-get update
|
||||||
|
ENV GO111MODULE=on \
|
||||||
|
CGO_ENABLED=0 \
|
||||||
|
GOOS=linux \
|
||||||
|
GOARCH=amd64
|
||||||
|
WORKDIR /go/src/app
|
||||||
|
COPY go.mod .
|
||||||
|
RUN go mod download
|
||||||
|
# cache step
|
||||||
|
COPY . .
|
||||||
|
RUN go install
|
||||||
|
|
||||||
|
FROM scratch
|
||||||
|
COPY --from=builder /go/bin/myapp .
|
||||||
|
ENTRYPOINT ["./myapp"]
|
44
_examples/auth/basicauth/database/README.md
Normal file
44
_examples/auth/basicauth/database/README.md
Normal file
|
@ -0,0 +1,44 @@
|
||||||
|
# BasicAuth + MySQL & Docker Example
|
||||||
|
|
||||||
|
## ⚡ Get Started
|
||||||
|
|
||||||
|
Download the folder.
|
||||||
|
|
||||||
|
### Install (Docker)
|
||||||
|
|
||||||
|
Install [Docker](https://www.docker.com/) and execute the command below
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ docker-compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install (Manually)
|
||||||
|
|
||||||
|
Run `go build` or `go run main.go` and read below.
|
||||||
|
|
||||||
|
#### MySQL
|
||||||
|
|
||||||
|
Environment variables:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
MYSQL_USER=user_myapp
|
||||||
|
MYSQL_PASSWORD=dbpassword
|
||||||
|
MYSQL_HOST=localhost
|
||||||
|
MYSQL_DATABASE=myapp
|
||||||
|
```
|
||||||
|
|
||||||
|
Download the schema from [migration/db.sql](migration/db.sql) and execute it against your MySQL server instance.
|
||||||
|
|
||||||
|
<http://localhost:8080>
|
||||||
|
|
||||||
|
```sh
|
||||||
|
username: admin
|
||||||
|
password: admin
|
||||||
|
```
|
||||||
|
|
||||||
|
```sh
|
||||||
|
username: iris
|
||||||
|
password: iris_password
|
||||||
|
```
|
||||||
|
|
||||||
|
The example does not contain code to add a user to the database, as this is out of the scope of this middleware. More features can be implemented by end-developers.
|
32
_examples/auth/basicauth/database/docker-compose.yml
Normal file
32
_examples/auth/basicauth/database/docker-compose.yml
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
version: '3.1'
|
||||||
|
|
||||||
|
services:
|
||||||
|
db:
|
||||||
|
image: mysql
|
||||||
|
command: --default-authentication-plugin=mysql_native_password
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: dbpassword
|
||||||
|
MYSQL_DATABASE: myapp
|
||||||
|
MYSQL_USER: user_myapp
|
||||||
|
MYSQL_PASSWORD: dbpassword
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- ./migration:/docker-entrypoint-initdb.d
|
||||||
|
app:
|
||||||
|
build: .
|
||||||
|
ports:
|
||||||
|
- 8080:8080
|
||||||
|
environment:
|
||||||
|
PORT: 8080
|
||||||
|
MYSQL_USER: user_myapp
|
||||||
|
MYSQL_PASSWORD: dbpassword
|
||||||
|
MYSQL_DATABASE: myapp
|
||||||
|
MYSQL_HOST: db
|
||||||
|
restart: on-failure
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "tcp://db:3306"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 10
|
||||||
|
depends_on:
|
||||||
|
- db
|
8
_examples/auth/basicauth/database/go.mod
Normal file
8
_examples/auth/basicauth/database/go.mod
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
module myapp
|
||||||
|
|
||||||
|
go 1.15
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-sql-driver/mysql v1.5.0
|
||||||
|
github.com/kataras/iris/v12 master
|
||||||
|
)
|
114
_examples/auth/basicauth/database/main.go
Normal file
114
_examples/auth/basicauth/database/main.go
Normal file
|
@ -0,0 +1,114 @@
|
||||||
|
package main // Look README.md
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/middleware/basicauth"
|
||||||
|
|
||||||
|
_ "github.com/go-sql-driver/mysql" // lint: mysql driver.
|
||||||
|
)
|
||||||
|
|
||||||
|
// User is just an example structure of a user,
|
||||||
|
// it MUST contain a Username and Password exported fields
|
||||||
|
// or/and complete the basicauth.User interface.
|
||||||
|
type User struct {
|
||||||
|
ID int64 `db:"id" json:"id"`
|
||||||
|
Username string `db:"username" json:"username"`
|
||||||
|
Password string `db:"password" json:"password"`
|
||||||
|
Email string `db:"email" json:"email"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsername returns the Username field.
|
||||||
|
func (u User) GetUsername() string {
|
||||||
|
return u.Username
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPassword returns the Password field.
|
||||||
|
func (u User) GetPassword() string {
|
||||||
|
return u.Password
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?parseTime=true&charset=utf8mb4&collation=utf8mb4_unicode_ci",
|
||||||
|
getenv("MYSQL_USER", "user_myapp"),
|
||||||
|
getenv("MYSQL_PASSWORD", "dbpassword"),
|
||||||
|
getenv("MYSQL_HOST", "localhost"),
|
||||||
|
getenv("MYSQL_DATABASE", "myapp"),
|
||||||
|
)
|
||||||
|
db, err := connect(dsn)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate a user from database.
|
||||||
|
allowFunc := func(ctx iris.Context, username, password string) (interface{}, bool) {
|
||||||
|
user, err := db.getUserByUsernameAndPassword(context.Background(), username, password)
|
||||||
|
return user, err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
opts := basicauth.Options{
|
||||||
|
Realm: basicauth.DefaultRealm,
|
||||||
|
ErrorHandler: basicauth.DefaultErrorHandler,
|
||||||
|
Allow: allowFunc,
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := basicauth.New(opts)
|
||||||
|
|
||||||
|
app := iris.New()
|
||||||
|
app.Use(auth)
|
||||||
|
app.Get("/", index)
|
||||||
|
app.Listen(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
func index(ctx iris.Context) {
|
||||||
|
user, _ := ctx.User().GetRaw()
|
||||||
|
// user is a type of main.User
|
||||||
|
ctx.JSON(user)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getenv(key string, def string) string {
|
||||||
|
v := os.Getenv(key)
|
||||||
|
if v == "" {
|
||||||
|
return def
|
||||||
|
}
|
||||||
|
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
||||||
|
type database struct {
|
||||||
|
*sql.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func connect(dsn string) (*database, error) {
|
||||||
|
conn, err := sql.Open("mysql", dsn)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = conn.Ping()
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &database{conn}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *database) getUserByUsernameAndPassword(ctx context.Context, username, password string) (User, error) {
|
||||||
|
query := fmt.Sprintf("SELECT * FROM %s WHERE %s = ? AND %s = ? LIMIT 1", "users", "username", "password")
|
||||||
|
rows, err := db.QueryContext(ctx, query, username, password)
|
||||||
|
if err != nil {
|
||||||
|
return User{}, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
if !rows.Next() {
|
||||||
|
return User{}, sql.ErrNoRows
|
||||||
|
}
|
||||||
|
|
||||||
|
var user User
|
||||||
|
err = rows.Scan(&user.ID, &user.Username, &user.Password, &user.Email)
|
||||||
|
return user, err
|
||||||
|
}
|
22
_examples/auth/basicauth/database/migration/db.sql
Normal file
22
_examples/auth/basicauth/database/migration/db.sql
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
CREATE DATABASE IF NOT EXISTS myapp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
USE myapp;
|
||||||
|
|
||||||
|
SET NAMES utf8mb4;
|
||||||
|
SET FOREIGN_KEY_CHECKS = 0;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS users;
|
||||||
|
CREATE TABLE users (
|
||||||
|
id int(11) NOT NULL AUTO_INCREMENT,
|
||||||
|
username varchar(255) NOT NULL,
|
||||||
|
password varchar(255) NOT NULL,
|
||||||
|
email varchar(255) NOT NULL,
|
||||||
|
PRIMARY KEY (id)
|
||||||
|
);
|
||||||
|
|
||||||
|
INSERT INTO users (username,password,email)
|
||||||
|
VALUES
|
||||||
|
('admin', 'admin', 'kataras2006@hotmail.com'),
|
||||||
|
("iris", 'iris_password', 'iris-go@outlook.com');
|
||||||
|
|
||||||
|
SET FOREIGN_KEY_CHECKS = 1;
|
30
_examples/auth/basicauth/users_file_bcrypt/main.go
Normal file
30
_examples/auth/basicauth/users_file_bcrypt/main.go
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/middleware/basicauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
auth := basicauth.Load("users.yml", basicauth.BCRYPT)
|
||||||
|
/* Same as:
|
||||||
|
opts := basicauth.Options{
|
||||||
|
Realm: basicauth.DefaultRealm,
|
||||||
|
Allow: basicauth.AllowUsersFile("users.yml", basicauth.BCRYPT),
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := basicauth.New(opts)
|
||||||
|
*/
|
||||||
|
|
||||||
|
app := iris.New()
|
||||||
|
app.Use(auth)
|
||||||
|
app.Get("/", index)
|
||||||
|
// kataras:kataras_pass
|
||||||
|
// makis:makis_pass
|
||||||
|
app.Listen(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
func index(ctx iris.Context) {
|
||||||
|
user := ctx.User()
|
||||||
|
ctx.JSON(user)
|
||||||
|
}
|
12
_examples/auth/basicauth/users_file_bcrypt/users.yml
Normal file
12
_examples/auth/basicauth/users_file_bcrypt/users.yml
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
# The file cannot be modified during the serve time.
|
||||||
|
# To support real-time users changes please use the Options.Allow instead,
|
||||||
|
# (see the database example for that).
|
||||||
|
#
|
||||||
|
# Again, the username and password (or capitalized) fields are required,
|
||||||
|
# the rest are optional, depending on your application needs.
|
||||||
|
- username: kataras
|
||||||
|
password: $2a$10$Irg8k8HWkDlvL0YDBKLCYee6j6zzIFTplJcvZYKA.B8/clHPZn2Ey # encrypted of kataras_pass
|
||||||
|
age: 27
|
||||||
|
role: admin
|
||||||
|
- username: makis
|
||||||
|
password: $2a$10$3GXzp3J5GhHThGisbpvpZuftbmzPivDMo94XPnkTnDe7254x7sJ3O # encrypted of makis_pass
|
58
_examples/auth/basicauth/users_list/main.go
Normal file
58
_examples/auth/basicauth/users_list/main.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/middleware/basicauth"
|
||||||
|
)
|
||||||
|
|
||||||
|
// User is just an example structure of a user,
|
||||||
|
// it MUST contain a Username and Password exported fields
|
||||||
|
// or complete the basicauth.User interface.
|
||||||
|
type User struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Roles []string `json:"roles"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var users = []User{
|
||||||
|
{"admin", "admin", []string{"admin"}},
|
||||||
|
{"kataras", "kataras_pass", []string{"manager", "author"}},
|
||||||
|
{"george", "george_pass", []string{"member"}},
|
||||||
|
{"john", "john_pass", []string{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
opts := basicauth.Options{
|
||||||
|
Realm: basicauth.DefaultRealm,
|
||||||
|
// Defaults to 0, no expiration.
|
||||||
|
// Prompt for new credentials on a client's request
|
||||||
|
// made after 10 minutes the user has logged in:
|
||||||
|
MaxAge: 10 * time.Minute,
|
||||||
|
// Clear any expired users from the memory every one hour,
|
||||||
|
// note that the user's expiration time will be
|
||||||
|
// reseted on the next valid request (when Allow passed).
|
||||||
|
GC: basicauth.GC{
|
||||||
|
Every: 2 * time.Hour,
|
||||||
|
},
|
||||||
|
// The users can be a slice of custom users structure
|
||||||
|
// or a map[string]string (username:password)
|
||||||
|
// or []map[string]interface{} with username and passwords required fields,
|
||||||
|
// read the godocs for more.
|
||||||
|
Allow: basicauth.AllowUsers(users),
|
||||||
|
}
|
||||||
|
|
||||||
|
auth := basicauth.New(opts)
|
||||||
|
// OR: basicauth.Default(users)
|
||||||
|
|
||||||
|
app := iris.New()
|
||||||
|
app.Use(auth)
|
||||||
|
app.Get("/", index)
|
||||||
|
app.Listen(":8080")
|
||||||
|
}
|
||||||
|
|
||||||
|
func index(ctx iris.Context) {
|
||||||
|
user, _ := ctx.User().GetRaw()
|
||||||
|
ctx.JSON(user)
|
||||||
|
}
|
|
@ -4,7 +4,7 @@ go 1.15
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/uuid v1.1.2
|
github.com/google/uuid v1.1.2
|
||||||
github.com/kataras/iris/v12 v12.2.0-alpha.0.20201106220849-7a19cfb2112f
|
github.com/kataras/iris/v12 v12.2.0-alpha.0.20201113181155-4d09475c290d
|
||||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -5,19 +5,19 @@
|
||||||
| Method | Path | Description | URL Parameters | Body | Auth Required |
|
| Method | Path | Description | URL Parameters | Body | Auth Required |
|
||||||
|--------|---------------------|------------------------|--------------- |----------------------------|---------------|
|
|--------|---------------------|------------------------|--------------- |----------------------------|---------------|
|
||||||
| ANY | /token | Prints a new JWT Token | - | - | - |
|
| ANY | /token | Prints a new JWT Token | - | - | - |
|
||||||
| GET | /category | Lists a set of Categories | offset, limit, order | - | - |
|
| GET | /category | Lists a set of Categories | offset, limit, order | - | Token |
|
||||||
| POST | /category | Creates a Category | - | JSON [Full Category](migration/api_category/create_category.json) | Token |
|
| POST | /category | Creates a Category | - | JSON [Full Category](migration/api_category/create_category.json) | Token |
|
||||||
| PUT | /category | Fully-Updates a Category | - | JSON [Full Category](migration/api_category/update_category.json) | Token |
|
| PUT | /category | Fully-Updates a Category | - | JSON [Full Category](migration/api_category/update_category.json) | Token |
|
||||||
| PATCH | /category/{id} | Partially-Updates a Category | - | JSON [Partial Category](migration/api_category/update_partial_category.json) | Token |
|
| PATCH | /category/{id} | Partially-Updates a Category | - | JSON [Partial Category](migration/api_category/update_partial_category.json) | Token |
|
||||||
| GET | /category/{id} | Prints a Category | - | - | - |
|
| GET | /category/{id} | Prints a Category | - | - | Token |
|
||||||
| DELETE | /category/{id} | Deletes a Category | - | - | Token |
|
| DELETE | /category/{id} | Deletes a Category | - | - | Token |
|
||||||
| GET | /category/{id}/products | Lists all Products from a Category | offset, limit, order | - | - |
|
| GET | /category/{id}/products | Lists all Products from a Category | offset, limit, order | - | Token |
|
||||||
| POST | /category/{id}/products | (Batch) Assigns one or more Products to a Category | - | JSON [Products](migration/api_category/insert_products_category.json) | Token |
|
| POST | /category/{id}/products | (Batch) Assigns one or more Products to a Category | - | JSON [Products](migration/api_category/insert_products_category.json) | Token |
|
||||||
| GET | /product | Lists a set of Products (cache) | offset, limit, order | - | - |
|
| GET | /product | Lists a set of Products (cache) | offset, limit, order | - | Token |
|
||||||
| POST | /product | Creates a Product | - | JSON [Full Product](migration/api_product/create_product.json) | Token |
|
| POST | /product | Creates a Product | - | JSON [Full Product](migration/api_product/create_product.json) | Token |
|
||||||
| PUT | /product | Fully-Updates a Product | - | JSON [Full Product](migration/api_product/update_product.json) | Token |
|
| PUT | /product | Fully-Updates a Product | - | JSON [Full Product](migration/api_product/update_product.json) | Token |
|
||||||
| PATCH | /product/{id} | Partially-Updates a Product | - | JSON [Partial Product](migration/api_product/update_partial_product.json) | Token |
|
| PATCH | /product/{id} | Partially-Updates a Product | - | JSON [Partial Product](migration/api_product/update_partial_product.json) | Token |
|
||||||
| GET | /product/{id} | Prints a Product (cache) | - | - | - |
|
| GET | /product/{id} | Prints a Product (cache) | - | - | Token |
|
||||||
| DELETE | /product/{id} | Deletes a Product | - | - | Token |
|
| DELETE | /product/{id} | Deletes a Product | - | - | Token |
|
||||||
|
|
||||||
|
|
||||||
|
@ -71,7 +71,7 @@ Download the folder.
|
||||||
Install [Docker](https://www.docker.com/) and execute the command below
|
Install [Docker](https://www.docker.com/) and execute the command below
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
$ docker-compose up
|
$ docker-compose up --build
|
||||||
```
|
```
|
||||||
|
|
||||||
### Install (Manually)
|
### Install (Manually)
|
||||||
|
@ -89,7 +89,7 @@ MYSQL_HOST=localhost
|
||||||
MYSQL_DATABASE=myapp
|
MYSQL_DATABASE=myapp
|
||||||
```
|
```
|
||||||
|
|
||||||
Download the schema from [migration/myapp.sql](migration/myapp.sql) and execute it against your MySQL server instance.
|
Download the schema from [migration/db.sql](migration/db.sql) and execute it against your MySQL server instance.
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
CREATE DATABASE IF NOT EXISTS myapp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
CREATE DATABASE IF NOT EXISTS myapp DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||||||
|
@ -139,7 +139,7 @@ Testing is important. The code is written in a way that testing should be trivia
|
||||||
|
|
||||||
## Packages
|
## Packages
|
||||||
|
|
||||||
- https://github.com/dgrijalva/jwt-go (JWT parsing)
|
- https://github.com/kataras/jwt (JWT parsing)
|
||||||
- https://github.com/go-sql-driver/mysql (Go Driver for MySQL)
|
- https://github.com/go-sql-driver/mysql (Go Driver for MySQL)
|
||||||
- https://github.com/DATA-DOG/go-sqlmock (Testing DB see [service/category_service_test.go](service/category_service_test.go))
|
- https://github.com/DATA-DOG/go-sqlmock (Testing DB see [service/category_service_test.go](service/category_service_test.go))
|
||||||
- https://github.com/kataras/iris (HTTP)
|
- https://github.com/kataras/iris (HTTP)
|
||||||
|
|
|
@ -15,18 +15,19 @@ import (
|
||||||
// Router accepts any required dependencies and returns the main server's handler.
|
// Router accepts any required dependencies and returns the main server's handler.
|
||||||
func Router(db sql.Database, secret string) func(iris.Party) {
|
func Router(db sql.Database, secret string) func(iris.Party) {
|
||||||
return func(r iris.Party) {
|
return func(r iris.Party) {
|
||||||
j := jwt.HMAC(15*time.Minute, secret)
|
|
||||||
|
|
||||||
r.Use(requestid.New())
|
r.Use(requestid.New())
|
||||||
r.Use(verifyToken(j))
|
|
||||||
|
signer := jwt.NewSigner(jwt.HS256, secret, 15*time.Minute)
|
||||||
|
r.Get("/token", writeToken(signer))
|
||||||
|
|
||||||
|
verify := jwt.NewVerifier(jwt.HS256, secret).Verify(nil)
|
||||||
|
r.Use(verify)
|
||||||
// Generate a token for testing by navigating to
|
// Generate a token for testing by navigating to
|
||||||
// http://localhost:8080/token endpoint.
|
// http://localhost:8080/token endpoint.
|
||||||
// Copy-paste it to a ?token=$token url parameter or
|
// Copy-paste it to a ?token=$token url parameter or
|
||||||
// open postman and put an Authentication: Bearer $token to get
|
// open postman and put an Authentication: Bearer $token to get
|
||||||
// access on create, update and delete endpoinds.
|
// access on create, update and delete endpoinds.
|
||||||
|
|
||||||
r.Get("/token", writeToken(j))
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
categoryService = service.NewCategoryService(db)
|
categoryService = service.NewCategoryService(db)
|
||||||
productService = service.NewProductService(db)
|
productService = service.NewProductService(db)
|
||||||
|
@ -73,25 +74,19 @@ func Router(db sql.Database, secret string) func(iris.Party) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeToken(j *jwt.JWT) iris.Handler {
|
func writeToken(signer *jwt.Signer) iris.Handler {
|
||||||
return func(ctx iris.Context) {
|
return func(ctx iris.Context) {
|
||||||
claims := jwt.Claims{
|
claims := jwt.Claims{
|
||||||
Issuer: "https://iris-go.com",
|
Issuer: "https://iris-go.com",
|
||||||
Audience: jwt.Audience{requestid.Get(ctx)},
|
Audience: []string{requestid.Get(ctx)},
|
||||||
}
|
}
|
||||||
|
|
||||||
j.WriteToken(ctx, claims)
|
token, err := signer.Sign(claims)
|
||||||
}
|
if err != nil {
|
||||||
}
|
ctx.StopWithStatus(iris.StatusInternalServerError)
|
||||||
|
|
||||||
func verifyToken(j *jwt.JWT) iris.Handler {
|
|
||||||
return func(ctx iris.Context) {
|
|
||||||
// Allow all GET.
|
|
||||||
if ctx.Method() == iris.MethodGet {
|
|
||||||
ctx.Next()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
j.Verify(ctx)
|
ctx.Write(token)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -168,6 +168,11 @@ func (h *CategoryHandler) Delete(ctx iris.Context) {
|
||||||
|
|
||||||
affected, err := h.service.DeleteByID(ctx.Request().Context(), id)
|
affected, err := h.service.DeleteByID(ctx.Request().Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
writeEntityNotFound(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
debugf("CategoryHandler.Delete(DB): %v", err)
|
debugf("CategoryHandler.Delete(DB): %v", err)
|
||||||
writeInternalServerError(ctx)
|
writeInternalServerError(ctx)
|
||||||
return
|
return
|
||||||
|
@ -201,7 +206,7 @@ func (h *CategoryHandler) ListProducts(ctx iris.Context) {
|
||||||
|
|
||||||
var products entity.Products
|
var products entity.Products
|
||||||
err := h.service.List(ctx.Request().Context(), &products, opts)
|
err := h.service.List(ctx.Request().Context(), &products, opts)
|
||||||
if err != nil {
|
if err != nil && err != sql.ErrNoRows {
|
||||||
debugf("CategoryHandler.ListProducts(DB) (table=%s where=%s=%v limit=%d offset=%d): %v",
|
debugf("CategoryHandler.ListProducts(DB) (table=%s where=%s=%v limit=%d offset=%d): %v",
|
||||||
opts.Table, opts.WhereColumn, opts.WhereValue, opts.Limit, opts.Offset, err)
|
opts.Table, opts.WhereColumn, opts.WhereValue, opts.Limit, opts.Offset, err)
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
package main
|
package main // Look README.md
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"info": {
|
"info": {
|
||||||
"_postman_id": "d3a2fdf6-9ebd-4e85-827d-385592a71fd6",
|
"_postman_id": "7b8b53f8-859a-425a-aa9c-28bc2a2d5ef7",
|
||||||
"name": "myapp (api-test)",
|
"name": "myapp (api-test)",
|
||||||
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
"schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json"
|
||||||
},
|
},
|
||||||
|
@ -15,8 +15,9 @@
|
||||||
"header": [
|
"header": [
|
||||||
{
|
{
|
||||||
"key": "Authorization",
|
"key": "Authorization",
|
||||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk2MzkzNjd9.cYohwgUpe-Z7ac0LPpz4Adi5QXJmtwD1ZRpXrMUMPN0",
|
"value": "Bearer {{token}}",
|
||||||
"type": "text"
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
|
@ -47,7 +48,14 @@
|
||||||
"name": "Get By ID",
|
"name": "Get By ID",
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"header": [],
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lyaXMtZ28uY29tIiwiYXVkIjpbIjljOTY5ZDg5LTBkZGYtNDlkMC1hYzcxLTI3MmU3YmY5NjkwMCJdLCJpYXQiOjE2MDYyNzE1NDYsImV4cCI6MTYwNjI3MjQ0Nn0.l2_5iqfEaC68UySTQNoDx2sfzn031tHiTdm2kZoNkWQ",
|
||||||
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "http://localhost:8080/category/1",
|
"raw": "http://localhost:8080/category/1",
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
|
@ -71,7 +79,14 @@
|
||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"header": [],
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lyaXMtZ28uY29tIiwiYXVkIjpbIjljOTY5ZDg5LTBkZGYtNDlkMC1hYzcxLTI3MmU3YmY5NjkwMCJdLCJpYXQiOjE2MDYyNzE1NDYsImV4cCI6MTYwNjI3MjQ0Nn0.l2_5iqfEaC68UySTQNoDx2sfzn031tHiTdm2kZoNkWQ",
|
||||||
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"body": {
|
"body": {
|
||||||
"mode": "raw",
|
"mode": "raw",
|
||||||
"raw": ""
|
"raw": ""
|
||||||
|
@ -113,7 +128,8 @@
|
||||||
{
|
{
|
||||||
"key": "Authorization",
|
"key": "Authorization",
|
||||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||||
"type": "text"
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
|
@ -150,7 +166,8 @@
|
||||||
{
|
{
|
||||||
"key": "Authorization",
|
"key": "Authorization",
|
||||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||||
"type": "text"
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"url": {
|
"url": {
|
||||||
|
@ -177,7 +194,8 @@
|
||||||
{
|
{
|
||||||
"key": "Authorization",
|
"key": "Authorization",
|
||||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||||
"type": "text"
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
|
@ -185,7 +203,7 @@
|
||||||
"raw": "{\r\n \"title\": \"computers-technology\"\r\n}"
|
"raw": "{\r\n \"title\": \"computers-technology\"\r\n}"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "http://localhost:8080/category/1",
|
"raw": "http://localhost:8080/category/3",
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"host": [
|
"host": [
|
||||||
"localhost"
|
"localhost"
|
||||||
|
@ -204,7 +222,14 @@
|
||||||
"name": "List Products",
|
"name": "List Products",
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"header": [],
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lyaXMtZ28uY29tIiwiYXVkIjpbIjljOTY5ZDg5LTBkZGYtNDlkMC1hYzcxLTI3MmU3YmY5NjkwMCJdLCJpYXQiOjE2MDYyNzE1NDYsImV4cCI6MTYwNjI3MjQ0Nn0.l2_5iqfEaC68UySTQNoDx2sfzn031tHiTdm2kZoNkWQ",
|
||||||
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "http://localhost:8080/category/1/products?offset=0&limit=30&by=price&order=asc",
|
"raw": "http://localhost:8080/category/1/products?offset=0&limit=30&by=price&order=asc",
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
|
@ -214,7 +239,7 @@
|
||||||
"port": "8080",
|
"port": "8080",
|
||||||
"path": [
|
"path": [
|
||||||
"category",
|
"category",
|
||||||
"3",
|
"1",
|
||||||
"products"
|
"products"
|
||||||
],
|
],
|
||||||
"query": [
|
"query": [
|
||||||
|
@ -248,7 +273,8 @@
|
||||||
{
|
{
|
||||||
"key": "Authorization",
|
"key": "Authorization",
|
||||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||||
"type": "text"
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
|
@ -256,7 +282,7 @@
|
||||||
"raw": "[{\r\n \"title\": \"product-1\",\r\n \"image_url\": \"https://images.product1.png\",\r\n \"price\": 42.42,\r\n \"description\": \"a description for product-1\"\r\n}, {\r\n \"title\": \"product-2\",\r\n \"image_url\": \"https://images.product2.png\",\r\n \"price\": 32.1,\r\n \"description\": \"a description for product-2\"\r\n}, {\r\n \"title\": \"product-3\",\r\n \"image_url\": \"https://images.product3.png\",\r\n \"price\": 52321321.32,\r\n \"description\": \"a description for product-3\"\r\n}, {\r\n \"title\": \"product-4\",\r\n \"image_url\": \"https://images.product4.png\",\r\n \"price\": 77.4221,\r\n \"description\": \"a description for product-4\"\r\n}, {\r\n \"title\": \"product-5\",\r\n \"image_url\": \"https://images.product5.png\",\r\n \"price\": 55.1,\r\n \"description\": \"a description for product-5\"\r\n}, {\r\n \"title\": \"product-6\",\r\n \"image_url\": \"https://images.product6.png\",\r\n \"price\": 53.32,\r\n \"description\": \"a description for product-6\"\r\n}]"
|
"raw": "[{\r\n \"title\": \"product-1\",\r\n \"image_url\": \"https://images.product1.png\",\r\n \"price\": 42.42,\r\n \"description\": \"a description for product-1\"\r\n}, {\r\n \"title\": \"product-2\",\r\n \"image_url\": \"https://images.product2.png\",\r\n \"price\": 32.1,\r\n \"description\": \"a description for product-2\"\r\n}, {\r\n \"title\": \"product-3\",\r\n \"image_url\": \"https://images.product3.png\",\r\n \"price\": 52321321.32,\r\n \"description\": \"a description for product-3\"\r\n}, {\r\n \"title\": \"product-4\",\r\n \"image_url\": \"https://images.product4.png\",\r\n \"price\": 77.4221,\r\n \"description\": \"a description for product-4\"\r\n}, {\r\n \"title\": \"product-5\",\r\n \"image_url\": \"https://images.product5.png\",\r\n \"price\": 55.1,\r\n \"description\": \"a description for product-5\"\r\n}, {\r\n \"title\": \"product-6\",\r\n \"image_url\": \"https://images.product6.png\",\r\n \"price\": 53.32,\r\n \"description\": \"a description for product-6\"\r\n}]"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "http://localhost:8080/category/1/products",
|
"raw": "http://localhost:8080/category/3/products",
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
"host": [
|
"host": [
|
||||||
"localhost"
|
"localhost"
|
||||||
|
@ -282,7 +308,14 @@
|
||||||
"name": "List",
|
"name": "List",
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"header": [],
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lyaXMtZ28uY29tIiwiYXVkIjpbIjljOTY5ZDg5LTBkZGYtNDlkMC1hYzcxLTI3MmU3YmY5NjkwMCJdLCJpYXQiOjE2MDYyNzE1NDYsImV4cCI6MTYwNjI3MjQ0Nn0.l2_5iqfEaC68UySTQNoDx2sfzn031tHiTdm2kZoNkWQ",
|
||||||
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "http://localhost:8080/product?offset=0&limit=30&by=price&order=asc",
|
"raw": "http://localhost:8080/product?offset=0&limit=30&by=price&order=asc",
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
|
@ -320,7 +353,14 @@
|
||||||
"name": "Get By ID",
|
"name": "Get By ID",
|
||||||
"request": {
|
"request": {
|
||||||
"method": "GET",
|
"method": "GET",
|
||||||
"header": [],
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lyaXMtZ28uY29tIiwiYXVkIjpbIjljOTY5ZDg5LTBkZGYtNDlkMC1hYzcxLTI3MmU3YmY5NjkwMCJdLCJpYXQiOjE2MDYyNzE1NDYsImV4cCI6MTYwNjI3MjQ0Nn0.l2_5iqfEaC68UySTQNoDx2sfzn031tHiTdm2kZoNkWQ",
|
||||||
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
|
}
|
||||||
|
],
|
||||||
"url": {
|
"url": {
|
||||||
"raw": "http://localhost:8080/product/1",
|
"raw": "http://localhost:8080/product/1",
|
||||||
"protocol": "http",
|
"protocol": "http",
|
||||||
|
@ -345,7 +385,8 @@
|
||||||
{
|
{
|
||||||
"key": "Authorization",
|
"key": "Authorization",
|
||||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||||
"type": "text"
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"url": {
|
"url": {
|
||||||
|
@ -372,7 +413,8 @@
|
||||||
{
|
{
|
||||||
"key": "Authorization",
|
"key": "Authorization",
|
||||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||||
"type": "text"
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
|
@ -402,7 +444,8 @@
|
||||||
{
|
{
|
||||||
"key": "Authorization",
|
"key": "Authorization",
|
||||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||||
"type": "text"
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
|
@ -432,7 +475,8 @@
|
||||||
{
|
{
|
||||||
"key": "Authorization",
|
"key": "Authorization",
|
||||||
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
"value": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1ODk1ODU1NjN9.PtfDS1niGoZ7pV6kplI-_q1fVKLnknQ3IwcrLZhoVCU",
|
||||||
"type": "text"
|
"type": "text",
|
||||||
|
"disabled": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"body": {
|
"body": {
|
||||||
|
@ -480,5 +524,44 @@
|
||||||
"response": []
|
"response": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"auth": {
|
||||||
|
"type": "bearer",
|
||||||
|
"bearer": [
|
||||||
|
{
|
||||||
|
"key": "token",
|
||||||
|
"value": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2lyaXMtZ28uY29tIiwiYXVkIjpbIjljOTY5ZDg5LTBkZGYtNDlkMC1hYzcxLTI3MmU3YmY5NjkwMCJdLCJpYXQiOjE2MDYyNzE1NDYsImV4cCI6MTYwNjI3MjQ0Nn0.l2_5iqfEaC68UySTQNoDx2sfzn031tHiTdm2kZoNkWQ",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "prerequest",
|
||||||
|
"script": {
|
||||||
|
"id": "f27c3c2d-efdc-4922-b70c-258784a1d59b",
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"id": "44d94797-9cc6-4ecd-adc5-7ad5329d79c4",
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
""
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"variable": [
|
||||||
|
{
|
||||||
|
"id": "38156b9f-e623-4974-a315-51c931670f23",
|
||||||
|
"key": "token",
|
||||||
|
"value": "token"
|
||||||
|
}
|
||||||
|
],
|
||||||
"protocolProfileBehavior": {}
|
"protocolProfileBehavior": {}
|
||||||
}
|
}
|
|
@ -5,8 +5,6 @@ package middleware
|
||||||
import "github.com/kataras/iris/v12/middleware/basicauth"
|
import "github.com/kataras/iris/v12/middleware/basicauth"
|
||||||
|
|
||||||
// BasicAuth middleware sample.
|
// BasicAuth middleware sample.
|
||||||
var BasicAuth = basicauth.New(basicauth.Config{
|
var BasicAuth = basicauth.Default(map[string]string{
|
||||||
Users: map[string]string{
|
|
||||||
"admin": "password",
|
"admin": "password",
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -74,10 +74,8 @@ func main() {
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
auth := basicauth.New(basicauth.Config{
|
auth := basicauth.Default(map[string]string{
|
||||||
Users: map[string]string{
|
|
||||||
"myusername": "mypassword",
|
"myusername": "mypassword",
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
||||||
filesRouter.Delete("/{file:path}", auth, deleteFile)
|
filesRouter.Delete("/{file:path}", auth, deleteFile)
|
||||||
|
|
|
@ -5,8 +5,6 @@ package middleware
|
||||||
import "github.com/kataras/iris/v12/middleware/basicauth"
|
import "github.com/kataras/iris/v12/middleware/basicauth"
|
||||||
|
|
||||||
// BasicAuth middleware sample.
|
// BasicAuth middleware sample.
|
||||||
var BasicAuth = basicauth.New(basicauth.Config{
|
var BasicAuth = basicauth.Default(map[string]string{
|
||||||
Users: map[string]string{
|
|
||||||
"admin": "password",
|
"admin": "password",
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,8 +5,6 @@ package middleware
|
||||||
import "github.com/kataras/iris/v12/middleware/basicauth"
|
import "github.com/kataras/iris/v12/middleware/basicauth"
|
||||||
|
|
||||||
// BasicAuth middleware sample.
|
// BasicAuth middleware sample.
|
||||||
var BasicAuth = basicauth.New(basicauth.Config{
|
var BasicAuth = basicauth.Default(map[string]string{
|
||||||
Users: map[string]string{
|
|
||||||
"admin": "password",
|
"admin": "password",
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
|
|
@ -17,7 +17,7 @@ func main() {
|
||||||
|
|
||||||
newtest := app.Subdomain("newtest")
|
newtest := app.Subdomain("newtest")
|
||||||
newtest.Get("/", newTestIndex)
|
newtest.Get("/", newTestIndex)
|
||||||
newtest.Get("/", newTestAbout)
|
newtest.Get("/about", newTestAbout)
|
||||||
|
|
||||||
redirects := rewrite.Load("redirects.yml")
|
redirects := rewrite.Load("redirects.yml")
|
||||||
app.WrapRouter(redirects)
|
app.WrapRouter(redirects)
|
||||||
|
@ -25,14 +25,13 @@ func main() {
|
||||||
// http://mydomain.com:8080/seo/about -> http://www.mydomain.com:8080/about
|
// http://mydomain.com:8080/seo/about -> http://www.mydomain.com:8080/about
|
||||||
// http://test.mydomain.com:8080 -> http://newtest.mydomain.com:8080
|
// http://test.mydomain.com:8080 -> http://newtest.mydomain.com:8080
|
||||||
// http://test.mydomain.com:8080/seo/about -> http://newtest.mydomain.com:8080/about
|
// http://test.mydomain.com:8080/seo/about -> http://newtest.mydomain.com:8080/about
|
||||||
// http://localhost:8080/seo -> http://localhost:8080
|
// http://mydomain.com:8080/seo -> http://www.mydomain.com:8080
|
||||||
// http://localhost:8080/about
|
// http://mydomain.com:8080/about
|
||||||
// http://localhost:8080/docs/v12/hello -> http://localhost:8080/docs
|
// http://mydomain.com:8080/docs/v12/hello -> http://www.mydomain.com:8080/docs
|
||||||
// http://localhost:8080/docs/v12some -> http://localhost:8080/docs
|
// http://mydomain.com:8080/docs/v12some -> http://www.mydomain.com:8080/docs
|
||||||
// http://localhost:8080/oldsome -> http://localhost:8080
|
// http://mydomain.com:8080/oldsome -> http://www.mydomain.com:8080
|
||||||
// http://localhost:8080/oldindex/random -> http://localhost:8080
|
// http://mydomain.com:8080/oldindex/random -> http://www.mydomain.com:8080
|
||||||
// http://localhost:8080/users.json -> http://localhost:8080/users.json
|
// http://mydomain.com:8080/users.json -> http://www.mydomain.com:8080/users?format=json
|
||||||
// ^ (but with an internal ?format=json, client can't see it)
|
|
||||||
app.Listen(":8080")
|
app.Listen(":8080")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,11 +8,11 @@ import (
|
||||||
func newApp() *iris.Application {
|
func newApp() *iris.Application {
|
||||||
app := iris.New()
|
app := iris.New()
|
||||||
|
|
||||||
authConfig := basicauth.Config{
|
opts := basicauth.Options{
|
||||||
Users: map[string]string{"myusername": "mypassword"},
|
Allow: basicauth.AllowUsers(map[string]string{"myusername": "mypassword"}),
|
||||||
}
|
}
|
||||||
|
|
||||||
authentication := basicauth.New(authConfig)
|
authentication := basicauth.New(opts) // or just: basicauth.Default(map...)
|
||||||
|
|
||||||
app.Get("/", func(ctx iris.Context) { ctx.Redirect("/admin") })
|
app.Get("/", func(ctx iris.Context) { ctx.Redirect("/admin") })
|
||||||
|
|
||||||
|
@ -36,7 +36,9 @@ func h(ctx iris.Context) {
|
||||||
username, password, _ := ctx.Request().BasicAuth()
|
username, password, _ := ctx.Request().BasicAuth()
|
||||||
// third parameter it will be always true because the middleware
|
// third parameter it will be always true because the middleware
|
||||||
// makes sure for that, otherwise this handler will not be executed.
|
// makes sure for that, otherwise this handler will not be executed.
|
||||||
|
// OR:
|
||||||
|
// ctx.User().GetUsername()
|
||||||
|
// ctx.User().GetPassword()
|
||||||
ctx.Writef("%s %s:%s", ctx.Path(), username, password)
|
ctx.Writef("%s %s:%s", ctx.Path(), username, password)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1343,6 +1343,7 @@ func (ctx *Context) GetContentType() string {
|
||||||
// trim-ed(without the charset and priority values)
|
// trim-ed(without the charset and priority values)
|
||||||
// header value of "Content-Type".
|
// header value of "Content-Type".
|
||||||
func (ctx *Context) GetContentTypeRequested() string {
|
func (ctx *Context) GetContentTypeRequested() string {
|
||||||
|
// could use mime.ParseMediaType too.
|
||||||
return TrimHeaderValue(ctx.GetHeader(ContentTypeHeaderKey))
|
return TrimHeaderValue(ctx.GetHeader(ContentTypeHeaderKey))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1792,7 +1793,7 @@ func (ctx *Context) PostValues(name string) ([]string, error) {
|
||||||
// See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
|
// See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
|
||||||
func (ctx *Context) PostValueMany(name string) (string, error) {
|
func (ctx *Context) PostValueMany(name string) (string, error) {
|
||||||
values, err := ctx.PostValues(name)
|
values, err := ctx.PostValues(name)
|
||||||
if err != nil {
|
if err != nil || len(values) == 0 {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1805,14 +1806,11 @@ func (ctx *Context) PostValueMany(name string) (string, error) {
|
||||||
// If not found then "def" is returned instead.
|
// If not found then "def" is returned instead.
|
||||||
func (ctx *Context) PostValueDefault(name string, def string) string {
|
func (ctx *Context) PostValueDefault(name string, def string) string {
|
||||||
values, err := ctx.PostValues(name)
|
values, err := ctx.PostValues(name)
|
||||||
if err != nil {
|
if err != nil || len(values) == 0 {
|
||||||
return def // it returns "def" even if it's empty here.
|
return def // it returns "def" even if it's empty here.
|
||||||
}
|
}
|
||||||
if len(values) > 0 {
|
|
||||||
return values[len(values)-1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return def
|
return values[len(values)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
// PostValue returns the last parsed form data from POST, PATCH,
|
// PostValue returns the last parsed form data from POST, PATCH,
|
||||||
|
@ -1835,8 +1833,8 @@ func (ctx *Context) PostValueTrim(name string) string {
|
||||||
// See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
|
// See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
|
||||||
func (ctx *Context) PostValueInt(name string) (int, error) {
|
func (ctx *Context) PostValueInt(name string) (int, error) {
|
||||||
values, err := ctx.PostValues(name)
|
values, err := ctx.PostValues(name)
|
||||||
if err != nil {
|
if err != nil || len(values) == 0 {
|
||||||
return -1, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return strconv.Atoi(values[len(values)-1])
|
return strconv.Atoi(values[len(values)-1])
|
||||||
|
@ -1860,8 +1858,8 @@ func (ctx *Context) PostValueIntDefault(name string, def int) int {
|
||||||
// See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
|
// See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
|
||||||
func (ctx *Context) PostValueInt64(name string) (int64, error) {
|
func (ctx *Context) PostValueInt64(name string) (int64, error) {
|
||||||
values, err := ctx.PostValues(name)
|
values, err := ctx.PostValues(name)
|
||||||
if err != nil {
|
if err != nil || len(values) == 0 {
|
||||||
return -1, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return strconv.ParseInt(values[len(values)-1], 10, 64)
|
return strconv.ParseInt(values[len(values)-1], 10, 64)
|
||||||
|
@ -1885,8 +1883,8 @@ func (ctx *Context) PostValueInt64Default(name string, def int64) int64 {
|
||||||
// See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
|
// See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
|
||||||
func (ctx *Context) PostValueFloat64(name string) (float64, error) {
|
func (ctx *Context) PostValueFloat64(name string) (float64, error) {
|
||||||
values, err := ctx.PostValues(name)
|
values, err := ctx.PostValues(name)
|
||||||
if err != nil {
|
if err != nil || len(values) == 0 {
|
||||||
return -1, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return strconv.ParseFloat(values[len(values)-1], 64)
|
return strconv.ParseFloat(values[len(values)-1], 64)
|
||||||
|
@ -1911,7 +1909,7 @@ func (ctx *Context) PostValueFloat64Default(name string, def float64) float64 {
|
||||||
// See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
|
// See ErrEmptyForm, ErrNotFound and ErrEmptyFormField respectfully.
|
||||||
func (ctx *Context) PostValueBool(name string) (bool, error) {
|
func (ctx *Context) PostValueBool(name string) (bool, error) {
|
||||||
values, err := ctx.PostValues(name)
|
values, err := ctx.PostValues(name)
|
||||||
if err != nil {
|
if err != nil || len(values) == 0 {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4726,7 +4724,7 @@ func (ctx *Context) UpsertCookie(cookie *http.Cookie, options ...CookieOption) b
|
||||||
// you can change it or simple, use the SetCookie for more control.
|
// you can change it or simple, use the SetCookie for more control.
|
||||||
//
|
//
|
||||||
// See `CookieExpires` and `AddCookieOptions` for more.
|
// See `CookieExpires` and `AddCookieOptions` for more.
|
||||||
var SetCookieKVExpiration = time.Duration(8760) * time.Hour
|
var SetCookieKVExpiration = 8760 * time.Hour
|
||||||
|
|
||||||
// SetCookieKV adds a cookie, requires the name(string) and the value(string).
|
// SetCookieKV adds a cookie, requires the name(string) and the value(string).
|
||||||
//
|
//
|
||||||
|
@ -5343,7 +5341,15 @@ const userContextKey = "iris.user"
|
||||||
// SetUser sets a value as a User for this request.
|
// SetUser sets a value as a User for this request.
|
||||||
// It's used by auth middlewares as a common
|
// It's used by auth middlewares as a common
|
||||||
// method to provide user information to the
|
// method to provide user information to the
|
||||||
// next handlers in the chain
|
// next handlers in the chain.
|
||||||
|
//
|
||||||
|
// The "i" input argument can be:
|
||||||
|
// - A value which completes the User interface
|
||||||
|
// - A map[string]interface{}.
|
||||||
|
// - A value which does not complete the whole User interface
|
||||||
|
// - A value which does not complete the User interface at all
|
||||||
|
// (only its `User().GetRaw` method is available).
|
||||||
|
//
|
||||||
// Look the `User` method to retrieve it.
|
// Look the `User` method to retrieve it.
|
||||||
func (ctx *Context) SetUser(i interface{}) error {
|
func (ctx *Context) SetUser(i interface{}) error {
|
||||||
if i == nil {
|
if i == nil {
|
||||||
|
@ -5371,6 +5377,9 @@ func (ctx *Context) SetUser(i interface{}) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// User returns the registered User of this request.
|
// User returns the registered User of this request.
|
||||||
|
// To get the original value (even if a value set by SetUser does not implement the User interface)
|
||||||
|
// use its GetRaw method.
|
||||||
|
// /
|
||||||
// See `SetUser` too.
|
// See `SetUser` too.
|
||||||
func (ctx *Context) User() User {
|
func (ctx *Context) User() User {
|
||||||
if v := ctx.values.Get(userContextKey); v != nil {
|
if v := ctx.values.Get(userContextKey); v != nil {
|
||||||
|
|
|
@ -33,6 +33,8 @@ var ErrNotSupported = errors.New("not supported")
|
||||||
// - UserMap (a wrapper by SetUser)
|
// - UserMap (a wrapper by SetUser)
|
||||||
// - UserPartial (a wrapper by SetUser)
|
// - UserPartial (a wrapper by SetUser)
|
||||||
type User interface {
|
type User interface {
|
||||||
|
// GetRaw should return the raw instance of the user, if supported.
|
||||||
|
GetRaw() (interface{}, error)
|
||||||
// GetAuthorization should return the authorization method,
|
// GetAuthorization should return the authorization method,
|
||||||
// e.g. Basic Authentication.
|
// e.g. Basic Authentication.
|
||||||
GetAuthorization() (string, error)
|
GetAuthorization() (string, error)
|
||||||
|
@ -83,7 +85,7 @@ type SimpleUser struct {
|
||||||
AuthorizedAt time.Time `json:"authorized_at,omitempty"`
|
AuthorizedAt time.Time `json:"authorized_at,omitempty"`
|
||||||
ID string `json:"id,omitempty"`
|
ID string `json:"id,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
Password string `json:"-"`
|
Password string `json:"password,omitempty"`
|
||||||
Email string `json:"email,omitempty"`
|
Email string `json:"email,omitempty"`
|
||||||
Roles []string `json:"roles,omitempty"`
|
Roles []string `json:"roles,omitempty"`
|
||||||
Token json.RawMessage `json:"token,omitempty"`
|
Token json.RawMessage `json:"token,omitempty"`
|
||||||
|
@ -92,6 +94,11 @@ type SimpleUser struct {
|
||||||
|
|
||||||
var _ User = (*SimpleUser)(nil)
|
var _ User = (*SimpleUser)(nil)
|
||||||
|
|
||||||
|
// GetRaw returns itself.
|
||||||
|
func (u *SimpleUser) GetRaw() (interface{}, error) {
|
||||||
|
return u, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetAuthorization returns the authorization method,
|
// GetAuthorization returns the authorization method,
|
||||||
// e.g. Basic Authentication.
|
// e.g. Basic Authentication.
|
||||||
func (u *SimpleUser) GetAuthorization() (string, error) {
|
func (u *SimpleUser) GetAuthorization() (string, error) {
|
||||||
|
@ -179,6 +186,11 @@ type UserMap Map
|
||||||
|
|
||||||
var _ User = UserMap{}
|
var _ User = UserMap{}
|
||||||
|
|
||||||
|
// GetRaw returns the underline map.
|
||||||
|
func (u UserMap) GetRaw() (interface{}, error) {
|
||||||
|
return Map(u), nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetAuthorization returns the authorization or Authorization value of the map.
|
// GetAuthorization returns the authorization or Authorization value of the map.
|
||||||
func (u UserMap) GetAuthorization() (string, error) {
|
func (u UserMap) GetAuthorization() (string, error) {
|
||||||
return u.str("authorization")
|
return u.str("authorization")
|
||||||
|
@ -292,11 +304,17 @@ type (
|
||||||
GetID() string
|
GetID() string
|
||||||
}
|
}
|
||||||
|
|
||||||
userGetUsername interface {
|
// UserGetUsername interface which
|
||||||
|
// requires a single method to complete
|
||||||
|
// a User on Context.SetUser.
|
||||||
|
UserGetUsername interface {
|
||||||
GetUsername() string
|
GetUsername() string
|
||||||
}
|
}
|
||||||
|
|
||||||
userGetPassword interface {
|
// UserGetPassword interface which
|
||||||
|
// requires a single method to complete
|
||||||
|
// a User on Context.SetUser.
|
||||||
|
UserGetPassword interface {
|
||||||
GetPassword() string
|
GetPassword() string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -319,78 +337,82 @@ type (
|
||||||
// UserPartial is a User.
|
// UserPartial is a User.
|
||||||
// It's a helper which wraps a struct value that
|
// It's a helper which wraps a struct value that
|
||||||
// may or may not complete the whole User interface.
|
// may or may not complete the whole User interface.
|
||||||
|
// See Context.SetUser.
|
||||||
UserPartial struct {
|
UserPartial struct {
|
||||||
Raw interface{}
|
Raw interface{} `json:"raw"`
|
||||||
userGetAuthorization
|
userGetAuthorization `json:",omitempty"`
|
||||||
userGetAuthorizedAt
|
userGetAuthorizedAt `json:",omitempty"`
|
||||||
userGetID
|
userGetID `json:",omitempty"`
|
||||||
userGetUsername
|
UserGetUsername `json:",omitempty"`
|
||||||
userGetPassword
|
UserGetPassword `json:",omitempty"`
|
||||||
userGetEmail
|
userGetEmail `json:",omitempty"`
|
||||||
userGetRoles
|
userGetRoles `json:",omitempty"`
|
||||||
userGetToken
|
userGetToken `json:",omitempty"`
|
||||||
userGetField
|
userGetField `json:",omitempty"`
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ User = (*UserPartial)(nil)
|
var _ User = (*UserPartial)(nil)
|
||||||
|
|
||||||
func newUserPartial(i interface{}) *UserPartial {
|
func newUserPartial(i interface{}) *UserPartial {
|
||||||
containsAtLeastOneMethod := false
|
if i == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
p := &UserPartial{Raw: i}
|
p := &UserPartial{Raw: i}
|
||||||
|
|
||||||
if u, ok := i.(userGetAuthorization); ok {
|
if u, ok := i.(userGetAuthorization); ok {
|
||||||
p.userGetAuthorization = u
|
p.userGetAuthorization = u
|
||||||
containsAtLeastOneMethod = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if u, ok := i.(userGetAuthorizedAt); ok {
|
if u, ok := i.(userGetAuthorizedAt); ok {
|
||||||
p.userGetAuthorizedAt = u
|
p.userGetAuthorizedAt = u
|
||||||
containsAtLeastOneMethod = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if u, ok := i.(userGetID); ok {
|
if u, ok := i.(userGetID); ok {
|
||||||
p.userGetID = u
|
p.userGetID = u
|
||||||
containsAtLeastOneMethod = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if u, ok := i.(userGetUsername); ok {
|
if u, ok := i.(UserGetUsername); ok {
|
||||||
p.userGetUsername = u
|
p.UserGetUsername = u
|
||||||
containsAtLeastOneMethod = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if u, ok := i.(userGetPassword); ok {
|
if u, ok := i.(UserGetPassword); ok {
|
||||||
p.userGetPassword = u
|
p.UserGetPassword = u
|
||||||
containsAtLeastOneMethod = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if u, ok := i.(userGetEmail); ok {
|
if u, ok := i.(userGetEmail); ok {
|
||||||
p.userGetEmail = u
|
p.userGetEmail = u
|
||||||
containsAtLeastOneMethod = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if u, ok := i.(userGetRoles); ok {
|
if u, ok := i.(userGetRoles); ok {
|
||||||
p.userGetRoles = u
|
p.userGetRoles = u
|
||||||
containsAtLeastOneMethod = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if u, ok := i.(userGetToken); ok {
|
if u, ok := i.(userGetToken); ok {
|
||||||
p.userGetToken = u
|
p.userGetToken = u
|
||||||
containsAtLeastOneMethod = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if u, ok := i.(userGetField); ok {
|
if u, ok := i.(userGetField); ok {
|
||||||
p.userGetField = u
|
p.userGetField = u
|
||||||
containsAtLeastOneMethod = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !containsAtLeastOneMethod {
|
// if !containsAtLeastOneMethod {
|
||||||
return nil
|
// return nil
|
||||||
}
|
// }
|
||||||
|
|
||||||
return p
|
return p
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetRaw returns the original raw instance of the user.
|
||||||
|
func (u *UserPartial) GetRaw() (interface{}, error) {
|
||||||
|
if u == nil {
|
||||||
|
return nil, ErrNotSupported
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.Raw, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetAuthorization should return the authorization method,
|
// GetAuthorization should return the authorization method,
|
||||||
// e.g. Basic Authentication.
|
// e.g. Basic Authentication.
|
||||||
func (u *UserPartial) GetAuthorization() (string, error) {
|
func (u *UserPartial) GetAuthorization() (string, error) {
|
||||||
|
@ -422,7 +444,7 @@ func (u *UserPartial) GetID() (string, error) {
|
||||||
|
|
||||||
// GetUsername should return the name of the User.
|
// GetUsername should return the name of the User.
|
||||||
func (u *UserPartial) GetUsername() (string, error) {
|
func (u *UserPartial) GetUsername() (string, error) {
|
||||||
if v := u.userGetUsername; v != nil {
|
if v := u.UserGetUsername; v != nil {
|
||||||
return v.GetUsername(), nil
|
return v.GetUsername(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -432,7 +454,7 @@ func (u *UserPartial) GetUsername() (string, error) {
|
||||||
// GetPassword should return the encoded or raw password
|
// GetPassword should return the encoded or raw password
|
||||||
// (depends on the implementation) of the User.
|
// (depends on the implementation) of the User.
|
||||||
func (u *UserPartial) GetPassword() (string, error) {
|
func (u *UserPartial) GetPassword() (string, error) {
|
||||||
if v := u.userGetPassword; v != nil {
|
if v := u.UserGetPassword; v != nil {
|
||||||
return v.GetPassword(), nil
|
return v.GetPassword(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
10
go.mod
10
go.mod
|
@ -12,7 +12,7 @@ require (
|
||||||
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385
|
github.com/eknkc/amber v0.0.0-20171010120322-cdade1c07385
|
||||||
github.com/fatih/structs v1.1.0
|
github.com/fatih/structs v1.1.0
|
||||||
github.com/flosch/pongo2/v4 v4.0.0
|
github.com/flosch/pongo2/v4 v4.0.0
|
||||||
github.com/go-redis/redis/v8 v8.3.3
|
github.com/go-redis/redis/v8 v8.4.0
|
||||||
github.com/google/uuid v1.1.2
|
github.com/google/uuid v1.1.2
|
||||||
github.com/hashicorp/go-version v1.2.1
|
github.com/hashicorp/go-version v1.2.1
|
||||||
github.com/iris-contrib/httpexpect/v2 v2.0.5
|
github.com/iris-contrib/httpexpect/v2 v2.0.5
|
||||||
|
@ -32,12 +32,12 @@ require (
|
||||||
github.com/russross/blackfriday/v2 v2.1.0
|
github.com/russross/blackfriday/v2 v2.1.0
|
||||||
github.com/schollz/closestmatch v2.1.0+incompatible
|
github.com/schollz/closestmatch v2.1.0+incompatible
|
||||||
github.com/tdewolff/minify/v2 v2.9.10
|
github.com/tdewolff/minify/v2 v2.9.10
|
||||||
github.com/vmihailenco/msgpack/v5 v5.0.0-rc.3
|
github.com/vmihailenco/msgpack/v5 v5.0.0
|
||||||
github.com/yosssi/ace v0.0.5
|
github.com/yosssi/ace v0.0.5
|
||||||
go.etcd.io/bbolt v1.3.5
|
go.etcd.io/bbolt v1.3.5
|
||||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897
|
golang.org/x/crypto v0.0.0-20201124201722-c8d3bf9c5392
|
||||||
golang.org/x/net v0.0.0-20201027133719-8eef5233e2a1
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b
|
||||||
golang.org/x/sys v0.0.0-20201028094953-708e7fb298ac
|
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68
|
||||||
golang.org/x/text v0.3.4
|
golang.org/x/text v0.3.4
|
||||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
|
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e
|
||||||
google.golang.org/protobuf v1.25.0
|
google.golang.org/protobuf v1.25.0
|
||||||
|
|
|
@ -1,205 +1,547 @@
|
||||||
// Package basicauth provides http basic authentication via middleware. See _examples/auth/basicauth
|
|
||||||
package basicauth
|
package basicauth
|
||||||
|
|
||||||
/*
|
|
||||||
Test files:
|
|
||||||
- ../../_examples/auth/basicauth/main_test.go
|
|
||||||
- ./basicauth_test.go
|
|
||||||
*/
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
stdContext "context"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/context"
|
"github.com/kataras/iris/v12/context"
|
||||||
|
"github.com/kataras/iris/v12/sessions"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
context.SetHandlerName("iris/middleware/basicauth.*", "iris.basicauth")
|
context.SetHandlerName("iris/middleware/basicauth.*", "iris.basicauth")
|
||||||
}
|
}
|
||||||
|
|
||||||
const authorizationType = "Basic Authentication"
|
const (
|
||||||
|
// DefaultRealm is the default realm directive value on Default and Load functions.
|
||||||
type (
|
DefaultRealm = "Authorization Required"
|
||||||
encodedUser struct {
|
// DefaultMaxTriesCookie is the default cookie name to store the
|
||||||
HeaderValue string
|
// current amount of login failures when MaxTries > 0.
|
||||||
Username string
|
DefaultMaxTriesCookie = "basicmaxtries"
|
||||||
Password string
|
// DefaultCookieMaxAge is the default cookie max age on MaxTries,
|
||||||
logged bool
|
// when the Options.MaxAge is zero.
|
||||||
forceLogout bool // in order to be able to invalidate and use a redirect response.
|
DefaultCookieMaxAge = time.Hour
|
||||||
authorizedAt time.Time // when from !logged to logged.
|
|
||||||
expires time.Time
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
basicAuthMiddleware struct {
|
|
||||||
config *Config
|
|
||||||
// these are filled from the config.Users map at the startup
|
|
||||||
auth []*encodedUser
|
|
||||||
realmHeaderValue string
|
|
||||||
|
|
||||||
// The below can be removed but they are here because on the future we may add dynamic options for those two fields,
|
|
||||||
// it is a bit faster to check the b.$bool as well.
|
|
||||||
expireEnabled bool // if the config.Expires is a valid date, default is disabled.
|
|
||||||
askHandlerEnabled bool // if the config.OnAsk is not nil, defaults to false.
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
//
|
// cookieExpireDelete may be set on Cookie.Expire for expiring the given cookie.
|
||||||
|
// Note that the MaxAge is set but we set Expires field in order to support very old browsers too.
|
||||||
|
var cookieExpireDelete = time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
|
||||||
|
|
||||||
// New accepts basicauth.Config and returns a new Handler
|
const (
|
||||||
// which will ask the client for basic auth (username, password),
|
authorizationType = "Basic Authentication"
|
||||||
// validate that and if valid continues to the next handler, otherwise
|
authenticateHeaderKey = "WWW-Authenticate"
|
||||||
// throws a StatusUnauthorized http error code.
|
proxyAuthenticateHeaderKey = "Proxy-Authenticate"
|
||||||
|
authorizationHeaderKey = "Authorization"
|
||||||
|
proxyAuthorizationHeaderKey = "Proxy-Authorization"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthFunc accepts the current request and the username and password user inputs
|
||||||
|
// and it should optionally return a user value and report whether the login succeed or not.
|
||||||
|
// Look the Options.Allow field.
|
||||||
//
|
//
|
||||||
// Use the `Context.User` method to retrieve the stored user.
|
// Default implementations are:
|
||||||
func New(c Config) context.Handler {
|
// AllowUsers and AllowUsersFile functions.
|
||||||
config := DefaultConfig()
|
type AuthFunc func(ctx *context.Context, username, password string) (interface{}, bool)
|
||||||
if c.Realm != "" {
|
|
||||||
config.Realm = c.Realm
|
// ErrorHandler should handle the given request credentials failure.
|
||||||
|
// See Options.ErrorHandler and DefaultErrorHandler for details.
|
||||||
|
type ErrorHandler func(ctx *context.Context, err error)
|
||||||
|
|
||||||
|
// Options holds the necessary information that the BasicAuth instance needs to perform.
|
||||||
|
// The only required value is the Allow field.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// opts := Options { ... }
|
||||||
|
// auth := New(opts)
|
||||||
|
type Options struct {
|
||||||
|
// Realm directive, read http://tools.ietf.org/html/rfc2617#section-1.2 for details.
|
||||||
|
// E.g. "Authorization Required".
|
||||||
|
Realm string
|
||||||
|
// In the case of proxies, the challenging status code is 407 (Proxy Authentication Required),
|
||||||
|
// the Proxy-Authenticate response header contains at least one challenge applicable to the proxy,
|
||||||
|
// and the Proxy-Authorization request header is used for providing the credentials to the proxy server.
|
||||||
|
//
|
||||||
|
// Proxy should be used to gain access to a resource behind a proxy server.
|
||||||
|
// It authenticates the request to the proxy server, allowing it to transmit the request further.
|
||||||
|
Proxy bool
|
||||||
|
// If set to true then any non-https request will immediately
|
||||||
|
// dropped with a 505 status code (StatusHTTPVersionNotSupported) response.
|
||||||
|
//
|
||||||
|
// Defaults to false.
|
||||||
|
HTTPSOnly bool
|
||||||
|
// Allow is the only one required field for the Options type.
|
||||||
|
// Can be customized to validate a username and password combination
|
||||||
|
// and return a user object, e.g. fetch from database.
|
||||||
|
//
|
||||||
|
// There are two available builtin values, the AllowUsers and AllowUsersFile,
|
||||||
|
// both of them decode a static list of users and compares with the user input (see BCRYPT function too).
|
||||||
|
// Usage:
|
||||||
|
// - Allow: AllowUsers(iris.Map{"username": "...", "password": "...", "other_field": ...}, [BCRYPT])
|
||||||
|
// - Allow: AllowUsersFile("users.yml", [BCRYPT])
|
||||||
|
// Look the user.go source file for details.
|
||||||
|
Allow AuthFunc
|
||||||
|
// MaxAge sets expiration duration for the in-memory credentials map.
|
||||||
|
// By default an old map entry will be removed when the user visits a page.
|
||||||
|
// In order to remove old entries automatically please take a look at the `GC` option too.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// MaxAge: 30 * time.Minute
|
||||||
|
MaxAge time.Duration
|
||||||
|
// If greater than zero then the server will send 403 forbidden status code afer
|
||||||
|
// MaxTries amount of sign in failures (see MaxTriesCookie).
|
||||||
|
// Note that the client can modify the cookie and its value,
|
||||||
|
// do NOT depend for any type of custom domain logic based on this field.
|
||||||
|
// By default the server will re-ask for credentials on invalid credentials, each time.
|
||||||
|
MaxTries int
|
||||||
|
// MaxTriesCookie is the cookie name the middleware uses to
|
||||||
|
// store the failures amount on the client side.
|
||||||
|
// The lifetime of the cookie is the same as the configured MaxAge or one hour,
|
||||||
|
// therefore a forbidden client can request for authentication again after expiration.
|
||||||
|
//
|
||||||
|
// You can always set custom logic on the Allow field as you have access to the current request instance.
|
||||||
|
//
|
||||||
|
// Defaults to "basicmaxtries".
|
||||||
|
// The MaxTries should be set to greater than zero.
|
||||||
|
MaxTriesCookie string
|
||||||
|
// If not empty then this session key will be used to store
|
||||||
|
// the current tries of login failures. If not a session manager
|
||||||
|
// was registered then the application will log an error.
|
||||||
|
// Note that this field has a priority over the MaxTriesCookie.
|
||||||
|
MaxTriesSession string
|
||||||
|
// ErrorHandler handles the given request credentials failure.
|
||||||
|
// E.g when the client tried to access a protected resource
|
||||||
|
// with empty or invalid or expired credentials or
|
||||||
|
// when Allow returned false and MaxTries consumed.
|
||||||
|
//
|
||||||
|
// Defaults to the DefaultErrorHandler, do not modify if you don't need to.
|
||||||
|
ErrorHandler ErrorHandler
|
||||||
|
// GC automatically clears old entries every x duration.
|
||||||
|
// Note that, by old entries we mean expired credentials therefore
|
||||||
|
// the `MaxAge` option should be already set,
|
||||||
|
// if it's not then all entries will be removed on "every" duration.
|
||||||
|
// The standard context can be used for the internal ticker cancelation, it can be nil.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// GC: basicauth.GC{Every: 2 * time.Hour}
|
||||||
|
GC GC
|
||||||
|
}
|
||||||
|
|
||||||
|
// GC holds the context and the tick duration to clear expired stored credentials.
|
||||||
|
// See the Options.GC field.
|
||||||
|
type GC struct {
|
||||||
|
Context stdContext.Context
|
||||||
|
Every time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// BasicAuth implements the basic access authentication.
|
||||||
|
// It is a method for an HTTP client (e.g. a web browser)
|
||||||
|
// to provide a user name and password when making a request.
|
||||||
|
// Basic authentication implementation is the simplest technique
|
||||||
|
// for enforcing access controls to web resources because it does not require
|
||||||
|
// cookies, session identifiers, or login pages; rather,
|
||||||
|
// HTTP Basic authentication uses standard fields in the HTTP header.
|
||||||
|
//
|
||||||
|
// As the username and password are passed over the network as clear text
|
||||||
|
// the basic authentication scheme is not secure on plain HTTP communication.
|
||||||
|
// It is base64 encoded, but base64 is a reversible encoding.
|
||||||
|
// HTTPS/TLS should be used with basic authentication.
|
||||||
|
// Without these additional security enhancements,
|
||||||
|
// basic authentication should NOT be used to protect sensitive or valuable information.
|
||||||
|
//
|
||||||
|
// Read https://tools.ietf.org/html/rfc2617 and
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication for details.
|
||||||
|
type BasicAuth struct {
|
||||||
|
opts Options
|
||||||
|
// built based on proxy field
|
||||||
|
askCode int
|
||||||
|
authorizationHeader string
|
||||||
|
authenticateHeader string
|
||||||
|
// built based on realm field.
|
||||||
|
authenticateHeaderValue string
|
||||||
|
|
||||||
|
// credentials stores the user expiration,
|
||||||
|
// key = username:password, value = expiration time (if MaxAge > 0).
|
||||||
|
credentials map[string]*time.Time // TODO: think of just a uint64 here (unix seconds).
|
||||||
|
// protects the credentials concurrent access.
|
||||||
|
mu sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new basic authentication middleware.
|
||||||
|
// The result should be used to wrap an existing handler or the HTTP application's root router.
|
||||||
|
//
|
||||||
|
// Example Code:
|
||||||
|
// opts := basicauth.Options{
|
||||||
|
// Realm: basicauth.DefaultRealm,
|
||||||
|
// ErrorHandler: basicauth.DefaultErrorHandler,
|
||||||
|
// MaxAge: 2 * time.Hour,
|
||||||
|
// GC: basicauth.GC{
|
||||||
|
// Every: 3 * time.Hour,
|
||||||
|
// },
|
||||||
|
// Allow: basicauth.AllowUsers(users),
|
||||||
|
// }
|
||||||
|
// auth := basicauth.New(opts)
|
||||||
|
// app.Use(auth)
|
||||||
|
//
|
||||||
|
// Access the user in the route handler with: ctx.User().GetRaw().(*myCustomType).
|
||||||
|
//
|
||||||
|
// Look the BasicAuth type docs for more information.
|
||||||
|
func New(opts Options) context.Handler {
|
||||||
|
var (
|
||||||
|
askCode = http.StatusUnauthorized
|
||||||
|
authorizationHeader = authorizationHeaderKey
|
||||||
|
authenticateHeader = authenticateHeaderKey
|
||||||
|
authenticateHeaderValue = "Basic"
|
||||||
|
)
|
||||||
|
|
||||||
|
if opts.Allow == nil {
|
||||||
|
panic("BasicAuth: Allow field is required")
|
||||||
}
|
}
|
||||||
config.Users = c.Users
|
|
||||||
config.Expires = c.Expires
|
|
||||||
config.OnAsk = c.OnAsk
|
|
||||||
|
|
||||||
b := &basicAuthMiddleware{config: &config}
|
if opts.Realm != "" {
|
||||||
b.init()
|
authenticateHeaderValue += " realm=" + strconv.Quote(opts.Realm)
|
||||||
return b.Serve
|
}
|
||||||
|
|
||||||
|
if opts.Proxy {
|
||||||
|
askCode = http.StatusProxyAuthRequired
|
||||||
|
authenticateHeader = proxyAuthenticateHeaderKey
|
||||||
|
authorizationHeader = proxyAuthorizationHeaderKey
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.MaxTries > 0 && opts.MaxTriesCookie == "" {
|
||||||
|
opts.MaxTriesCookie = DefaultMaxTriesCookie
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.ErrorHandler == nil {
|
||||||
|
opts.ErrorHandler = DefaultErrorHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
b := &BasicAuth{
|
||||||
|
opts: opts,
|
||||||
|
askCode: askCode,
|
||||||
|
authorizationHeader: authorizationHeader,
|
||||||
|
authenticateHeader: authenticateHeader,
|
||||||
|
authenticateHeaderValue: authenticateHeaderValue,
|
||||||
|
credentials: make(map[string]*time.Time),
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.GC.Every > 0 {
|
||||||
|
go b.runGC(opts.GC.Context, opts.GC.Every)
|
||||||
|
}
|
||||||
|
|
||||||
|
return b.serveHTTP
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default accepts only the users and returns a new Handler
|
// Default returns a new basic authentication middleware
|
||||||
// which will ask the client for basic auth (username, password),
|
// based on pre-defined user list.
|
||||||
// validate that and if valid continues to the next handler, otherwise
|
// A user can hold any custom fields but the username and password
|
||||||
// throws a StatusUnauthorized http error code.
|
// are required as they are compared against the user input
|
||||||
func Default(users map[string]string) context.Handler {
|
// when access to protected resource is requested.
|
||||||
c := DefaultConfig()
|
// A user list can defined with one of the following values:
|
||||||
c.Users = users
|
// map[string]string form of: {username:password, ...}
|
||||||
return New(c)
|
// map[string]interface{} form of: {"username": {"password": "...", "other_field": ...}, ...}
|
||||||
|
// []T which T completes the User interface, where T is a struct value
|
||||||
|
// []T which T contains at least Username and Password fields.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// auth := Default(map[string]string{
|
||||||
|
// "admin": "admin",
|
||||||
|
// "john": "p@ss",
|
||||||
|
// })
|
||||||
|
func Default(users interface{}, userOpts ...UserAuthOption) context.Handler {
|
||||||
|
opts := Options{
|
||||||
|
Realm: DefaultRealm,
|
||||||
|
Allow: AllowUsers(users, userOpts...),
|
||||||
|
}
|
||||||
|
return New(opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *basicAuthMiddleware) init() {
|
// Load same as Default but instead of a hard-coded user list it accepts
|
||||||
// pass the encoded users from the user's config's Users value
|
// a filename to load the users from.
|
||||||
b.auth = make([]*encodedUser, 0, len(b.config.Users))
|
//
|
||||||
|
// Usage:
|
||||||
|
// auth := Load("users.yml")
|
||||||
|
func Load(jsonOrYamlFilename string, userOpts ...UserAuthOption) context.Handler {
|
||||||
|
opts := Options{
|
||||||
|
Realm: DefaultRealm,
|
||||||
|
Allow: AllowUsersFile(jsonOrYamlFilename, userOpts...),
|
||||||
|
}
|
||||||
|
return New(opts)
|
||||||
|
}
|
||||||
|
|
||||||
for k, v := range b.config.Users {
|
func (b *BasicAuth) getCurrentTries(ctx *context.Context) (tries int) {
|
||||||
fullUser := k + ":" + v
|
if key := b.opts.MaxTriesSession; key != "" {
|
||||||
header := "Basic " + base64.StdEncoding.EncodeToString([]byte(fullUser))
|
if sess := sessions.Get(ctx); sess != nil {
|
||||||
b.auth = append(b.auth, &encodedUser{
|
tries = sess.GetIntDefault(key, 0)
|
||||||
HeaderValue: header,
|
} else {
|
||||||
Username: k,
|
ctx.Application().Logger().Error("basicauth: getCurrentTries: session key: %s but no session manager is registered", key)
|
||||||
Password: v,
|
return
|
||||||
logged: false,
|
}
|
||||||
expires: DefaultExpireTime,
|
} else {
|
||||||
|
cookie := ctx.GetCookie(b.opts.MaxTriesCookie)
|
||||||
|
if cookie != "" {
|
||||||
|
tries, _ = strconv.Atoi(cookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasicAuth) setCurrentTries(ctx *context.Context, tries int) {
|
||||||
|
if key := b.opts.MaxTriesSession; key != "" {
|
||||||
|
if sess := sessions.Get(ctx); sess != nil {
|
||||||
|
sess.Set(key, tries)
|
||||||
|
} else {
|
||||||
|
ctx.Application().Logger().Error("basicauth: setCurrentTries: session key: %s but no session manager is registered", key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
maxAge := b.opts.MaxAge
|
||||||
|
if maxAge == 0 {
|
||||||
|
maxAge = DefaultCookieMaxAge // 1 hour.
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &http.Cookie{
|
||||||
|
Name: b.opts.MaxTriesCookie,
|
||||||
|
Path: "/",
|
||||||
|
Value: url.QueryEscape(strconv.Itoa(tries)),
|
||||||
|
HttpOnly: true,
|
||||||
|
Expires: time.Now().Add(maxAge),
|
||||||
|
MaxAge: int(maxAge.Seconds()),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.SetCookie(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasicAuth) resetCurrentTries(ctx *context.Context) {
|
||||||
|
if key := b.opts.MaxTriesSession; key != "" {
|
||||||
|
if sess := sessions.Get(ctx); sess != nil {
|
||||||
|
sess.Delete(key)
|
||||||
|
} else {
|
||||||
|
ctx.Application().Logger().Error("basicauth: resetCurrentTries: session key: %s but no session manager is registered", key)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx.RemoveCookie(b.opts.MaxTriesCookie)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func isHTTPS(r *http.Request) bool {
|
||||||
|
return (strings.EqualFold(r.URL.Scheme, "https") || r.TLS != nil) && r.ProtoMajor == 2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *BasicAuth) handleError(ctx *context.Context, err error) {
|
||||||
|
ctx.Application().Logger().Debug(err)
|
||||||
|
|
||||||
|
// should not be nil as it's defaulted on New.
|
||||||
|
b.opts.ErrorHandler(ctx, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// serveHTTP is the main method of this middleware,
|
||||||
|
// checks and verifies the auhorization header for basic authentication,
|
||||||
|
// next handlers will only be executed when the client is allowed to continue.
|
||||||
|
func (b *BasicAuth) serveHTTP(ctx *context.Context) {
|
||||||
|
if b.opts.HTTPSOnly && !isHTTPS(ctx.Request()) {
|
||||||
|
b.handleError(ctx, ErrHTTPVersion{})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
header := ctx.GetHeader(b.authorizationHeader)
|
||||||
|
fullUser, username, password, ok := decodeHeader(header)
|
||||||
|
if !ok { // Header is malformed or missing (e.g. browser cancel button on user prompt).
|
||||||
|
b.handleError(ctx, ErrCredentialsMissing{
|
||||||
|
Header: header,
|
||||||
|
AuthenticateHeader: b.authenticateHeader,
|
||||||
|
AuthenticateHeaderValue: b.authenticateHeaderValue,
|
||||||
|
Code: b.askCode,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
// set the auth realm header's value
|
|
||||||
b.realmHeaderValue = "Basic realm=" + strconv.Quote(b.config.Realm)
|
|
||||||
|
|
||||||
b.expireEnabled = b.config.Expires > 0
|
|
||||||
b.askHandlerEnabled = b.config.OnAsk != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *basicAuthMiddleware) findAuth(headerValue string) (*encodedUser, bool) {
|
|
||||||
if headerValue != "" {
|
|
||||||
for _, user := range b.auth {
|
|
||||||
if user.HeaderValue == headerValue {
|
|
||||||
return user, true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *basicAuthMiddleware) askForCredentials(ctx *context.Context) {
|
|
||||||
ctx.Header("WWW-Authenticate", b.realmHeaderValue)
|
|
||||||
ctx.StatusCode(401)
|
|
||||||
if b.askHandlerEnabled {
|
|
||||||
b.config.OnAsk(ctx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serve the actual basic authentication middleware.
|
|
||||||
// Use the Context.User method to retrieve the stored user.
|
|
||||||
func (b *basicAuthMiddleware) Serve(ctx *context.Context) {
|
|
||||||
auth, found := b.findAuth(ctx.GetHeader("Authorization"))
|
|
||||||
if !found || auth.forceLogout {
|
|
||||||
if auth != nil {
|
|
||||||
auth.mu.Lock()
|
|
||||||
auth.forceLogout = false
|
|
||||||
auth.mu.Unlock()
|
|
||||||
}
|
|
||||||
|
|
||||||
b.askForCredentials(ctx)
|
|
||||||
ctx.StopExecution()
|
|
||||||
return
|
return
|
||||||
// don't continue to the next handler
|
|
||||||
}
|
}
|
||||||
|
|
||||||
auth.mu.RLock()
|
var (
|
||||||
logged := auth.logged
|
maxTries = b.opts.MaxTries
|
||||||
auth.mu.RUnlock()
|
tries int
|
||||||
if !logged {
|
)
|
||||||
auth.mu.Lock()
|
|
||||||
auth.authorizedAt = time.Now()
|
if maxTries > 0 {
|
||||||
auth.mu.Unlock()
|
tries = b.getCurrentTries(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
// all ok
|
user, ok := b.opts.Allow(ctx, username, password)
|
||||||
if b.expireEnabled {
|
if !ok { // This username:password combination was not allowed.
|
||||||
if !logged {
|
if maxTries > 0 {
|
||||||
auth.mu.Lock()
|
tries++
|
||||||
auth.expires = auth.authorizedAt.Add(b.config.Expires)
|
b.setCurrentTries(ctx, tries)
|
||||||
auth.logged = true
|
if tries >= maxTries { // e.g. if MaxTries == 1 then it should be allowed only once, so we must send forbidden now.
|
||||||
auth.mu.Unlock()
|
b.handleError(ctx, ErrCredentialsForbidden{
|
||||||
}
|
Username: username,
|
||||||
|
Password: password,
|
||||||
auth.mu.RLock()
|
Tries: tries,
|
||||||
expired := time.Now().After(auth.expires)
|
Age: b.opts.MaxAge,
|
||||||
auth.mu.RUnlock()
|
})
|
||||||
if expired {
|
|
||||||
auth.mu.Lock()
|
|
||||||
auth.logged = false
|
|
||||||
auth.forceLogout = false
|
|
||||||
auth.mu.Unlock()
|
|
||||||
b.askForCredentials(ctx) // ask for authentication again
|
|
||||||
ctx.StopExecution()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !b.config.DisableContextUser {
|
b.handleError(ctx, ErrCredentialsInvalid{
|
||||||
ctx.SetLogoutFunc(b.Logout)
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
CurrentTries: tries,
|
||||||
|
AuthenticateHeader: b.authenticateHeader,
|
||||||
|
AuthenticateHeaderValue: b.authenticateHeaderValue,
|
||||||
|
Code: b.askCode,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
auth.mu.RLock()
|
if tries > 0 {
|
||||||
user := &context.SimpleUser{
|
// had failures but it's ok, reset the tries on success.
|
||||||
|
b.resetCurrentTries(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.mu.RLock()
|
||||||
|
expiresAt, ok := b.credentials[fullUser]
|
||||||
|
b.mu.RUnlock()
|
||||||
|
var authorizedAt time.Time
|
||||||
|
if ok {
|
||||||
|
if expiresAt != nil { // Has expiration.
|
||||||
|
if expiresAt.Before(time.Now()) { // Has been expired.
|
||||||
|
b.mu.Lock() // Delete the entry.
|
||||||
|
delete(b.credentials, fullUser)
|
||||||
|
b.mu.Unlock()
|
||||||
|
|
||||||
|
// Re-ask for new credentials.
|
||||||
|
b.handleError(ctx, ErrCredentialsExpired{
|
||||||
|
Username: username,
|
||||||
|
Password: password,
|
||||||
|
AuthenticateHeader: b.authenticateHeader,
|
||||||
|
AuthenticateHeaderValue: b.authenticateHeaderValue,
|
||||||
|
Code: b.askCode,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// It's ok, find the time authorized to fill the user below, if necessary.
|
||||||
|
authorizedAt = expiresAt.Add(-b.opts.MaxAge)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Saved credential not found, first login.
|
||||||
|
if b.opts.MaxAge > 0 { // Expiration is enabled, set the value.
|
||||||
|
authorizedAt = time.Now()
|
||||||
|
t := authorizedAt.Add(b.opts.MaxAge)
|
||||||
|
expiresAt = &t
|
||||||
|
}
|
||||||
|
b.mu.Lock()
|
||||||
|
b.credentials[fullUser] = expiresAt
|
||||||
|
b.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
if user == nil {
|
||||||
|
// No custom uset was set by the auth func,
|
||||||
|
// it is passed though, set a simple user here:
|
||||||
|
user = &context.SimpleUser{
|
||||||
Authorization: authorizationType,
|
Authorization: authorizationType,
|
||||||
AuthorizedAt: auth.authorizedAt,
|
AuthorizedAt: authorizedAt,
|
||||||
Username: auth.Username,
|
Username: username,
|
||||||
Password: auth.Password,
|
Password: password,
|
||||||
}
|
}
|
||||||
auth.mu.RUnlock()
|
}
|
||||||
|
|
||||||
|
// Store user instance and logout function.
|
||||||
|
// Note that the end-developer has always have access
|
||||||
|
// to the Request.BasicAuth, however, we support any user struct,
|
||||||
|
// so we must store it on this request instance so it can be retrieved later on.
|
||||||
ctx.SetUser(user)
|
ctx.SetUser(user)
|
||||||
}
|
ctx.SetLogoutFunc(b.logout)
|
||||||
|
|
||||||
ctx.Next() // continue
|
ctx.Next()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logout sends a 401 so the browser/client can invalidate the
|
// logout clears the current user's credentials.
|
||||||
// Basic Authentication and also sets the underline user's logged field to false,
|
func (b *BasicAuth) logout(ctx *context.Context) {
|
||||||
// so its expiration resets when re-ask for credentials.
|
var (
|
||||||
//
|
fullUser, username, password string
|
||||||
// End-developers should call the `Context.Logout()` method
|
ok bool
|
||||||
// to fire this method as this structure is hidden.
|
)
|
||||||
func (b *basicAuthMiddleware) Logout(ctx *context.Context) {
|
|
||||||
ctx.StatusCode(401)
|
if u := ctx.User(); u != nil { // Get the saved ones, if any.
|
||||||
if auth, found := b.findAuth(ctx.GetHeader("Authorization")); found {
|
username, _ = u.GetUsername()
|
||||||
auth.mu.Lock()
|
password, _ = u.GetPassword()
|
||||||
auth.logged = false
|
fullUser = username + colonLiteral + password
|
||||||
auth.forceLogout = true
|
ok = username != "" && password != ""
|
||||||
auth.mu.Unlock()
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
// If the custom user does
|
||||||
|
// not implement the User interface, then extract from the request header (most common scenario):
|
||||||
|
header := ctx.GetHeader(b.authorizationHeader)
|
||||||
|
fullUser, username, password, ok = decodeHeader(header)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok { // If it's authorized then try to lock and delete.
|
||||||
|
ctx.SetUser(nil)
|
||||||
|
ctx.SetLogoutFunc(nil)
|
||||||
|
|
||||||
|
if b.opts.Proxy {
|
||||||
|
ctx.Request().Header.Del(proxyAuthorizationHeaderKey)
|
||||||
|
}
|
||||||
|
// delete the request header so future Request().BasicAuth are empty.
|
||||||
|
ctx.Request().Header.Del(authorizationHeaderKey)
|
||||||
|
|
||||||
|
b.mu.Lock()
|
||||||
|
delete(b.credentials, fullUser)
|
||||||
|
b.mu.Unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// runGC runs a function in a separate go routine
|
||||||
|
// every x duration to clear in-memory expired credential entries.
|
||||||
|
func (b *BasicAuth) runGC(ctx stdContext.Context, every time.Duration) {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = stdContext.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
t := time.NewTicker(every)
|
||||||
|
defer t.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-t.C:
|
||||||
|
b.gc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// gc removes all entries expired based on the max age or all entries (if max age is missing),
|
||||||
|
// note that this does not mean that the server will send 401/407 to the next request,
|
||||||
|
// when the request header credentials are still valid (Allow passed).
|
||||||
|
func (b *BasicAuth) gc() int {
|
||||||
|
now := time.Now()
|
||||||
|
var markedForDeletion []string
|
||||||
|
|
||||||
|
b.mu.RLock()
|
||||||
|
for fullUser, expiresAt := range b.credentials {
|
||||||
|
if expiresAt == nil || expiresAt.Before(now) {
|
||||||
|
markedForDeletion = append(markedForDeletion, fullUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.mu.RUnlock()
|
||||||
|
|
||||||
|
n := len(markedForDeletion)
|
||||||
|
if n > 0 {
|
||||||
|
for _, fullUser := range markedForDeletion {
|
||||||
|
b.mu.Lock()
|
||||||
|
delete(b.credentials, fullUser)
|
||||||
|
b.mu.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
|
@ -18,7 +18,13 @@ func TestBasicAuthUseRouter(t *testing.T) {
|
||||||
"admin": "admin",
|
"admin": "admin",
|
||||||
}
|
}
|
||||||
|
|
||||||
app.UseRouter(basicauth.Default(users))
|
auth := basicauth.New(basicauth.Options{
|
||||||
|
Allow: basicauth.AllowUsers(users),
|
||||||
|
Realm: basicauth.DefaultRealm,
|
||||||
|
MaxTries: 1,
|
||||||
|
})
|
||||||
|
|
||||||
|
app.UseRouter(auth)
|
||||||
|
|
||||||
app.Get("/user_json", func(ctx iris.Context) {
|
app.Get("/user_json", func(ctx iris.Context) {
|
||||||
ctx.JSON(ctx.User())
|
ctx.JSON(ctx.User())
|
||||||
|
@ -80,9 +86,9 @@ func TestBasicAuthUseRouter(t *testing.T) {
|
||||||
e.GET("/").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
e.GET("/").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
// Test invalid auth.
|
// Test invalid auth.
|
||||||
e.GET("/").WithBasicAuth(username, "invalid_password").Expect().
|
e.GET("/").WithBasicAuth(username, "invalid_password").Expect().
|
||||||
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
Status(httptest.StatusForbidden)
|
||||||
e.GET("/").WithBasicAuth("invaid_username", password).Expect().
|
e.GET("/").WithBasicAuth("invaid_username", password).Expect().
|
||||||
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
Status(httptest.StatusForbidden)
|
||||||
|
|
||||||
// Test different method, it should pass the authentication (no stop on 401)
|
// Test different method, it should pass the authentication (no stop on 401)
|
||||||
// but it doesn't fire the GET route, instead it gives 405.
|
// but it doesn't fire the GET route, instead it gives 405.
|
||||||
|
@ -97,9 +103,9 @@ func TestBasicAuthUseRouter(t *testing.T) {
|
||||||
e.GET("/notfound").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
e.GET("/notfound").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
// Test invalid auth.
|
// Test invalid auth.
|
||||||
e.GET("/notfound").WithBasicAuth(username, "invalid_password").Expect().
|
e.GET("/notfound").WithBasicAuth(username, "invalid_password").Expect().
|
||||||
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
Status(httptest.StatusForbidden)
|
||||||
e.GET("/notfound").WithBasicAuth("invaid_username", password).Expect().
|
e.GET("/notfound").WithBasicAuth("invaid_username", password).Expect().
|
||||||
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
Status(httptest.StatusForbidden)
|
||||||
|
|
||||||
// Test subdomain inherited.
|
// Test subdomain inherited.
|
||||||
sub := e.Builder(func(req *httptest.Request) {
|
sub := e.Builder(func(req *httptest.Request) {
|
||||||
|
@ -114,9 +120,9 @@ func TestBasicAuthUseRouter(t *testing.T) {
|
||||||
sub.GET("/").Expect().Status(httptest.StatusUnauthorized)
|
sub.GET("/").Expect().Status(httptest.StatusUnauthorized)
|
||||||
// Test invalid auth.
|
// Test invalid auth.
|
||||||
sub.GET("/").WithBasicAuth(username, "invalid_password").Expect().
|
sub.GET("/").WithBasicAuth(username, "invalid_password").Expect().
|
||||||
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
Status(httptest.StatusForbidden)
|
||||||
sub.GET("/").WithBasicAuth("invaid_username", password).Expect().
|
sub.GET("/").WithBasicAuth("invaid_username", password).Expect().
|
||||||
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
Status(httptest.StatusForbidden)
|
||||||
|
|
||||||
// Test pass the authentication but route not found.
|
// Test pass the authentication but route not found.
|
||||||
sub.GET("/notfound").WithBasicAuth(username, password).Expect().
|
sub.GET("/notfound").WithBasicAuth(username, password).Expect().
|
||||||
|
@ -126,9 +132,9 @@ func TestBasicAuthUseRouter(t *testing.T) {
|
||||||
sub.GET("/notfound").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
sub.GET("/notfound").Expect().Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
||||||
// Test invalid auth.
|
// Test invalid auth.
|
||||||
sub.GET("/notfound").WithBasicAuth(username, "invalid_password").Expect().
|
sub.GET("/notfound").WithBasicAuth(username, "invalid_password").Expect().
|
||||||
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
Status(httptest.StatusForbidden)
|
||||||
sub.GET("/notfound").WithBasicAuth("invaid_username", password).Expect().
|
sub.GET("/notfound").WithBasicAuth("invaid_username", password).Expect().
|
||||||
Status(httptest.StatusUnauthorized).Body().Equal("Unauthorized")
|
Status(httptest.StatusForbidden)
|
||||||
|
|
||||||
// Test a reset-ed Party with a single one UseRouter
|
// Test a reset-ed Party with a single one UseRouter
|
||||||
// which writes on matched routes and reset and send the error on errors.
|
// which writes on matched routes and reset and send the error on errors.
|
||||||
|
|
|
@ -1,59 +0,0 @@
|
||||||
package basicauth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/kataras/iris/v12/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// DefaultBasicAuthRealm is "Authorization Required"
|
|
||||||
DefaultBasicAuthRealm = "Authorization Required"
|
|
||||||
)
|
|
||||||
|
|
||||||
// DefaultExpireTime zero time
|
|
||||||
var DefaultExpireTime time.Time // 0001-01-01 00:00:00 +0000 UTC
|
|
||||||
|
|
||||||
// Config the configs for the basicauth middleware
|
|
||||||
type Config struct {
|
|
||||||
// Users a map of login and the value (username/password)
|
|
||||||
Users map[string]string
|
|
||||||
// Realm http://tools.ietf.org/html/rfc2617#section-1.2. Default is "Authorization Required"
|
|
||||||
Realm string
|
|
||||||
// Expires expiration duration, default is 0 never expires.
|
|
||||||
Expires time.Duration
|
|
||||||
|
|
||||||
// OnAsk fires each time the server asks to the client for credentials in order to gain access and continue to the next handler.
|
|
||||||
//
|
|
||||||
// You could also ignore this option and
|
|
||||||
// - just add a listener for unauthorized status codes with:
|
|
||||||
// `app.OnErrorCode(iris.StatusUnauthorized, unauthorizedWantsAccessHandler)`
|
|
||||||
// - or register a middleware which will force `ctx.Next/or direct call`
|
|
||||||
// the basicauth middleware and check its `ctx.GetStatusCode()`.
|
|
||||||
//
|
|
||||||
// However, this option is very useful when you want the framework to fire a handler
|
|
||||||
// ONLY when the Basic Authentication sends an `iris.StatusUnauthorized`,
|
|
||||||
// and free the error code listener to catch other types of unauthorized access, i.e Kerberos.
|
|
||||||
// Also with this one, not recommended at all but, you are able to "force-allow" other users by calling the `ctx.StatusCode` inside this handler;
|
|
||||||
// i.e when it is possible to create authorized users dynamically but
|
|
||||||
// if that is the case then you should go with something like sessions instead of basic authentication.
|
|
||||||
//
|
|
||||||
// Usage: basicauth.New(basicauth.Config{..., OnAsk: unauthorizedWantsAccessViaBasicAuthHandler})
|
|
||||||
//
|
|
||||||
// Defaults to nil.
|
|
||||||
OnAsk context.Handler
|
|
||||||
|
|
||||||
// DisableContextUser disables the registration of the custom basicauth Context.Logout
|
|
||||||
// and the User.
|
|
||||||
DisableContextUser bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// DefaultConfig returns the default configs for the BasicAuth middleware
|
|
||||||
func DefaultConfig() Config {
|
|
||||||
return Config{make(map[string]string), DefaultBasicAuthRealm, 0, nil, false}
|
|
||||||
}
|
|
||||||
|
|
||||||
// User returns the user from context key same as ctx.Request().BasicAuth().
|
|
||||||
func (c Config) User(ctx *context.Context) (string, string, bool) {
|
|
||||||
return ctx.Request().BasicAuth()
|
|
||||||
}
|
|
108
middleware/basicauth/error.go
Normal file
108
middleware/basicauth/error.go
Normal file
|
@ -0,0 +1,108 @@
|
||||||
|
package basicauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// ErrHTTPVersion is fired when Options.HTTPSOnly was enabled
|
||||||
|
// and the current request is a plain http one.
|
||||||
|
ErrHTTPVersion struct{}
|
||||||
|
|
||||||
|
// ErrCredentialsForbidden is fired when Options.MaxTries have been consumed
|
||||||
|
// by the user and the client is forbidden to retry at least for "Age" time.
|
||||||
|
ErrCredentialsForbidden struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
Tries int
|
||||||
|
Age time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrCredentialsMissing is fired when the authorization header is empty or malformed.
|
||||||
|
ErrCredentialsMissing struct {
|
||||||
|
Header string
|
||||||
|
|
||||||
|
AuthenticateHeader string
|
||||||
|
AuthenticateHeaderValue string
|
||||||
|
Code int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrCredentialsInvalid is fired when the user input does not match with an existing user.
|
||||||
|
ErrCredentialsInvalid struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
CurrentTries int
|
||||||
|
|
||||||
|
AuthenticateHeader string
|
||||||
|
AuthenticateHeaderValue string
|
||||||
|
Code int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ErrCredentialsExpired is fired when the username:password combination is valid
|
||||||
|
// but the memory stored user has been expired.
|
||||||
|
ErrCredentialsExpired struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
|
||||||
|
AuthenticateHeader string
|
||||||
|
AuthenticateHeaderValue string
|
||||||
|
Code int
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func (e ErrHTTPVersion) Error() string {
|
||||||
|
return "http version not supported"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrCredentialsForbidden) Error() string {
|
||||||
|
return fmt.Sprintf("credentials: forbidden <%s:%s> for <%s> after <%d> attempts", e.Username, e.Password, e.Age, e.Tries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrCredentialsMissing) Error() string {
|
||||||
|
if e.Header != "" {
|
||||||
|
return fmt.Sprintf("credentials: malformed <%s>", e.Header)
|
||||||
|
}
|
||||||
|
|
||||||
|
return "empty credentials"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrCredentialsInvalid) Error() string {
|
||||||
|
return fmt.Sprintf("credentials: invalid <%s:%s> current tries <%d>", e.Username, e.Password, e.CurrentTries)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e ErrCredentialsExpired) Error() string {
|
||||||
|
return fmt.Sprintf("credentials: expired <%s:%s>", e.Username, e.Password)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DefaultErrorHandler is the default error handler for the Options.ErrorHandler field.
|
||||||
|
func DefaultErrorHandler(ctx *context.Context, err error) {
|
||||||
|
switch e := err.(type) {
|
||||||
|
case ErrHTTPVersion:
|
||||||
|
ctx.StopWithStatus(http.StatusHTTPVersionNotSupported)
|
||||||
|
case ErrCredentialsForbidden:
|
||||||
|
// If a (proxy) server receives valid credentials that are inadequate to access a given resource,
|
||||||
|
// the server should respond with the 403 Forbidden status code.
|
||||||
|
// Unlike 401 Unauthorized or 407 Proxy Authentication Required, authentication is impossible for this user.
|
||||||
|
ctx.StopWithStatus(http.StatusForbidden)
|
||||||
|
case ErrCredentialsMissing:
|
||||||
|
unauthorize(ctx, e.AuthenticateHeader, e.AuthenticateHeaderValue, e.Code)
|
||||||
|
case ErrCredentialsInvalid:
|
||||||
|
unauthorize(ctx, e.AuthenticateHeader, e.AuthenticateHeaderValue, e.Code)
|
||||||
|
case ErrCredentialsExpired:
|
||||||
|
unauthorize(ctx, e.AuthenticateHeader, e.AuthenticateHeaderValue, e.Code)
|
||||||
|
default:
|
||||||
|
// This will never happen.
|
||||||
|
ctx.StopWithText(http.StatusInternalServerError, "unknown error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// unauthorize sends a 401 status code (or 407 if Proxy was set to true)
|
||||||
|
// which client should catch and prompt for username:password credentials.
|
||||||
|
func unauthorize(ctx *context.Context, authHeader, authHeaderValue string, code int) {
|
||||||
|
ctx.Header(authHeader, authHeaderValue)
|
||||||
|
ctx.StopWithStatus(code)
|
||||||
|
}
|
88
middleware/basicauth/header.go
Normal file
88
middleware/basicauth/header.go
Normal file
|
@ -0,0 +1,88 @@
|
||||||
|
package basicauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
spaceChar = ' '
|
||||||
|
colonChar = ':'
|
||||||
|
colonLiteral = string(colonChar)
|
||||||
|
basicLiteral = "Basic"
|
||||||
|
basicSpaceLiteral = "Basic "
|
||||||
|
basicSpaceLiteralLen = len(basicSpaceLiteral)
|
||||||
|
)
|
||||||
|
|
||||||
|
// The username and password are combined with a single colon (:).
|
||||||
|
// This means that the username itself cannot contain a colon.
|
||||||
|
// URL encoding (e.g. https://Aladdin:OpenSesame@www.example.com/index.html)
|
||||||
|
// has been deprecated by rfc3986.
|
||||||
|
func encodeHeader(username, password string) (string, bool) {
|
||||||
|
if strings.Contains(username, colonLiteral) || strings.Contains(password, colonLiteral) {
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
fullUser := []byte(username + colonLiteral + password)
|
||||||
|
header := basicSpaceLiteral + base64.StdEncoding.EncodeToString(fullUser)
|
||||||
|
|
||||||
|
return header, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Like net/http.parseBasicAuth
|
||||||
|
func decodeHeader(header string) (fullUser, username, password string, ok bool) {
|
||||||
|
if len(header) < basicSpaceLiteralLen || !strings.EqualFold(header[:basicSpaceLiteralLen], basicSpaceLiteral) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c, err := base64.StdEncoding.DecodeString(header[basicSpaceLiteralLen:])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cs := string(c)
|
||||||
|
s := strings.IndexByte(cs, colonChar)
|
||||||
|
if s < 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return cs, cs[:s], cs[s+1:], true
|
||||||
|
|
||||||
|
/*
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
if header[i] == spaceChar {
|
||||||
|
prefix := header[:i]
|
||||||
|
if prefix != basicLiteral {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if n <= i+1 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedFullUser, err := base64.RawStdEncoding.DecodeString(header[i+1:])
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fullUser = string(decodedFullUser)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
n = len(fullUser)
|
||||||
|
for i := n - 1; i > -1; i-- {
|
||||||
|
if fullUser[i] == colonChar {
|
||||||
|
username = fullUser[:i]
|
||||||
|
password = fullUser[i+1:]
|
||||||
|
|
||||||
|
if strings.TrimSpace(username) == "" || strings.TrimSpace(password) == "" {
|
||||||
|
ok = false
|
||||||
|
} else {
|
||||||
|
ok = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return*/
|
||||||
|
}
|
103
middleware/basicauth/header_test.go
Normal file
103
middleware/basicauth/header_test.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package basicauth
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestHeaderEncode(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
header string
|
||||||
|
ok bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
username: "user",
|
||||||
|
password: "pass",
|
||||||
|
header: "Basic dXNlcjpwYXNz",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "user",
|
||||||
|
password: "p:(notallowed)ass",
|
||||||
|
header: "",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "123u%ser",
|
||||||
|
password: "pass132$",
|
||||||
|
header: "Basic MTIzdSVzZXI6cGFzczEzMiQ=",
|
||||||
|
ok: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
got, ok := encodeHeader(tt.username, tt.password)
|
||||||
|
if tt.ok != ok {
|
||||||
|
t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s)", i, tt.ok, ok, tt.username, tt.password)
|
||||||
|
}
|
||||||
|
if tt.header != got {
|
||||||
|
t.Fatalf("[%d] expected result header: %q but got: %q", i, tt.header, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHeaderDecode(t *testing.T) {
|
||||||
|
var tests = []struct {
|
||||||
|
header string
|
||||||
|
ok bool
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
header: "Basic dXNlcjpwYXNz",
|
||||||
|
ok: true,
|
||||||
|
username: "user",
|
||||||
|
password: "pass",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "dXNlcjpwYXNz",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Basic ",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Basic dXNlcjp",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "dXNlcjpwYXNz Basic",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "dXNlcjpwYXNzBasic",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
fullUser, username, password, ok := decodeHeader(tt.header)
|
||||||
|
if expected, got := tt.ok, ok; expected != got {
|
||||||
|
t.Fatalf("[%d] expected: %v but got: %v (header=%s)", i, expected, got, tt.header)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := tt.username, username; expected != got {
|
||||||
|
t.Fatalf("[%d] expected username: %q but got: %q", i, expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := tt.password, password; expected != got {
|
||||||
|
t.Fatalf("[%d] expected password: %q but got: %q", i, expected, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tt.username != "" || tt.password != "" {
|
||||||
|
if expected, got := tt.username+colonLiteral+tt.password, fullUser; expected != got {
|
||||||
|
t.Fatalf("[%d] expected username:password to be: %q but got: %q", i, expected, got)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if fullUser != "" {
|
||||||
|
t.Fatalf("[%d] expected username:password to be empty but got: %q", i, fullUser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
300
middleware/basicauth/user.go
Normal file
300
middleware/basicauth/user.go
Normal file
|
@ -0,0 +1,300 @@
|
||||||
|
package basicauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ReadFile can be used to customize the way the
|
||||||
|
// AllowUsersFile function is loading the filename from.
|
||||||
|
// Example of usage: embedded users.yml file.
|
||||||
|
// Defaults to the `ioutil.ReadFile` which reads the file from the physical disk.
|
||||||
|
var ReadFile = ioutil.ReadFile
|
||||||
|
|
||||||
|
// User is a partial part of the iris.User interface.
|
||||||
|
// It's used to declare a static slice of registered User for authentication.
|
||||||
|
type User interface {
|
||||||
|
context.UserGetUsername
|
||||||
|
context.UserGetPassword
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAuthOptions holds optional user authentication options
|
||||||
|
// that can be given to the builtin Default and Load (and AllowUsers, AllowUsersFile) functions.
|
||||||
|
type UserAuthOptions struct {
|
||||||
|
// Defaults to plain check, can be modified for encrypted passwords,
|
||||||
|
// see the BCRYPT optional function.
|
||||||
|
ComparePassword func(stored, userPassword string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserAuthOption is the option function type
|
||||||
|
// for the Default and Load (and AllowUsers, AllowUsersFile) functions.
|
||||||
|
//
|
||||||
|
// See BCRYPT for an implementation.
|
||||||
|
type UserAuthOption func(*UserAuthOptions)
|
||||||
|
|
||||||
|
// BCRYPT it is a UserAuthOption, it compares a bcrypt hashed password with its user input.
|
||||||
|
// Reports true on success and false on failure.
|
||||||
|
//
|
||||||
|
// Useful when the users passwords are encrypted
|
||||||
|
// using the Provos and Mazières's bcrypt adaptive hashing algorithm.
|
||||||
|
// See https://www.usenix.org/legacy/event/usenix99/provos/provos.pdf.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// Default(..., BCRYPT) OR
|
||||||
|
// Load(..., BCRYPT) OR
|
||||||
|
// Options.Allow = AllowUsers(..., BCRYPT) OR
|
||||||
|
// OPtions.Allow = AllowUsersFile(..., BCRYPT)
|
||||||
|
func BCRYPT(opts *UserAuthOptions) {
|
||||||
|
opts.ComparePassword = func(stored, userPassword string) bool {
|
||||||
|
err := bcrypt.CompareHashAndPassword([]byte(stored), []byte(userPassword))
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toUserAuthOptions(opts []UserAuthOption) (options UserAuthOptions) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt(&options)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.ComparePassword == nil {
|
||||||
|
options.ComparePassword = func(stored, userPassword string) bool {
|
||||||
|
return stored == userPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowUsers is an AuthFunc which authenticates user input based on a (static) user list.
|
||||||
|
// The "users" input parameter can be one of the following forms:
|
||||||
|
// map[string]string e.g. {username: password, username: password...}.
|
||||||
|
// []map[string]interface{} e.g. []{"username": "...", "password": "...", "other_field": ...}, ...}.
|
||||||
|
// []T which T completes the User interface.
|
||||||
|
// []T which T contains at least Username and Password fields.
|
||||||
|
//
|
||||||
|
// Usage:
|
||||||
|
// New(Options{Allow: AllowUsers(..., [BCRYPT])})
|
||||||
|
func AllowUsers(users interface{}, opts ...UserAuthOption) AuthFunc {
|
||||||
|
// create a local user structure to be used in the map copy,
|
||||||
|
// takes longer to initialize but faster to serve.
|
||||||
|
type user struct {
|
||||||
|
password string
|
||||||
|
ref interface{}
|
||||||
|
}
|
||||||
|
cp := make(map[string]*user)
|
||||||
|
|
||||||
|
v := reflect.Indirect(reflect.ValueOf(users))
|
||||||
|
switch v.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
for i := 0; i < v.Len(); i++ {
|
||||||
|
elem := v.Index(i).Interface()
|
||||||
|
// MUST contain a username and password.
|
||||||
|
username, password, ok := extractUsernameAndPassword(elem)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cp[username] = &user{
|
||||||
|
password: password,
|
||||||
|
ref: elem,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case reflect.Map:
|
||||||
|
elem := v.Interface()
|
||||||
|
switch m := elem.(type) {
|
||||||
|
case map[string]string:
|
||||||
|
return userMap(m, opts...)
|
||||||
|
case map[string]interface{}:
|
||||||
|
username, password, ok := mapUsernameAndPassword(m)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
cp[username] = &user{
|
||||||
|
password: password,
|
||||||
|
ref: m,
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unsupported type of map: %T", users))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("unsupported type: %T", users))
|
||||||
|
}
|
||||||
|
|
||||||
|
options := toUserAuthOptions(opts)
|
||||||
|
|
||||||
|
return func(_ *context.Context, username, password string) (interface{}, bool) {
|
||||||
|
if u, ok := cp[username]; ok { // fast map access,
|
||||||
|
if options.ComparePassword(u.password, password) {
|
||||||
|
return u.ref, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func userMap(usernamePassword map[string]string, opts ...UserAuthOption) AuthFunc {
|
||||||
|
options := toUserAuthOptions(opts)
|
||||||
|
|
||||||
|
return func(_ *context.Context, username, password string) (interface{}, bool) {
|
||||||
|
pass, ok := usernamePassword[username]
|
||||||
|
return nil, ok && options.ComparePassword(pass, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AllowUsersFile is an AuthFunc which authenticates user input based on a (static) user list
|
||||||
|
// loaded from a file on initialization.
|
||||||
|
//
|
||||||
|
// Example Code:
|
||||||
|
// New(Options{Allow: AllowUsersFile("users.yml", BCRYPT)})
|
||||||
|
// The users.yml file looks like the following:
|
||||||
|
// - username: kataras
|
||||||
|
// password: kataras_pass
|
||||||
|
// age: 27
|
||||||
|
// role: admin
|
||||||
|
// - username: makis
|
||||||
|
// password: makis_password
|
||||||
|
// ...
|
||||||
|
func AllowUsersFile(jsonOrYamlFilename string, opts ...UserAuthOption) AuthFunc {
|
||||||
|
var (
|
||||||
|
usernamePassword map[string]string
|
||||||
|
// no need to support too much forms, this would be for:
|
||||||
|
// "$username": { "password": "$pass", "other_field": ...}
|
||||||
|
userList []map[string]interface{}
|
||||||
|
)
|
||||||
|
|
||||||
|
if err := decodeFile(jsonOrYamlFilename, &usernamePassword, &userList); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(usernamePassword) > 0 {
|
||||||
|
// JSON Form: { "$username":"$pass", "$username": "$pass" }
|
||||||
|
// YAML Form: $username: $pass
|
||||||
|
// $username: $pass
|
||||||
|
return userMap(usernamePassword, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(userList) > 0 {
|
||||||
|
// JSON Form: [{"username": "$username", "password": "$pass", "other_field": ...}, {"username": ...}, ... ]
|
||||||
|
// YAML Form:
|
||||||
|
// - username: $username
|
||||||
|
// password: $password
|
||||||
|
// other_field: ...
|
||||||
|
return AllowUsers(userList, opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
panic("malformed document file: " + jsonOrYamlFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decodeFile(src string, dest ...interface{}) error {
|
||||||
|
data, err := ReadFile(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We use unmarshal instead of file decoder
|
||||||
|
// as we may need to read it more than once (dests, see below).
|
||||||
|
var (
|
||||||
|
unmarshal func(data []byte, v interface{}) error
|
||||||
|
ext string
|
||||||
|
)
|
||||||
|
|
||||||
|
if idx := strings.LastIndexByte(src, '.'); idx > 0 {
|
||||||
|
ext = src[idx:]
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ext {
|
||||||
|
case "", ".json":
|
||||||
|
unmarshal = json.Unmarshal
|
||||||
|
case ".yml", ".yaml":
|
||||||
|
unmarshal = yaml.Unmarshal
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unexpected file extension: %s", ext)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ok bool
|
||||||
|
lastErr error
|
||||||
|
)
|
||||||
|
|
||||||
|
for _, d := range dest {
|
||||||
|
if err = unmarshal(data, d); err == nil {
|
||||||
|
ok = true
|
||||||
|
} else {
|
||||||
|
lastErr = err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return lastErr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil // if at least one is succeed we are ok.
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractUsernameAndPassword(s interface{}) (username, password string, ok bool) {
|
||||||
|
if s == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch u := s.(type) {
|
||||||
|
case User:
|
||||||
|
username = u.GetUsername()
|
||||||
|
password = u.GetPassword()
|
||||||
|
ok = username != "" && password != ""
|
||||||
|
return
|
||||||
|
case map[string]interface{}:
|
||||||
|
return mapUsernameAndPassword(u)
|
||||||
|
default:
|
||||||
|
b, err := json.Marshal(u)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var m map[string]interface{}
|
||||||
|
if err = json.Unmarshal(b, &m); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return mapUsernameAndPassword(m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapUsernameAndPassword(m map[string]interface{}) (username, password string, ok bool) {
|
||||||
|
// type of username: password.
|
||||||
|
if len(m) == 1 {
|
||||||
|
for username, v := range m {
|
||||||
|
if password, ok := v.(string); ok {
|
||||||
|
ok := username != "" && password != ""
|
||||||
|
return username, password, ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var usernameFound, passwordFound bool
|
||||||
|
|
||||||
|
for k, v := range m {
|
||||||
|
switch k {
|
||||||
|
case "username", "Username":
|
||||||
|
username, usernameFound = v.(string)
|
||||||
|
case "password", "Password":
|
||||||
|
password, passwordFound = v.(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
if usernameFound && passwordFound {
|
||||||
|
ok = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
288
middleware/basicauth/user_test.go
Normal file
288
middleware/basicauth/user_test.go
Normal file
|
@ -0,0 +1,288 @@
|
||||||
|
package basicauth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/bcrypt"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IUserRepository interface {
|
||||||
|
GetByUsernameAndPassword(dest interface{}, username, password string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test a custom implementation of AuthFunc with a user repository.
|
||||||
|
// This is a usage example of custom AuthFunc implementation.
|
||||||
|
func UserRepository(repo IUserRepository, newUserPtr func() interface{}) AuthFunc {
|
||||||
|
return func(_ *context.Context, username, password string) (interface{}, bool) {
|
||||||
|
dest := newUserPtr()
|
||||||
|
err := repo.GetByUsernameAndPassword(dest, username, password)
|
||||||
|
if err == nil {
|
||||||
|
return dest, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testUser struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
email string // custom field.
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUsername & Getpassword complete the User interface.
|
||||||
|
func (u *testUser) GetUsername() string {
|
||||||
|
return u.username
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *testUser) GetPassword() string {
|
||||||
|
return u.password
|
||||||
|
}
|
||||||
|
|
||||||
|
type testRepo struct {
|
||||||
|
entries []testUser
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implements IUserRepository interface.
|
||||||
|
func (r *testRepo) GetByUsernameAndPassword(dest interface{}, username, password string) error {
|
||||||
|
for _, e := range r.entries {
|
||||||
|
if e.username == username && e.password == password {
|
||||||
|
*dest.(*testUser) = e
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return errors.New("invalid credentials")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllowUserRepository(t *testing.T) {
|
||||||
|
repo := &testRepo{
|
||||||
|
entries: []testUser{
|
||||||
|
{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
allow := UserRepository(repo, func() interface{} {
|
||||||
|
return new(testUser)
|
||||||
|
})
|
||||||
|
|
||||||
|
var tests = []struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
ok bool
|
||||||
|
user *testUser
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
username: "kataras",
|
||||||
|
password: "kataras_pass",
|
||||||
|
ok: true,
|
||||||
|
user: &testUser{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "makis",
|
||||||
|
password: "makis_password",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
v, ok := allow(nil, tt.username, tt.password)
|
||||||
|
|
||||||
|
if tt.ok != ok {
|
||||||
|
t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s)", i, tt.ok, ok, tt.username, tt.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u, ok := v.(*testUser)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("[%d] a user should be type of *testUser but got: %#+v (%T)", i, v, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(tt.user, u) {
|
||||||
|
t.Fatalf("[%d] expected user:\n%#+v\nbut got:\n%#+v", i, tt.user, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAllowUsers(t *testing.T) {
|
||||||
|
users := []User{
|
||||||
|
&testUser{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"},
|
||||||
|
}
|
||||||
|
|
||||||
|
allow := AllowUsers(users)
|
||||||
|
|
||||||
|
var tests = []struct {
|
||||||
|
username string
|
||||||
|
password string
|
||||||
|
ok bool
|
||||||
|
user *testUser
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
username: "kataras",
|
||||||
|
password: "kataras_pass",
|
||||||
|
ok: true,
|
||||||
|
user: &testUser{username: "kataras", password: "kataras_pass", email: "kataras2006@hotmail.com"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "makis",
|
||||||
|
password: "makis_password",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tt := range tests {
|
||||||
|
v, ok := allow(nil, tt.username, tt.password)
|
||||||
|
|
||||||
|
if tt.ok != ok {
|
||||||
|
t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s)", i, tt.ok, ok, tt.username, tt.password)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u, ok := v.(*testUser)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("[%d] a user should be type of *testUser but got: %#+v (%T)", i, v, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(tt.user, u) {
|
||||||
|
t.Fatalf("[%d] expected user:\n%#+v\nbut got:\n%#+v", i, tt.user, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test YAML user loading with b-encrypted passwords.
|
||||||
|
func TestAllowUsersFile(t *testing.T) {
|
||||||
|
f, err := ioutil.TempFile("", "*users.yml")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
f.Close()
|
||||||
|
os.Remove(f.Name())
|
||||||
|
}()
|
||||||
|
|
||||||
|
// f.WriteString(`
|
||||||
|
// - username: kataras
|
||||||
|
// password: kataras_pass
|
||||||
|
// age: 27
|
||||||
|
// role: admin
|
||||||
|
// - username: makis
|
||||||
|
// password: makis_password
|
||||||
|
// `)
|
||||||
|
// This form is supported too, although its features are limited (no custom fields):
|
||||||
|
// f.WriteString(`
|
||||||
|
// kataras: kataras_pass
|
||||||
|
// makis: makis_password
|
||||||
|
// `)
|
||||||
|
|
||||||
|
var tests = []struct {
|
||||||
|
username string
|
||||||
|
password string // hashed, auto-filled later on.
|
||||||
|
inputPassword string
|
||||||
|
ok bool
|
||||||
|
user context.Map
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
username: "kataras",
|
||||||
|
inputPassword: "kataras_pass",
|
||||||
|
ok: true,
|
||||||
|
user: context.Map{"age": 27, "role": "admin"}, // username and password are auto-filled in our tests below.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "makis",
|
||||||
|
inputPassword: "makis_password",
|
||||||
|
ok: true,
|
||||||
|
user: context.Map{},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "invalid",
|
||||||
|
password: "invalid_pass",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: "notvalid",
|
||||||
|
password: "",
|
||||||
|
ok: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the tests to the users YAML file.
|
||||||
|
var usersToWrite []context.Map
|
||||||
|
for _, tt := range tests {
|
||||||
|
if tt.ok {
|
||||||
|
// store the hashed password.
|
||||||
|
tt.password = mustGeneratePassword(t, tt.inputPassword)
|
||||||
|
|
||||||
|
// store and write the username and hashed password.
|
||||||
|
tt.user["username"] = tt.username
|
||||||
|
tt.user["password"] = tt.password
|
||||||
|
|
||||||
|
// cannot write it as a stream, write it as a slice.
|
||||||
|
// enc.Encode(tt.user)
|
||||||
|
usersToWrite = append(usersToWrite, tt.user)
|
||||||
|
}
|
||||||
|
// bcrypt.GenerateFromPassword([]byte("kataras_pass"), bcrypt.DefaultCost)
|
||||||
|
}
|
||||||
|
|
||||||
|
fileContents, err := yaml.Marshal(usersToWrite)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
f.Write(fileContents)
|
||||||
|
|
||||||
|
// Build the authentication func.
|
||||||
|
allow := AllowUsersFile(f.Name(), BCRYPT)
|
||||||
|
for i, tt := range tests {
|
||||||
|
v, ok := allow(nil, tt.username, tt.inputPassword)
|
||||||
|
|
||||||
|
if tt.ok != ok {
|
||||||
|
t.Fatalf("[%d] expected: %v but got: %v (username=%s,password=%s,user=%#+v)", i, tt.ok, ok, tt.username, tt.inputPassword, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(tt.user) == 0 { // when username: password form.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
u, ok := v.(context.Map)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("[%d] a user loaded from external source or file should be alway type of map[string]interface{} but got: %#+v (%T)", i, v, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if expected, got := len(tt.user), len(u); expected != got {
|
||||||
|
t.Fatalf("[%d] expected user map length to be equal, expected: %d but got: %d\n%#+v\n%#+v", i, expected, got, tt.user, u)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range tt.user {
|
||||||
|
if u[k] != v {
|
||||||
|
t.Fatalf("[%d] expected user map %q to be %q but got: %q", i, k, v, u[k])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGeneratePassword(t *testing.T, userPassword string) string {
|
||||||
|
t.Helper()
|
||||||
|
hashed, err := bcrypt.GenerateFromPassword([]byte(userPassword), bcrypt.DefaultCost)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(hashed)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user