Skip to content

Instantly share code, notes, and snippets.

@kvakes
Last active October 17, 2019 13:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kvakes/e19ac7c081163addc3b44bb5f216ca96 to your computer and use it in GitHub Desktop.
Save kvakes/e19ac7c081163addc3b44bb5f216ca96 to your computer and use it in GitHub Desktop.
ORG.ID Authentication API Endpoint Example

ORG.ID Authentication API Endpoint Example

Install

  • Make sure you've set up your Go environment correctly, e.g. $ echo $GOPATH should work
  • Install
    • go-ethereum: $ go get -u github.com/ethereum/go-ethereum
    • mux: $ go get -u github.com/gorilla/mux
    • shortuuid: $ go get -u github.com/lithammer/shortuuid

Run

$ go run main.go

Use

$ curl -X GET http://127.0.0.1:8888/auth/ -H 'X-ORG-ID: 0x97117E2678264c8D59bcc46C06de83a9869129E3' -H 'X-ORG-ID-Associated-Key: 0xb2f9d9070defc131d833e5e695384eefaf4afe6e' -H 'X-ORG-ID-Challenge-Timestamp: 1570546348' -H 'X-ORG-ID-Signature: 0x34c76145962d4f9196d5f111632050ed5a4f52447d72f4a04b833fddeefb07580f38f86878dd9d99646643b1af4b40f0adb44cec2b1aa8b720b6d3b843e47ba81c'

curl -X POST http://127.0.0.1:8888/auth/ -H 'X-ORG-ID: 0x97117E2678264c8D59bcc46C06de83a9869129E3' -H 'X-ORG-ID-Associated-Key: 0xb2f9d9070defc131d833e5e695384eefaf4afe6e' -H 'X-ORG-ID-Challenge-Timestamp: 1570546348' -H 'X-ORG-ID-Signature: 0x34c76145962d4f9196d5f111632050ed5a4f52447d72f4a04b833fddeefb07580f38f86878dd9d99646643b1af4b40f0adb44cec2b1aa8b720b6d3b843e47ba81c'

package main
import (
"bytes"
"errors"
"fmt"
"log"
"net/http"
"strconv"
"time"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/gorilla/mux"
"github.com/lithammer/shortuuid"
"github.com/windingtree/go-orgid/organization"
)
var apiTokens = make(map[string]string)
func main() {
router := mux.NewRouter().StrictSlash(true)
// GET /auth/
// Returns an ORG.ID authentication status at this server
// Returns either an API token or an Unauthorized status (with error message)
// curl -X GET http://127.0.0.1:8888/auth/
// -H 'X-ORG-ID: 0xFCea05bB6FBCC34Ae4115B7AE0C2c5E2EBA95eC0'
// -H 'X-ORG-Associated-Key: 0xFCea05bB6FBCC34Ae4115B7AE0C2c5E2EBA95eC0'
// -H 'X-ORG-ID-Challenge-Timestamp: 1570450590'
// -H 'X-ORG-ID-Signature: 0x24953054e098dab97102a96dae87644c39001e8a5cd9a74a174f53183ef7f4dd59a15d81b2c4d0a60ec3d166122aa7c5464e455f4c748bf7bb5cd3a70c00255d1b'
// "0xFCea..." is my ORG.ID, challenge is timestamp within 1 minute of current time, signature is an Ethereum signature of challenge
router.HandleFunc("/auth/", getAuthHandler).Methods("GET")
// POST /auth/
// Attempts to authenticate ORG.ID on this server
// Returns a new API token
router.HandleFunc("/auth/", postAuthHandler).Methods("POST")
log.Fatal(http.ListenAndServe(":8888", router))
}
func getAuthHandler(w http.ResponseWriter, r *http.Request) {
oid := r.Header.Get("X-ORG-ID")
_, status, err := isOrgIDValid(w, r)
if err != nil {
w.WriteHeader(status)
fmt.Fprintf(w, err.Error())
return
}
// Check the "DB" for the API key
if apiTokens[oid] == "" {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "API Token Not Found")
} else {
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, apiTokens[oid])
}
return
}
func postAuthHandler(w http.ResponseWriter, r *http.Request) {
oid := r.Header.Get("X-ORG-ID")
_, status, err := isOrgIDValid(w, r)
if err != nil {
w.WriteHeader(status)
fmt.Fprintf(w, err.Error())
return
}
newAPIKey := shortuuid.New()
apiTokens[oid] = newAPIKey
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, newAPIKey)
}
func isOrgIDValid(w http.ResponseWriter, r *http.Request) (bool, int, error) {
// ORG.ID
oid := r.Header.Get("X-ORG-ID")
if !common.IsHexAddress(oid) {
return false, http.StatusBadRequest, fmt.Errorf("ORG.ID is missing or invalid")
}
orgid := common.HexToAddress(oid)
// Associated Key
aKey := r.Header.Get("X-ORG-ID-Associated-Key")
if !common.IsHexAddress(aKey) {
return false, http.StatusBadRequest, fmt.Errorf("Associated key is missing or invalid")
}
associatedKey := common.HexToAddress(aKey)
// Challenge timestamp
challenge := r.Header.Get("X-ORG-ID-Challenge-Timestamp")
t, err := strconv.ParseInt(challenge, 10, 64)
if err != nil {
return false, http.StatusBadRequest, fmt.Errorf("There was a problem with challenge timestamp. %v", err)
}
challengeTime := time.Unix(t, 0)
now := time.Now()
margin := now.Add(-time.Hour * 100) // Probably should be 60 seconds
if !((challengeTime.After(margin) && challengeTime.Before(now)) || challengeTime.Equal(now)) {
return false, http.StatusBadRequest, fmt.Errorf("Challenge timestamp is older than 60 minutes: %v", challengeTime)
}
// Signature
verified, err := verifySignature(associatedKey, challenge, r.Header.Get("X-ORG-ID-Signature"))
if err != nil {
return false, http.StatusBadRequest, fmt.Errorf("There was a problem with signature verification. %v", err)
}
if !verified {
return false, http.StatusUnauthorized, fmt.Errorf("ORG.ID authorization was unsuccessful")
}
// Check whether the ORG.ID lists the associated key it tries to provide
client, err := ethclient.Dial("https://ropsten.infura.io")
if err != nil {
log.Fatal(err)
return false, http.StatusInternalServerError, fmt.Errorf("Could not connect to Ropsten")
}
org, err := organization.NewOrganization(orgid, client)
if err != nil {
log.Panic(err)
return false, http.StatusInternalServerError, fmt.Errorf("Problem connecting to the smart contract")
}
hasKey, err := org.HasAssociatedKey(nil, associatedKey)
if err != nil {
log.Panic(err)
return false, http.StatusInternalServerError, fmt.Errorf("Problem connecting to the smart contract")
}
if !hasKey {
return false, http.StatusUnauthorized, fmt.Errorf("The key does not belong to the ORG.ID")
}
return true, http.StatusOK, nil
}
func verifySignature(from common.Address, message string, signature string) (bool, error) {
msg := []byte(message)
sig := hexutil.MustDecode(signature)
if bytes.NewReader(sig).Len() != 65 {
return false, fmt.Errorf("The signature was expected to have 65 bytes, got %d intead", bytes.NewReader(sig).Len())
}
// https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L442
if sig[64] != 27 && sig[64] != 28 {
return false, errors.New("The signature must conform to the secp256k1 curve R, S and V values, where the V value must be be 27 or 28 for legacy reasons. https://github.com/ethereum/go-ethereum/blob/55599ee95d4151a2502465e0afc7c47bd1acba77/internal/ethapi/api.go#L442")
}
sig[64] -= 27
// Recover public key from the signature
pubKey, err := crypto.SigToPub(signHash(msg), sig)
if err != nil {
return false, errors.New("There was a problem recovering public key from the signature")
}
recoveredAddr := crypto.PubkeyToAddress(*pubKey)
return from == recoveredAddr, nil
}
// This is how message signing in Ethereum works, don't ask
func signHash(data []byte) []byte {
msg := fmt.Sprintf("\x19Ethereum Signed Message:\n%d%s", len(data), data)
return crypto.Keccak256([]byte(msg))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment