Skip to content

Instantly share code, notes, and snippets.

@iansmith
Last active December 5, 2016 20:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save iansmith/5d99cb81965df7ccdf64ddafd41bfddd to your computer and use it in GitHub Desktop.
Save iansmith/5d99cb81965df7ccdf64ddafd41bfddd to your computer and use it in GitHub Desktop.
Explains the purpose of the YikYak status api and how to use it.

Purpose

The purpose of the Yik Yak status API is to allow a third-party application to set the status of a Yik Yak user programmatically. A secondary purpose is to allow a third-party application to set the location of the user (latitude, longitude), even though this is less likely to be less useful to most applications.

Mode Of Use

Let's assume you are an application developer, Joe. Your app, class2day allows college students to go to your website (perhaps, http://joesapps.com/class2day or similar) and enter their class schedule for the current term. They can also add outside activities, like "flag football practice" or "spanish club lunch". Once entered, class2day will constantly run and monitor the time and when the student should (hah!) be in class or at an activity automatically set the student's Yik Yak status to reflect that: "#organicChem322" or "flagFootballPractice".

Outside of building class2day itself, two preconditions have to be met for Joe's app to work correctly with YikYak. First, Joe must become a registered developer with Yik Yak. Once registered and approved by Yik Yak, Joe will be issued a developer id and a (secret) developer key. Because only Joe and Yik Yak know the developer key, Joe can use it to send messages to Yik Yak servers that can be checked for their origin: "Yep, only Joe could have sent this API request because was signed with his key."

Second, and perhaps more important, any user, say Mary, that wishes to use class2day must themselves indicate to Yik Yak that they will allow Joe's app to set their status. This step is performed through the Yik Yak app itself and, similarly, Mary can use the Yik Yak app to revoke a previous authorization that she granted to class2day.
Neither of these choices can be affected by class2day or Joe in any way--it's Mary's call.

Hackathon Changes

To get things going right away at a hackathon, Yik Yak has a procedure to expedite both of the steps above. Talk to the Yik Yak point person about these two things:

1)We have a set of "pre-approved" application ids and application keys that can just be issued to hackathon participants. This avoids the time-consuming approval process.

2)We can pre-authorize Yakkers for use with a particular application. In other words, if three team members Amanda, Barney, and Chris are building an app, we can authorize all three of their production Yik Yak accounts to be able to work with the application they are building. Naturally, this connection is made via the application ID issued in #1!

How To Use The API

We'll assume you have an application ID and key (see #1 above) already, and some number of pre-approved users. Each user is represented with a token that will look like this: cf68a02a-180e-4c52-b72b-92a7a3e1e360. The application id will be a six character string (representing a 32bit integer) like this, rMj38Q and the application key is a hex-encoded bunch of bits like this 01babf359dd5bfac3c9b4bcc025641e2.

When you want to set a user's status (and, optionally location) you send a JSON-encoded POST to this URL https://api.yikyak.io/status/v1beta1/setstatus. Here is an example message, with the "Checksum" field omitted for clarity:

{
  "UserToken":"cf68a02a-180e-4c52-b72b-92a7a3e1e360",
  "ApplicationId":"rMj38Q",
  "Status":"YOLO",
  "Latitude":41.0814,
  "Longitude":-81.519,
  "IgnoreLatLng":true,
  "VelocityEsitmateMagnitude":0,
  "VelocityEsitmateTheta":0
}

The Checksum

The Checksum field, which would normally be included in the example above, is how Yik Yak can be sure that the POST message it has received was actually generated by the application associated with the id rMj38Q. Yik Yak's third party API uses JSON Web Tokens. (That website has libraries for creating JWTs in many languages.) To create a JSON web token, you basically need all the data that are planning to send (such as the above) and your application key. The resulting token effectively encodes two things, who created the token (by virtue of having the correct key) and what the data was. In principle, one could not bother posting the data to the Yik Yak status api, and send only the JWT since it encodes all the what was sent. We have chosen to send the data in addition to the token (we do cross check that they are the same data!) because it makes the whole system easier to debug when things go wrong at the cost of a few extra bytes.

Here the example above but including the JWT token in the Checksum field.
Note that without knowing the (secret) Application Key used to create the token, it's not much use to anyone. Also note that the Checksum field itself is not included in the content used to create the Token!

{
  "UserToken":"cf68a02a-180e-4c52-b72b-92a7a3e1e360",
  "ApplicationId":"rMj38Q",
  "Status":"feelDaBern",
  "Latitude":41.0814,
  "Longitude":-81.519,
  "IgnoreLatLng":true,
  "VelocityEsitmateMagnitude":0,
  "VelocityEsitmateTheta":0,
  "Checksum":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJBcHBsaWNhdGlvbklkIjoick1qMzhRIiwiQ2hlY2tzdW0iOiIiLCJJZ25vcmVMYXRMbmciOiJ0cnVlIiwiTGF0aXR1ZGUiOiI0MS4wODE0IiwiTG9uZ2l0dWRlIjoiLTgxLjUxOSIsIlN0YXR1cyI6IllPTE8iLCJVc2VyVG9rZW4iOiJjZjY4YTAyYS0xODBlLTRjNTItYjcyYi05MmE3YTNlMWUzNjAiLCJWZWxvY2l0eUVzaXRtYXRlTWFnbml0dWRlIjoiMCIsIlZlbG9jaXR5RXNpdG1hdGVUaGV0YSI6IjAifQ.QMpNMXiu0IZpC6bB8Yo8_oOns_uJzDajQMuXQ-Fx5bCzWGn_ytpu5vsgsRJKMJ7gy6-Kw_2zKpY6UGfIYOLSrg"
}

When creating the JWT a couple of important details.

  • Fields names are case-sensitive and must match the above exactly.
  • All fields are compulsory. The two Velocity fields are currently unused, but must be present. You must set Latitude and Longitude (they can be 0.0) even if you have set IgnoreLatLng to true.

Sample Program In Go

package main

import (
	"bytes"
	"crypto/ecdsa"
	"crypto/rsa"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"io/ioutil"
	"net/http"
	"reflect"
	"strings"
	"time"

	jwtimpl "github.com/dgrijalva/jwt-go"
)

var (
	errNotStruct    = errors.New("Not a structure")
	errBadFieldType = errors.New("Not a field type that we can flatten")
)

// setStatusRequest request is the actual encoded structure you send to the server
// UserToken is issued by YikYak and represents a user.
// ApplicationId is the string associated with your app, issued by YikYak
// You must supply lat, long, and the two velocities even if you don't actually
// use them (e.g IgnoreLatLng is true).  Status should be no more than 18 characters
// (unicode ok) and cannot have spaces.  Checksum is computed by the helper
// function jwtIssueTokenForPacket with your secret key (issued by YikYak)

type setStatusRequest struct {
	UserToken                 string
	ApplicationID             string `json:"ApplicationId"` //make linter happy
	Status                    string
	Latitude                  float64
	Longitude                 float64
	IgnoreLatLng              bool
	VelocityEsitmateMagnitude float64 //ignored but have to be there
	VelocityEsitmateTheta     float64 //ignored but have to be there
	Checksum                  string
}

func main() {

	//this simulates an HTTP request through the gateway
	ssr := setStatusRequest{
		UserToken:                 "cf68a02a-180e-4c52-b72b-92a7a3e1e360",
		ApplicationID:             "rMj38Q",
		Status:                    "feelDaBern",
		Latitude:                  41.0814,
		Longitude:                 -81.5190,
		IgnoreLatLng:              true,
		VelocityEsitmateMagnitude: 0,
		VelocityEsitmateTheta:     0,
	}

	//the secret key is connected to your application id
	secretKey := "01babf359dd5bfac3c9b4bcc025641e2"
	checksum, err := jwtIssueTokenForPacket(secretKey, &ssr)
	if err != nil {
		fmt.Printf("unable to get a checksum for packet: %v", err)
		return
	}
	//you compute the checksum using all the fields of the structure (ssr) *except*
	//the checksum field
	ssr.Checksum = checksum

	//This is the endpoint you must POST to and send the data (see above) as a
	//json encoded blob in the body.
	urlString := "https://api.yikyak.io/status/v1beta1/setstatus"

	var buf bytes.Buffer
	enc := json.NewEncoder(&buf)
	if encodingErr := enc.Encode(&ssr); encodingErr != nil {
		fmt.Printf("unable to encode parameters: %v", err)
		return
	}

	fmt.Printf("encoded message to be sent to server\n\n%s\n\n", buf.String())

	//you should label the content/type as application/json when you send
	resp, err := http.Post(urlString, "application/json", &buf)
	if err != nil {
		fmt.Printf("unable to post parameters to api: %v", err)
		return
	}

	defer resp.Body.Close() //be careful to close body when done

	if resp.StatusCode/100 != 2 {
		fmt.Printf("error response: %s\n", resp.Status)
	}
	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		fmt.Printf("unable to read the body after response: %v\n", err)
		return
	}
	fmt.Printf("server response (body)\n\n%s\n\n", string(body)) //convert from bytes to string

}

func jwtIssueTokenForPacket(key string, x interface{}) (string, error) {
	//get the key from the record
	k, err := hex.DecodeString(key)
	if err != nil {
		return "", err
	}

	tokenizer := NewJWT(AlgHMAC512, k)
	data, err := structToArgsForJWT(x)
	if err != nil {
		return "", err
	}

	token, err := tokenizer.IssueNonExpiring(data)
	if err != nil {
		return "", err
	}

	return token, nil
}

//structToArgsForJWT takes the structure given in the first arg and returns
//a map[string]string as the first return val.  If it can't figure out how
//to convert a field to a string, it returns an error.  If the value passed
//as x is not a struct or pointer to a struct, it returns an error.
func structToArgsForJWT(x interface{}) (map[string]string, error) {
	v := reflect.ValueOf(x)
	if v.Type().Kind() == reflect.Ptr {
		v = v.Elem()
	}
	if v.Kind() != reflect.Struct {
		return nil, errNotStruct
	}
	result := make(map[string]string)
	for i := 0; i < v.Type().NumField(); i++ {
		f := v.Type().Field(i)

		//check that f is a plausible type
		switch f.Type.Kind() {
		case reflect.String, reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64,
			reflect.Float32, reflect.Float64, reflect.Int8, reflect.Bool:
		default:
			return nil, errBadFieldType
		}

		//extract the actual field value
		field := v.FieldByName(f.Name)

		name := f.Name
		//this is a horrible hack to deal with our linter at yikyak
		if strings.HasSuffix(name, "ID") {
			name = strings.TrimSuffix(name, "ID") + "Id"
		}

		//convert to entry in the result
		switch f.Type.Kind() {
		case reflect.String:
			result[name] = field.String()
		case reflect.Float32, reflect.Float64:
			result[name] = fmt.Sprintf("%v", field.Float())
		case reflect.Bool:
			result[name] = fmt.Sprintf("%v", field.Bool())
		default: //we've already checked the types above
			result[name] = fmt.Sprintf("%v", field.Int())
		}
	}
	return result, nil
}

//
// All the code below has just been inserted here to make it simpler to use this
// program as a single, self-contained file.
//

// Alg specifies a signing method type
type Alg int

const (
	// AlgRSA based signing
	AlgRSA = iota
	// AlgHMAC512 based signing
	AlgHMAC512 = iota
	// AlgECDSA based signing
	AlgECDSA = iota
	// AlgHMAC256 based signing
	AlgHMAC256 = iota
)

const (
	// EXPIRE defines the key used into the claim data for the expiration date
	EXPIRE string = "exp"
)

// JWT encapsulates JWT operations
type JWT struct {
	algorithm Alg
	key       interface{}
}

// NewJWT creates a new JWT structure
func NewJWT(algorithm Alg, key interface{}) *JWT {
	return &JWT{algorithm, key}
}

func (jwt *JWT) getAlgorithm() (jwtimpl.SigningMethod, error) {
	switch jwt.algorithm {
	case AlgRSA:
		return jwtimpl.SigningMethodRS512, nil
	case AlgHMAC512:
		return jwtimpl.SigningMethodHS512, nil
	case AlgHMAC256:
		return jwtimpl.SigningMethodHS256, nil
	case AlgECDSA:
		return jwtimpl.SigningMethodES256, nil
	default:
		return nil, errors.New("Unrecognized algorithm")
	}
}

// Verify allows the verification of tokens
func (jwt *JWT) Verify(unverifiedToken string) (*map[string]string, error) {
	token, err := jwtimpl.Parse(unverifiedToken, func(token *jwtimpl.Token) (interface{}, error) {
		// Checks if the token signing method is equal to the one specified by our verifier
		alg, err := jwt.getAlgorithm()
		if err != nil {
			return nil, err
		}
		if alg != token.Method {
			return nil, fmt.Errorf("Unsupported signing method: %v", token.Method)
		}

		switch alg {
		case jwtimpl.SigningMethodRS512:
			return &jwt.key.(*rsa.PrivateKey).PublicKey, nil
		case jwtimpl.SigningMethodES256:
			return &jwt.key.(*ecdsa.PrivateKey).PublicKey, nil
		}
		return jwt.key, nil
	})

	// Reject invalid signature
	if err != nil || !token.Valid {
		return nil, err
	}

	// Check if token is expired
	if exp, ok := token.Claims[EXPIRE]; ok {
		if time.Now().Unix() >= int64(exp.(float64)) {
			return nil, errors.New("Expired token")
		}
	}

	data := make(map[string]string)
	for key, value := range token.Claims {
		if s, ok := value.(string); ok {
			data[key] = s
		}
	}
	return &data, nil
}

func (jwt *JWT) createToken() *jwtimpl.Token {
	alg, err := jwt.getAlgorithm()
	if err != nil {
		panic(err)
	}
	return jwtimpl.New(alg)
}

// IssueNonExpiring issues a token without an expiration time
func (jwt *JWT) IssueNonExpiring(data map[string]string) (string, error) {
	token := jwt.createToken()

	for k := range data {
		token.Claims[k] = data[k]
	}

	// Sign and get the complete encoded token as a string
	return token.SignedString(jwt.key)
}

// Issue is used to issue a new token
func (jwt *JWT) Issue(data map[string]string, expireins time.Duration) (string, error) {
	token := jwt.createToken()

	for k := range data {
		token.Claims[k] = data[k]
	}
	token.Claims["exp"] = time.Now().Add(expireins).Unix()

	// Sign and get the complete encoded token as a string
	return token.SignedString(jwt.key)
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment