You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
jfa-go/auth.go

249 lines
7.6 KiB

4 years ago
package main
import (
"encoding/base64"
"fmt"
"os"
"strconv"
4 years ago
"strings"
"time"
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/lithammer/shortuuid/v3"
4 years ago
)
func (app *appContext) webAuth() gin.HandlerFunc {
return app.authenticate
4 years ago
}
4 years ago
// 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
}
// Check header for token
func (app *appContext) authenticate(gc *gin.Context) {
4 years ago
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
if header[0] != "Basic" {
app.debug.Println("Invalid authentication header")
4 years ago
respond(401, "Unauthorized", gc)
return
}
auth, _ := base64.StdEncoding.DecodeString(header[1])
creds := strings.SplitN(string(auth), ":", 2)
4 years ago
token, err := jwt.Parse(creds[0], checkToken)
4 years ago
if err != nil {
app.debug.Printf("Auth denied: %s", err)
4 years ago
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)
4 years ago
if !(ok && token.Valid && claims["type"].(string) == "bearer" && expiry.After(time.Now())) {
app.debug.Printf("Auth denied: Invalid token")
4 years ago
respond(401, "Unauthorized", gc)
return
}
4 years ago
userID := claims["id"].(string)
jfID := claims["jfid"].(string)
4 years ago
match := false
for _, user := range app.users {
4 years ago
if user.UserID == userID {
4 years ago
match = true
4 years ago
break
4 years ago
}
}
if !match {
4 years ago
app.debug.Printf("Couldn't find user ID \"%s\"", userID)
4 years ago
respond(401, "Unauthorized", gc)
return
}
4 years ago
gc.Set("jfId", jfID)
gc.Set("userId", userID)
app.debug.Println("Auth succeeded")
4 years ago
gc.Next()
}
4 years ago
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
}
type getTokenDTO struct {
Token string `json:"token" example:"kjsdklsfdkljfsjsdfklsdfkldsfjdfskjsdfjklsdf"` // API token for use with everything else.
}
4 years ago
// getToken checks the header for a username and password, as well as checking the refresh cookie.
// @Summary Grabs an API token using username & password, or via a refresh cookie.
// @description Click the lock icon next to this, login with your normal jfa-go credentials. Click 'try it out', then 'execute' and an API Key will be returned, copy it (not including quotes). On any of the other routes, click the lock icon and use the token as your -Username-. The password can be anything.
// @Produce json
// @Success 200 {object} getTokenDTO
// @Failure 401 {object} stringResponse
// @Router /getToken [get]
// @tags Auth
// @Security getTokenAuth
4 years ago
func (app *appContext) getToken(gc *gin.Context) {
app.info.Println("Token requested (login attempt)")
4 years ago
header := strings.SplitN(gc.Request.Header.Get("Authorization"), " ", 2)
auth, _ := base64.StdEncoding.DecodeString(header[1])
creds := strings.SplitN(string(auth), ":", 2)
4 years ago
// 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")
4 years ago
userID = user.UserID
break
}
4 years ago
}
4 years ago
if !app.jellyfinLogin && !match {
app.info.Println("Auth denied: Invalid username/password")
4 years ago
respond(401, "Unauthorized", gc)
return
}
4 years ago
if !match {
var status int
var err error
var user map[string]interface{}
user, status, err = app.authJf.Authenticate(creds[0], creds[1])
4 years ago
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")
4 years ago
if err == nil && cookie != "" {
for _, token := range app.invalidTokens {
if cookie == token {
4 years ago
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
}
}
4 years ago
token, err := jwt.Parse(cookie, checkToken)
if err != nil {
4 years ago
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 {
4 years ago
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)
4 years ago
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
}
4 years ago
app.debug.Printf("getToken: Invalid token (%s) but username/password provided", err)
return
}
4 years ago
userID = claims["id"].(string)
jfID = claims["jfid"].(string)
valid = true
4 years ago
}
}
4 years ago
checkCookie()
if !valid && !noLogin {
checkLogin()
4 years ago
}
4 years ago
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, getTokenDTO{token})
4 years ago
} else {
gc.AbortWithStatus(401)
}
4 years ago
}