Skip to content

Instantly share code, notes, and snippets.

@iansmith
Last active December 5, 2016 20:13
Show Gist options
  • Save iansmith/5d99cb81965df7ccdf64ddafd41bfddd to your computer and use it in GitHub Desktop.
Save iansmith/5d99cb81965df7ccdf64ddafd41bfddd to your computer and use it in GitHub Desktop.

Revisions

  1. iansmith revised this gist Sep 30, 2016. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion statusapi.md
    Original file line number Diff line number Diff line change
    @@ -96,7 +96,7 @@ note that the Checksum field itself is *not* included in the content used to cre

    When creating the JWT a couple of important details.

    * Fields names are case-sensitive and must the above exactly.
    * 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.

  2. iansmith revised this gist Sep 30, 2016. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions statusapi.md
    Original file line number Diff line number Diff line change
    @@ -53,7 +53,7 @@ When you want to set a user's status (and, optionally location) you send a JSON
    {
    "UserToken":"cf68a02a-180e-4c52-b72b-92a7a3e1e360",
    "ApplicationId":"rMj38Q",
    "Status":"feelDaBern",
    "Status":"YOLO",
    "Latitude":41.0814,
    "Longitude":-81.519,
    "IgnoreLatLng":true,
    @@ -90,7 +90,7 @@ note that the Checksum field itself is *not* included in the content used to cre
    "IgnoreLatLng":true,
    "VelocityEsitmateMagnitude":0,
    "VelocityEsitmateTheta":0,
    "Checksum":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJBcHBsaWNhdGlvbklkIjoick1qMzhRIiwiQ2hlY2tzdW0iOiIiLCJJZ25vcmVMYXRMbmciOiJ0cnVlIiwiTGF0aXR1ZGUiOiI0MS4wODE0IiwiTG9uZ2l0dWRlIjoiLTgxLjUxOSIsIlN0YXR1cyI6ImZlZWxEYUJlcm4iLCJVc2VyVG9rZW4iOiJjZjY4YTAyYS0xODBlLTRjNTItYjcyYi05MmE3YTNlMWUzNjAiLCJWZWxvY2l0eUVzaXRtYXRlTWFnbml0dWRlIjoiMCIsIlZlbG9jaXR5RXNpdG1hdGVUaGV0YSI6IjAifQ.9Wm4xjkSI722iu3quAnnAiDpdV3vdNpR6E8e5ggS8eZxTzGAk_BkAsukBK1GpIiooZC5kjQw_RSXlv8DOgyQqg"
    "Checksum":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJBcHBsaWNhdGlvbklkIjoick1qMzhRIiwiQ2hlY2tzdW0iOiIiLCJJZ25vcmVMYXRMbmciOiJ0cnVlIiwiTGF0aXR1ZGUiOiI0MS4wODE0IiwiTG9uZ2l0dWRlIjoiLTgxLjUxOSIsIlN0YXR1cyI6IllPTE8iLCJVc2VyVG9rZW4iOiJjZjY4YTAyYS0xODBlLTRjNTItYjcyYi05MmE3YTNlMWUzNjAiLCJWZWxvY2l0eUVzaXRtYXRlTWFnbml0dWRlIjoiMCIsIlZlbG9jaXR5RXNpdG1hdGVUaGV0YSI6IjAifQ.QMpNMXiu0IZpC6bB8Yo8_oOns_uJzDajQMuXQ-Fx5bCzWGn_ytpu5vsgsRJKMJ7gy6-Kw_2zKpY6UGfIYOLSrg"
    }
    ```

  3. iansmith revised this gist Sep 30, 2016. 1 changed file with 310 additions and 0 deletions.
    310 changes: 310 additions & 0 deletions statusapi.md
    Original file line number Diff line number Diff line change
    @@ -93,3 +93,313 @@ note that the Checksum field itself is *not* included in the content used to cre
    "Checksum":"eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJBcHBsaWNhdGlvbklkIjoick1qMzhRIiwiQ2hlY2tzdW0iOiIiLCJJZ25vcmVMYXRMbmciOiJ0cnVlIiwiTGF0aXR1ZGUiOiI0MS4wODE0IiwiTG9uZ2l0dWRlIjoiLTgxLjUxOSIsIlN0YXR1cyI6ImZlZWxEYUJlcm4iLCJVc2VyVG9rZW4iOiJjZjY4YTAyYS0xODBlLTRjNTItYjcyYi05MmE3YTNlMWUzNjAiLCJWZWxvY2l0eUVzaXRtYXRlTWFnbml0dWRlIjoiMCIsIlZlbG9jaXR5RXNpdG1hdGVUaGV0YSI6IjAifQ.9Wm4xjkSI722iu3quAnnAiDpdV3vdNpR6E8e5ggS8eZxTzGAk_BkAsukBK1GpIiooZC5kjQw_RSXlv8DOgyQqg"
    }
    ```

    When creating the JWT a couple of important details.

    * Fields names are case-sensitive and must 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)
    }
    ```

  4. iansmith revised this gist Sep 30, 2016. 1 changed file with 30 additions and 0 deletions.
    30 changes: 30 additions & 0 deletions statusapi.md
    Original file line number Diff line number Diff line change
    @@ -62,4 +62,34 @@ When you want to set a user's status (and, optionally location) you send a JSON
    }
    ```

    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](https://jwt.io). (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.eyJBcHBsaWNhdGlvbklkIjoick1qMzhRIiwiQ2hlY2tzdW0iOiIiLCJJZ25vcmVMYXRMbmciOiJ0cnVlIiwiTGF0aXR1ZGUiOiI0MS4wODE0IiwiTG9uZ2l0dWRlIjoiLTgxLjUxOSIsIlN0YXR1cyI6ImZlZWxEYUJlcm4iLCJVc2VyVG9rZW4iOiJjZjY4YTAyYS0xODBlLTRjNTItYjcyYi05MmE3YTNlMWUzNjAiLCJWZWxvY2l0eUVzaXRtYXRlTWFnbml0dWRlIjoiMCIsIlZlbG9jaXR5RXNpdG1hdGVUaGV0YSI6IjAifQ.9Wm4xjkSI722iu3quAnnAiDpdV3vdNpR6E8e5ggS8eZxTzGAk_BkAsukBK1GpIiooZC5kjQw_RSXlv8DOgyQqg"
    }
    ```
  5. iansmith revised this gist Sep 30, 2016. 1 changed file with 22 additions and 0 deletions.
    22 changes: 22 additions & 0 deletions statusapi.md
    Original file line number Diff line number Diff line change
    @@ -39,5 +39,27 @@ This avoids the time-consuming approval process.
    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":"feelDaBern",
    "Latitude":41.0814,
    "Longitude":-81.519,
    "IgnoreLatLng":true,
    "VelocityEsitmateMagnitude":0,
    "VelocityEsitmateTheta":0
    }
    ```


  6. iansmith revised this gist Sep 30, 2016. 1 changed file with 13 additions and 1 deletion.
    14 changes: 13 additions & 1 deletion statusapi.md
    Original file line number Diff line number Diff line change
    @@ -26,6 +26,18 @@ Yik Yak that they will allow Joe's app to set their status. This step is perfor
    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!


    Yik phone app allows

  7. iansmith revised this gist Sep 30, 2016. 1 changed file with 29 additions and 0 deletions.
    29 changes: 29 additions & 0 deletions statusapi.md
    Original file line number Diff line number Diff line change
    @@ -1,2 +1,31 @@
    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.


    Yik phone app allows

  8. iansmith created this gist Sep 30, 2016.
    2 changes: 2 additions & 0 deletions statusapi.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,2 @@
    Purpose
    =======