Skip to content

Instantly share code, notes, and snippets.

@rbrick
Created September 7, 2016 08:43
Show Gist options
  • Save rbrick/ff0e9947b28f8b18a74bf0598db4ab48 to your computer and use it in GitHub Desktop.
Save rbrick/ff0e9947b28f8b18a74bf0598db4ab48 to your computer and use it in GitHub Desktop.
Simple implementation of Yggdrasil (Mojang's authentication service)
// 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