1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-12-22 17:10:10 +00:00
jfa-go/auth.go
Harvey Tindall ee3b421566
Fixed flaw with jellyfin_login; store refresh token in cookies
with jellyfin_login enabled, the username and password vals in the User
struct would be "". If you disabled 'required' on the login form, blank
username and password would allow you in.
2020-08-20 20:20:31 +01:00

220 lines
6.3 KiB
Go

package main
import (
"encoding/base64"
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/lithammer/shortuuid/v3"
)
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] == "" {
fmt.Println("Checking:", cookie)
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) {
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
}
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()
}