feat: add authentication via JWT token

This commit is contained in:
Deluan
2020-02-06 16:48:35 -05:00
parent 690f92a671
commit abb99a8501
10 changed files with 185 additions and 56 deletions
+2 -1
View File
@@ -7,6 +7,7 @@ import (
"strings"
"github.com/deluan/navidrome/assets"
"github.com/deluan/navidrome/engine/auth"
"github.com/deluan/navidrome/model"
"github.com/deluan/rest"
"github.com/go-chi/chi"
@@ -36,7 +37,7 @@ func (app *Router) routes() http.Handler {
r.Post("/createAdmin", CreateAdmin(app.ds))
r.Route("/api", func(r chi.Router) {
r.Use(jwtauth.Verifier(TokenAuth))
r.Use(jwtauth.Verifier(auth.TokenAuth))
r.Use(Authenticator(app.ds))
app.R(r, "/user", model.User{})
app.R(r, "/song", model.MediaFile{})
+6 -35
View File
@@ -10,6 +10,7 @@ import (
"time"
"github.com/deluan/navidrome/consts"
"github.com/deluan/navidrome/engine/auth"
"github.com/deluan/navidrome/log"
"github.com/deluan/navidrome/model"
"github.com/deluan/rest"
@@ -20,13 +21,11 @@ import (
var (
once sync.Once
jwtSecret []byte
TokenAuth *jwtauth.JWTAuth
ErrFirstTime = errors.New("no users created")
)
func Login(ds model.DataStore) func(w http.ResponseWriter, r *http.Request) {
initTokenAuth(ds)
auth.InitTokenAuth(ds)
return func(w http.ResponseWriter, r *http.Request) {
username, password, err := getCredentialsFromBody(r)
@@ -52,7 +51,7 @@ func handleLogin(ds model.DataStore, username string, password string, w http.Re
return
}
tokenString, err := createToken(user)
tokenString, err := auth.CreateToken(user)
if err != nil {
rest.RespondWithError(w, http.StatusInternalServerError, "Unknown error authenticating user. Please try again")
return
@@ -82,7 +81,7 @@ func getCredentialsFromBody(r *http.Request) (username string, password string,
}
func CreateAdmin(ds model.DataStore) func(w http.ResponseWriter, r *http.Request) {
initTokenAuth(ds)
auth.InitTokenAuth(ds)
return func(w http.ResponseWriter, r *http.Request) {
username, password, err := getCredentialsFromBody(r)
@@ -129,16 +128,6 @@ func createDefaultUser(ctx context.Context, ds model.DataStore, username, passwo
return nil
}
func initTokenAuth(ds model.DataStore) {
once.Do(func() {
secret, err := ds.Property(nil).DefaultGet(consts.JWTSecretKey, "not so secret")
if err != nil {
log.Error("No JWT secret found in DB. Setting a temp one, but please report this error", err)
}
jwtSecret = []byte(secret)
TokenAuth = jwtauth.New("HS256", jwtSecret, nil)
})
}
func validateLogin(userRepo model.UserRepository, userName, password string) (*model.User, error) {
u, err := userRepo.FindByUsername(userName)
if err == model.ErrNotFound {
@@ -157,24 +146,6 @@ func validateLogin(userRepo model.UserRepository, userName, password string) (*m
return u, nil
}
func createToken(u *model.User) (string, error) {
token := jwt.New(jwt.SigningMethodHS256)
claims := token.Claims.(jwt.MapClaims)
claims["iss"] = consts.JWTIssuer
claims["sub"] = u.UserName
claims["adm"] = u.IsAdmin
return touchToken(token)
}
func touchToken(token *jwt.Token) (string, error) {
expireIn := time.Now().Add(consts.JWTTokenExpiration).Unix()
claims := token.Claims.(jwt.MapClaims)
claims["exp"] = expireIn
return token.SignedString(jwtSecret)
}
func contextWithUser(ctx context.Context, ds model.DataStore, claims jwt.MapClaims) context.Context {
userName := claims["sub"].(string)
user, _ := ds.User(ctx).FindByUsername(userName)
@@ -199,7 +170,7 @@ func getToken(ds model.DataStore, ctx context.Context) (*jwt.Token, error) {
}
func Authenticator(ds model.DataStore) func(next http.Handler) http.Handler {
initTokenAuth(ds)
auth.InitTokenAuth(ds)
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -216,7 +187,7 @@ func Authenticator(ds model.DataStore) func(next http.Handler) http.Handler {
claims := token.Claims.(jwt.MapClaims)
newCtx := contextWithUser(r.Context(), ds, claims)
newTokenString, err := touchToken(token)
newTokenString, err := auth.TouchToken(token)
if err != nil {
log.Error(r, "signing new token", err)
rest.RespondWithError(w, http.StatusUnauthorized, "Not authenticated")
+2 -5
View File
@@ -44,10 +44,6 @@ func checkRequiredParameters(next http.Handler) http.Handler {
}
}
if ParamString(r, "p") == "" && (ParamString(r, "s") == "" || ParamString(r, "t") == "") {
log.Warn(r, "Missing authentication information")
}
user := ParamString(r, "u")
client := ParamString(r, "c")
version := ParamString(r, "v")
@@ -69,8 +65,9 @@ func authenticate(users engine.Users) func(next http.Handler) http.Handler {
pass := ParamString(r, "p")
token := ParamString(r, "t")
salt := ParamString(r, "s")
jwt := ParamString(r, "jwt")
usr, err := users.Authenticate(r.Context(), username, pass, token, salt)
usr, err := users.Authenticate(r.Context(), username, pass, token, salt, jwt)
if err == model.ErrInvalidAuth {
log.Warn(r, "Invalid login", "username", username, err)
} else if err != nil {
+5 -3
View File
@@ -113,7 +113,7 @@ var _ = Describe("Middlewares", func() {
})
It("passes all parameters to users.Authenticate ", func() {
r := newGetRequest("u=valid", "p=password", "t=token", "s=salt")
r := newGetRequest("u=valid", "p=password", "t=token", "s=salt", "jwt=jwt")
cp := authenticate(mockedUser)(next)
cp.ServeHTTP(w, r)
@@ -121,6 +121,7 @@ var _ = Describe("Middlewares", func() {
Expect(mockedUser.password).To(Equal("password"))
Expect(mockedUser.token).To(Equal("token"))
Expect(mockedUser.salt).To(Equal("salt"))
Expect(mockedUser.jwt).To(Equal("jwt"))
Expect(next.called).To(BeTrue())
user := next.req.Context().Value("user").(*model.User)
Expect(user.UserName).To(Equal("valid"))
@@ -149,14 +150,15 @@ func (mh *mockHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
type mockUsers struct {
engine.Users
username, password, token, salt string
username, password, token, salt, jwt string
}
func (m *mockUsers) Authenticate(ctx context.Context, username, password, token, salt string) (*model.User, error) {
func (m *mockUsers) Authenticate(ctx context.Context, username, password, token, salt, jwt string) (*model.User, error) {
m.username = username
m.password = password
m.token = token
m.salt = salt
m.jwt = jwt
if username == "valid" {
return &model.User{UserName: username, Password: password}, nil
}