From d12335bb4a891f35e7c8bc2d3d8c09fe8f2f3d10 Mon Sep 17 00:00:00 2001 From: Harvey Tindall Date: Sun, 23 Aug 2020 14:59:07 +0100 Subject: [PATCH] cleaned up auth --- .gitignore | 1 + auth.go | 282 +++++++++++++++++++++++++++++------------------------ main.go | 2 +- 3 files changed, 157 insertions(+), 128 deletions(-) diff --git a/.gitignore b/.gitignore index 6615d3c..5763668 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ dist/* jfa-go build/ pkg/ +old/ diff --git a/auth.go b/auth.go index b4a9185..77d36e6 100644 --- a/auth.go +++ b/auth.go @@ -17,6 +17,44 @@ func (app *appContext) webAuth() gin.HandlerFunc { return app.authenticate } +// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens. +func CreateToken(userId, jfId string) (string, string, error) { + var token, refresh string + claims := jwt.MapClaims{ + "valid": true, + "id": userId, + "exp": strconv.FormatInt(time.Now().Add(time.Minute*20).Unix(), 10), + "jfid": jfId, + "type": "bearer", + } + + tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + token, err := tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) + if err != nil { + return "", "", err + } + claims["exp"] = strconv.FormatInt(time.Now().Add(time.Hour*24).Unix(), 10) + claims["type"] = "refresh" + tk = jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + refresh, err = tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) + if err != nil { + return "", "", err + } + return token, refresh, nil +} + +func respond(code int, message string, gc *gin.Context) { + resp := map[string]string{} + if code == 200 || code == 204 { + resp["response"] = message + } else { + resp["error"] = message + } + gc.JSON(code, resp) + gc.Abort() +} + +// Check header for token func (app *appContext) authenticate(gc *gin.Context) { header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2) if header[0] != "Basic" { @@ -26,21 +64,13 @@ func (app *appContext) authenticate(gc *gin.Context) { } auth, _ := base64.StdEncoding.DecodeString(header[1]) creds := strings.SplitN(string(auth), ":", 2) - token, err := jwt.Parse(creds[0], func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - app.debug.Printf("Invalid JWT signing method %s", token.Header["alg"]) - return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"]) - } - return []byte(os.Getenv("JFA_SECRET")), nil - }) + token, err := jwt.Parse(creds[0], checkToken) if err != nil { app.debug.Printf("Auth denied: %s", err) respond(401, "Unauthorized", gc) return } claims, ok := token.Claims.(jwt.MapClaims) - var userId string - var jfId string expiryUnix, err := strconv.ParseInt(claims["exp"].(string), 10, 64) if err != nil { app.debug.Printf("Auth denied: %s", err) @@ -48,171 +78,169 @@ func (app *appContext) authenticate(gc *gin.Context) { return } expiry := time.Unix(expiryUnix, 0) - if ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now()) { - userId = claims["id"].(string) - jfId = claims["jfid"].(string) - } else { - app.debug.Printf("Invalid token") + if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) { + app.debug.Printf("Auth denied: Invalid token") respond(401, "Unauthorized", gc) return } + userID := claims["id"].(string) + jfID := claims["jfid"].(string) match := false for _, user := range app.users { - if user.UserID == userId { + if user.UserID == userID { match = true + break } } if !match { - app.debug.Printf("Couldn't find user ID %s", userId) + app.debug.Printf("Couldn't find user ID \"%s\"", userID) respond(401, "Unauthorized", gc) return } - gc.Set("jfId", jfId) - gc.Set("userId", userId) - app.debug.Println("Authentication successful") + gc.Set("jfId", jfID) + gc.Set("userId", userID) + app.debug.Println("Auth succeeded") gc.Next() } -func (app *appContext) GetToken(gc *gin.Context) { +func checkToken(token *jwt.Token) (interface{}, error) { + if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { + return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"]) + } + return []byte(os.Getenv("JFA_SECRET")), nil +} + +// getToken checks the header for a username and password, as well as checking the refresh cookie. +func (app *appContext) getToken(gc *gin.Context) { app.info.Println("Token requested (login attempt)") header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2) - if header[0] != "Basic" { - app.debug.Println("Invalid authentication header") - respond(401, "Unauthorized", gc) - return - } auth, _ := base64.StdEncoding.DecodeString(header[1]) creds := strings.SplitN(string(auth), ":", 2) - match := false - var userId, jfId string - for _, user := range app.users { - if user.Username == creds[0] && user.Password == creds[1] { - if creds[0] != "" && creds[1] != "" { + // check cookie first + var userID, jfID string + valid := false + noLogin := false + checkLogin := func() { + if creds[0] == "" || creds[1] == "" { + app.debug.Println("Auth denied: blank username/password") + respond(401, "Unauthorized", gc) + return + } + match := false + for _, user := range app.users { + if user.Username == creds[0] && user.Password == creds[1] { match = true app.debug.Println("Found existing user") - userId = user.UserID + userID = user.UserID + break } } - } - if !match { - if !app.jellyfinLogin { - app.info.Println("Auth failed: Invalid username and/or password") + if !app.jellyfinLogin && !match { + app.info.Println("Auth denied: Invalid username/password") respond(401, "Unauthorized", gc) return } + if !match { + var status int + var err error + var user map[string]interface{} + user, status, err = app.authJf.authenticate(creds[0], creds[1]) + if status != 200 || err != nil { + if status == 401 || status == 400 { + app.info.Println("Auth denied: Invalid username/password (Jellyfin)") + respond(401, "Unauthorized", gc) + return + } + app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err) + respond(500, "Jellyfin error", gc) + return + } + jfID = user["Id"].(string) + if app.config.Section("ui").Key("admin_only").MustBool(true) { + if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) { + app.debug.Printf("Auth denied: Users \"%s\" isn't admin", creds[0]) + respond(401, "Unauthorized", gc) + return + } + } + // New users are only added when using jellyfinLogin. + userID = shortuuid.New() + newUser := User{ + UserID: userID, + } + app.debug.Printf("Token generated for user \"%s\"", creds[0]) + app.users = append(app.users, newUser) + } + valid = true + } + checkCookie := func() { cookie, err := gc.Cookie("refresh") - if err == nil && cookie != "" && creds[0] == "" && creds[1] == "" { + if err == nil && cookie != "" { for _, token := range app.invalidTokens { if cookie == token { - app.debug.Printf("Auth denied: Refresh token in blocklist") - respond(401, "Unauthorized", gc) + if creds[0] == "" || creds[1] == "" { + app.debug.Println("getToken denied: Invalid refresh token and no username/password provided") + respond(401, "Unauthorized", gc) + noLogin = true + return + } + app.debug.Println("getToken: Invalid token but username/password provided") return } } - token, err := jwt.Parse(cookie, func(token *jwt.Token) (interface{}, error) { - if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { - app.debug.Printf("Invalid JWT signing method %s", token.Header["alg"]) - return nil, fmt.Errorf("Unexpected signing method %v", token.Header["alg"]) - } - return []byte(os.Getenv("JFA_SECRET")), nil - }) + token, err := jwt.Parse(cookie, checkToken) if err != nil { - app.debug.Printf("Auth denied: %s", err) - respond(401, "Unauthorized", gc) + if creds[0] == "" || creds[1] == "" { + app.debug.Println("getToken denied: Invalid refresh token and no username/password provided") + respond(401, "Unauthorized", gc) + noLogin = true + return + } + app.debug.Println("getToken: Invalid token but username/password provided") return } claims, ok := token.Claims.(jwt.MapClaims) expiryUnix, err := strconv.ParseInt(claims["exp"].(string), 10, 64) if err != nil { - app.debug.Printf("Auth denied: %s", err) - respond(401, "Unauthorized", gc) + if creds[0] == "" || creds[1] == "" { + app.debug.Printf("getToken denied: Invalid token (%s) and no username/password provided", err) + respond(401, "Unauthorized", gc) + noLogin = true + return + } + app.debug.Printf("getToken: Invalid token (%s) but username/password provided", err) return } expiry := time.Unix(expiryUnix, 0) - if ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now()) { - userId = claims["id"].(string) - jfId = claims["jfid"].(string) - } else { - app.debug.Printf("Invalid token (invalid or not refresh type)") - respond(401, "Unauthorized", gc) - return - } - } else { - var status int - var err error - var user map[string]interface{} - user, status, err = app.authJf.authenticate(creds[0], creds[1]) - if status != 200 || err != nil { - if status == 401 || status == 400 { - app.info.Println("Auth failed: Invalid username and/or password") - respond(401, "Invalid username/password", gc) + if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) { + if creds[0] == "" || creds[1] == "" { + app.debug.Printf("getToken denied: Invalid token (%s) and no username/password provided", err) + respond(401, "Unauthorized", gc) + noLogin = true return } - app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin: Code %d", status) - respond(500, "Jellyfin error", gc) + app.debug.Printf("getToken: Invalid token (%s) but username/password provided", err) return - } else { - jfId = user["Id"].(string) - if app.config.Section("ui").Key("admin_only").MustBool(true) { - if !user["Policy"].(map[string]interface{})["IsAdministrator"].(bool) { - app.debug.Printf("Auth failed: User \"%s\" isn't admin", creds[0]) - respond(401, "Unauthorized", gc) - } - } - newuser := User{} - newuser.UserID = shortuuid.New() - userId = newuser.UserID - // uuid, nothing else identifiable! - app.debug.Printf("Token generated for user \"%s\"", creds[0]) - app.users = append(app.users, newuser) } + userID = claims["id"].(string) + jfID = claims["jfid"].(string) + valid = true } } - token, refresh, err := CreateToken(userId, jfId) - if err != nil { - respond(500, "Error generating token", gc) + checkCookie() + if !valid && !noLogin { + checkLogin() } - resp := map[string]string{"token": token} - gc.SetCookie("refresh", refresh, (3600 * 24), "/", gc.Request.URL.Hostname(), true, true) - gc.JSON(200, resp) -} - -func CreateToken(userId string, jfId string) (string, string, error) { - var token, refresh string - var err error - claims := jwt.MapClaims{ - "valid": true, - "id": userId, - "exp": strconv.FormatInt(time.Now().Add(time.Minute*20).Unix(), 10), - "jfid": jfId, - "type": "bearer", - } - - tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - token, err = tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) - if err != nil { - return "", "", err - } - - claims = jwt.MapClaims{ - "valid": true, - "id": userId, - "exp": strconv.FormatInt(time.Now().Add(time.Hour*24).Unix(), 10), - "jfid": jfId, - "type": "refresh", - } - - tk = jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - refresh, err = tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) - if err != nil { - return "", "", err + if valid { + token, refresh, err := CreateToken(userID, jfID) + if err != nil { + app.err.Printf("getToken failed: Couldn't generate token (%s)", err) + respond(500, "Couldn't generate token", gc) + return + } + gc.SetCookie("refresh", refresh, (3600 * 24), "/", gc.Request.URL.Hostname(), true, true) + gc.JSON(200, map[string]string{"token": token}) + } else { + gc.AbortWithStatus(401) } - - return token, refresh, nil -} - -func respond(code int, message string, gc *gin.Context) { - resp := map[string]string{"error": message} - gc.JSON(code, resp) - gc.Abort() } diff --git a/main.go b/main.go index a41680f..493eb2d 100644 --- a/main.go +++ b/main.go @@ -324,7 +324,7 @@ func main() { } if !firstRun { router.GET("/", app.AdminPage) - router.GET("/getToken", app.GetToken) + router.GET("/getToken", app.getToken) router.POST("/newUser", app.NewUser) router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(app.local_path, "static"), false))) router.GET("/invite/:invCode", app.InviteProxy)