[Auth Providers] how to add scope for google

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

@Gwenole_Midy Instead of overriding whole user permissions just to add scope for SSO auth.
I used a different approach for it which found out to be a easy solution.
I copied the purest SSO code into my src\extensions\users-permissions\strapi-server.js and added the scopes I wanted from Facebook but please note there isn’t any way to add scopes for google.

I hope this will help someone :slight_smile: .

Seems way better !

Do you have the whole completes files and folder ?