[Auth Providers] how to add scope for google

Hi all,

When using google provider, we’re redirected to :
client_id=…&response_type=code&redirect_uri=…&scope=email&flowName=GeneralOAuthFlow

From where, we don’t have any username information.

I’d like to change the default behavior for this redirection (manually changing it, and it works fine) :
client_id=…&response_type=code&redirect_uri=…&scope=email%20profile&flowName=GeneralOAuthFlow

Steps to reproduce the behavior

  1. Launch Strapi with Google provider
  2. Login with Google provider and get the access token
  3. Check the answer by calling userinfo with header : { “Authorization”, “Bearer {access_token}” }
  4. The body is :
    {
    “sub”: “XXX”,
    “email”: “xxx@gmail.com”,
    “email_verified”: true,
    }

Expected behavior

  1. Launch Strapi with Google provider
  2. Login with Google provider and get the access token
  3. Check the answer by calling userinfo with header : { “Authorization”, “Bearer {access_token}” }
  4. The body is :
    {
    “sub”: “XXX”,
    “name”: “XXX”,
    “given_name”: “XXX”,
    “family_name”: “XXX”,
    “picture”: “XXX”,
    “email”: “xxx@gmail.com”,
    “email_verified”: true,
    “locale”: “xx”
    }

System

  • Node.js version: 14.18.1
  • NPM version:
  • Strapi version: 3.6.8
  • Database: Mongo
  • Operating system: Windows 10

Additional context

Also i modifier the providers.js for that (i guess it’s dirty) :

case “google”: {

  const instance = axios.create({

    baseURL: 'https://www.googleapis.com/oauth2/v3',

    headers: {'Authorization': 'Bearer ' + access_token }

  });

  instance.get('userinfo').then(x =>  {

    callback(null, {

      username: x.data.given_name,

      providerId: x.data.sub,

      email: x.data.email,

    });

  }, err => callback(err));

  break;

}

Anyone can please help me ? :slight_smile:

So i just did it :

image

Adding bootstrap.js file with this content :

'use strict';

/**
 * An asynchronous bootstrap function that runs before
 * your application gets started.
 *
 * This gives you an opportunity to set up your data model,
 * run jobs, or perform some special logic.
 */
const _ = require('lodash');
const uuid = require('uuid/v4');

module.exports = async () => {
  const pluginStore = strapi.store({
    environment: '',
    type: 'plugin',
    name: 'users-permissions',
  });

  const grantConfig = {
    email: {
      enabled: true,
      icon: 'envelope',
    },
    discord: {
      enabled: false,
      icon: 'discord',
      key: '',
      secret: '',
      callback: `${strapi.config.server.url}/auth/discord/callback`,
      scope: ['identify', 'email'],
    },
    facebook: {
      enabled: false,
      icon: 'facebook-square',
      key: '',
      secret: '',
      callback: `${strapi.config.server.url}/auth/facebook/callback`,
      scope: ['email'],
    },
    google: {
      enabled: false,
      icon: 'google',
      key: '',
      secret: '',
      callback: `${strapi.config.server.url}/auth/google/callback`,
      scope: ['email', 'profile'],
    },
    github: {
      enabled: false,
      icon: 'github',
      key: '',
      secret: '',
      callback: `${strapi.config.server.url}/auth/github/callback`,
      scope: ['user', 'user:email'],
    },
    microsoft: {
      enabled: false,
      icon: 'windows',
      key: '',
      secret: '',
      callback: `${strapi.config.server.url}/auth/microsoft/callback`,
      scope: ['user.read'],
    },
    twitter: {
      enabled: false,
      icon: 'twitter',
      key: '',
      secret: '',
      callback: `${strapi.config.server.url}/auth/twitter/callback`,
    },
    instagram: {
      enabled: false,
      icon: 'instagram',
      key: '',
      secret: '',
      callback: `${strapi.config.server.url}/auth/instagram/callback`,
    },
    vk: {
      enabled: false,
      icon: 'vk',
      key: '',
      secret: '',
      callback: `${strapi.config.server.url}/auth/vk/callback`,
      scope: ['email'],
    },
    twitch: {
      enabled: false,
      icon: 'twitch',
      key: '',
      secret: '',
      callback: `${strapi.config.server.url}/auth/twitch/callback`,
      scope: ['user:read:email'],
    },
  };
  const prevGrantConfig = (await pluginStore.get({ key: 'grant' })) || {};
  // store grant auth config to db
  // when plugin_users-permissions_grant is not existed in db
  // or we have added/deleted provider here.
  if (!prevGrantConfig || !_.isEqual(_.keys(prevGrantConfig), _.keys(grantConfig))) {
    // merge with the previous provider config.
    _.keys(grantConfig).forEach(key => {
      if (key in prevGrantConfig) {
        grantConfig[key] = _.merge(grantConfig[key], prevGrantConfig[key]);
      }
    });
    await pluginStore.set({ key: 'grant', value: grantConfig });
  }

  if (!(await pluginStore.get({ key: 'email' }))) {
    const value = {
      reset_password: {
        display: 'Email.template.reset_password',
        icon: 'sync',
        options: {
          from: {
            name: 'Administration Panel',
            email: 'no-reply@strapi.io',
          },
          response_email: '',
          object: 'Reset password',
          message: `<p>We heard that you lost your password. Sorry about that!</p>

<p>But don’t worry! You can use the following link to reset your password:</p>
<p><%= URL %>?code=<%= TOKEN %></p>

<p>Thanks.</p>`,
        },
      },
      email_confirmation: {
        display: 'Email.template.email_confirmation',
        icon: 'check-square',
        options: {
          from: {
            name: 'Administration Panel',
            email: 'no-reply@strapi.io',
          },
          response_email: '',
          object: 'Account confirmation',
          message: `<p>Thank you for registering!</p>

<p>You have to confirm your email address. Please click on the link below.</p>

<p><%= URL %>?confirmation=<%= CODE %></p>

<p>Thanks.</p>`,
        },
      },
    };

    await pluginStore.set({ key: 'email', value });
  }

  if (!(await pluginStore.get({ key: 'advanced' }))) {
    const value = {
      unique_email: true,
      allow_register: true,
      email_confirmation: false,
      email_confirmation_redirection: `${strapi.config.admin.url}/admin`,
      email_reset_password: `${strapi.config.admin.url}/admin`,
      default_role: 'authenticated',
    };

    await pluginStore.set({ key: 'advanced', value });
  }

  await strapi.plugins['users-permissions'].services.userspermissions.initialize();

  if (!_.get(strapi.plugins['users-permissions'], 'config.jwtSecret')) {
    const jwtSecret = uuid();
    _.set(strapi.plugins['users-permissions'], 'config.jwtSecret', jwtSecret);

    strapi.reload.isWatching = false;

    await strapi.fs.writePluginFile(
      'users-permissions',
      'config/jwt.js',
      `module.exports = {\n  jwtSecret: process.env.JWT_SECRET || '${jwtSecret}'\n};`
    );

    strapi.reload.isWatching = true;
  }
};

2 Likes

Wow, thanks for this. I’ve been fighting with Google about scopes. It’s strange because some accounts return profile information and others do not.

For V4, I did this in src/index.js


  async bootstrap({ strapi }) {
    const pluginStore = strapi.store({
      environment: '',
      type: 'plugin',
      name: 'users-permissions',
    });
    // Ensure profile scope for Google Auth
    const grantConfig = await pluginStore.get({ key: 'grant' })
    if(grantConfig){
      if(grantConfig.google && grantConfig.google.scope){
        grantConfig.google.scope = ['openid', 'email', 'profile']
        await pluginStore.set({ key: 'grant', value: grantConfig });
      }
    }
  },
3 Likes

For those using 3.6.x, I also put this into a startup hook

Thanks for this !

What an improvement ! I guess it’s just the basic grant config.

1 Like

Well using hooks i did this on v3 :

using bootstrap as i was doing before breaks the settings UI for roles and permissions, but it’s not happenging with the hook.

Thanks !

After you get all the profile information through the Google Sign In, how to store that information like Firstname LastName Gender to the users collection?

You need to override providers.js

'use strict';

/**
 * Module dependencies.
 */

// Public node modules.
const _ = require('lodash');
const request = require('request');

// Purest strategies.
const purest = require('purest')({ request });
const purestConfig = require('@purest/providers');

const axios = require('axios').default;

/**
 * Connect thanks to a third-party provider.
 *
 *
 * @param {String}    provider
 * @param {String}    access_token
 *
 * @return  {*}
 */

exports.connect = (provider, query) => {
  const access_token = query.access_token || query.code || query.oauth_token;

  return new Promise((resolve, reject) => {
    if (!access_token) {
      return reject([null, { message: 'No access_token.' }]);
    }

    // Get the profile.
    getProfile(provider, query, async (err, profile) => {
      if (err) {
        return reject([null, err]);
      }

      // We need at least the mail.
      if (!profile.email) {
        return reject([null, { message: 'Email was not available.' }]);
      }

      try {
        const users = await strapi.query('user', 'users-permissions').find({
          email: profile.email,
        });

        const advanced = await strapi
          .store({
            environment: '',
            type: 'plugin',
            name: 'users-permissions',
            key: 'advanced',
          })
          .get();

        if (
          _.isEmpty(_.find(users, { provider })) &&
          !advanced.allow_register
        ) {
          return resolve([
            null,
            [{ messages: [{ id: 'Auth.advanced.allow_register' }] }],
            'Register action is actualy not available.',
          ]);
        }

        const user = _.find(users, { provider });

        if (!_.isEmpty(user)) {
          return resolve([user, null]);
        }

        if (
          !_.isEmpty(_.find(users, user => user.provider !== provider)) &&
          advanced.unique_email
        ) {
          return resolve([
            null,
            [{ messages: [{ id: 'Auth.form.error.email.taken' }] }],
            'Email is already taken.',
          ]);
        }

        // Retrieve default role.
        const defaultRole = await strapi
          .query('role', 'users-permissions')
          .findOne({ type: advanced.default_role }, []);

        // Create the new user.
        const params = _.assign(profile, {
          provider: provider,
          role: defaultRole.id,
          confirmed: true,
        });

        const createdUser = await strapi
          .query('user', 'users-permissions')
          .create(params);

        return resolve([createdUser, null]);
      } catch (err) {
        reject([null, err]);
      }
    });
  });
};

/**
 * Helper to get profiles
 *
 * @param {String}   provider
 * @param {Function} callback
 */

const getProfile = async (provider, query, callback) => {
  const access_token = query.access_token || query.code || query.oauth_token;

  const grant = await strapi
    .store({
      environment: '',
      type: 'plugin',
      name: 'users-permissions',
      key: 'grant',
    })
    .get();

  switch (provider) {
    case 'discord': {
      const discord = purest({
        provider: 'discord',
        config: {
          discord: {
            'https://discordapp.com/api/': {
              __domain: {
                auth: {
                  auth: { bearer: '[0]' },
                },
              },
              '{endpoint}': {
                __path: {
                  alias: '__default',
                },
              },
            },
          },
        },
      });
      discord
        .query()
        .get('users/@me')
        .auth(access_token)
        .request((err, res, body) => {
          if (err) {
            callback(err);
          } else {
            // Combine username and discriminator because discord username is not unique
            var username = `${body.username}#${body.discriminator}`;
            callback(null, {
              username: username,
              email: body.email,
            });
          }
        });
      break;
    }
    case 'facebook': {
      const facebook = purest({
        provider: 'facebook',
        config: purestConfig,
      });

      facebook
        .query()
        .get('me?fields=name,email')
        .auth(access_token)
        .request((err, res, body) => {
          if (err) {
            callback(err);
          } else {
            callback(null, {
              providerId: body.id,
              username: body.name,
              email: body.email ? body.email : body.id + '@facebook.com',
            });
          }
        });
      break;
    }
    case 'google': {
      // TODO : send picture to the picture-api
      const instance = axios.create({
        baseURL: 'https://www.googleapis.com/oauth2/v3',
        headers: {'Authorization': 'Bearer ' + access_token }
      });

      instance.get('userinfo').then(x =>  {
        callback(null, {
          providerId: x.data.sub,
          username: x.data.given_name,
          email: x.data.email
        });
      }, err => callback(err));
      break;
    }
    case 'github': {
      const github = purest({
        provider: 'github',
        config: purestConfig,
        defaults: {
          headers: {
            'user-agent': 'strapi',
          },
        },
      });

      request.post(
        {
          url: 'https://github.com/login/oauth/access_token',
          form: {
            client_id: grant.github.key,
            client_secret: grant.github.secret,
            code: access_token,
          },
        },
        (err, res, body) => {
          github
            .query()
            .get('user')
            .auth(body.split('&')[0].split('=')[1])
            .request((err, res, userbody) => {
              if (err) {
                return callback(err);
              }

              // This is the public email on the github profile
              if (userbody.email) {
                return callback(null, {
                  username: userbody.login,
                  email: userbody.email,
                });
              }

              // Get the email with Github's user/emails API
              github
                .query()
                .get('user/emails')
                .auth(body.split('&')[0].split('=')[1])
                .request((err, res, emailsbody) => {
                  if (err) {
                    return callback(err);
                  }

                  return callback(null, {
                    username: userbody.login,
                    email: Array.isArray(emailsbody)
                      ? emailsbody.find(email => email.primary === true).email
                      : null,
                  });
                });
            });
        }
      );
      break;
    }
    case 'microsoft': {
      const microsoft = purest({
        provider: 'microsoft',
        config: purestConfig,
      });

      microsoft
        .query()
        .get('me')
        .auth(access_token)
        .request((err, res, body) => {
          if (err) {
            callback(err);
          } else {
            callback(null, {
              username: body.userPrincipalName,
              email: body.userPrincipalName,
            });
          }
        });
      break;
    }
    case 'twitter': {
      const twitter = purest({
        provider: 'twitter',
        config: purestConfig,
        key: grant.twitter.key,
        secret: grant.twitter.secret,
      });

      twitter
        .query()
        .get('account/verify_credentials')
        .auth(access_token, query.access_secret)
        .qs({ screen_name: query['raw[screen_name]'], include_email: 'true' })
        .request((err, res, body) => {
          if (err) {
            callback(err);
          } else {
            callback(null, {
              username: body.screen_name,
              email: body.email,
            });
          }
        });
      break;
    }
    case 'instagram': {
      const instagram = purest({
        config: purestConfig,
        provider: 'instagram',
        key: grant.instagram.key,
        secret: grant.instagram.secret,
      });

      instagram
        .query()
        .get('users/self')
        .qs({ access_token })
        .request((err, res, body) => {
          if (err) {
            callback(err);
          } else {
            callback(null, {
              username: body.data.username,
              email: `${body.data.username}@strapi.io`, // dummy email as Instagram does not provide user email
            });
          }
        });
      break;
    }
    case 'vk': {
      const vk = purest({
        provider: 'vk',
        config: purestConfig,
      });

      vk.query()
        .get('users.get')
        .auth(access_token)
        .qs({ id: query.raw.user_id, v: '5.013' })
        .request((err, res, body) => {
          if (err) {
            callback(err);
          } else {
            callback(null, {
              username: `${body.response[0].last_name} ${body.response[0].first_name}`,
              email: query.raw.email,
            });
          }
        });
      break;
    }
    default:
      callback({
        message: 'Unknown provider.',
      });
      break;
  }
};

I got this, tell me if it’s fine for you

Do a console.log in the then() of the get function to see the model retrieved by google oauth

Can anyone specify how to migrate provider.js in v4

Hi @venkateshganta,

Well it’s a little more complicated, you’d have to override user plugin i guess :

And so recreate the nodes_modules folder ‘@strapi/plugin-users-permissions/server’ to override what you’d like to.

And then only override what you need. I guess for you it’s the service folder and then the providers-registry.js file and more specifically the google part of this :

Is that clear enough for you ?

Thanks @Gwenole_Midy , Yes its clear

Does anyone know how to tell the Purest Google provider how to specify a scope array? Currently, the body only returns the very limited email information.

It’s not clear enough before so.

You have to override the complete module and change service for google

Hey did you figure it out yet ? Thanks a lots !

Hi,

I’ll figure this out on v4 (but not the last version).

You’ll have to override user-permissions by looking at the node_modules

Actually, you’ll have something looks like that :

As you can see in index.js, we only override some files but not all (the other files are taken from node_modules).

Then you override only what you need :

Put a console.log in the google part to see what can be override.

You’ll have to take the node_modules files as example

I haven’t tested it but it seems an easier way by patching packages exists :

How to Add a Custom OAuth2/OpenID Connect Provider to Strapi v4

Did this actually work for V4 (4.9.0)?

Yeah it should i’ll take a try, but it was tested in 4.5.0