Fixed access token issues causing errors logging in!

This commit is contained in:
eccentric 2023-12-19 16:47:13 +00:00
parent 25c2e59b16
commit 9e983fff93
13 changed files with 196 additions and 125 deletions

View File

@ -1,7 +1,7 @@
package aid
import (
"math/rand"
m "math/rand"
"os"
"os/signal"
"strconv"
@ -32,7 +32,7 @@ func RandomString(n int) string {
s := make([]rune, n)
for i := range s {
s[i] = letters[rand.Intn(len(letters))]
s[i] = letters[m.Intn(len(letters))]
}
return string(s)
}

74
aid/crypto.go Normal file
View File

@ -0,0 +1,74 @@
package aid
import (
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
)
type keyPair struct {
PrivateKey rsa.PrivateKey
PublicKey rsa.PublicKey
}
var KeyPair = GeneratePublicPrivateKeyPair()
func GeneratePublicPrivateKeyPair() keyPair {
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
publicKey := privateKey.PublicKey
return keyPair{
PrivateKey: *privateKey,
PublicKey: publicKey,
}
}
func (k *keyPair) EncryptAndSign(message []byte) ([]byte, []byte) {
encryptedMessage, _ := rsa.EncryptPKCS1v15(rand.Reader, &k.PublicKey, message)
signature, _ := rsa.SignPKCS1v15(rand.Reader, &k.PrivateKey, 0, encryptedMessage)
return encryptedMessage, signature
}
func (k *keyPair) EncryptAndSignB64(message []byte) (string, string) {
encryptedMessage, signature := k.EncryptAndSign(message)
return Base64Encode(encryptedMessage), Base64Encode(signature)
}
func (k *keyPair) DecryptAndVerify(encryptedMessage []byte, signature []byte) []byte {
decryptedMessage, _ := rsa.DecryptPKCS1v15(rand.Reader, &k.PrivateKey, encryptedMessage)
_ = rsa.VerifyPKCS1v15(&k.PublicKey, 0, encryptedMessage, signature)
return decryptedMessage
}
func (k *keyPair) ExportPrivateKey() []byte {
privateKey := x509.MarshalPKCS1PrivateKey(&k.PrivateKey)
return privateKey
}
func (k *keyPair) ExportPublicKey() []byte {
publicKey := x509.MarshalPKCS1PublicKey(&k.PublicKey)
return publicKey
}
func Base64Encode(input []byte) string {
return base64.StdEncoding.EncodeToString(input)
}
func Base64Decode(input string) ([]byte, bool) {
data, err := base64.StdEncoding.DecodeString(input)
if err != nil {
return []byte{}, true
}
return data, false
}
func Hash(input []byte) string {
shaBytes := sha256.Sum256(input)
return hex.EncodeToString(shaBytes[:])
}

View File

@ -1,44 +0,0 @@
package aid
import (
"fmt"
"github.com/golang-jwt/jwt/v5"
)
func JWTSign(m JSON) (string, error) {
claims := jwt.MapClaims{}
for k, v := range m {
claims[k] = v
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(Config.JWT.Secret))
}
func JWTVerify(tokenString string) (JSON, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return []byte(Config.JWT.Secret), nil
})
if err != nil {
return nil, err
}
if !token.Valid {
return nil, fmt.Errorf("invalid token")
}
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
return nil, fmt.Errorf("invalid claims")
}
json := JSON{}
for k, v := range claims {
json[k] = v
}
return json, nil
}

View File

@ -1,3 +1,16 @@
package aid
type JSON map[string]interface{}
import "github.com/goccy/go-json"
type JSON map[string]interface{}
func JSONFromBytes(input []byte) JSON {
var output JSON
json.Unmarshal(input, &output)
return output
}
func (j *JSON) ToBytes() []byte {
json, _ := json.Marshal(j)
return json
}

View File

@ -145,4 +145,6 @@ func GiveEverything(person *p.Person) {
}
storage.Repo.BulkCreateItems(&items)
aid.Print("Gave everything to " + person.DisplayName)
person.Save()
}

View File

@ -366,7 +366,7 @@ func GenerateRandomStorefront() {
}
minimumItems := 8
if aid.Config.Fortnite.Season < 14 {
if aid.Config.Fortnite.Season < 11 {
minimumItems = 3
}

View File

@ -6,6 +6,7 @@ import (
"github.com/ectrc/snow/aid"
p "github.com/ectrc/snow/person"
"github.com/ectrc/snow/storage"
"github.com/gofiber/fiber/v2"
)
@ -37,22 +38,19 @@ func PostFortniteToken(c *fiber.Ctx) error {
}
func PostTokenClientCredentials(c *fiber.Ctx, body *FortniteTokenBody) error {
credentials, err := aid.JWTSign(aid.JSON{
"snow_id": 0, // custom
"creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"),
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer)
}
client, sig := aid.KeyPair.EncryptAndSignB64([]byte(c.IP()))
hash := aid.Hash([]byte(client + "." + sig))
return c.Status(fiber.StatusOK).JSON(aid.JSON{
"access_token": "eg1~"+credentials,
"access_token": hash,
"token_type": "bearer",
"client_id": c.IP(),
"client_service": "fortnite",
"internal_client": true,
"expires_in": 3600,
"expires_at": time.Now().Add(time.Hour).Format("2006-01-02T15:04:05.999Z"),
"product_id": "prod-fn",
"sandbox_id": "fn",
})
}
@ -66,24 +64,31 @@ func PostTokenPassword(c *fiber.Ctx, body *FortniteTokenBody) error {
return c.Status(fiber.StatusBadRequest).JSON(aid.ErrorBadRequest("No Account Found"))
}
access, err := aid.JWTSign(aid.JSON{
"snow_id": person.ID, // custom
"creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"),
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer)
}
access, ac_sig := aid.KeyPair.EncryptAndSignB64([]byte(person.ID))
ac_hash := aid.Hash([]byte(access + "." + ac_sig))
refresh, err := aid.JWTSign(aid.JSON{
"snow_id": person.ID,
"creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"),
})
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer)
ac_token := &storage.DB_GameToken{
ID: ac_hash,
PersonID: person.ID,
AccessToken: access + "." + ac_sig,
Type: "access",
}
storage.Repo.SaveToken(ac_token)
refresh, re_sig := aid.KeyPair.EncryptAndSignB64([]byte(person.ID))
re_hash := aid.Hash([]byte(refresh + "." + re_sig))
re_token := &storage.DB_GameToken{
ID: re_hash,
PersonID: person.ID,
AccessToken: refresh + "." + re_sig,
Type: "refresh",
}
storage.Repo.SaveToken(re_token)
return c.Status(fiber.StatusOK).JSON(aid.JSON{
"access_token": "eg1~"+access,
// "access_token": access + "." + ac_sig,
"access_token": ac_hash,
"account_id": person.ID,
"client_id": c.IP(),
"client_service": "fortnite",
@ -95,8 +100,11 @@ func PostTokenPassword(c *fiber.Ctx, body *FortniteTokenBody) error {
"internal_client": true,
"refresh_expires": 86400,
"refresh_expires_at": time.Now().Add(time.Hour * 24).Format("2006-01-02T15:04:05.999Z"),
"refresh_token": "eg1~"+refresh,
// "refresh_token": refresh + "." + re_sig,
"refresh_token": re_hash,
"token_type": "bearer",
"product_id": "prod-fn",
"sandbox_id": "fn",
})
}
@ -105,21 +113,13 @@ func GetOAuthVerify(c *fiber.Ctx) error {
if auth == "" {
return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Authorization Header is empty"))
}
real := strings.ReplaceAll(auth, "bearer eg1~", "")
real := strings.ReplaceAll(auth, "bearer ", "")
claims, err := aid.JWTVerify(real)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token"))
}
if claims["snow_id"] == nil {
return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token"))
}
snowId, ok := claims["snow_id"].(string)
if !ok {
found := storage.Repo.GetToken(real)
if found == nil {
return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token"))
}
snowId := found.PersonID
person := p.Find(snowId)
if person == nil {
@ -128,7 +128,7 @@ func GetOAuthVerify(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(aid.JSON{
"app": "fortnite",
"token": "eg1~"+real,
"token": real,
"token_type": "bearer",
"expires_at": time.Now().Add(time.Hour * 24).Format("2006-01-02T15:04:05.999Z"),
"expires_in": 86400,
@ -140,6 +140,8 @@ func GetOAuthVerify(c *fiber.Ctx) error {
"in_app_id": person.ID,
"account_id": person.ID,
"displayName": person.DisplayName,
"product_id": "prod-fn",
"sandbox_id": "fn",
})
}
@ -148,21 +150,13 @@ func FortniteMiddleware(c *fiber.Ctx) error {
if auth == "" {
return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Authorization Header is empty"))
}
real := strings.ReplaceAll(auth, "bearer eg1~", "")
real := strings.ReplaceAll(auth, "bearer ", "")
claims, err := aid.JWTVerify(real)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token"))
}
if claims["snow_id"] == nil {
return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token"))
}
snowId, ok := claims["snow_id"].(string)
if !ok {
found := storage.Repo.GetToken(real)
if found == nil {
return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token"))
}
snowId := found.PersonID
person := p.Find(snowId)
if person == nil {
@ -179,23 +173,11 @@ func FrontendMiddleware(c *fiber.Ctx) error {
return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Authorization Header is empty"))
}
claims, err := aid.JWTVerify(auth)
if err != nil {
return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token"))
}
if claims["snow_id"] == nil {
return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token"))
}
if claims["frontend"] == nil {
return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Claims"))
}
snowId, ok := claims["snow_id"].(string)
if !ok {
found := storage.Repo.GetToken(auth)
if found == nil {
return c.Status(fiber.StatusForbidden).JSON(aid.ErrorBadRequest("Invalid Access Token"))
}
snowId := found.PersonID
person := p.Find(snowId)
if person == nil {

View File

@ -3,7 +3,6 @@ package handlers
import (
"net/http"
"net/url"
"time"
"github.com/ectrc/snow/aid"
p "github.com/ectrc/snow/person"
@ -75,18 +74,14 @@ func GetDiscordOAuthURL(c *fiber.Ctx) error {
person.Discord.Banner = user.Banner
storage.Repo.SaveDiscordPerson(person.Discord)
access, err := aid.JWTSign(aid.JSON{
"snow_id": person.ID, // custom
"frontend": true,
"creation_date": time.Now().Format("2006-01-02T15:04:05.999Z"),
})
access, sig := aid.KeyPair.EncryptAndSignB64([]byte(person.ID + ".frontend"))
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(aid.ErrorInternalServer)
}
c.Cookie(&fiber.Cookie{
Name: "access_token",
Value: access,
Value: access + "." + sig,
})
return c.Redirect(aid.Config.API.Host + aid.Config.API.FrontendPort + "/attempt")
}

View File

@ -8,6 +8,7 @@ import (
"github.com/ectrc/snow/discord"
"github.com/ectrc/snow/fortnite"
"github.com/ectrc/snow/handlers"
"github.com/ectrc/snow/person"
"github.com/ectrc/snow/storage"
"github.com/goccy/go-json"
@ -42,8 +43,11 @@ func init() {
fortnite.PreloadCosmetics(aid.Config.Fortnite.Season)
fortnite.GenerateRandomStorefront()
fortnite.GeneratePlaylistImages()
}
if found := person.FindByDisplay("god"); found == nil {
fortnite.NewFortnitePerson("god", true)
}
}
func main() {
r := fiber.New(fiber.Config{
DisableStartupMessage: true,

View File

@ -21,15 +21,12 @@ Performance first, universal Fortnite private server backend written in Go.
### Supported
- **_Chapter 1 Season 2_** `Fortnite+Release-2.5-CL-3889387-Windows` I started with this build of the game as it requires more work to get working, this means snow can support _most_ versions of the game.
- **_Chapter 1 Season 4_** `Fortnite+Release-4.5-CL-4159770-Windows` Fixed the access token issue causing errors logging in!
- **_Chapter 1 Season 5_** `Fortnite+Release-5.41-CL-4363240-Windows` This build was used to make sure challenges, variants and lobby backgrounds work.
- **_Chapter 1 Season 8_** `Fortnite+Release-8.51-CL-6165369-Windows` Fixed the invisible player bug caused by invalid account responses. Also fixed the issue with the item shop spamming the api.
- **_Chapter 2 Season 2_** `Fortnite+Release-12.41-CL-12905909-Windows` Item Shop length is correct, also Creative profile stopping login has also been fixed.
- **_Chapter 3 Season 1_** `Fortnite+Release-19.10-CL-Unknown-Windows` This is a very new build of fortnite that introfuces alot of different methods e.g. locker data is now stored as an item. Every MCP action is now fully working and tested. You need to start using easy anticheat otherwise this will not work.
### Broken
- **_Chapter 1 Season 4_** `Fortnite+Release-4.5-CL-4159770-Windows` Does not accept the Access Token for user authentication. I have some ideas why however not planned for a fix.
## How do I use this?
- **[Discord OAuth Setup Guide](oauth.md)** How to setup Discord OAuth for your backend. This enabled the ability to login to the web app with Discord.

View File

@ -43,9 +43,10 @@ func (s *PostgresStorage) MigrateAll() {
s.Migrate(&DB_Loot{}, "Loot")
s.Migrate(&DB_VariantChannel{}, "Variants")
s.Migrate(&DB_PAttribute{}, "Attributes")
s.Migrate(&DB_TemporaryCode{}, "Exchanges")
s.Migrate(&DB_TemporaryCode{}, "ExchangeCodes")
s.Migrate(&DB_DiscordPerson{}, "Discords")
s.Migrate(&DB_SeasonStat{}, "Stats")
s.Migrate(&DB_GameToken{}, "GameTokens")
}
func (s *PostgresStorage) DropTables() {
@ -241,4 +242,23 @@ func (s *PostgresStorage) SaveDiscordPerson(discordPerson *DB_DiscordPerson) {
func (s *PostgresStorage) DeleteDiscordPerson(discordPersonId string) {
s.Postgres.Delete(&DB_DiscordPerson{}, "id = ?", discordPersonId)
}
func (s *PostgresStorage) SaveToken(token *DB_GameToken) {
s.Postgres.Save(token)
}
func (s *PostgresStorage) DeleteToken(tokenId string) {
s.Postgres.Delete(&DB_GameToken{}, "id = ?", tokenId)
}
func (s *PostgresStorage) GetToken(tokenId string) *DB_GameToken {
var token DB_GameToken
s.Postgres.Model(&DB_GameToken{}).Where("id = ?", tokenId).Find(&token)
if token.ID == "" {
return nil
}
return &token
}

View File

@ -48,6 +48,10 @@ type Storage interface {
SaveDiscordPerson(person *DB_DiscordPerson)
DeleteDiscordPerson(personId string)
SaveToken(token *DB_GameToken)
GetToken(tokenId string) *DB_GameToken
DeleteToken(tokenId string)
}
type Repository struct {
@ -189,4 +193,16 @@ func (r *Repository) SaveDiscordPerson(person *DB_DiscordPerson) {
func (r *Repository) DeleteDiscordPerson(personId string) {
r.Storage.DeleteDiscordPerson(personId)
}
func (r *Repository) SaveToken(token *DB_GameToken) {
r.Storage.SaveToken(token)
}
func (r *Repository) GetToken(tokenId string) *DB_GameToken {
return r.Storage.GetToken(tokenId)
}
func (r *Repository) DeleteToken(tokenId string) {
r.Storage.DeleteToken(tokenId)
}

View File

@ -146,7 +146,7 @@ type DB_TemporaryCode struct {
}
func (DB_TemporaryCode) TableName() string {
return "Exchanges"
return "ExchangeCodes"
}
type DB_DiscordPerson struct {
@ -177,4 +177,16 @@ type DB_SeasonStat struct {
func (DB_SeasonStat) TableName() string {
return "Stats"
}
type DB_GameToken struct {
ID string `gorm:"primary_key"`
PersonID string
AccessToken string
Type string
ExpiresAt int64
}
func (DB_GameToken) TableName() string {
return "GameTokens"
}