Add user token validation
This commit is contained in:
parent
3375481b21
commit
5e81b17e28
@ -4,8 +4,11 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gitlab.quimbo.fr/odwrtw/canape/backend/tokens"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@ -17,6 +20,8 @@ var (
|
||||
ErrInvalidSecret = fmt.Errorf("Invalid secret")
|
||||
// ErrInvalidToken returned when the jwt token is invalid
|
||||
ErrInvalidToken = fmt.Errorf("Invalid token")
|
||||
// ErrUnauthenticatedUser returned when a user is not authenticated
|
||||
ErrUnauthenticatedUser = fmt.Errorf("Unauthenticated user")
|
||||
)
|
||||
|
||||
// UserBackend interface for user backend
|
||||
@ -35,6 +40,7 @@ type User interface {
|
||||
|
||||
// Authorizer handle sesssion
|
||||
type Authorizer struct {
|
||||
db *sqlx.DB
|
||||
Params
|
||||
}
|
||||
|
||||
@ -48,8 +54,9 @@ type Params struct {
|
||||
|
||||
// New Authorizer pepper is like a salt but not stored in database,
|
||||
// cost is the bcrypt cost for hashing the password
|
||||
func New(params Params) *Authorizer {
|
||||
func New(db *sqlx.DB, params Params) *Authorizer {
|
||||
return &Authorizer{
|
||||
db: db,
|
||||
Params: params,
|
||||
}
|
||||
}
|
||||
@ -64,7 +71,7 @@ func (a *Authorizer) GenHash(password string) (string, error) {
|
||||
}
|
||||
|
||||
// Login cheks password and creates a jwt token
|
||||
func (a *Authorizer) Login(rw http.ResponseWriter, req *http.Request, username, password string) (User, error) {
|
||||
func (a *Authorizer) Login(username, password string) (*tokens.Token, error) {
|
||||
u, err := a.Backend.GetUser(username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -76,7 +83,34 @@ func (a *Authorizer) Login(rw http.ResponseWriter, req *http.Request, username,
|
||||
return nil, ErrInvalidPassword
|
||||
}
|
||||
|
||||
return u, nil
|
||||
// Create a jwt token
|
||||
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
// Not before
|
||||
"nbf": time.Now().Unix(),
|
||||
// Issued at
|
||||
"iat": time.Now().Unix(),
|
||||
// Private claims
|
||||
"username": u.GetName(),
|
||||
"isAdmin": u.IsAdmin(),
|
||||
"isActivated": u.IsActivated(),
|
||||
})
|
||||
|
||||
// Sign the token
|
||||
ss, err := jwtToken.SignedString([]byte(a.Secret))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
t := &tokens.Token{
|
||||
Token: ss,
|
||||
Username: u.GetName(),
|
||||
}
|
||||
|
||||
if err := t.Add(a.db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// CurrentUser returns the logged in username from session and verifies the token
|
||||
@ -92,14 +126,14 @@ func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (Use
|
||||
if tokenStr == "" {
|
||||
tokenCookie, err := req.Cookie("token")
|
||||
if err != nil || tokenCookie == nil {
|
||||
return nil, nil
|
||||
return nil, ErrUnauthenticatedUser
|
||||
}
|
||||
tokenStr = tokenCookie.Value
|
||||
}
|
||||
|
||||
// No user logged
|
||||
if tokenStr == "" {
|
||||
return nil, nil
|
||||
return nil, ErrUnauthenticatedUser
|
||||
}
|
||||
|
||||
// Keyfunc to decode the token
|
||||
@ -111,13 +145,13 @@ func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (Use
|
||||
Username string `json:"username"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
token, err := jwt.ParseWithClaims(tokenStr, &tokenClaims, keyfunc)
|
||||
jwtToken, err := jwt.ParseWithClaims(tokenStr, &tokenClaims, keyfunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check the token validity
|
||||
if !token.Valid {
|
||||
if !jwtToken.Valid {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
@ -127,5 +161,16 @@ func (a *Authorizer) CurrentUser(rw http.ResponseWriter, req *http.Request) (Use
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check the token in database
|
||||
token, err := tokens.GetUserToken(a.db, u.GetName(), tokenStr)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
token.UserAgent = req.UserAgent()
|
||||
if err := token.Update(a.db); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return u, nil
|
||||
}
|
||||
|
@ -25,20 +25,21 @@ func NewMiddleware(authorizer *Authorizer, log *logrus.Entry) *Middleware {
|
||||
|
||||
func (m *Middleware) ServeHTTP(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
|
||||
user, err := m.authorizer.CurrentUser(w, r)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
switch err {
|
||||
case nil:
|
||||
m.log.Debugf("setting user %s in the context", user.GetName())
|
||||
} else {
|
||||
m.log.Debugf("got a nil user in the context")
|
||||
}
|
||||
|
||||
ctxKey := authContextKey("auth.user")
|
||||
ctx := context.WithValue(r.Context(), ctxKey, user)
|
||||
r = r.WithContext(ctx)
|
||||
case ErrUnauthenticatedUser:
|
||||
m.log.Debugf("unauthenticated user")
|
||||
case ErrInvalidToken:
|
||||
m.log.Debugf("user has an invalid token")
|
||||
default:
|
||||
m.log.Error(err)
|
||||
http.Error(w, err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
next(w, r)
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ func main() {
|
||||
Cost: cf.Authorizer.Cost,
|
||||
Secret: cf.Authorizer.Secret,
|
||||
}
|
||||
authorizer := auth.New(authParams)
|
||||
authorizer := auth.New(db, authParams)
|
||||
|
||||
// Create web environment needed by the app
|
||||
env := web.NewEnv(web.EnvParams{
|
||||
|
@ -16,6 +16,9 @@ func setupRoutes(env *web.Env) {
|
||||
env.Handle("/users/signup", users.SignupPOSTHandler).Methods("POST")
|
||||
env.Handle("/users/details", users.DetailsHandler).WithRole(users.UserRole).Methods("GET")
|
||||
env.Handle("/users/edit", users.EditHandler).WithRole(users.UserRole).Methods("POST")
|
||||
env.Handle("/users/tokens", users.GetTokensHandler).WithRole(users.UserRole).Methods("GET")
|
||||
env.Handle("/users/tokens/{token}", users.EditTokenHandler).WithRole(users.UserRole).Methods("POST")
|
||||
env.Handle("/users/tokens/{token}", users.DeleteTokenHandler).WithRole(users.UserRole).Methods("DELETE")
|
||||
|
||||
// Movies routes
|
||||
env.Handle("/movies/polochon", movies.PolochonMoviesHandler).WithRole(users.UserRole).Methods("GET")
|
||||
|
96
backend/tokens/tokens.go
Normal file
96
backend/tokens/tokens.go
Normal file
@ -0,0 +1,96 @@
|
||||
package tokens
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
"gitlab.quimbo.fr/odwrtw/canape/backend/sqly"
|
||||
)
|
||||
|
||||
const (
|
||||
addTokenQuery = `INSERT INTO tokens (token, username) VALUES ($1, $2);`
|
||||
getTokenQuery = `SELECT * FROM tokens WHERE token=$1;`
|
||||
getUserTokenQuery = `SELECT * FROM tokens WHERE username=$1 and token=$2;`
|
||||
getUserTokensQuery = `SELECT * FROM tokens WHERE username=$1;`
|
||||
deleteTokenQuery = `DELETE FROM tokens WHERE username=$1 AND token=$2;`
|
||||
updateTokenQuery = `UPDATE tokens SET description=:description, user_agent=:user_agent WHERE token=:token RETURNING *;`
|
||||
)
|
||||
|
||||
// Custom errors
|
||||
var (
|
||||
ErrTokenNotFound = errors.New("tokens: token not found")
|
||||
)
|
||||
|
||||
// Token represents a token
|
||||
type Token struct {
|
||||
sqly.BaseModel
|
||||
Username string `db:"username" json:"username"`
|
||||
Token string `db:"token" json:"token"`
|
||||
Description string `db:"description" json:"description"`
|
||||
UserAgent string `db:"user_agent" json:"user_agent"`
|
||||
}
|
||||
|
||||
// Add a token to the database
|
||||
func (t *Token) Add(db *sqlx.DB) error {
|
||||
_, err := db.Queryx(addTokenQuery, t.Token, t.Username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserToken returns the token linked to a user
|
||||
func GetUserToken(db *sqlx.DB, username, token string) (*Token, error) {
|
||||
t := &Token{}
|
||||
err := db.QueryRowx(getUserTokenQuery, username, token).StructScan(t)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrTokenNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// DeleteToken deletes a token
|
||||
func DeleteToken(db *sqlx.DB, username, token string) error {
|
||||
r, err := db.Exec(deleteTokenQuery, username, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
count, err := r.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count != 1 {
|
||||
return fmt.Errorf("Unexpected number of row deleted: %d", count)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetUserTokens returns all tokens owned by the user
|
||||
func GetUserTokens(db *sqlx.DB, username string) ([]*Token, error) {
|
||||
tokens := []*Token{}
|
||||
err := db.Select(&tokens, getUserTokensQuery, username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// Update updates a token
|
||||
func (t *Token) Update(db *sqlx.DB) error {
|
||||
rows, err := db.NamedQuery(updateTokenQuery, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for rows.Next() {
|
||||
rows.StructScan(t)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -4,12 +4,11 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
jwt "github.com/dgrijalva/jwt-go"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
"gitlab.quimbo.fr/odwrtw/canape/backend/auth"
|
||||
"gitlab.quimbo.fr/odwrtw/canape/backend/config"
|
||||
"gitlab.quimbo.fr/odwrtw/canape/backend/tokens"
|
||||
"gitlab.quimbo.fr/odwrtw/canape/backend/web"
|
||||
)
|
||||
|
||||
@ -65,37 +64,21 @@ func LoginPOSTHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error
|
||||
return err
|
||||
}
|
||||
|
||||
user, err := e.Auth.Login(w, r, data.Username, data.Password)
|
||||
token, err := e.Auth.Login(data.Username, data.Password)
|
||||
if err != nil {
|
||||
if err == auth.ErrInvalidPassword || err == ErrUnknownUser {
|
||||
return e.RenderError(w, fmt.Errorf("Error invalid user or password"))
|
||||
}
|
||||
return err
|
||||
}
|
||||
e.Log.Debugf("logged %s", user.GetName())
|
||||
e.Log.Debugf("logged %s", token.Username)
|
||||
|
||||
// Create a jwt token
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||
// Not before
|
||||
"nbf": time.Now().Unix(),
|
||||
// Issued at
|
||||
"iat": time.Now().Unix(),
|
||||
// Private claims
|
||||
"username": user.GetName(),
|
||||
"isAdmin": user.IsAdmin(),
|
||||
"isActivated": user.IsActivated(),
|
||||
})
|
||||
|
||||
// Sign the token
|
||||
ss, err := token.SignedString([]byte(e.Auth.Secret))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// TODO: add token expiration / keep me login stuff
|
||||
|
||||
var out = struct {
|
||||
Token string `json:"token"`
|
||||
}{
|
||||
Token: ss,
|
||||
Token: token.Token,
|
||||
}
|
||||
|
||||
return e.RenderJSON(w, out)
|
||||
@ -167,3 +150,68 @@ func EditHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
|
||||
|
||||
return e.RenderOK(w, "user updated")
|
||||
}
|
||||
|
||||
// GetTokensHandler lists the tokens of a user
|
||||
func GetTokensHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
|
||||
v := auth.GetCurrentUser(r, e.Log)
|
||||
user, ok := v.(*User)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid user type")
|
||||
}
|
||||
|
||||
tokens, err := tokens.GetUserTokens(e.Database, user.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.RenderJSON(w, tokens)
|
||||
}
|
||||
|
||||
// DeleteTokenHandler helps delete a token
|
||||
func DeleteTokenHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
token := vars["token"]
|
||||
|
||||
v := auth.GetCurrentUser(r, e.Log)
|
||||
user, ok := v.(*User)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid user type")
|
||||
}
|
||||
|
||||
if err := tokens.DeleteToken(e.Database, user.Name, token); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.RenderOK(w, "token deleted")
|
||||
}
|
||||
|
||||
// EditTokenHandler helps delete a token
|
||||
func EditTokenHandler(e *web.Env, w http.ResponseWriter, r *http.Request) error {
|
||||
vars := mux.Vars(r)
|
||||
token := vars["token"]
|
||||
|
||||
v := auth.GetCurrentUser(r, e.Log)
|
||||
user, ok := v.(*User)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid user type")
|
||||
}
|
||||
|
||||
t, err := tokens.GetUserToken(e.Database, user.Name, token)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data := struct {
|
||||
Description string `json:"description"`
|
||||
}{}
|
||||
if err := json.NewDecoder(r.Body).Decode(&data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
t.Description = data.Description
|
||||
if err := t.Update(e.Database); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.RenderJSON(w, t)
|
||||
}
|
||||
|
@ -11,7 +11,6 @@ import (
|
||||
"github.com/odwrtw/papi"
|
||||
|
||||
"gitlab.quimbo.fr/odwrtw/canape/backend/config"
|
||||
"gitlab.quimbo.fr/odwrtw/canape/backend/random"
|
||||
"gitlab.quimbo.fr/odwrtw/canape/backend/sqly"
|
||||
)
|
||||
|
||||
@ -22,11 +21,6 @@ const (
|
||||
updateUserQuery = `UPDATE users SET name=:name, hash=:hash, admin=:admin, activated=:activated, rawconfig=:rawconfig WHERE id=:id RETURNING *;`
|
||||
deleteUseQuery = `DELETE FROM users WHERE id=:id;`
|
||||
|
||||
addTokenQuery = `INSERT INTO tokens (value, user_id) VALUES ($1, $2) RETURNING id;`
|
||||
getTokensQuery = `SELECT id, value FROM tokens WHERE user_id=$1;`
|
||||
checkTokenQuery = `SELECT count(*) FROM tokens WHERE user_id=$1 AND value=$2;`
|
||||
deleteTokenQuery = `DELETE FROM tokens WHERE user_id=$1 AND value=$2;`
|
||||
|
||||
getAllUsersQuery = `SELECT * FROM users order by created_at;`
|
||||
)
|
||||
|
||||
@ -119,12 +113,6 @@ func (u *User) NewPapiClient() (*papi.Client, error) {
|
||||
return client, nil
|
||||
}
|
||||
|
||||
// Token represents a token
|
||||
type Token struct {
|
||||
sqly.BaseModel
|
||||
Value string
|
||||
}
|
||||
|
||||
// Get returns user with specified name
|
||||
func Get(q sqlx.Queryer, name string) (*User, error) {
|
||||
u := &User{}
|
||||
@ -193,57 +181,6 @@ func (u *User) Delete(ex *sqlx.DB) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetTokens returns all tokens owned by the user
|
||||
func (u *User) GetTokens(ex *sqlx.DB) ([]*Token, error) {
|
||||
tokens := []*Token{}
|
||||
err := ex.Select(&tokens, getTokensQuery, u.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tokens, nil
|
||||
}
|
||||
|
||||
// NewToken generates a new token for the user
|
||||
func (u *User) NewToken(ex *sqlx.DB) (*Token, error) {
|
||||
t := &Token{
|
||||
Value: random.String(50),
|
||||
}
|
||||
|
||||
var id string
|
||||
err := ex.QueryRowx(addTokenQuery, t.Value, u.ID).Scan(&id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.ID = id
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// CheckToken checks if specified value exists in token's values for the user
|
||||
func (u *User) CheckToken(ex *sqlx.DB, value string) (bool, error) {
|
||||
var count int
|
||||
err := ex.QueryRowx(checkTokenQuery, u.ID, value).Scan(&count)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if count != 1 {
|
||||
return false, nil
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// DeleteToken delete token by value
|
||||
func (u *User) DeleteToken(ex *sqlx.DB, value string) error {
|
||||
r, err := ex.Exec(deleteTokenQuery, u.ID, value)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
count, _ := r.RowsAffected()
|
||||
if count != 1 {
|
||||
return fmt.Errorf("Unexpected number of row deleted: %d", count)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetHash implements auth.User interface
|
||||
func (u *User) GetHash() string {
|
||||
return u.Hash
|
||||
|
1
migrations/0006_user_token.down.sql
Normal file
1
migrations/0006_user_token.down.sql
Normal file
@ -0,0 +1 @@
|
||||
DROP TABLE tokens;
|
12
migrations/0006_user_token.up.sql
Normal file
12
migrations/0006_user_token.up.sql
Normal file
@ -0,0 +1,12 @@
|
||||
CREATE TABLE tokens (
|
||||
token text PRIMARY KEY NOT NULL UNIQUE,
|
||||
username text NOT NULL REFERENCES users(name),
|
||||
description text NOT NULL DEFAULT '-',
|
||||
user_agent text NOT NULL DEFAULT '-',
|
||||
ip inet NOT NULL,
|
||||
last_used timestamp with time zone NOT NULL DEFAULT current_timestamp,
|
||||
LIKE base INCLUDING DEFAULTS
|
||||
);
|
||||
|
||||
CREATE INDEX ON tokens (username);
|
||||
CREATE TRIGGER update_tokens_updated_at BEFORE UPDATE ON tokens FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
|
Loading…
x
Reference in New Issue
Block a user