How to: Authorize (Hardware)

How to Authenticate and Authorize a Hardware Device


Introduction

In this guide, we will learn how to Authenticate and Authorize a C2C hardware device on a Frame.io Project.

What will I need?

If you haven’t read the Implementing C2C: Setting Up guide, give it a quick glance before moving on!

Additionally, you should have received a client_secret from our team that will be used to identify your integration. If you have not received a client_secret, please take a look at this introduction to the C2C ecosystem and reach out to our team.

If you received a client_id instead of a client_secret, then we have set you up as a C2C Application, rather than a hardware device, and you will either want to follow the C2C Application guide or reach out to our team to be issued a client_secret instead.

Walking through the hardware auth flow

Let’s make sure we have a high-level understanding of the expected user experience for the Authorization Flow we want to implement. Check out the following resources to see this flow from the user’s point of view:

The hardware authorization flow is designed to lift as many details as possible off the implementer, and therefore the device UI. With this flow, you do not need to worry about:

  • Redirecting to a web browser.
  • Handling Frame.io user login / authentication
  • Listing / selecting the account and project to connect to.
  • Any UI elements beyond basic information displays.

Device identification (client_id)

When connecting to Camera to Cloud, each physical hardware device will need to identify itself uniquely.

For hardware devices, we call this the client_idof the device. When setting up your implementation, you should think about how you want to identify a specific device. You could use a serial number of the hardware device, a UUID, or some uniquely identifying string.

Be careful not to leak personally identifying information. The user’s e-mail, for instance, is not a valid value to use as a client_id.

Likewise, make sure you own the unique identifier. Do not use the device’s MAC Address, for instance. The MAC address is not owned by your software, and might also be considered personally identifying information.

If you’re not sure what value you would like to use, we can talk through this choice together and make sure a suitable value is chosen that makes integration as easy as possible.

Step 1: Requesting a device code

Let's start implementing. The first thing we need to is request a device code to give the user for a device. We do that by calling the /v2/auth/device/code endpoint:

Shell
curl -X POST https://api.frame.io/v2/auth/device/code \
    --form 'client_id=[client_id]' \
    --form 'client_secret=[client_secret]' \
    --form  'scope=asset_create offline' \
    | python -m json.tool
API endpoint specificaton

Docs for /v2/auth/device/code can be found here

form data vs JSON data

We are using form data here rather than JSON data. The C2C authentication endpoints only accept form data. Once you are authenticated, other endpoints will accept JSON payload, but the authentication endpoints will return an error if JSON payloads are sent.

Let's take a closer look at the payload parameters:

client_id: A unique identifier for the physical hardware device. This value needs to be guaranteed to be unique for the device. This could be a serial number or a randomly generated UUID.

client_secret: This will be issued to you by Frame.io support and identifies the your device model. This value should be kept secret from the user, and should be encrypted at rest.

scope: The permissions we are requesting, with spaces used as delimiters. Hardware devices can only request the following two scopes:

  • asset_create: Allows the device to create and upload assets.
  • offline: Allows the device to refresh its own authorization using a refresh token. Authorization tokens expire after 8 hours, so without this scope a user would need to re-authorize their device every 8 hours.

In practice, devices will almost always want to request both scopes.

When we make the request, we will get a response similar to the following:

JSON
{
    "device_code": "[device_code]",
    "expires_in": 120,
    "interval": 5,
    "name": "MyDevice-[client_id]",
    "user_code": "573131"
}

Let's break down this response:

device_code: The device code should be hidden from the user, and is used to identify this authorization request when polling to see if the user has entered their code successfully.

expires_in: The number of seconds until this code expires.

interval: How long the we should wait between polling requests to see if the user has entered the code.

name: The name of the device we are trying to connect.

user_code: The six-digit code the user will enter into Frame.io to pair the device to a project.

Now we can display the user code and expiration time to the user to pair their device!

Step 2: Polling for user authorization

Once we have handed the pairing code to the user, we need to check and see if they've entered it. To do so, we can make the following request:

Shell
curl -X POST https://api.frame.io/v2/auth/token \
    --form 'client_id=[client_id]' \
    --form 'device_code=[device_code]' \
    --form 'grant_type=urn:ietf:params:oauth:grant-type:device_code' \
    | python -m json.tool
API endpoint specificaton

Docs for /v2/auth/token can be found here

Payload parameters:

client_id: The same client_id sent in step 1.

device_code: The device code returned by /v2/auth/device/code.

grant_type: The type of authorization grant our Oauth system is issuing. This value will always be urn:ietf:params:oauth:grant-type:device_code.

The first few times we make this request, we'll likely get a response like so:

JSON
{
    "error": "authorization_pending"
}

But not to worry! This is not a fatal error! It simply means the user has not yet entered the user_code into Frame.io's UI. All we need to do is keep polling until they have!

If instead, we get an error like so:

JSON
{
    "error": "expired_token"
}

That means our code expired before the user could enter it. In such a case, we should generate a new pairing code using Step 1, display it to the user, then resume polling.

Eventually we should get a response like:

JSON
{
    "access_token": "[access_token]",
    "expires_in": 28800,
    "refresh_token": "[refresh_token]",
    "token_type": "bearer"
}

If you're response payload looks like that: Whoo! You did it! You've authorized your first Camera to Cloud device. Take a moment to celebrate!

After you've celebrated, let's take a look at that response payload to make sure we understand it:

access_token: This is your key to the rest of the Frame.io backend. We will need to add this to the header of the rest of the requests we are going to make in these tutorials.

expires_in: The number of seconds until access_token expires. After the token's time is up, it will need to be refreshed, which we will go over in a future tutorial.

refresh_token: A token we can use to manage our access_token. It will most commonly be used to refresh our authorization, but it can also be used to revoke it.

token_type: Will always be bearer for the C2C API, and is not actionable.

Putting the steps together

Now that we know the two calls we need to make, let's put them together into some python-like pseudocode. Remember, it's possible for our device code to expire, so we need to handle that possibility when setting up our logic:

Python
def authorize_with_frame():
    """
    Handles authorizing our device with Frame.io.
    """

    # Our client ID can be a serial number, UUID, or some other unique string.
    client_id = THIS_DEVICE.get_serial_number()

    # Make the call to Frame.io to get our device codes.
    pairing_codes = c2c.get_device_codes(client_id)

    # We need to keep track of how long we have been polling for
    polling_started = datetime.now()

    # Now we are going to poll for authorization until the user enters the code.
    while True:

        # Re-write this ourput each time we poll: Note: This message will only update 
        # once per `interval` (5 seconds), so if a smooth countdown is desired, that 
        # will need a different implementation
        print(
            f"\rPAIRING CODE: {pairing_codes.user_code}" 
            f", EXPIRES IN: {pairing_codes.expired_in - (datetime.now() - polling_started)} seconds"
        )

        # Wait for `interval` before polling each time.
        time.sleep(pairing_codes.interval)

        try
            # Make a call to Frame.io to see if the user has entered the code and authorized 
            # the device.
            authorization, error = c2c.poll_for_authorization(
                client_id, pairing_codes.device_code
            )
        except FrameioError as error:
            if error.message == "authorization_pending":
                # If the authorization is pending, try again.
                continue
            elif error.message == "expired_token":
                # If the pairing codes have expired, then we need to generate new ones.
                pairing_codes = c2c.get_device_codes(client_id)
            elif error != None:
                # If we get any other error, we should raise it. (advanced error handling will
                # be covered in another tutorial)
                raise error

        return authorization
Error handling

The errors guide is a great resource for a deeper dive on handling errors.

The last thing we should do is fetch the information about the project we have connected to from Frame.io and display it to the user for an extra layer of confirmation that the device was paired to the intended project. We'll show that in a later tutorial.

Troubleshooting

If you find yourself here then something has gone wrong! What would integrating with a third party be without some sort of error? This section lists a set of common issues and will walk you through the steps most likely to solve them. Take a look through the following list and see if anything matches your problem. The errors guide is also great resource for looking up API errors.

If you don’t find a solution here, we would love to hear the issue you ran into so we can add it here.

I don’t see a 'Connect Device' button: If you go to the C2C Management panel and don't see a Connect Device button, then one fo two things is happening:

  • C2C is not enabled for your account: if the screen is blank and there is a message about C2C not being available for your account, the account manager needs to enable it for your project in the account settings!
  • You are not a device manger: if the screen is blank and there is a message about not having permissions, then the account manager either needs to change the permissions for who is allowed to connect C2C devices, or add you into a role that has those permissions.
  • You already have a device connected: After the first device is connected, the big blue Add New Device goes away, and instead you need to go to the three-dot menu in the upper-right hand corner of the C2C Connections panel.

Invalid Client error: invalid_client is returned when the information you are providing to us about the device does not match anything we have on record. This most likely means that your client_secret is incorrect.

Bad Request error: bad_request is returned when the request data is malformed in some way. Double check that you have not misspelled a field name or forgotten to add a required field.

I get a Slow Down error when generating pairing codes: If your networking library is using TCP connection pooling (where the library re-uses a TCP connection across multiple requests), then any additional requests made to the get new pairing codes after the initial ones will return a Slow Down error. This is a known bug that occurs when a TCP connection is reused to generate more than one pairing code. This error will be returned no matter how long it has been since the first request.

For now, pairing code requests should be made on their own TCP connection. This may mean spinning up a new HTTP client (or similar abstraction) to make pairing code requests.

Next up

If you haven’t already, we encourage you to reach out to our team, then continue to the next guide. We look forward to hearing from you!