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,169 +17,9 @@ func (app *appContext) webAuth() gin.HandlerFunc { return app.authenticate } -func (app *appContext) authenticate(gc *gin.Context) { - 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) - 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 - }) - 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) - respond(401, "Unauthorized", gc) - 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") - respond(401, "Unauthorized", gc) - return - } - match := false - for _, user := range app.users { - if user.UserID == userId { - match = true - } - } - if !match { - 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.Next() -} - -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] != "" { - match = true - app.debug.Println("Found existing user") - userId = user.UserID - } - } - } - if !match { - if !app.jellyfinLogin { - app.info.Println("Auth failed: Invalid username and/or password") - respond(401, "Unauthorized", gc) - return - } - cookie, err := gc.Cookie("refresh") - if err == nil && cookie != "" && creds[0] == "" && creds[1] == "" { - for _, token := range app.invalidTokens { - if cookie == token { - app.debug.Printf("Auth denied: Refresh token in blocklist") - respond(401, "Unauthorized", gc) - 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 - }) - if err != nil { - app.debug.Printf("Auth denied: %s", err) - respond(401, "Unauthorized", gc) - 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) - 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) - return - } - app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin: Code %d", status) - respond(500, "Jellyfin error", gc) - 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) - } - } - } - token, refresh, err := CreateToken(userId, jfId) - if err != nil { - respond(500, "Error generating token", gc) - } - 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) { +// 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 - var err error claims := jwt.MapClaims{ "valid": true, "id": userId, @@ -189,30 +29,218 @@ func CreateToken(userId string, jfId string) (string, string, error) { } tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) - token, err = tk.SignedString([]byte(os.Getenv("JFA_SECRET"))) + 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", - } - + 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{"error": message} + 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" { + app.debug.Println("Invalid authentication header") + respond(401, "Unauthorized", gc) + return + } + auth, _ := base64.StdEncoding.DecodeString(header[1]) + creds := strings.SplitN(string(auth), ":", 2) + 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) + expiryUnix, err := strconv.ParseInt(claims["exp"].(string), 10, 64) + if err != nil { + app.debug.Printf("Auth denied: %s", err) + respond(401, "Unauthorized", gc) + return + } + expiry := time.Unix(expiryUnix, 0) + 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 { + match = true + break + } + } + if !match { + 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("Auth succeeded") + gc.Next() +} + +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) + auth, _ := base64.StdEncoding.DecodeString(header[1]) + creds := strings.SplitN(string(auth), ":", 2) + // 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 + break + } + } + 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 != "" { + for _, token := range app.invalidTokens { + if cookie == token { + 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, checkToken) + if err != nil { + 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 { + 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())) { + 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 + } + userID = claims["id"].(string) + jfID = claims["jfid"].(string) + valid = true + } + } + checkCookie() + if !valid && !noLogin { + checkLogin() + } + 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) + } +} 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)