Created
June 14, 2021 11:27
-
-
Save imCorfitz/35252d6cadec811693b9c4a23200a1ef to your computer and use it in GitHub Desktop.
Strapi Refresh Token
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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); | |
} | |
}, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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": "" | |
} | |
} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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; | |
}, | |
}, | |
}, | |
}, | |
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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