Skip to content

Instantly share code, notes, and snippets.

@imCorfitz
Created June 14, 2021 11:27
Show Gist options
  • Save imCorfitz/35252d6cadec811693b9c4a23200a1ef to your computer and use it in GitHub Desktop.
Save imCorfitz/35252d6cadec811693b9c4a23200a1ef to your computer and use it in GitHub Desktop.
Strapi Refresh Token
// extensions/users-permissions/controllers/Auth.js
"use strict";
/**
* Auth.js controller
*
* @description: A set of functions called "actions" for managing `Auth`.
*/
/* eslint-disable no-useless-escape */
const crypto = require("crypto");
const _ = require("lodash");
const grant = require("grant-koa");
const { sanitizeEntity } = require("strapi-utils");
const emailRegExp =
/^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
const formatError = (error) => [
{ messages: [{ id: error.id, message: error.message, field: error.field }] },
];
const generateRefreshToken = (user) => {
return strapi.plugins["users-permissions"].services.jwt.issue(
{
tkv: user.tokenVersion, // Token Version
},
{
subject: user.id.toString(),
expiresIn: "60d",
}
);
}
module.exports = {
async refreshToken(ctx) {
const params = _.assign(ctx.request.body);
// Params should consist of:
// * token - string - jwt refresh token
// * renew - boolean - if true, also return an updated refresh token.
// Parse Token
try {
// Unpack refresh token
const {tkv, iat, exp, sub} = await strapi.plugins[
"users-permissions"
].services.jwt.verify(params.token);
// Check iif refresh token has expired
if (Date.now() / 1000 > exp)
return ctx.badRequest(null, "Expired refresh token");
// fetch user based on subject
const user = await strapi
.query("user", "users-permissions")
.findOne({ id: sub });
// Check here if user token version is the same as in refresh token
// This will ensure that the refresh token hasn't been made invalid by a password change or similar.
if (tkv !== user.tokenVersion) return ctx.badRequest(null, "Refresh token is invalid");
// Otherwise we are good to go.
ctx.send({
jwt: strapi.plugins["users-permissions"].services.jwt.issue({
id: user.id,
}),
refresh: params.renew ? generateRefreshToken(user) : null
});
} catch (e) {
return ctx.badRequest(null, "Invalid token");
}
},
async revoke(ctx) {
const params = _.assign(ctx.request.body);
// Params should consist of:
// * token - string - jwt refresh token
// * renew - boolean - if true, also return an updated refresh token.
// Parse Token
try {
// Unpack refresh token
const { tkv, iat, exp, sub } = await strapi.plugins[
"users-permissions"
].services.jwt.verify(params.token);
// Check iif refresh token has expired
if (Date.now() / 1000 > exp)
return ctx.badRequest(null, "Expired refresh token");
// fetch user based on subject
const user = await strapi
.query("user", "users-permissions")
.findOne({ id: sub });
// Check here if user token version is the same as in refresh token
// This will ensure that the refresh token hasn't been made invalid by a password change or similar.
if (tkv !== user.tokenVersion)
return ctx.badRequest(null, "Refresh token is invalid");
// Update the user.
await strapi
.query("user", "users-permissions")
.update({ id: sub }, { tokenVersion: user.tokenVersion + 1 });
// Otherwise we are good to go.
ctx.send({
confirmed: true,
});
} catch (e) {
return ctx.badRequest(null, "Invalid token");
}
},
async callback(ctx) {
const provider = ctx.params.provider || "local";
const params = ctx.request.body;
const store = await strapi.store({
environment: "",
type: "plugin",
name: "users-permissions",
});
if (provider === "local") {
if (!_.get(await store.get({ key: "grant" }), "email.enabled")) {
return ctx.badRequest(null, "This provider is disabled.");
}
// The identifier is required.
if (!params.identifier) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.email.provide",
message: "Please provide your username or your e-mail.",
})
);
}
// The password is required.
if (!params.password) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.password.provide",
message: "Please provide your password.",
})
);
}
const query = { provider };
// Check if the provided identifier is an email or not.
const isEmail = emailRegExp.test(params.identifier);
// Set the identifier to the appropriate query field.
if (isEmail) {
query.email = params.identifier.toLowerCase();
} else {
query.username = params.identifier;
}
// Check if the user exists.
const user = await strapi
.query("user", "users-permissions")
.findOne(query);
if (!user) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.invalid",
message: "Identifier or password invalid.",
})
);
}
if (
_.get(await store.get({ key: "advanced" }), "email_confirmation") &&
user.confirmed !== true
) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.confirmed",
message: "Your account email is not confirmed",
})
);
}
if (user.blocked === true) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.blocked",
message: "Your account has been blocked by an administrator",
})
);
}
// The user never authenticated with the `local` provider.
if (!user.password) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.password.local",
message:
"This user never set a local password, please login with the provider used during account creation.",
})
);
}
const validPassword = await strapi.plugins[
"users-permissions"
].services.user.validatePassword(params.password, user.password);
if (!validPassword) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.invalid",
message: "Identifier or password invalid.",
})
);
} else {
ctx.send({
jwt: strapi.plugins["users-permissions"].services.jwt.issue({
id: user.id,
}),
refresh: generateRefreshToken(user),
user: sanitizeEntity(user.toJSON ? user.toJSON() : user, {
model: strapi.query("user", "users-permissions").model,
}),
});
}
} else {
if (!_.get(await store.get({ key: "grant" }), [provider, "enabled"])) {
return ctx.badRequest(
null,
formatError({
id: "provider.disabled",
message: "This provider is disabled.",
})
);
}
// Connect the user with the third-party provider.
let user;
let error;
try {
[user, error] = await strapi.plugins[
"users-permissions"
].services.providers.connect(provider, ctx.query);
} catch ([user, error]) {
return ctx.badRequest(null, error === "array" ? error[0] : error);
}
if (!user) {
return ctx.badRequest(null, error === "array" ? error[0] : error);
}
ctx.send({
jwt: strapi.plugins["users-permissions"].services.jwt.issue({
id: user.id,
}),
refresh: generateRefreshToken(user),
user: sanitizeEntity(user.toJSON ? user.toJSON() : user, {
model: strapi.query("user", "users-permissions").model,
}),
});
}
},
async resetPassword(ctx) {
const params = _.assign({}, ctx.request.body, ctx.params);
if (
params.password &&
params.passwordConfirmation &&
params.password === params.passwordConfirmation &&
params.code
) {
const user = await strapi
.query("user", "users-permissions")
.findOne({ resetPasswordToken: `${params.code}` });
if (!user) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.code.provide",
message: "Incorrect code provided.",
})
);
}
const password = await strapi.plugins[
"users-permissions"
].services.user.hashPassword({
password: params.password,
});
// Update the user.
await strapi
.query("user", "users-permissions")
.update({ id: user.id }, { resetPasswordToken: null, password });
ctx.send({
jwt: strapi.plugins["users-permissions"].services.jwt.issue({
id: user.id,
}),
refresh: generateRefreshToken(user),
user: sanitizeEntity(user.toJSON ? user.toJSON() : user, {
model: strapi.query("user", "users-permissions").model,
}),
});
} else if (
params.password &&
params.passwordConfirmation &&
params.password !== params.passwordConfirmation
) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.password.matching",
message: "Passwords do not match.",
})
);
} else {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.params.provide",
message: "Incorrect params provided.",
})
);
}
},
async connect(ctx, next) {
const grantConfig = await strapi
.store({
environment: "",
type: "plugin",
name: "users-permissions",
key: "grant",
})
.get();
const [requestPath] = ctx.request.url.split("?");
const provider = requestPath.split("/")[2];
if (!_.get(grantConfig[provider], "enabled")) {
return ctx.badRequest(null, "This provider is disabled.");
}
if (!strapi.config.server.url.startsWith("http")) {
strapi.log.warn(
"You are using a third party provider for login. Make sure to set an absolute url in config/server.js. More info here: https://strapi.io/documentation/developer-docs/latest/development/plugins/users-permissions.html#setting-up-the-server-url"
);
}
// Ability to pass OAuth callback dynamically
grantConfig[provider].callback =
_.get(ctx, "query.callback") || grantConfig[provider].callback;
grantConfig[provider].redirect_uri =
strapi.plugins["users-permissions"].services.providers.buildRedirectUri(
provider
);
return grant(grantConfig)(ctx, next);
},
async forgotPassword(ctx) {
let { email } = ctx.request.body;
// Check if the provided email is valid or not.
const isEmail = emailRegExp.test(email);
if (isEmail) {
email = email.toLowerCase();
} else {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.email.format",
message: "Please provide valid email address.",
})
);
}
const pluginStore = await strapi.store({
environment: "",
type: "plugin",
name: "users-permissions",
});
// Find the user by email.
const user = await strapi
.query("user", "users-permissions")
.findOne({ email: email.toLowerCase() });
// User not found.
if (!user) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.user.not-exist",
message: "This email does not exist.",
})
);
}
// Generate random token.
const resetPasswordToken = crypto.randomBytes(64).toString("hex");
const settings = await pluginStore
.get({ key: "email" })
.then((storeEmail) => {
try {
return storeEmail["reset_password"].options;
} catch (error) {
return {};
}
});
const advanced = await pluginStore.get({
key: "advanced",
});
const userInfo = sanitizeEntity(user, {
model: strapi.query("user", "users-permissions").model,
});
settings.message = await strapi.plugins[
"users-permissions"
].services.userspermissions.template(settings.message, {
URL: advanced.email_reset_password,
USER: userInfo,
TOKEN: resetPasswordToken,
});
settings.object = await strapi.plugins[
"users-permissions"
].services.userspermissions.template(settings.object, {
USER: userInfo,
});
try {
// Send an email to the user.
await strapi.plugins["email"].services.email.send({
to: user.email,
from:
settings.from.email || settings.from.name
? `${settings.from.name} <${settings.from.email}>`
: undefined,
replyTo: settings.response_email,
subject: settings.object,
text: settings.message,
html: settings.message,
});
} catch (err) {
return ctx.badRequest(null, err);
}
// Update the user.
await strapi
.query("user", "users-permissions")
.update({ id: user.id }, { resetPasswordToken });
ctx.send({ ok: true });
},
async register(ctx) {
const pluginStore = await strapi.store({
environment: "",
type: "plugin",
name: "users-permissions",
});
const settings = await pluginStore.get({
key: "advanced",
});
if (!settings.allow_register) {
return ctx.badRequest(
null,
formatError({
id: "Auth.advanced.allow_register",
message: "Register action is currently disabled.",
})
);
}
const params = {
..._.omit(ctx.request.body, [
"confirmed",
"confirmationToken",
"resetPasswordToken",
]),
provider: "local",
};
// Password is required.
if (!params.password) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.password.provide",
message: "Please provide your password.",
})
);
}
// Email is required.
if (!params.email) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.email.provide",
message: "Please provide your email.",
})
);
}
// Throw an error if the password selected by the user
// contains more than three times the symbol '$'.
if (
strapi.plugins["users-permissions"].services.user.isHashed(
params.password
)
) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.password.format",
message:
"Your password cannot contain more than three times the symbol `$`.",
})
);
}
const role = await strapi
.query("role", "users-permissions")
.findOne({ type: settings.default_role }, []);
if (!role) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.role.notFound",
message: "Impossible to find the default role.",
})
);
}
// Check if the provided email is valid or not.
const isEmail = emailRegExp.test(params.email);
if (isEmail) {
params.email = params.email.toLowerCase();
} else {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.email.format",
message: "Please provide valid email address.",
})
);
}
params.role = role.id;
params.password = await strapi.plugins[
"users-permissions"
].services.user.hashPassword(params);
const user = await strapi.query("user", "users-permissions").findOne({
email: params.email,
});
if (user && user.provider === params.provider) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.email.taken",
message: "Email is already taken.",
})
);
}
if (user && user.provider !== params.provider && settings.unique_email) {
return ctx.badRequest(
null,
formatError({
id: "Auth.form.error.email.taken",
message: "Email is already taken.",
})
);
}
try {
if (!settings.email_confirmation) {
params.confirmed = true;
}
const user = await strapi
.query("user", "users-permissions")
.create(params);
const sanitizedUser = sanitizeEntity(user, {
model: strapi.query("user", "users-permissions").model,
});
if (settings.email_confirmation) {
try {
await strapi.plugins[
"users-permissions"
].services.user.sendConfirmationEmail(user);
} catch (err) {
return ctx.badRequest(null, err);
}
return ctx.send({ user: sanitizedUser });
}
const jwt = strapi.plugins["users-permissions"].services.jwt.issue(
_.pick(user, ["id"])
);
return ctx.send({
jwt,
refresh: generateRefreshToken(user),
user: sanitizedUser,
});
} catch (err) {
const adminError = _.includes(err.message, "username")
? {
id: "Auth.form.error.username.taken",
message: "Username already taken",
}
: { id: "Auth.form.error.email.taken", message: "Email already taken" };
ctx.badRequest(null, formatError(adminError));
}
},
async emailConfirmation(ctx, next, returnUser) {
const { confirmation: confirmationToken } = ctx.query;
const { user: userService, jwt: jwtService } =
strapi.plugins["users-permissions"].services;
if (_.isEmpty(confirmationToken)) {
return ctx.badRequest("token.invalid");
}
const user = await userService.fetch({ confirmationToken }, []);
if (!user) {
return ctx.badRequest("token.invalid");
}
await userService.edit(
{ id: user.id },
{ confirmed: true, confirmationToken: null }
);
if (returnUser) {
ctx.send({
jwt: jwtService.issue({ id: user.id }),
user: sanitizeEntity(user, {
model: strapi.query("user", "users-permissions").model,
}),
});
} else {
const settings = await strapi
.store({
environment: "",
type: "plugin",
name: "users-permissions",
key: "advanced",
})
.get();
ctx.redirect(settings.email_confirmation_redirection || "/");
}
},
async sendEmailConfirmation(ctx) {
const params = _.assign(ctx.request.body);
if (!params.email) {
return ctx.badRequest("missing.email");
}
const isEmail = emailRegExp.test(params.email);
if (isEmail) {
params.email = params.email.toLowerCase();
} else {
return ctx.badRequest("wrong.email");
}
const user = await strapi.query("user", "users-permissions").findOne({
email: params.email,
});
if (user.confirmed) {
return ctx.badRequest("already.confirmed");
}
if (user.blocked) {
return ctx.badRequest("blocked.user");
}
try {
await strapi.plugins[
"users-permissions"
].services.user.sendConfirmationEmail(user);
ctx.send({
email: user.email,
sent: true,
});
} catch (err) {
return ctx.badRequest(null, err);
}
},
};
// extensions/users-permissions/config/routes.json
{
"routes": [
{
"method": "POST",
"path": "/auth/refreshToken",
"handler": "Auth.refreshToken",
"config": {
"policies": [],
"prefix": ""
}
},
{
"method": "POST",
"path": "/auth/revoke",
"handler": "Auth.revoke",
"config": {
"policies": [],
"prefix": ""
}
}
]
}
// extensions/users-permissions/config/schema.graphql.js
const _ = require("lodash");
/**
* Throws an ApolloError if context body contains a bad request
* @param contextBody - body of the context object given to the resolver
* @throws ApolloError if the body is a bad request
*/
function checkBadRequest(contextBody) {
if (_.get(contextBody, "statusCode", 200) !== 200) {
const message = _.get(contextBody, "error", "Bad Request");
const exception = new Error(message);
exception.code = _.get(contextBody, "statusCode", 400);
exception.data = contextBody;
throw exception;
}
}
module.exports = {
definition: /* GraphQL */ `
type UsersPermissionsRefreshTokenPayload {
jwt: String!
refresh: String
}
type UsersPermissionsRevokeTokenPayload {
confirmed: Boolean
}
`,
mutation: `
refreshToken(token: String!, renew: Boolean): UsersPermissionsRefreshTokenPayload!
revokeToken(token: String!): UsersPermissionsRevokeTokenPayload!
`,
resolver: {
Mutation: {
refreshToken: {
description: "Refresh JWT Token",
resolverOf: "plugins::users-permissions.auth.refreshToken",
resolver: async (obj, options, { context }) => {
context.query = _.toPlainObject(options);
await strapi.plugins[
"users-permissions"
].controllers.auth.refreshToken(context);
let output = context.body.toJSON
? context.body.toJSON()
: context.body;
checkBadRequest(output);
return output;
},
},
revokeToken: {
description: "Revoke Refresh Token",
resolverOf: "plugins::users-permissions.auth.revoke",
resolver: async (obj, options, { context }) => {
context.query = _.toPlainObject(options);
await strapi.plugins["users-permissions"].controllers.auth.revoke(
context
);
let output = context.body.toJSON
? context.body.toJSON()
: context.body;
checkBadRequest(output);
return output;
},
},
},
},
};
// extensions/users-permissions/models/User.settings.json
{
"kind": "collectionType",
"collectionName": "users-permissions_user",
"info": {
"name": "user",
"description": ""
},
"options": {
"draftAndPublish": false,
"timestamps": true
},
"attributes": {
"username": {
"type": "string",
"minLength": 3,
"unique": true,
"configurable": false,
"required": true
},
"email": {
"type": "email",
"minLength": 6,
"configurable": false,
"required": true
},
"provider": {
"type": "string",
"configurable": false
},
"password": {
"type": "password",
"minLength": 6,
"configurable": false,
"private": true
},
"resetPasswordToken": {
"type": "string",
"configurable": false,
"private": true
},
"confirmationToken": {
"type": "string",
"configurable": false,
"private": true
},
"confirmed": {
"type": "boolean",
"default": false,
"configurable": false
},
"blocked": {
"type": "boolean",
"default": false,
"configurable": false
},
"role": {
"model": "role",
"via": "users",
"plugin": "users-permissions",
"configurable": false
},
"tokenVersion": {
"type": "integer",
"default": 1,
"private": true
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment