How to Limit OAuth Scopes & Particle Hirearchy

Hey everyone–

I’m in the midst of setting up two-legged authentication between our NodeJS server and Particle. I’ve run into a handful of questions I was unable to find on the forums or docs, hoping some of ya’ll may have some insights.

My big-picture goals are:

  1. Create customers
  2. Claim devices
  3. Provision tokens to client-side code that only allows reading of a particular customer’s devices

What I’ve managed to do:

  1. Create a two-legged OAuth client using the Particle Console (granting full permissions)
  2. Can get a token using the client id/secret from the OAuth Client
  3. Can use this token to create customers (I think, more on this below)
  4. Outside of the OAuth client, have claimed a device (did this using the JS-api last week)

Question 1:
The Particle guides mention Scopes briefly and link to the RFC section about scopes. I’m new to OAuth, but noticed this line in the RFC:

The value of the scope parameter is expressed as a list of space-
delimited, case-sensitive strings. The strings are defined by the
authorization server.

Under creating an OAuth2 client in the Particle docs, the scope argument has this line in it:

Limits the scope of what the access tokens created using the client are allowed to do. Provide a space separated list of scopes. The only current valid scope is create_customer. Leave blank for full control

Does this mean the only scopes available for our OAuth clients are create_customer or full_control? I was hoping that I could use these OAuth tokens to meet my #3 goal above.

Question 2:
When creating a customer, is there a difference between creating them under an org vs a device? I.e.:
v1/orgs/myOrgId/customers vs.
v1/products/myDeviceId/customers

Question 3:
What’s needed to have a customer show up on the Particle Console? I’ve managed to create a customer successfully using one of the above endpoints (I honestly don’t remember which one). Under Devices I see the Owner set to the same email as the customer I created. So, as far as I know, I have a Customer with a Claimed device, but don’t see any Customers on the Particle Console.

Question 4:
Is there any documentation that explains the relationships between all the Particle objects? I’ve been trouble find anything that explictily explains the relationship between Orgs, Devices, Customers, etc.

Q1: Correct, there are only two scopes, full and create customer. You typically use full from your web server, and create customer when you create customer from a mobile app directly (not via your server).

Q2: Use the products API. The orgs API is the old one, and you should avoid using any of the org endpoints.

Q3: The customer must successfully claim a device in order to appear as a customer for a product. Until then, the customer in a state of limbo where they partially exist (you’ll get an error if you try to create the same customer) but also does not show up anywhere.

I think it’s also necessary for the customer to have claimed using a customer product claim code.

This might help explain some of the relationships between the various things.

2 Likes

@rickkas7 Ah, you’re a godsend. It turns out the piece I was missing was the claimCode. Also, thank you for your notes, they are incredibly helpful.

I’ve gone down the rabbit hole a bit looking through your notes. I’m looking to setup my Photon’s wifi creds over the air, so I went through your section on setting up wifi. It appears that I’m following your steps correctly, but after attempting to setup the wifi creds via both curl and Node’s request package, all I get is {r:0} and the Particle continues to blink blue.

I assumed that it would go back to breathing cyan once wifi creds were set, is that correct?

Do you have any links to documentation around setting wifi credentials this way? I wasn’t having any luck digging around myself.

In case there’s something I’m doing obviously wrong, here’s a streamlined version of what I wrote up:

const wifiSSID = 'myWifiSSID'
const wifiPass = 'myWifiPassword'

// Scan for access points
sap.scan(function (error, data) {
  // Grab the AP you've indicated above
  const myWifi = _.find(data, ['ssid', wifiSSID])

  // Now grab the public key off of the Particle board
  request({
    method: 'GET',
    url: 'http://192.168.0.1/public-key'
  }, function (error, response, body) {
    const asJSON = JSON.parse(body)

    var keyBuf = new Buffer(asJSON.b, 'hex')
    var rsa = new NodeRSA(keyBuf.slice(22), 'pkcs1-public-der', {
      encryptionScheme: 'pkcs1'
    })

    // Encrypt the wifiPass
    const encryptedWifiPass = rsa.encrypt(wifiPass, 'hex')

    // Configure the Photon's wifi
    request({
      method: 'POST',
      url: 'http://192.168.0.1/configure-ap',
      form: {
        idx: 0,
        ssid: myWifi.ssid,
        sec: myWifi.sec,
        ch: myWifi.ch,
        pwd: encryptedWifiPass
      }
    }, function (error, response, body) {
      console.log(body) // <--- {r:0}
    })
  })
})
1 Like

The configure-ap step normally stays in listening mode. In order to reboot and use the config you need to send a:

curl -X POST http://192.168.0.1/connect-ap -d '{"idx":0}'

That tells the Photon to reboot and try using the configuration in the specified idx, which should match the one you just set.

2 Likes

Thanks again!

I wanted to follow up with my working solution in case anyone else is having issues with setting up via Wifi with Node. I found someone else’s solution, which looks like it’s based on some of your RSA work in JS. What I ended up building is based off of their solution and what’s in your notes

I was able to configure the particle using curl, but when translating that into HTTP requests in Node, there were some issues. Initially I tried to build this out with just using the request package, but some of the commands seemed to not “work.” For example, the POST request to http://192.168.0.1/configure-ap plain wouldn’t work with request even though the headers, content length, etc in the requests seemed to be the same as curl. The Particle responded, but the settings never took.

Here’s a working example using Node (I’m intending on cleaning this up to not use SoftAPSetup, request and XMLHttpRequest, but this gets the idea across):

const SoftAPSetup = require('softap-setup')
const XMLHttpRequest = require("xmlhttprequest").XMLHttpRequest
const _ = require('lodash')
const request = require('request')
const NodeRSA = require('node-rsa')

const sap = new SoftAPSetup()
const wifiSSID = 'mySSID'
const wifiPass = 'myPass'

function postRequest (url, jsonData, callback) {
  var dataString = JSON.stringify(jsonData)
  var xmlhttp = new XMLHttpRequest()
  xmlhttp.open('POST', url, true) //true specifies async
  xmlhttp.timeout = 4000
  xmlhttp.setRequestHeader('Content-Type', 'multipart/form-data')
  xmlhttp.withCredentials = false

  xmlhttp.send(dataString)

  // Handle response
  xmlhttp.onreadystatechange = function () {
    if (xmlhttp.readyState == 4) {
      if (xmlhttp.status === 200) {
        callback(null, JSON.parse(xmlhttp.responseText))
      } else {
        callback(xmlhttp.status + '\n' + xmlhttp.responseTex)
      }
    }
  }
}

// 1) See if you can find the specified wifi network
sap.scan(function (error, data) {
  if (error) {
    console.log(error)
    return
  }

  // Grab the AP you've indicated above
  const myWifi = _.find(data, ['ssid', wifiSSID])

  if (!myWifi) {
    console.log('Couldn\'t find wifi with ssid: ' + wifiSSID + ' in \n', data)
    return
  }

  // 2) Grab the Particle's public key
  request({
    method: 'GET',
    url: 'http://192.168.0.1/public-key'
  }, function (error, response, body) {
    if (error) {
      console.log(error)
      return
    }

    const asJSON = JSON.parse(body)

    if (!asJSON.b) {
      console.log('Error in getting public key: ', body)
      return
    }

    const publicKey = asJSON.b

    // 3) Encrypt the wifi password using the public key
    var keyBuf = new Buffer(publicKey, 'hex')

    var rsa = new NodeRSA(keyBuf.slice(22), 'pkcs1-public-der', {
      encryptionScheme: 'pkcs1'
    })

    const encryptedWifiPass = rsa.encrypt(wifiPass, 'hex')

    // 4) Save to the Photon the wifi creds
    // idx is the index to save to, 0 is the first.
    // ssid is the SSID of the network
    // sec is the security type of the network
    // ch is the channel number
    // pwd is the encrypted password
    const configuration = {
      idx: 0,
      ssid: myWifi.ssid,
      sec: myWifi.sec,
      ch: myWifi.ch,
      pwd: encryptedWifiPass
    }

    console.log('Configuring: ', '\n', configuration, '\n')

    postRequest('http://192.168.0.1/configure-ap', configuration, function (error, response) {
      if (error) {
        console.log('Error in setting creds: ', error)
        return
      }

      console.log('Creds set')

      // 5) Reset the photon
      request({
        method: 'POST',
        url: 'http://192.168.0.1/connect-ap',
        form: {
          idx: 0
        }
      }, function (error, response, body) {
        console.log('Should be connecting...')
      })
    })
  })
})