How to: Authorize

How to Authenticate and Authorize a C2C Device


Introduction

This guide demonstrates the authentication and authorization process for Camera to Cloud (C2C) devices on a Frame.io project. We'll explore both the standard manual code entry method and the enhanced QR code pairing approach for optimal user experience.

What Will I Need?

Please review the Before We Start Implementing guide if you haven't already.

You should have received a client_secret from our team to identify your integration. If not, please consult this introduction to the C2C ecosystem and contact our team.

Prerequisites for URL & QR Code Pairing

To implement URL & QR code pairing, ensure you meet these requirements:

  • Device Compatibility: Verify your device supports URL/QR code generation during the pairing process.

Walking Through the Auth Flow

To understand the authorization flow from a user perspective, please reference these resources:

This authorization process minimizes implementation requirements. You won't need to:

  • Redirect to web browsers (unless using URL Code Pairing)
  • Handle Frame.io user authentication
  • Present account/project selection interfaces
  • Develop complex UI components beyond basic information displays

Enhancing the User Experience with URL Code Pairing

Modern users expect efficient device interactions. While the current manual pairing process functions adequately, it can be optimized.

By implementing URL & QR code pairing—similar to streaming services like Netflix or Disney+—we can significantly streamline the process, minimize input errors, and reduce pairing time.

Device Identification (client_id)

Each physical device requires a unique identifier for connection tracking within a user's project.

For devices, this identifier is the client_id, which is essential during authorization. When implementing, consider appropriate identifier sources such as device serial numbers, UUIDs, or other unique strings. If you are integrating on an Apple device, we recommend using a unique persistent UUID that is consistent across device reboots.

Exercise caution regarding personally identifiable information. User email addresses are not appropriate client_id values.

Additionally, ensure you control the identifier. Device MAC addresses are unsuitable as they aren't owned by your software and may constitute personally identifiable information.

If you need guidance on selecting an appropriate identifier, our team can assist in determining a suitable value that simplifies integration.

Step 1: Requesting a Device Code

To begin implementation, request a device code through the /v2/auth/device/code endpoint:

Traditional Pairing Method

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

Enabling URL Code Pairing

For URL code pairing, modify the API call with additional headers:

curl -X POST https://api.frame.io/v2/auth/device/code \
    --header "x-client-version: 2.0.0" \
    --header "x-client-platypus-enabled: true" \  # New header to enable URL pairing
    --form 'client_id=[client_id]' \
    --form 'client_secret=[client_secret]' \
    --form 'scope=asset_create offline' \
    | python -m json.tool

Note: These authentication endpoints accept form data exclusively, not JSON. Post-authentication, other endpoints will accept JSON payloads, but authentication endpoints will reject JSON requests.

Payload Parameters

  • client_id: The unique identifier for your physical device. This must be guaranteed unique, such as a serial number or UUID.
  • client_secret: Provided by Frame.io support to identify your device model. This confidential value should remain protected from users and encrypted when stored.
  • scope: The requested permissions, separated by spaces. Devices may request:

    • asset_create: Enables asset creation and uploading.
    • offline: Permits authorization refreshing via refresh token. Without this scope, users would need to re-authorize their device every 8 hours as authorization tokens expire.

In practical implementations, devices typically request both scopes.

Understanding the API Response

The request generates a response similar to:

Traditional Pairing Response

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

URL Pairing Response

{
  "device_code": "[device_code]",
  "expires_in": 120,
  "interval": 5,
  "name": "MyDevice-[client_id]",
  "user_code": "573131",
  "verification_uri": "https://next.frame.io/pair",
  "verification_uri_complete": "https://next.frame.io/pair/573131"
}

Response Breakdown

  • device_code: This internal identifier should remain hidden from users and identifies the authorization request during polling.
  • expires_in: The code's validity period in seconds.
  • interval: The recommended polling interval in seconds.
  • name: The connecting device's identifier.
  • user_code: The six-digit code for manual entry into Frame.io for device pairing.
  • verification_uri: The base URL for manual entry if QR scanning is unavailable.
  • verificationuricomplete: The full URL containing the pairing code, intended for hyperlinking within a mobile app or QR code generation to streamline user navigation to the pairing interface.

Displaying the QR Code to the User

Using the verification_uri_complete, generate and display a QR code on the device screen for the user to scan, facilitating efficient pairing.

Example: Device Screen with QR Code Displayed

Example device QR code pairing screen

Always provide fallback options: display the user_code and verification_uri for manual entry when QR scanning isn't possible. Alternatively, consider displaying the verification_uri as a static QR code for mobile scanning.

For mobile app integrations, include the verification_uri_complete as a tappable hyperlink since users cannot scan QR codes from the device running the app.

Step 2: Polling for User Authorization

After providing the pairing code or URL code, verify user entry with this request:

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

Payload Parameters

  • client_id: The same identifier used in Step 1.
  • device_code: The device_code value returned previously.
  • grant_type: The OAuth grant type identifier, consistently urn:ietf:params:oauth:grant-type:device_code for this implementation.

Initial polling attempts typically return:

{
  "error": "authorization_pending"
}

This non-fatal error indicates the user hasn't completed code entry. Continue polling until completion.

If you receive:

{
  "error": "expired_token"
}

The code expired before user entry. Generate a new code/QR code via Step 1, present it to the user, and resume polling.

Successful authorization produces:

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

Congratulations on successfully authorizing your Camera to Cloud device!

Let's examine this response:

  • access_token: Your authentication credential for Frame.io backend access, required in headers for future API requests.
  • expires_in: The access token validity period in seconds, after which refreshing is necessary.
  • refresh_token: Used for access token management, primarily for refreshing authorization but also applicable for revocation.
  • token_type: Consistently bearer for C2C API implementations, requiring no action.

Putting the Steps Together

Now let's implement these API calls in Python-like pseudocode, handling potential device code expiration:

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()

    while True:
        # 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 output each time we poll. Note: This message will only update once
            # per `interval` (e.g., 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.expires_in - (datetime.now() - polling_started).seconds} seconds"
            )

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

            # 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
            )

            if error and error.message == "authorization_pending":
                # If the authorization is pending, try again.
                continue
            elif error and error.message == "expired_token":
                # If the pairing codes have expired, break to generate new codes.
                break
            elif error:
                # If there was some other error, raise it.
                raise error
            else:
                # If there was no error, we have our authorization!
                return authorization

        # If we get here, our pairing codes expired. Let's try again.
        print("\nPairing code expired. Generating a new one...")

Note: The outer loop handles cases where pairing codes expire and new codes are required.

As a final step, retrieve and display project information from Frame.io to confirm successful pairing to the intended project. We'll cover this in the next tutorial.

Creating and Displaying QR Codes for Pairing

When implementing URL/QR code pairing, you'll need to generate a QR code from the verification_uri_complete value in the response. Here are examples using popular libraries in different programming languages:

Python Example using qrcode

Python
import qrcode
from PIL import Image
import io

def generate_qr_code(verification_uri_complete, size=250):
    """
    Generate a QR code from the verification_uri_complete URL.
    
    Args:
        verification_uri_complete (str): The complete verification URI returned by Frame.io
        size (int, optional): Size of the QR code in pixels. Defaults to 250.
    
    Returns:
        PIL.Image: QR code image that can be displayed or saved
    """
    qr = qrcode.QRCode(
        version=1,
        error_correction=qrcode.constants.ERROR_CORRECT_L,
        box_size=10,
        border=4,
    )
    qr.add_data(verification_uri_complete)
    qr.make(fit=True)
    
    img = qr.make_image(fill_color="black", back_color="white")
    
    # Resize the image if needed
    img = img.resize((size, size))
    return img

# Example usage in authorization flow
def display_qr_for_pairing(pairing_codes):
    """
    Generate and display QR code along with manual pairing instructions.
    """
    if hasattr(pairing_codes, 'verification_uri_complete'):
        # Generate QR code from the verification URI
        qr_img = generate_qr_code(pairing_codes.verification_uri_complete)
        
        # Display the QR code on screen
        # For GUI applications like Tkinter, PyQt, etc.
        # display_image(qr_img)
        
        # For headless devices or testing, save to file
        qr_img.save("frame_io_pairing_qr.png")
        
        print(f"Scan the QR code or visit: {pairing_codes.verification_uri}")
        print(f"Manual code: {pairing_codes.user_code}")
    else:
        # Fallback for devices that received traditional pairing response
        print(f"Enter code on Frame.io: {pairing_codes.user_code}")

JavaScript Example (Web or Electron)

JavaScript
import QRCode from 'qrcode';

/**
 * Generate and display a QR code from the verification URI
 * @param {string} verificationUriComplete - The complete verification URI from Frame.io
 * @param {string} elementId - ID of the HTML element to display the QR code in
 */
function displayQRCode(verificationUriComplete, elementId = 'qrcode-container') {
  const element = document.getElementById(elementId);
  
  if (!element) {
    console.error(`Element with ID ${elementId} not found`);
    return;
  }
  
  // Clear any existing content
  element.innerHTML = '';
  
  // Generate QR code
  QRCode.toCanvas(element, verificationUriComplete, { width: 250 }, function(error) {
    if (error) {
      console.error('Error generating QR code:', error);
      // Fallback to displaying the URL as a link
      element.innerHTML = `<a href="${verificationUriComplete}" target="_blank">Click here to pair</a>`;
    }
  });
  
  // Also display manual pairing information
  const manualInfoDiv = document.createElement('div');
  manualInfoDiv.innerHTML = `
    <p>Scan the QR code or <a href="${verificationUriComplete}" target="_blank">click here</a> to pair your device.</p>
    <p>Manual code: ${userCode}</p>
  `;
  element.parentNode.appendChild(manualInfoDiv);
}

// Example usage in authorization flow
async function requestDeviceCode() {
  try {
    const response = await fetch('https://api.frame.io/v2/auth/device/code', {
      method: 'POST',
      headers: {
        'x-client-version': '2.0.0',
        'x-client-platypus-enabled': 'true'
      },
      body: new URLSearchParams({
        'client_id': YOUR_CLIENT_ID,
        'client_secret': YOUR_CLIENT_SECRET,
        'scope': 'asset_create offline'
      })
    });
    
    const data = await response.json();
    
    if (data.verification_uri_complete) {
      displayQRCode(data.verification_uri_complete);
      window.userCode = data.user_code; // Store for display purposes
    } else {
      // Fallback for traditional pairing
      displayManualPairingCode(data.user_code);
    }
    
    // Begin polling for authorization
    beginPollingForAuthorization(data.device_code, data.interval);
    
  } catch (error) {
    console.error('Error requesting device code:', error);
  }
}

Android Example (Java)

Java
import android.graphics.Bitmap;
import android.widget.ImageView;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.common.BitMatrix;
import com.journeyapps.barcodescanner.BarcodeEncoder;

public void generateAndDisplayQRCode(String verificationUriComplete, ImageView qrCodeImageView) {
    try {
        MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
        BitMatrix bitMatrix = multiFormatWriter.encode(verificationUriComplete, 
            BarcodeFormat.QR_CODE, 250, 250);
        BarcodeEncoder barcodeEncoder = new BarcodeEncoder();
        Bitmap bitmap = barcodeEncoder.createBitmap(bitMatrix);
        
        // Display in ImageView
        qrCodeImageView.setImageBitmap(bitmap);
        
    } catch (Exception e) {
        e.printStackTrace();
        // Fallback to displaying the URL as text
    }
}

iOS Example (Swift)

Swift
import UIKit
import CoreImage

func generateQRCode(from string: String) -> UIImage? {
    let data = string.data(using: String.Encoding.utf8)
    
    if let filter = CIFilter(name: "CIQRCodeGenerator") {
        filter.setValue(data, forKey: "inputMessage")
        filter.setValue("H", forKey: "inputCorrectionLevel")
        
        if let outputImage = filter.outputImage {
            // Scale the image
            let transform = CGAffineTransform(scaleX: 10, y: 10)
            let scaledImage = outputImage.transformed(by: transform)
            
            // Convert to UIImage
            let context = CIContext()
            if let cgImage = context.createCGImage(scaledImage, from: scaledImage.extent) {
                return UIImage(cgImage: cgImage)
            }
        }
    }
    
    return nil
}

// Usage in your view controller
func displayPairingQRCode(verificationUriComplete: String) {
    if let qrCodeImage = generateQRCode(from: verificationUriComplete) {
        qrCodeImageView.image = qrCodeImage
        
        // Also show manual pairing information
        pairingInstructionsLabel.text = "Scan the QR code or enter code manually"
        pairingCodeLabel.text = userCode
    } else {
        // Fallback to manual code display
        pairingInstructionsLabel.text = "Enter this code on Frame.io:"
        pairingCodeLabel.text = userCode
    }
}

Best Practices for QR Code Display

When implementing QR code pairing, consider these guidelines for the best user experience:

  1. Optimal Size: Display QR codes at least 200-250 pixels square for reliable scanning.
  2. Contrast: Ensure high contrast between QR code and background (black on white is ideal).
  3. Error Correction: Use moderate error correction levels (L or M) to balance code density and reliability.
  4. Clear Instructions: Provide clear guidance on how to scan the code, such as "Scan this code with your smartphone camera to pair your device."
  5. Multiple Options: Always provide the manual pairing code alongside the QR code as a fallback:

    Scan to pair:
    [QR CODE]
    
    Or enter code manually: 573131
  6. Hyperlink for Mobile Apps: If your integration is a mobile application, include the verification_uri_complete as a tappable link since users cannot scan a QR code from the same device.
  7. Testing: Test your QR codes with various devices and lighting conditions to ensure reliable scanning.

Example QR Code Display

Troubleshooting

If you encounter issues, consult these common scenarios and solutions:

  • "Connect Device" Button Not Visible: When accessing the C2C management panel, this could indicate:

    • Insufficient Permissions: If you see a permissions message, contact your account manager to adjust permissions or assign an appropriate role.
    • Existing Device Connection: After connecting one device, the primary "Add New Device" button is replaced by a three-dot menu in the upper-right corner of the C2C Connections panel.
  • Invalid Client Error: An invalid_client response indicates device information mismatch, typically due to an incorrect client_secret.
  • Bad Request Error: A bad_request response indicates malformed request data. Verify field names and ensure all required fields are included.

If your issue isn't addressed here, please share your experience so we can enhance this troubleshooting section.

Next Steps

We encourage you to contact our team and proceed to the authorization management guide. We look forward to your feedback!