Created
September 7, 2016 08:43
-
-
Save rbrick/ff0e9947b28f8b18a74bf0598db4ab48 to your computer and use it in GitHub Desktop.
Simple implementation of Yggdrasil (Mojang's authentication service)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is a client implementation of Yggdrasil (Mojang's authentication server used for their games.) | |
// Big thanks for documentation provided by http://wiki.vg/Authentication | |
package main | |
import ( | |
"bytes" | |
"encoding/json" | |
"io" | |
"io/ioutil" | |
"log" | |
"net/http" | |
) | |
// MojangAuthServer is the URL for Mojang's auth server address | |
const MojangAuthServer = "https://authserver.mojang.com/" | |
var ( | |
// MinecraftAgent is Minecraft's agent for Yggdrasil | |
MinecraftAgent = &YggdrasilAgent{"Minecraft", 1} | |
// ScrollsAgent is Scrolls' agent for Yggdrasil | |
ScrollsAgent = &YggdrasilAgent{"Scrolls", 1} | |
) | |
// YggdrasilAgent represents an Yggdrasil agent | |
type YggdrasilAgent struct { | |
Name string `json:"name"` | |
Version int `json:"version"` | |
} | |
// YggdrasilError represents an error from Yggdrasil. | |
type YggdrasilError struct { | |
// Error is a short description of the error | |
Error string `json:"error"` | |
// ErrorMessage is a longer description of the error which can be shown to a user. | |
ErrorMessage string `json:"errorMessage"` | |
// Cause is the cause of the error. (Optional) | |
Cause string `json:"cause,omitempty"` | |
// StatusCode is the status code returned by the Yggdrasil server. | |
StatusCode int `json:"-"` | |
} | |
// YggdrasilProfile represents a profile returned by Yggdrasil. | |
type YggdrasilProfile struct { | |
ID string `json:"id"` | |
Name string `json:"name"` | |
Legacy bool `json:"legacy,omitempty"` | |
} | |
// YggdrasilPayload represents a payload we send to Yggdrasil | |
type YggdrasilPayload map[string]interface{} | |
// stream turns the payload into an io.Reader. | |
func (p *YggdrasilPayload) stream() io.Reader { | |
d, err := json.Marshal(p) // Turn it into a slice of bytes | |
if err != nil { | |
// Panic if an error occurs | |
log.Panicln(err) | |
return nil | |
} | |
return bytes.NewReader(d) // create a new byte reader | |
} | |
// YggdrasilResponse represents a response from Yggdrasil | |
type YggdrasilResponse struct { | |
AccessToken string `json:"accessToken"` | |
ClientToken string `json:"clientToken,omitempty"` | |
AvailableProfiles []YggdrasilProfile `json:"availableProfiles,omitempty"` | |
SelectedProfile YggdrasilProfile `json:"selectedProfile,omitempty"` | |
StatusCode int `json:"-"` | |
} | |
// YggdrasilService provides a way of interfacing with Yggdrasil | |
type YggdrasilService interface { | |
// Authenticate authenticates a user using their password. | |
Authenticate(username, password, clientToken string) (*YggdrasilResponse, *YggdrasilError) | |
// Invalidate invalidates access tokens using a access/client token pair. | |
Invalidate(accessToken, clientToken string) *YggdrasilError | |
// Refresh refreshes a valid access token | |
Refresh(accessToken, clientToken string, profile *YggdrasilProfile, requestProfile bool) (*YggdrasilResponse, *YggdrasilError) | |
// Signout invalidates a users authentication token using the accounts's username and password. | |
Signout(username, password string) *YggdrasilError | |
// Validate checks if an accessToken is usable for authentication | |
Validate(accessToken, clientToken string) *YggdrasilError | |
} | |
type yggdrasilimpl struct { | |
agent *YggdrasilAgent | |
} | |
func (impl *yggdrasilimpl) Authenticate(username, password, clientToken string) (*YggdrasilResponse, *YggdrasilError) { | |
payload := YggdrasilPayload{} | |
payload["agent"] = impl.agent | |
payload["username"] = username | |
payload["password"] = password | |
return impl.makeRequest("authenticate", &payload) | |
} | |
func (impl *yggdrasilimpl) Signout(username, password string) *YggdrasilError { | |
payload := YggdrasilPayload{} | |
payload["username"] = username | |
payload["password"] = password | |
_, err := impl.makeRequest("signout", &payload) | |
return err | |
} | |
func (impl *yggdrasilimpl) Invalidate(accessToken, clientToken string) *YggdrasilError { | |
payload := YggdrasilPayload{} | |
payload["accessToken"] = accessToken | |
payload["clientToken"] = clientToken | |
_, err := impl.makeRequest("invalidate", &payload) | |
return err | |
} | |
func (impl *yggdrasilimpl) Validate(accessToken, clientToken string) *YggdrasilError { | |
payload := YggdrasilPayload{} | |
payload["accessToken"] = accessToken | |
payload["clientToken"] = clientToken | |
_, err := impl.makeRequest("validate", &payload) | |
return err | |
} | |
func (impl *yggdrasilimpl) Refresh(accessToken, clientToken string, profile *YggdrasilProfile, requestProfile bool) (*YggdrasilResponse, *YggdrasilError) { | |
payload := YggdrasilPayload{} | |
payload["accessToken"] = accessToken | |
payload["clientToken"] = clientToken | |
payload["selectedProfile"] = profile | |
payload["requestProfile"] = requestProfile | |
return impl.makeRequest("refresh", &payload) | |
} | |
func (*yggdrasilimpl) makeRequest(path string, payload *YggdrasilPayload) (*YggdrasilResponse, *YggdrasilError) { | |
resp, err := http.Post(MojangAuthServer+path, "application/json", payload.stream()) | |
if err != nil { | |
log.Panicln(err) | |
} | |
// Read the body | |
d, err := ioutil.ReadAll(resp.Body) | |
if err != nil { | |
log.Panicln(err) | |
} | |
if resp.StatusCode != 200 { | |
// "signout" returns a 204 No Content if successful. Just ignore. | |
if resp.StatusCode == http.StatusNoContent { | |
return &YggdrasilResponse{ | |
StatusCode: http.StatusNoContent, | |
}, nil | |
} | |
var yggdrasilerror YggdrasilError | |
err = json.Unmarshal(d, &yggdrasilerror) | |
if err != nil { | |
log.Panicln(err) | |
} | |
yggdrasilerror.StatusCode = resp.StatusCode | |
return nil, &yggdrasilerror | |
} | |
var response YggdrasilResponse | |
err = json.Unmarshal(d, &response) | |
if err != nil { | |
log.Panicln(err) | |
} | |
response.StatusCode = resp.StatusCode | |
return &response, nil | |
} | |
// NewYggdrasilService creates a new YggdrasilService | |
func NewYggdrasilService(agent *YggdrasilAgent) YggdrasilService { | |
return &yggdrasilimpl{agent} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment