mirror of
https://github.com/hrfee/jfa-go.git
synced 2024-12-22 09:00:10 +00:00
cleaned up auth
This commit is contained in:
parent
0e39b2b699
commit
d12335bb4a
1
.gitignore
vendored
1
.gitignore
vendored
@ -12,3 +12,4 @@ dist/*
|
|||||||
jfa-go
|
jfa-go
|
||||||
build/
|
build/
|
||||||
pkg/
|
pkg/
|
||||||
|
old/
|
||||||
|
376
auth.go
376
auth.go
@ -17,169 +17,9 @@ func (app *appContext) webAuth() gin.HandlerFunc {
|
|||||||
return app.authenticate
|
return app.authenticate
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *appContext) authenticate(gc *gin.Context) {
|
// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens.
|
||||||
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
func CreateToken(userId, jfId string) (string, string, error) {
|
||||||
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) {
|
|
||||||
var token, refresh string
|
var token, refresh string
|
||||||
var err error
|
|
||||||
claims := jwt.MapClaims{
|
claims := jwt.MapClaims{
|
||||||
"valid": true,
|
"valid": true,
|
||||||
"id": userId,
|
"id": userId,
|
||||||
@ -189,30 +29,218 @@ func CreateToken(userId string, jfId string) (string, string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tk := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
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 {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
claims["exp"] = strconv.FormatInt(time.Now().Add(time.Hour*24).Unix(), 10)
|
||||||
claims = jwt.MapClaims{
|
claims["type"] = "refresh"
|
||||||
"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)
|
tk = jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||||
refresh, err = tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
refresh, err = tk.SignedString([]byte(os.Getenv("JFA_SECRET")))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", "", err
|
return "", "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
return token, refresh, nil
|
return token, refresh, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func respond(code int, message string, gc *gin.Context) {
|
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.JSON(code, resp)
|
||||||
gc.Abort()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
2
main.go
2
main.go
@ -324,7 +324,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
if !firstRun {
|
if !firstRun {
|
||||||
router.GET("/", app.AdminPage)
|
router.GET("/", app.AdminPage)
|
||||||
router.GET("/getToken", app.GetToken)
|
router.GET("/getToken", app.getToken)
|
||||||
router.POST("/newUser", app.NewUser)
|
router.POST("/newUser", app.NewUser)
|
||||||
router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(app.local_path, "static"), false)))
|
router.Use(static.Serve("/invite/", static.LocalFile(filepath.Join(app.local_path, "static"), false)))
|
||||||
router.GET("/invite/:invCode", app.InviteProxy)
|
router.GET("/invite/:invCode", app.InviteProxy)
|
||||||
|
Loading…
Reference in New Issue
Block a user