2020-07-29 21:11:28 +00:00
package main
import (
"encoding/base64"
"fmt"
"os"
2020-08-19 21:30:54 +00:00
"strconv"
2020-07-29 21:11:28 +00:00
"strings"
"time"
2020-08-16 12:36:54 +00:00
"github.com/dgrijalva/jwt-go"
"github.com/gin-gonic/gin"
"github.com/lithammer/shortuuid/v3"
2020-07-29 21:11:28 +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
}
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.
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
2020-08-16 12:36:54 +00:00
func ( app * appContext ) authenticate ( gc * gin . Context ) {
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" {
app . debug . Println ( "Invalid authorization header" )
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 {
2020-08-16 12:36:54 +00:00
app . debug . Printf ( "Auth denied: %s" , err )
2020-07-29 21:11:28 +00:00
respond ( 401 , "Unauthorized" , gc )
return
}
claims , ok := token . Claims . ( jwt . MapClaims )
2020-08-19 21:30:54 +00:00
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 )
2020-08-23 13:59:07 +00:00
if ! ( ok && token . Valid && claims [ "type" ] . ( string ) == "bearer" && expiry . After ( time . Now ( ) ) ) {
app . debug . Printf ( "Auth denied: Invalid token" )
2020-07-29 21:11:28 +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
2020-08-16 12:36:54 +00:00
for _ , user := range app . users {
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 {
2020-08-23 13:59:07 +00:00
app . debug . Printf ( "Couldn't find user ID \"%s\"" , 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 )
app . debug . Println ( "Auth succeeded" )
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.
}
2020-11-12 21:04:35 +00:00
// @Summary Grabs an API token using username & password.
2020-11-12 21:25:52 +00:00
// @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 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 ) {
2020-08-16 12:36:54 +00:00
app . info . Println ( "Token requested (login attempt)" )
2020-07-29 21:11:28 +00:00
header := strings . SplitN ( gc . Request . Header . Get ( "Authorization" ) , " " , 2 )
auth , _ := base64 . StdEncoding . DecodeString ( header [ 1 ] )
creds := strings . SplitN ( string ( auth ) , ":" , 2 )
2020-08-23 13:59:07 +00:00
var userID , jfID string
2020-11-12 21:04:35 +00:00
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
2020-08-23 13:59:07 +00:00
}
2020-11-12 21:04:35 +00:00
}
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
2020-08-20 19:20:31 +00:00
}
2020-11-12 21:04:35 +00:00
app . err . Printf ( "Auth failed: Couldn't authenticate with Jellyfin (%d/%s)" , status , err )
respond ( 500 , "Jellyfin error" , gc )
2020-07-29 21:11:28 +00:00
return
}
2020-11-12 21:04:35 +00:00
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 )
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
}
2020-11-12 21:04:35 +00:00
app . debug . Printf ( "Token generated for user \"%s\"" , creds [ 0 ] )
app . users = append ( app . users , newUser )
2020-07-29 21:11:28 +00:00
}
2020-11-12 21:04:35 +00:00
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
2020-07-29 21:11:28 +00:00
}
2020-11-12 21:04:35 +00:00
gc . SetCookie ( "refresh" , refresh , ( 3600 * 24 ) , "/" , gc . Request . URL . Hostname ( ) , true , true )
gc . JSON ( 200 , getTokenDTO { token } )
}
// @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 ) {
app . debug . Println ( "Token requested (refresh token)" )
cookie , err := gc . Cookie ( "refresh" )
if err != nil || cookie == "" {
app . debug . Printf ( "getTokenRefresh denied: Couldn't get token: %s" , err )
respond ( 400 , "Couldn't get token" , gc )
return
}
for _ , token := range app . invalidTokens {
if cookie == token {
app . debug . Println ( "getTokenRefresh: Invalid token" )
respond ( 401 , "Invalid token" , 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 {
app . debug . Println ( "getTokenRefresh: Invalid token" )
respond ( 400 , "Invalid token" , gc )
return
}
claims , ok := token . Claims . ( jwt . MapClaims )
expiryUnix , err := strconv . ParseInt ( claims [ "exp" ] . ( string ) , 10 , 64 )
if err != nil {
app . debug . Printf ( "getTokenRefresh: Invalid token expiry: %s" , err )
respond ( 401 , "Invalid token" , gc )
return
}
expiry := time . Unix ( expiryUnix , 0 )
if ! ( ok && token . Valid && claims [ "type" ] . ( string ) == "refresh" && expiry . After ( time . Now ( ) ) ) {
app . debug . Printf ( "getTokenRefresh: Invalid token: %s" , err )
respond ( 401 , "Invalid token" , gc )
return
}
userID := claims [ "id" ] . ( string )
jfID := claims [ "jfid" ] . ( string )
jwt , refresh , err := CreateToken ( userID , jfID )
if err != nil {
app . err . Printf ( "getTokenRefresh 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 { jwt } )
2020-07-29 21:11:28 +00:00
}