|
|
@ -9,6 +9,7 @@ import (
|
|
|
|
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/golang-jwt/jwt"
|
|
|
|
"github.com/golang-jwt/jwt"
|
|
|
|
|
|
|
|
lm "github.com/hrfee/jfa-go/logmessages"
|
|
|
|
"github.com/hrfee/mediabrowser"
|
|
|
|
"github.com/hrfee/mediabrowser"
|
|
|
|
"github.com/lithammer/shortuuid/v3"
|
|
|
|
"github.com/lithammer/shortuuid/v3"
|
|
|
|
)
|
|
|
|
)
|
|
|
@ -41,6 +42,8 @@ func (app *appContext) webAuth() gin.HandlerFunc {
|
|
|
|
return app.authenticate
|
|
|
|
return app.authenticate
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (app *appContext) authLog(v any) { app.debug.Printf(lm.FailedAuthRequest, v) }
|
|
|
|
|
|
|
|
|
|
|
|
// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens.
|
|
|
|
// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens.
|
|
|
|
func CreateToken(userId, jfId string, admin bool) (string, string, error) {
|
|
|
|
func CreateToken(userId, jfId string, admin bool) (string, string, error) {
|
|
|
|
var token, refresh string
|
|
|
|
var token, refresh string
|
|
|
@ -72,32 +75,26 @@ func (app *appContext) decodeValidateAuthHeader(gc *gin.Context) (claims jwt.Map
|
|
|
|
ok = false
|
|
|
|
ok = false
|
|
|
|
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
|
|
|
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
|
|
|
|
if header[0] != "Bearer" {
|
|
|
|
if header[0] != "Bearer" {
|
|
|
|
app.debug.Println("Invalid authorization header")
|
|
|
|
app.authLog(lm.InvalidAuthHeader)
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
token, err := jwt.Parse(string(header[1]), checkToken)
|
|
|
|
token, err := jwt.Parse(string(header[1]), checkToken)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
app.debug.Printf("Auth denied: %s", err)
|
|
|
|
app.authLog(fmt.Sprintf(lm.FailedParseJWT, err))
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
claims, ok = token.Claims.(jwt.MapClaims)
|
|
|
|
claims, ok = token.Claims.(jwt.MapClaims)
|
|
|
|
if !ok {
|
|
|
|
if !ok {
|
|
|
|
app.debug.Println("Invalid JWT")
|
|
|
|
app.authLog(lm.FailedCastJWT)
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
expiryUnix := int64(claims["exp"].(float64))
|
|
|
|
expiryUnix := int64(claims["exp"].(float64))
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
app.debug.Printf("Auth denied: %s", err)
|
|
|
|
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
|
|
|
|
ok = false
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
expiry := time.Unix(expiryUnix, 0)
|
|
|
|
expiry := time.Unix(expiryUnix, 0)
|
|
|
|
if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) {
|
|
|
|
if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) {
|
|
|
|
app.debug.Printf("Auth denied: Invalid token")
|
|
|
|
app.authLog(lm.InvalidJWT)
|
|
|
|
// app.debug.Printf("Expiry: %+v, OK: %t, Valid: %t, ClaimType: %s\n", expiry, ok, token.Valid, claims["type"].(string))
|
|
|
|
// app.debug.Printf("Expiry: %+v, OK: %t, Valid: %t, ClaimType: %s\n", expiry, ok, token.Valid, claims["type"].(string))
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
ok = false
|
|
|
|
ok = false
|
|
|
@ -115,7 +112,7 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
isAdminToken := claims["admin"].(bool)
|
|
|
|
isAdminToken := claims["admin"].(bool)
|
|
|
|
if !isAdminToken {
|
|
|
|
if !isAdminToken {
|
|
|
|
app.debug.Printf("Auth denied: Token was not for admin access")
|
|
|
|
app.authLog(lm.NonAdminToken)
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -130,14 +127,13 @@ func (app *appContext) authenticate(gc *gin.Context) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !match {
|
|
|
|
if !match {
|
|
|
|
app.debug.Printf("Couldn't find user ID \"%s\"", userID)
|
|
|
|
app.authLog(fmt.Sprintf(lm.NonAdminUser, userID))
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
gc.Set("jfId", jfID)
|
|
|
|
gc.Set("jfId", jfID)
|
|
|
|
gc.Set("userId", userID)
|
|
|
|
gc.Set("userId", userID)
|
|
|
|
gc.Set("userMode", false)
|
|
|
|
gc.Set("userMode", false)
|
|
|
|
app.debug.Println("Auth succeeded")
|
|
|
|
|
|
|
|
gc.Next()
|
|
|
|
gc.Next()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -160,7 +156,7 @@ func (app *appContext) decodeValidateLoginHeader(gc *gin.Context, userpage bool)
|
|
|
|
password = creds[1]
|
|
|
|
password = creds[1]
|
|
|
|
ok = false
|
|
|
|
ok = false
|
|
|
|
if username == "" || password == "" {
|
|
|
|
if username == "" || password == "" {
|
|
|
|
app.logIpDebug(gc, userpage, "Auth denied: blank username/password")
|
|
|
|
app.logIpDebug(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.EmptyUserOrPass))
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -173,16 +169,16 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
|
|
|
|
user, status, err := app.authJf.Authenticate(username, password)
|
|
|
|
user, status, err := app.authJf.Authenticate(username, password)
|
|
|
|
if status != 200 || err != nil {
|
|
|
|
if status != 200 || err != nil {
|
|
|
|
if status == 401 || status == 400 {
|
|
|
|
if status == 401 || status == 400 {
|
|
|
|
app.logIpInfo(gc, userpage, "Auth denied: Invalid username/password (Jellyfin)")
|
|
|
|
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass))
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if status == 403 {
|
|
|
|
if status == 403 {
|
|
|
|
app.logIpInfo(gc, userpage, "Auth denied: Jellyfin account disabled")
|
|
|
|
app.logIpInfo(gc, userpage, fmt.Sprintf(lm.FailedAuthRequest, lm.UserDisabled))
|
|
|
|
respond(403, "yourAccountWasDisabled", gc)
|
|
|
|
respond(403, "yourAccountWasDisabled", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
app.err.Printf("Auth failed: Couldn't authenticate with Jellyfin (%d/%s)", status, err)
|
|
|
|
app.authLog(fmt.Sprintf(lm.FailedAuthJellyfin, app.jf.Server, status, err))
|
|
|
|
respond(500, "Jellyfin error", gc)
|
|
|
|
respond(500, "Jellyfin error", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -199,7 +195,7 @@ func (app *appContext) validateJellyfinCredentials(username, password string, gc
|
|
|
|
// @tags Auth
|
|
|
|
// @tags Auth
|
|
|
|
// @Security getTokenAuth
|
|
|
|
// @Security getTokenAuth
|
|
|
|
func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|
|
|
func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|
|
|
app.logIpInfo(gc, false, "Token requested (login attempt)")
|
|
|
|
app.logIpInfo(gc, false, fmt.Sprintf(lm.RequestingToken, lm.TokenLoginAttempt))
|
|
|
|
username, password, ok := app.decodeValidateLoginHeader(gc, false)
|
|
|
|
username, password, ok := app.decodeValidateLoginHeader(gc, false)
|
|
|
|
if !ok {
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
return
|
|
|
@ -209,13 +205,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|
|
|
for _, user := range app.adminUsers {
|
|
|
|
for _, user := range app.adminUsers {
|
|
|
|
if user.Username == username && user.Password == password {
|
|
|
|
if user.Username == username && user.Password == password {
|
|
|
|
match = true
|
|
|
|
match = true
|
|
|
|
app.debug.Println("Found existing user")
|
|
|
|
|
|
|
|
userID = user.UserID
|
|
|
|
userID = user.UserID
|
|
|
|
break
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !app.jellyfinLogin && !match {
|
|
|
|
if !app.jellyfinLogin && !match {
|
|
|
|
app.logIpInfo(gc, false, "Auth denied: Invalid username/password")
|
|
|
|
app.logIpInfo(gc, false, fmt.Sprintf(lm.FailedAuthRequest, lm.InvalidUserOrPass))
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -233,7 +228,7 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator)
|
|
|
|
accountsAdmin = accountsAdmin || (adminOnly && user.Policy.IsAdministrator)
|
|
|
|
if !accountsAdmin {
|
|
|
|
if !accountsAdmin {
|
|
|
|
app.debug.Printf("Auth denied: Users \"%s\" isn't admin", username)
|
|
|
|
app.authLog(fmt.Sprintf(lm.NonAdminUser, username))
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
respond(401, "Unauthorized", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -243,12 +238,12 @@ func (app *appContext) getTokenLogin(gc *gin.Context) {
|
|
|
|
newUser := User{
|
|
|
|
newUser := User{
|
|
|
|
UserID: userID,
|
|
|
|
UserID: userID,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
app.debug.Printf("Token generated for user \"%s\"", username)
|
|
|
|
app.debug.Printf(lm.GenerateToken, username)
|
|
|
|
app.adminUsers = append(app.adminUsers, newUser)
|
|
|
|
app.adminUsers = append(app.adminUsers, newUser)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
token, refresh, err := CreateToken(userID, jfID, true)
|
|
|
|
token, refresh, err := CreateToken(userID, jfID, true)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
app.err.Printf("getToken failed: Couldn't generate token (%s)", err)
|
|
|
|
app.err.Printf(lm.FailedGenerateToken, err)
|
|
|
|
respond(500, "Couldn't generate token", gc)
|
|
|
|
respond(500, "Couldn't generate token", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -261,35 +256,29 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
|
|
|
|
ok = false
|
|
|
|
ok = false
|
|
|
|
cookie, err := gc.Cookie(cookieName)
|
|
|
|
cookie, err := gc.Cookie(cookieName)
|
|
|
|
if err != nil || cookie == "" {
|
|
|
|
if err != nil || cookie == "" {
|
|
|
|
app.debug.Printf("getTokenRefresh denied: Couldn't get token: %s", err)
|
|
|
|
app.authLog(fmt.Sprintf(lm.FailedGetCookies, cookieName, err))
|
|
|
|
respond(400, "Couldn't get token", gc)
|
|
|
|
respond(400, "Couldn't get token", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
for _, token := range app.invalidTokens {
|
|
|
|
for _, token := range app.invalidTokens {
|
|
|
|
if cookie == token {
|
|
|
|
if cookie == token {
|
|
|
|
app.debug.Println("getTokenRefresh: Invalid token")
|
|
|
|
app.authLog(lm.LocallyInvalidatedJWT)
|
|
|
|
respond(401, "Invalid token", gc)
|
|
|
|
respond(401, lm.InvalidJWT, gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
token, err := jwt.Parse(cookie, checkToken)
|
|
|
|
token, err := jwt.Parse(cookie, checkToken)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
app.debug.Println("getTokenRefresh: Invalid token")
|
|
|
|
app.authLog(lm.FailedParseJWT)
|
|
|
|
respond(400, "Invalid token", gc)
|
|
|
|
respond(400, lm.InvalidJWT, gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|
claims, ok = token.Claims.(jwt.MapClaims)
|
|
|
|
claims, ok = token.Claims.(jwt.MapClaims)
|
|
|
|
expiryUnix := int64(claims["exp"].(float64))
|
|
|
|
expiryUnix := int64(claims["exp"].(float64))
|
|
|
|
if err != nil {
|
|
|
|
|
|
|
|
app.debug.Printf("getTokenRefresh: Invalid token expiry: %s", err)
|
|
|
|
|
|
|
|
respond(401, "Invalid token", gc)
|
|
|
|
|
|
|
|
ok = false
|
|
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
expiry := time.Unix(expiryUnix, 0)
|
|
|
|
expiry := time.Unix(expiryUnix, 0)
|
|
|
|
if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
|
|
|
|
if !(ok && token.Valid && claims["type"].(string) == "refresh" && expiry.After(time.Now())) {
|
|
|
|
app.debug.Printf("getTokenRefresh: Invalid token: %+v", err)
|
|
|
|
app.authLog(lm.InvalidJWT)
|
|
|
|
respond(401, "Invalid token", gc)
|
|
|
|
respond(401, lm.InvalidJWT, gc)
|
|
|
|
ok = false
|
|
|
|
ok = false
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
@ -304,7 +293,7 @@ func (app *appContext) decodeValidateRefreshCookie(gc *gin.Context, cookieName s
|
|
|
|
// @Router /token/refresh [get]
|
|
|
|
// @Router /token/refresh [get]
|
|
|
|
// @tags Auth
|
|
|
|
// @tags Auth
|
|
|
|
func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
|
|
|
func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
|
|
|
app.logIpInfo(gc, false, "Token requested (refresh token)")
|
|
|
|
app.logIpInfo(gc, false, fmt.Sprintf(lm.RequestingToken, lm.TokenRefresh))
|
|
|
|
claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
|
|
|
|
claims, ok := app.decodeValidateRefreshCookie(gc, "refresh")
|
|
|
|
if !ok {
|
|
|
|
if !ok {
|
|
|
|
return
|
|
|
|
return
|
|
|
@ -313,7 +302,7 @@ func (app *appContext) getTokenRefresh(gc *gin.Context) {
|
|
|
|
jfID := claims["jfid"].(string)
|
|
|
|
jfID := claims["jfid"].(string)
|
|
|
|
jwt, refresh, err := CreateToken(userID, jfID, true)
|
|
|
|
jwt, refresh, err := CreateToken(userID, jfID, true)
|
|
|
|
if err != nil {
|
|
|
|
if err != nil {
|
|
|
|
app.err.Printf("getTokenRefresh failed: Couldn't generate token (%s)", err)
|
|
|
|
app.err.Printf(lm.FailedGenerateToken, err)
|
|
|
|
respond(500, "Couldn't generate token", gc)
|
|
|
|
respond(500, "Couldn't generate token", gc)
|
|
|
|
return
|
|
|
|
return
|
|
|
|
}
|
|
|
|
}
|
|
|
|