2020-07-29 21:11:28 +00:00
package main
import (
"encoding/base64"
2024-08-06 13:48:31 +00:00
"errors"
2020-07-29 21:11:28 +00:00
"fmt"
"os"
"strings"
"time"
2020-08-16 12:36:54 +00:00
"github.com/gin-gonic/gin"
2021-07-27 09:08:01 +00:00
"github.com/golang-jwt/jwt"
2024-08-01 19:17:05 +00:00
lm "github.com/hrfee/jfa-go/logmessages"
2023-06-15 20:32:18 +00:00
"github.com/hrfee/mediabrowser"
2020-08-16 12:36:54 +00:00
"github.com/lithammer/shortuuid/v3"
2020-07-29 21:11:28 +00:00
)
2023-06-15 20:32:18 +00:00
const (
TOKEN_VALIDITY_SEC = 20 * 60
REFRESH_TOKEN_VALIDITY_SEC = 3600 * 24
)
2023-12-23 20:54:55 +00:00
func ( app * appContext ) logIpInfo ( gc * gin . Context , user bool , out string ) {
if ( user && LOGIPU ) || ( ! user && LOGIP ) {
2023-12-23 21:47:41 +00:00
out += fmt . Sprintf ( " (ip=%s)" , gc . ClientIP ( ) )
2023-12-23 20:54:55 +00:00
}
2023-12-23 21:47:41 +00:00
app . info . Println ( out )
2023-12-23 20:54:55 +00:00
}
func ( app * appContext ) logIpDebug ( gc * gin . Context , user bool , out string ) {
if ( user && LOGIPU ) || ( ! user && LOGIP ) {
2023-12-23 21:47:41 +00:00
out += fmt . Sprintf ( " (ip=%s)" , gc . ClientIP ( ) )
2023-12-23 20:54:55 +00:00
}
2023-12-23 21:47:41 +00:00
app . debug . Println ( out )
2023-12-23 20:54:55 +00:00
}
func ( app * appContext ) logIpErr ( gc * gin . Context , user bool , out string ) {
if ( user && LOGIPU ) || ( ! user && LOGIP ) {
2023-12-23 21:47:41 +00:00
out += fmt . Sprintf ( " (ip=%s)" , gc . ClientIP ( ) )
2023-12-23 20:54:55 +00:00
}
2023-12-23 21:47:41 +00:00
app . err . Println ( out )
2023-12-23 20:54:55 +00:00
}
2020-08-16 12:36:54 +00:00
func ( app * appContext ) webAuth ( ) gin . HandlerFunc {
return app . authenticate
2020-07-29 21:11:28 +00:00
}
2024-08-10 18:30:14 +00:00
func ( app * appContext ) authLog ( v any ) { app . debug . PrintfCustomLevel ( 4 , lm . FailedAuthRequest , v ) }
2024-08-01 19:17:05 +00:00
2020-08-23 13:59:07 +00:00
// CreateToken returns a web token as well as a refresh token, which can be used to obtain new tokens.
2023-06-15 20:32:18 +00:00
func CreateToken ( userId , jfId string , admin bool ) ( string , string , error ) {
2020-08-23 13:59:07 +00:00
var token , refresh string
claims := jwt . MapClaims {
"valid" : true ,
"id" : userId ,
2023-06-15 20:32:18 +00:00
"exp" : time . Now ( ) . Add ( time . Second * TOKEN_VALIDITY_SEC ) . Unix ( ) ,
2020-08-23 13:59:07 +00:00
"jfid" : jfId ,
2023-06-15 20:32:18 +00:00
"admin" : admin ,
2020-08-23 13:59:07 +00:00
"type" : "bearer" ,
}
tk := jwt . NewWithClaims ( jwt . SigningMethodHS256 , claims )
token , err := tk . SignedString ( [ ] byte ( os . Getenv ( "JFA_SECRET" ) ) )
if err != nil {
return "" , "" , err
}
2023-06-15 20:32:18 +00:00
claims [ "exp" ] = time . Now ( ) . Add ( time . Second * REFRESH_TOKEN_VALIDITY_SEC ) . Unix ( )
2020-08-23 13:59:07 +00:00
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
}
2023-06-15 20:32:18 +00:00
// Caller should return if this returns false.
func ( app * appContext ) decodeValidateAuthHeader ( gc * gin . Context ) ( claims jwt . MapClaims , ok bool ) {
ok = false
2020-07-29 21:11:28 +00:00
header := strings . SplitN ( gc . Request . Header . Get ( "Authorization" ) , " " , 2 )
2020-11-12 21:04:35 +00:00
if header [ 0 ] != "Bearer" {
2024-08-01 19:17:05 +00:00
app . authLog ( lm . InvalidAuthHeader )
2020-07-29 21:11:28 +00:00
respond ( 401 , "Unauthorized" , gc )
return
}
2020-11-12 21:25:52 +00:00
token , err := jwt . Parse ( string ( header [ 1 ] ) , checkToken )
2020-07-29 21:11:28 +00:00
if err != nil {
2024-08-01 19:17:05 +00:00
app . authLog ( fmt . Sprintf ( lm . FailedParseJWT , err ) )
2020-07-29 21:11:28 +00:00
respond ( 401 , "Unauthorized" , gc )
return
}
2023-06-15 20:32:18 +00:00
claims , ok = token . Claims . ( jwt . MapClaims )
if ! ok {
2024-08-01 19:17:05 +00:00
app . authLog ( lm . FailedCastJWT )
2023-06-15 20:32:18 +00:00
respond ( 401 , "Unauthorized" , gc )
return
}
2021-08-22 13:13:44 +00:00
expiryUnix := int64 ( claims [ "exp" ] . ( float64 ) )
2020-08-19 21:30:54 +00:00
expiry := time . Unix ( expiryUnix , 0 )
2020-08-23 13:59:07 +00:00
if ! ( ok && token . Valid && claims [ "type" ] . ( string ) == "bearer" && expiry . After ( time . Now ( ) ) ) {
2024-08-01 19:17:05 +00:00
app . authLog ( lm . InvalidJWT )
2023-06-15 20:59:34 +00:00
// app.debug.Printf("Expiry: %+v, OK: %t, Valid: %t, ClaimType: %s\n", expiry, ok, token.Valid, claims["type"].(string))
2020-07-29 21:11:28 +00:00
respond ( 401 , "Unauthorized" , gc )
2023-06-15 20:59:34 +00:00
ok = false
2020-07-29 21:11:28 +00:00
return
}
2023-06-15 20:32:18 +00:00
ok = true
return
}
// Check header for token
func ( app * appContext ) authenticate ( gc * gin . Context ) {
claims , ok := app . decodeValidateAuthHeader ( gc )
if ! ok {
return
}
isAdminToken := claims [ "admin" ] . ( bool )
if ! isAdminToken {
2024-08-01 19:17:05 +00:00
app . authLog ( lm . NonAdminToken )
2023-06-15 20:32:18 +00:00
respond ( 401 , "Unauthorized" , gc )
return
}
2020-08-23 13:59:07 +00:00
userID := claims [ "id" ] . ( string )
jfID := claims [ "jfid" ] . ( string )
2020-07-29 21:11:28 +00:00
match := false
2023-06-15 20:32:18 +00:00
for _ , user := range app . adminUsers {
2020-08-23 13:59:07 +00:00
if user . UserID == userID {
2020-07-29 21:11:28 +00:00
match = true
2020-08-23 13:59:07 +00:00
break
2020-07-29 21:11:28 +00:00
}
}
if ! match {
2024-08-01 19:17:05 +00:00
app . authLog ( fmt . Sprintf ( lm . NonAdminUser , userID ) )
2020-07-29 21:11:28 +00:00
respond ( 401 , "Unauthorized" , gc )
return
}
2020-08-23 13:59:07 +00:00
gc . Set ( "jfId" , jfID )
gc . Set ( "userId" , userID )
2023-06-15 20:32:18 +00:00
gc . Set ( "userMode" , false )
2020-07-29 21:11:28 +00:00
gc . Next ( )
}
2020-08-23 13:59:07 +00:00
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
}
2020-09-24 17:50:03 +00:00
type getTokenDTO struct {
Token string ` json:"token" example:"kjsdklsfdkljfsjsdfklsdfkldsfjdfskjsdfjklsdf" ` // API token for use with everything else.
}
2023-12-23 20:54:55 +00:00
func ( app * appContext ) decodeValidateLoginHeader ( gc * gin . Context , userpage bool ) ( username , password string , ok bool ) {
2023-06-15 20:32:18 +00:00
header := strings . SplitN ( gc . Request . Header . Get ( "Authorization" ) , " " , 2 )
auth , _ := base64 . StdEncoding . DecodeString ( header [ 1 ] )
creds := strings . SplitN ( string ( auth ) , ":" , 2 )
username = creds [ 0 ]
password = creds [ 1 ]
ok = false
if username == "" || password == "" {
2024-08-01 19:17:05 +00:00
app . logIpDebug ( gc , userpage , fmt . Sprintf ( lm . FailedAuthRequest , lm . EmptyUserOrPass ) )
2023-06-15 20:32:18 +00:00
respond ( 401 , "Unauthorized" , gc )
return
}
ok = true
return
}
2023-12-23 20:54:55 +00:00
func ( app * appContext ) validateJellyfinCredentials ( username , password string , gc * gin . Context , userpage bool ) ( user mediabrowser . User , ok bool ) {
2023-06-15 20:32:18 +00:00
ok = false
2024-08-06 13:48:31 +00:00
user , err := app . authJf . Authenticate ( username , password )
if err != nil {
2024-08-29 13:30:00 +00:00
if errors . As ( err , & mediabrowser . ErrUnauthorized { } ) {
2024-08-01 19:17:05 +00:00
app . logIpInfo ( gc , userpage , fmt . Sprintf ( lm . FailedAuthRequest , lm . InvalidUserOrPass ) )
2023-06-15 20:32:18 +00:00
respond ( 401 , "Unauthorized" , gc )
return
2024-08-29 13:30:00 +00:00
} else if errors . As ( err , & mediabrowser . ErrForbidden { } ) {
2024-08-01 19:17:05 +00:00
app . logIpInfo ( gc , userpage , fmt . Sprintf ( lm . FailedAuthRequest , lm . UserDisabled ) )
2023-06-17 12:57:48 +00:00
respond ( 403 , "yourAccountWasDisabled" , gc )
return
}
2024-08-29 13:30:00 +00:00
app . authLog ( fmt . Sprintf ( lm . FailedAuthJellyfin , app . jf . Server , 0 , err ) )
2023-06-15 20:32:18 +00:00
respond ( 500 , "Jellyfin error" , gc )
return
}
ok = true
return
}
2020-11-12 21:04:35 +00:00
// @Summary Grabs an API token using username & password.
2021-11-15 00:17:39 +00:00
// @description If viewing docs locally, 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 set the API key as "Bearer `your api key`".
2020-09-24 17:50:03 +00:00
// @Produce json
// @Success 200 {object} getTokenDTO
// @Failure 401 {object} stringResponse
2020-11-12 21:04:35 +00:00
// @Router /token/login [get]
2020-09-24 17:50:03 +00:00
// @tags Auth
// @Security getTokenAuth
2020-11-12 21:04:35 +00:00
func ( app * appContext ) getTokenLogin ( gc * gin . Context ) {
2024-08-01 19:17:05 +00:00
app . logIpInfo ( gc , false , fmt . Sprintf ( lm . RequestingToken , lm . TokenLoginAttempt ) )
2023-12-23 20:54:55 +00:00
username , password , ok := app . decodeValidateLoginHeader ( gc , false )
2023-06-15 20:32:18 +00:00
if ! ok {
2020-11-12 21:04:35 +00:00
return
}
2023-06-15 20:32:18 +00:00
var userID , jfID string
2020-11-12 21:04:35 +00:00
match := false
2023-06-15 20:32:18 +00:00
for _ , user := range app . adminUsers {
if user . Username == username && user . Password == password {
2020-11-12 21:04:35 +00:00
match = true
userID = user . UserID
break
2020-08-23 13:59:07 +00:00
}
2020-11-12 21:04:35 +00:00
}
if ! app . jellyfinLogin && ! match {
2024-08-01 19:17:05 +00:00
app . logIpInfo ( gc , false , fmt . Sprintf ( lm . FailedAuthRequest , lm . InvalidUserOrPass ) )
2020-11-12 21:04:35 +00:00
respond ( 401 , "Unauthorized" , gc )
return
}
if ! match {
2023-12-23 20:54:55 +00:00
user , ok := app . validateJellyfinCredentials ( username , password , gc , false )
2023-06-15 20:32:18 +00:00
if ! ok {
2020-07-29 21:11:28 +00:00
return
}
2021-02-19 00:47:01 +00:00
jfID = user . ID
2022-01-09 19:29:17 +00:00
if ! app . config . Section ( "ui" ) . Key ( "allow_all" ) . MustBool ( false ) {
accountsAdmin := false
adminOnly := app . config . Section ( "ui" ) . Key ( "admin_only" ) . MustBool ( true )
2023-06-20 11:19:24 +00:00
if emailStore , ok := app . storage . GetEmailsKey ( jfID ) ; ok {
2022-01-09 19:29:17 +00:00
accountsAdmin = emailStore . Admin
}
accountsAdmin = accountsAdmin || ( adminOnly && user . Policy . IsAdministrator )
if ! accountsAdmin {
2024-08-01 19:17:05 +00:00
app . authLog ( fmt . Sprintf ( lm . NonAdminUser , username ) )
2020-11-12 21:04:35 +00:00
respond ( 401 , "Unauthorized" , gc )
2020-08-23 13:59:07 +00:00
return
}
}
2020-11-12 21:04:35 +00:00
// New users are only added when using jellyfinLogin.
userID = shortuuid . New ( )
newUser := User {
UserID : userID ,
2020-07-29 21:11:28 +00:00
}
2024-08-01 19:17:05 +00:00
app . debug . Printf ( lm . GenerateToken , username )
2023-06-15 20:32:18 +00:00
app . adminUsers = append ( app . adminUsers , newUser )
2020-07-29 21:11:28 +00:00
}
2023-06-15 20:32:18 +00:00
token , refresh , err := CreateToken ( userID , jfID , true )
2020-11-12 21:04:35 +00:00
if err != nil {
2024-08-01 19:17:05 +00:00
app . err . Printf ( lm . FailedGenerateToken , err )
2020-11-12 21:04:35 +00:00
respond ( 500 , "Couldn't generate token" , gc )
return
2020-07-29 21:11:28 +00:00
}
2024-08-13 19:39:06 +00:00
// host := gc.Request.URL.Hostname()
host := app . ExternalDomain
2024-08-24 14:25:08 +00:00
// Before you think this is broken: the first "true" arg is for "secure", i.e. only HTTPS!
2023-12-22 17:46:57 +00:00
gc . SetCookie ( "refresh" , refresh , REFRESH_TOKEN_VALIDITY_SEC , "/" , host , true , true )
2020-11-12 21:04:35 +00:00
gc . JSON ( 200 , getTokenDTO { token } )
}
2023-06-18 11:30:23 +00:00
func ( app * appContext ) decodeValidateRefreshCookie ( gc * gin . Context , cookieName string ) ( claims jwt . MapClaims , ok bool ) {
2023-06-15 20:32:18 +00:00
ok = false
2023-06-18 11:30:23 +00:00
cookie , err := gc . Cookie ( cookieName )
2020-11-12 21:04:35 +00:00
if err != nil || cookie == "" {
2024-08-01 19:17:05 +00:00
app . authLog ( fmt . Sprintf ( lm . FailedGetCookies , cookieName , err ) )
2020-11-12 21:04:35 +00:00
respond ( 400 , "Couldn't get token" , gc )
return
}
for _ , token := range app . invalidTokens {
if cookie == token {
2024-08-01 19:17:05 +00:00
app . authLog ( lm . LocallyInvalidatedJWT )
respond ( 401 , lm . InvalidJWT , gc )
2020-08-23 13:59:07 +00:00
return
}
2020-08-19 21:30:54 +00:00
}
2020-11-12 21:04:35 +00:00
token , err := jwt . Parse ( cookie , checkToken )
if err != nil {
2024-08-10 18:30:14 +00:00
app . authLog ( fmt . Sprintf ( lm . FailedParseJWT , err ) )
2024-08-01 19:17:05 +00:00
respond ( 400 , lm . InvalidJWT , gc )
2020-11-12 21:04:35 +00:00
return
}
2023-06-15 20:32:18 +00:00
claims , ok = token . Claims . ( jwt . MapClaims )
2021-08-22 14:00:20 +00:00
expiryUnix := int64 ( claims [ "exp" ] . ( float64 ) )
2020-11-12 21:04:35 +00:00
expiry := time . Unix ( expiryUnix , 0 )
if ! ( ok && token . Valid && claims [ "type" ] . ( string ) == "refresh" && expiry . After ( time . Now ( ) ) ) {
2024-08-01 19:17:05 +00:00
app . authLog ( lm . InvalidJWT )
respond ( 401 , lm . InvalidJWT , gc )
2023-06-15 20:59:34 +00:00
ok = false
2020-11-12 21:04:35 +00:00
return
}
2023-06-15 20:32:18 +00:00
ok = true
return
}
// @Summary Grabs an API token using a refresh token from cookies.
// @Produce json
// @Success 200 {object} getTokenDTO
// @Failure 401 {object} stringResponse
// @Router /token/refresh [get]
// @tags Auth
func ( app * appContext ) getTokenRefresh ( gc * gin . Context ) {
2024-08-01 19:17:05 +00:00
app . logIpInfo ( gc , false , fmt . Sprintf ( lm . RequestingToken , lm . TokenRefresh ) )
2023-06-18 11:30:23 +00:00
claims , ok := app . decodeValidateRefreshCookie ( gc , "refresh" )
2023-06-15 20:32:18 +00:00
if ! ok {
return
}
2020-11-12 21:04:35 +00:00
userID := claims [ "id" ] . ( string )
jfID := claims [ "jfid" ] . ( string )
2023-06-15 20:32:18 +00:00
jwt , refresh , err := CreateToken ( userID , jfID , true )
2020-11-12 21:04:35 +00:00
if err != nil {
2024-08-01 19:17:05 +00:00
app . err . Printf ( lm . FailedGenerateToken , err )
2020-11-12 21:04:35 +00:00
respond ( 500 , "Couldn't generate token" , gc )
return
}
2024-08-13 19:39:06 +00:00
// host := gc.Request.URL.Hostname()
host := app . ExternalDomain
2023-12-22 17:46:57 +00:00
gc . SetCookie ( "refresh" , refresh , REFRESH_TOKEN_VALIDITY_SEC , "/" , host , true , true )
2020-11-12 21:04:35 +00:00
gc . JSON ( 200 , getTokenDTO { jwt } )
2020-07-29 21:11:28 +00:00
}