diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml
index 26ad532..51ee0b5 100644
--- a/.idea/inspectionProfiles/Project_Default.xml
+++ b/.idea/inspectionProfiles/Project_Default.xml
@@ -3,5 +3,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/yggdrasil/authenticate.go b/yggdrasil/authenticate.go
index 25da0f6..caacf0e 100644
--- a/yggdrasil/authenticate.go
+++ b/yggdrasil/authenticate.go
@@ -1,7 +1,6 @@
package yggdrasil
import (
- "fmt"
"github.com/google/uuid"
)
@@ -16,6 +15,16 @@ type agent struct {
Version int `json:"version"`
}
+type proof struct {
+ UserName string `json:"username"`
+ Password string `json:"password"`
+}
+
+type tokens struct {
+ AccessToken string `json:"accessToken"`
+ ClientToken string `json:"clientToken"`
+}
+
var defaultAgent = agent{
Name: "Minecraft",
Version: 1,
@@ -23,48 +32,44 @@ var defaultAgent = agent{
// authPayload is a yggdrasil request struct
type authPayload struct {
- Agent agent `json:"agent"`
- UserName string `json:"username"`
- Password string `json:"password"`
+ Agent agent `json:"agent"`
+ proof
ClientToken string `json:"clientToken"`
RequestUser bool `json:"requestUser"`
}
// authResp is the response from Mojang's auth server
type authResp struct {
- Error string `json:"error"`
- ErrorMessage string `json:"errorMessage"`
- Cause string `json:"cause"`
+ tokens
+ AvailableProfiles []Profile `json:"availableProfiles"` // only present if the agent field was received
- AccessToken string `json:"accessToken"`
- ClientToken string `json:"clientToken"` // identical to the one received
- AvailableProfiles []struct {
- ID string `json:"ID"` // hexadecimal
- Name string `json:"name"`
- Legacy bool `json:"legacy"` // In practice, this field only appears in the response if true. Default to false.
- } `json:"availableProfiles"` // only present if the agent field was received
-
- SelectedProfile struct { // only present if the agent field was received
- ID string `json:"id"`
- Name string `json:"name"`
- Legacy bool `json:"legacy"`
- } `json:"selectedProfile"`
- User struct { // only present if requestUser was true in the request authPayload
+ SelectedProfile Profile `json:"selectedProfile"` // only present if the agent field was received
+ User struct { // only present if requestUser was true in the request authPayload
ID string `json:"id"` // hexadecimal
Properties []struct {
Name string `json:"name"`
Value string `json:"value"`
}
} `json:"user"`
+
+ *Error
+}
+
+type Profile struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ //Legacy bool `json:"legacy"` // we don't care
}
// Authenticate authenticates a user using their password.
func Authenticate(user, password string) (*Access, error) {
// Payload
pl := authPayload{
- Agent: defaultAgent,
- UserName: user,
- Password: password,
+ Agent: defaultAgent,
+ proof: proof{
+ UserName: user,
+ Password: password,
+ },
ClientToken: uuid.New().String(),
RequestUser: true,
}
@@ -77,10 +82,8 @@ func Authenticate(user, password string) (*Access, error) {
return nil, err
}
- if ar.Error != "" {
- err = fmt.Errorf("auth fail: %s: %s, %s}",
- ar.Error, ar.ErrorMessage, ar.Cause)
- return nil, err
+ if ar.Error != nil {
+ return nil, *ar.Error
}
return &Access{ar: ar, ct: pl.ClientToken}, nil
@@ -93,3 +96,7 @@ func (a *Access) SelectedProfile() (ID, Name string) {
func (a *Access) AccessToken() string {
return a.ar.AccessToken
}
+
+func (a *Access) AvailableProfiles() []Profile {
+ return a.ar.AvailableProfiles
+}
diff --git a/yggdrasil/authenticate_test.go b/yggdrasil/authenticate_test.go
deleted file mode 100644
index cf8ec38..0000000
--- a/yggdrasil/authenticate_test.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package yggdrasil
-
-import (
- "fmt"
-)
-
-func ExampleAuthenticate() {
- resp, err := Authenticate("", "")
- if err != nil {
- panic(err)
- }
-
- fmt.Println(resp.SelectedProfile())
- fmt.Println(resp.AccessToken())
-}
diff --git a/yggdrasil/refresh.go b/yggdrasil/refresh.go
new file mode 100644
index 0000000..37c2d22
--- /dev/null
+++ b/yggdrasil/refresh.go
@@ -0,0 +1,39 @@
+package yggdrasil
+
+import "fmt"
+
+type refreshPayload struct {
+ tokens
+ SelectedProfile *Profile `json:"selectedProfile,omitempty"`
+
+ RequestUser bool `json:"requestUser"`
+}
+
+// Refresh refreshes a valid accessToken.
+//
+// It can be used to keep a user logged in between
+// gaming sessions and is preferred over storing
+// the user's password in a file
+func (a *Access) Refresh(profile *Profile) error {
+ pl := refreshPayload{
+ tokens: a.ar.tokens,
+ SelectedProfile: profile, //used to change profile, don't use now
+ RequestUser: true,
+ }
+
+ resp := struct {
+ *authResp
+ *Error
+ }{authResp: &a.ar}
+
+ err := post("/refresh", pl, &resp)
+ if err != nil {
+ return fmt.Errorf("post fail: %v", err)
+ }
+
+ if resp.Error != nil {
+ return resp.Error
+ }
+
+ return nil
+}
diff --git a/yggdrasil/signout.go b/yggdrasil/signout.go
new file mode 100644
index 0000000..b6e5943
--- /dev/null
+++ b/yggdrasil/signout.go
@@ -0,0 +1,30 @@
+package yggdrasil
+
+import (
+ "encoding/json"
+ "fmt"
+)
+
+// SignOut invalidates accessTokens using an account's username and password.
+func SignOut(user, password string) error {
+ pl := proof{
+ UserName: user,
+ Password: password,
+ }
+
+ resp, err := rowPost("/signout", pl)
+ if err != nil {
+ return fmt.Errorf("request fail: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 204 {
+ var err Error
+ if err := json.NewDecoder(resp.Body).Decode(&err); err != nil {
+ return fmt.Errorf("unmarshal error fail: %v", err)
+ }
+ return fmt.Errorf("invalidate error: %v", err)
+ }
+
+ return nil
+}
diff --git a/yggdrasil/validate.go b/yggdrasil/validate.go
index 2ee79f4..5c35907 100644
--- a/yggdrasil/validate.go
+++ b/yggdrasil/validate.go
@@ -1,16 +1,13 @@
package yggdrasil
-import "fmt"
+import (
+ "fmt"
+ "io/ioutil"
+)
// Validate checks if an accessToken is usable for authentication with a Minecraft server.
func (a *Access) Validate() (bool, error) {
- pl := struct {
- AccessToken string `json:"accessToken"`
- ClientToken string `json:"clientToken"`
- }{
- AccessToken: a.ar.AccessToken,
- ClientToken: a.ar.ClientToken,
- }
+ pl := a.ar.tokens
resp, err := rowPost("/validate", pl)
if err != nil {
@@ -19,3 +16,21 @@ func (a *Access) Validate() (bool, error) {
return resp.StatusCode == 204, resp.Body.Close()
}
+
+// Invalidate invalidates accessTokens using a client/access token pair.
+func (a *Access) Invalidate() error {
+ pl := a.ar.tokens
+
+ resp, err := rowPost("/invalidate", pl)
+ if err != nil {
+ return fmt.Errorf("request fail: %v", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != 204 {
+ content, _ := ioutil.ReadAll(resp.Body)
+ return fmt.Errorf("invalidate error: %v: %s", resp.Status, content)
+ }
+
+ return nil
+}
diff --git a/yggdrasil/yggdrasil.go b/yggdrasil/yggdrasil.go
index 19320b4..fd5dfa1 100644
--- a/yggdrasil/yggdrasil.go
+++ b/yggdrasil/yggdrasil.go
@@ -14,14 +14,24 @@ import (
"net/http"
)
+type Error struct {
+ Err string `json:"error"`
+ ErrMsg string `json:"errorMessage"`
+ Cause string `json:"cause"`
+}
+
+func (e Error) Error() string {
+ return e.Err + ": " + e.ErrMsg + ", " + e.Cause
+}
+
var AuthURL = "https://authserver.mojang.com"
var client http.Client
func post(endpoint string, payload interface{}, resp interface{}) error {
- rowResp,err:=rowPost(endpoint,payload)
+ rowResp, err := rowPost(endpoint, payload)
if err != nil {
- return fmt.Errorf("request fail: %v",err)
+ return fmt.Errorf("request fail: %v", err)
}
defer rowResp.Body.Close()
diff --git a/yggdrasil/yggdrasil_test.go b/yggdrasil/yggdrasil_test.go
new file mode 100644
index 0000000..02dac73
--- /dev/null
+++ b/yggdrasil/yggdrasil_test.go
@@ -0,0 +1,72 @@
+package yggdrasil
+
+import (
+ "fmt"
+ "os"
+)
+
+func ExampleAuthenticate() {
+ resp, err := Authenticate("", "")
+ if err != nil {
+ panic(err)
+ }
+
+ fmt.Println(resp.SelectedProfile())
+ fmt.Println(resp.AccessToken())
+}
+
+func Example(){
+ var user, password string// set your proof
+
+ // Sign in
+ resp, err := Authenticate(user, password)
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ id, name := resp.SelectedProfile()
+ fmt.Println("user:", name)
+ fmt.Println("uuid:", id)
+ fmt.Println("astk:", resp.AccessToken())
+
+ // Refresh access token
+ if err := resp.Refresh(nil); err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+
+ id, name = resp.SelectedProfile()
+ fmt.Println("user:", name)
+ fmt.Println("uuid:", id)
+ fmt.Println("astk:", resp.AccessToken())
+
+ // Check access token
+ ok, err := resp.Validate()
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ fmt.Println("at status: ", ok)
+
+ // Invalidate access token
+ err=resp.Invalidate()
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+
+ // Check access token
+ ok, err = resp.Validate()
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+ fmt.Println("at status: ", ok)
+
+ // Sign out
+ err = SignOut(user, password)
+ if err != nil {
+ fmt.Println(err)
+ os.Exit(1)
+ }
+}
\ No newline at end of file