1

I have a web application through which I'd like to be able create playlists, add videos to a playlist, delete a video etc, to my youtube channel. I have created a service account and downloaded the service account credentials key file and set up my OAuth 2.0 Client IDs in the Google Developer Console. To authenticate my app, I followed the instructions in the README.md for the google-api-nodejs-client here https://github.com/googleapis/google-api-nodejs-client - under service account credentials

Here is my controller file... I should note that the project uses ES Modules and thus "type": "module" is set in package.json. This is why you'l notice for example that I am importing __dirname as a utility since ES Modules do not support the regular __dirname.

import googleapi from "googleapis";
const { google } = googleapi;
import Auth from "@google-cloud/local-auth";
const { authenticate } = Auth;
import path from "path";
import __dirname from "../utils/dirname.js";

async function initialize() {
  try {
    const auth = await authenticate({
      keyfilePath: path.join(__dirname, "../service_account_credentials.json"),
      scopes: ["https://www.googleapis.com/auth/youtube"],
    });
    console.log("Auth details");
    console.log(auth);
    google.options({ auth });
  } catch (e) {
    console.log(e);
  }
}
initialize();

const oauth2Client = new google.auth.OAuth2(
  "YOUR_CLIENT_ID",
  "YOUR_CLIENT_SECRET",
  "http://localhost:5000/oauth2callback"
);

// initialize the Youtube API library
const youtube = google.youtube({ version: "v3", auth: oauth2Client });

class YoutubeController {
  static async createPlaylist(req, res) {
    const { name } = req.body;
    const playlist = await youtube.playlists.insert({
      part: "snippet,status",
      resource: {
        snippet: {
          title: name,
          description: `${name} videos.`,
        },
        status: {
          privacyStatus: "private",
        },
      },
    });

    res.json(playlist);
  }
}

The initialize function, is the one that throws the error and I can't quite figure it out. I think because of that, when I make a POST request to the route that calls the method createPlaylist inside the class, I get back No access, refresh token or API key is set.

I've been going through the docs trying to understand how everything flows but I'm a little stuck.

A similar question was asked here - TypeError: Cannot read property 'redirect_uris' of undefined but there are no answers and the suggested workflow does not work for my case so I'd really appreciate your help on this.

DaImTo
  • 72,534
  • 21
  • 122
  • 346
Nelson King
  • 499
  • 6
  • 22

1 Answers1

2

service accounts

The YouTube API does not support service account authentication you need to use OAuth2.

OAuth2 Authorization

You might want to consider following the YouTube API quick start for nodejs.

The issue is that you are using service account authentication with the YouTube API which it does not support.

var fs = require('fs');
var readline = require('readline');
var {google} = require('googleapis');
var OAuth2 = google.auth.OAuth2;

// If modifying these scopes, delete your previously saved credentials
// at ~/.credentials/youtube-nodejs-quickstart.json
var SCOPES = ['https://www.googleapis.com/auth/youtube.readonly'];
var TOKEN_DIR = (process.env.HOME || process.env.HOMEPATH ||
    process.env.USERPROFILE) + '/.credentials/';
var TOKEN_PATH = TOKEN_DIR + 'youtube-nodejs-quickstart.json';

// Load client secrets from a local file.
fs.readFile('client_secret.json', function processClientSecrets(err, content) {
  if (err) {
    console.log('Error loading client secret file: ' + err);
    return;
  }
  // Authorize a client with the loaded credentials, then call the YouTube API.
  authorize(JSON.parse(content), getChannel);
});

/**
 * Create an OAuth2 client with the given credentials, and then execute the
 * given callback function.
 *
 * @param {Object} credentials The authorization client credentials.
 * @param {function} callback The callback to call with the authorized client.
 */
function authorize(credentials, callback) {
  var clientSecret = credentials.installed.client_secret;
  var clientId = credentials.installed.client_id;
  var redirectUrl = credentials.installed.redirect_uris[0];
  var oauth2Client = new OAuth2(clientId, clientSecret, redirectUrl);

  // Check if we have previously stored a token.
  fs.readFile(TOKEN_PATH, function(err, token) {
    if (err) {
      getNewToken(oauth2Client, callback);
    } else {
      oauth2Client.credentials = JSON.parse(token);
      callback(oauth2Client);
    }
  });
}

/**
 * Get and store new token after prompting for user authorization, and then
 * execute the given callback with the authorized OAuth2 client.
 *
 * @param {google.auth.OAuth2} oauth2Client The OAuth2 client to get token for.
 * @param {getEventsCallback} callback The callback to call with the authorized
 *     client.
 */
function getNewToken(oauth2Client, callback) {
  var authUrl = oauth2Client.generateAuthUrl({
    access_type: 'offline',
    scope: SCOPES
  });
  console.log('Authorize this app by visiting this url: ', authUrl);
  var rl = readline.createInterface({
    input: process.stdin,
    output: process.stdout
  });
  rl.question('Enter the code from that page here: ', function(code) {
    rl.close();
    oauth2Client.getToken(code, function(err, token) {
      if (err) {
        console.log('Error while trying to retrieve access token', err);
        return;
      }
      oauth2Client.credentials = token;
      storeToken(token);
      callback(oauth2Client);
    });
  });
}

/**
 * Store token to disk be used in later program executions.
 *
 * @param {Object} token The token to store to disk.
 */
function storeToken(token) {
  try {
    fs.mkdirSync(TOKEN_DIR);
  } catch (err) {
    if (err.code != 'EEXIST') {
      throw err;
    }
  }
  fs.writeFile(TOKEN_PATH, JSON.stringify(token), (err) => {
    if (err) throw err;
    console.log('Token stored to ' + TOKEN_PATH);
  });
}

/**
 * Lists the names and IDs of up to 10 files.
 *
 * @param {google.auth.OAuth2} auth An authorized OAuth2 client.
 */
function getChannel(auth) {
  var service = google.youtube('v3');
  service.channels.list({
    auth: auth,
    part: 'snippet,contentDetails,statistics',
    forUsername: 'GoogleDevelopers'
  }, function(err, response) {
    if (err) {
      console.log('The API returned an error: ' + err);
      return;
    }
    var channels = response.data.items;
    if (channels.length == 0) {
      console.log('No channel found.');
    } else {
      console.log('This channel\'s ID is %s. Its title is \'%s\', and ' +
                  'it has %s views.',
                  channels[0].id,
                  channels[0].snippet.title,
                  channels[0].statistics.viewCount);
    }
  });
}

Code shamelessly ripped from YouTube API quick start for nodejs.

backend access

Due to the fact that YouTube API does not support service accounts. Accessing the data from a backend service can be tricky but it is not imposible.

  1. Run your application locally once.
  2. Aurhtoirze it to access your account data.
  3. In your code find the credentials that are stored they should contain a refresh token.
  4. Save this refresh token as part of your application.
  5. set up your code to read this refresh token when loading.

Unfortunately i am not a node.js developer so i cant help you with the code required to do that. The library should be storing things into a credentials object if you can find that and how thats loaded then you should be able to do what i have suggested.

I would start by digging around into what ever storeToken(token); is doing.

DaImTo
  • 72,534
  • 21
  • 122
  • 346
  • So is there no way of avoiding the navigation to a different URL for authentication? I thought maybe it was possible to authenticate silently on the back-end and just post a video to my channel. – Nelson King Aug 17 '20 at 07:50
  • 2
    No Oauth2 requires that the user be presented with the consent form in order to consent access to your application. You can do this just once store the refresh token and use that refresh token in your bacckend then you will be able to access the account as needed. access_type: 'offline', gives you the refresh token you need. – DaImTo Aug 17 '20 at 07:57
  • My use case was a bit weird. As opposed to storing a few video files elsewhere, I thought maybe it was possible to just upload them to my youtube channel and pull them into my website. With Oauth2, this would mean I have to authenticate myself every time I open the site from a different computer and no one else can upload on my behalf. Hence, my use case seems not to be possible, so I guess google cloud storage will do then? – Nelson King Aug 17 '20 at 08:18
  • 1
    Actually with my solution you would only have to authenticate your application once have a script running in the backend with a stored refresh token to access your account. Assuming that you upload them public to your channel then you should be able to show them on your site. I cant help you with cloud storage sorry but i would think that would be quite an expensive option. – DaImTo Aug 17 '20 at 08:30
  • Just saw your edit on backend access. I'l try that. If I figure it out, I'l include the code on here. Thank you mate. – Nelson King Aug 17 '20 at 08:32
  • @NelsonKing NP, My option does work i implemented it for a customer a few years back in PHP. If you have issues getting it working post another question. – DaImTo Aug 17 '20 at 08:41
  • Your solution worked beautifully btw. However, now I'm getting `Error: Unauthorized` for some reason whenever I try to create a play list for example and I haven't even changed anything. Might you have any ideas as to why this is happening? – Nelson King Sep 01 '20 at 07:11