-
Notifications
You must be signed in to change notification settings - Fork 77
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrating from Rules to Actions #165
Comments
Tested conversion of auth0-account-link-extension from Rule to Action below.
const request = require("request");
const queryString = require("querystring");
const Promise = require("native-or-bluebird");
const jwt = require("jsonwebtoken");
const axios = require("axios");
exports.onExecutePostLogin = async (event, api) => {
var LOG_TAG = '[ACTION_ACCOUNT_LINK] ';
var CONTINUE_PROTOCOL = 'redirect-callback';
var Auth0ManagementAccessToken = '';
event.request.query = event.request.query || {};
var config = {
endpoints: {
linking: `https://${event.secrets.AUTH0_DOMAIN}.webtask.run/4cb95bf92ced903b9b84ebedbf5ebffd`,
userApi: `https://${event.secrets.AUTH0_DOMAIN}.auth0.com/api/v2/users`,
usersByEmailApi: `https://${event.secrets.AUTH0_DOMAIN}.auth0.com/api/v2/users-by-email`
},
token: {
clientId: event.secrets.M2M_CLIENT_ID,
clientSecret: event.secrets.M2M_CLIENT_SECRET,
issuer: `${event.secrets.AUTH0_DOMAIN}.auth0.com`
}
};
if (event.user.email === undefined) {
console.log(LOG_TAG, 'Account Link Action: No event.user.email');
return;
}
await createStrategy().then(callbackWithSuccess).catch(callbackWithFailure);
async function createStrategy()
{
if (shouldLink())
{
await setManagementAccessToken(true);
return linkAccounts();
}
if (shouldPrompt())
{
await setManagementAccessToken(false);
return promptUser();
}
return continueAuth();
function shouldLink() {
return !!event.request.query.link_account_token;
}
function shouldPrompt() {
return !insideRedirect() && !redirectingToContinue() && firstLogin();
function insideRedirect() {
return event.request.query.redirect_uri &&
event.request.query.redirect_uri.indexOf(config.endpoints.linking) !== -1;
}
function firstLogin() {
return event.stats.logins_count <= 1;
}
function redirectingToContinue() {
return event.protocol === CONTINUE_PROTOCOL;
}
}
}
async function setManagementAccessToken(shouldLink)
{
if (Auth0ManagementAccessToken !== ''){
return;
}
if (shouldLink) {
var options = {
method: 'POST',
url: `https://${event.secrets.AUTH0_DOMAIN}.auth0.com/oauth/token`,
headers: {'content-type': 'application/x-www-form-urlencoded'},
data: new URLSearchParams({
grant_type: 'client_credentials',
client_id: event.secrets.APP_CLIENT_ID,
client_secret: event.secrets.APP_CLIENT_SECRET,
audience: event.secrets.BASE_URL + "/"
})
};
}
else {
var options = {
method: 'POST',
url: `https://${event.secrets.AUTH0_DOMAIN}.auth0.com/oauth/token`,
headers: {'content-type': 'application/x-www-form-urlencoded'},
data: new URLSearchParams({
grant_type: 'client_credentials',
client_id: event.secrets.M2M_CLIENT_ID,
client_secret: event.secrets.M2M_CLIENT_SECRET,
audience: event.secrets.BASE_URL + "/"
})
};
}
try {
const response = await axios.request(options);
Auth0ManagementAccessToken = response.data.access_token;
} catch (error) {
console.error(LOG_TAG, "axios error:" + error + " for options: " + JSON.stringify(options, null, 2));
return;
}
}
function verifyToken(token, secret) {
return new Promise(function(resolve, reject) {
jwt.verify(token, secret, function(err, decoded) {
if (err) {
console.error(LOG_TAG, `verifyToken error: ${err}`);
return reject(err);
}
return resolve(decoded);
});
});
}
function linkAccounts() {
var secondAccountToken = event.request.query.link_account_token;
return verifyToken(secondAccountToken, config.token.clientSecret)
.then(function(decodedToken) {
// Redirect early if tokens are mismatched
if (event.user.email !== decodedToken.email) {
console.error(LOG_TAG, 'User: ', decodedToken.email, 'tried to link to account ', event.user.email);
event.redirect = {
url: buildRedirectUrl(secondAccountToken, event.request.query, 'accountMismatch')
};
return event.user;
}
var headers = {
Authorization: 'Bearer ' + Auth0ManagementAccessToken,
'Content-Type': 'application/json',
'Cache-Control': 'no-cache'
};
var getUrl = config.endpoints.userApi+'/'+decodedToken.sub+'?fields=identities';
return apiCall({
method: 'GET',
url: getUrl,
headers: headers
})
.then(function(secondaryUser) {
var provider = secondaryUser &&
secondaryUser.identities &&
secondaryUser.identities[0] &&
secondaryUser.identities[0].provider;
var linkUri = config.endpoints.userApi + '/' + event.user.user_id + '/identities';
return apiCall({
method: 'POST',
url: linkUri,
headers,
json: { user_id: decodedToken.sub, provider: provider }
});
})
.then(function(_) {
console.info(LOG_TAG, 'Successfully linked accounts for user: ', event.user.email);
return _;
});
});
}
function continueAuth() {
return Promise.resolve();
}
function promptUser() {
return searchUsersWithSameEmail().then(function transformUsers(users) {
return users.filter(function(u) {
return u.user_id !== event.user.user_id;
}).map(function(user) {
return {
userId: user.user_id,
email: user.email,
picture: user.picture,
connections: user.identities.map(function(identity) {
return identity.connection;
})
};
});
}).then(function redirectToExtension(targetUsers) {
if (targetUsers.length > 0) {
event.redirect = {
url: buildRedirectUrl(createToken(config.token), event.request.query)
};
}
});
}
function callbackWithSuccess(_) {
if (api.redirect.canRedirect() && event.redirect) {
api.redirect.sendUserTo(event.redirect.url);
}
return;
}
function callbackWithFailure(err) {
console.error(LOG_TAG, err.message, err.stack);
api.access.deny(err.message);
}
function createToken(tokenInfo, targetUsers) {
var options = {
expiresIn: '5m',
audience: tokenInfo.clientId,
issuer: qualifyDomain(tokenInfo.issuer)
};
var userSub = {
sub: event.user.user_id,
email: event.user.email,
base: event.secrets.BASE_URL
};
return jwt.sign(userSub, tokenInfo.clientSecret, options);
}
function searchUsersWithSameEmail() {
return apiCall({
url: config.endpoints.usersByEmailApi,
qs: {
email: event.user.email
}
});
}
// Consider moving this logic out of the rule and into the extension
function buildRedirectUrl(token, q, errorType) {
var params = {
child_token: token,
audience: q.audience,
client_id: q.client_id,
redirect_uri: q.redirect_uri,
scope: q.scope,
response_type: q.response_type,
response_mode: q.response_mode,
auth0Client: q.auth0Client,
original_state: q.original_state || q.state,
nonce: q.nonce,
error_type: errorType
};
return config.endpoints.linking + '?' + queryString.encode(params);
}
function qualifyDomain(domain) {
return 'https://'+domain+'/';
}
function apiCall(options) {
return new Promise(function(resolve, reject) {
var reqOptions = Object.assign({
url: options.url,
headers: {
Authorization: 'Bearer ' + Auth0ManagementAccessToken,
Accept: 'application/json'
},
json: true
}, options);
request(reqOptions, function handleResponse(err, response, body) {
if (err) {
reject(err);
} else if (response.statusCode < 200 || response.statusCode >= 300) {
console.error(LOG_TAG, 'API call failed: ', body);
reject(new Error(body));
} else {
resolve(response.body);
}
});
});
}
}; Secrets explanations:
|
Thanks for this @EgilSandfeld,
|
Maybe I am mistaken. I did a comparison and it seems a lot of global variables are changed in Actions, so we'll need to do plenty of search and replace. I think the code above is the result of that. |
Why isn't |
You're right, missed that the Rule already had versions and package names. Fixed in the code example |
I think most of the changes in variable names could be avoided if we just use the following instead of exports.onExecutePostLogin = async (context, api) => {
const user = context.user;
const auth0 = {
baseUrl: context.tenant.baseUrl, // or is it api.baseUrl ?
domain: context.tenant.id,
accessToken: api.accessToken,
};
// ...
} This will help most of the code remain intact from the original Rule code, as we're ensuring the existing variable names are reused. Regardless, are your replacements of |
Again, not tested.
And yes great idea to restore the variable names.
Bedste hilsner / Best regards
Egil Sandfeld
…On Oct 9, 2023, 16:46 +0200, ADTC ***@***.***>, wrote:
I think most of the changes in variable names could be avoided if we just use the following instead of exports.onExecutePostLogin = async (event, api):
exports.onExecutePostLogin = async (context, api) => {
const user = context.user;
const auth0 = {
baseUrl: context.tenant.baseUrl, // or is it api.baseUrl ?
domain: context.tenant.id,
accessToken: api.accessToken,
};
// ...
}
This will help most of the code remain intact from the original Rule code, as we're ensuring the existing variable names are reused.
Regardless, are your replacements of callback(...) with return; or api.access.deny(err.message); tested and verified correct? That will be my main concern.
—
Reply to this email directly, view it on GitHub, or unsubscribe.
You are receiving this because you were mentioned.Message ID: ***@***.***>
|
Okay. Thank you anyway, this is a great start. I'll take some time soon to look through the official guides for rule-to-action conversion and do a conversion of my own. Perhaps posting steps here. Wish I could create a pull request, but that would require deeper dive into the API on how to create an Action programmatically. And if that's not possible or only partially possible, then instructions would have to be added in the Readme or the installation interface. |
@ADTC I've updated and tested my code today. Works now with Actions, so I was able to successfully link a Twitch account into an email account, and I could therefore disable the existing Rule doing the same things. Obviously the code can be improved, but it works for now 🙌 |
Thank you for testing it! BTW, may I suggest listing the dependencies here in text form, rather than out-linking to Imgur? It can be second level bullet points. * First level
* Second level Looks like:
Thank you! PS: Also, it's possible to paste images here in GitHub comments if you really want to. |
@EgilSandfeld Thank you for your comment with the adjusted action. Could you please adjust [email protected] to [email protected] in your comment. That should prevent others from searching for a solution why the action build in auth0 fails when simply copy pasting your dependencies from the comment. |
🤣 It's funny how the double asterisks makes the list look weird, with both first level and second level bullets on the same line: * First level
* * Second level
(compared to:) * First level
* Second level
Dear GitHub, WTH? 😂 |
@EgilSandfeld thank you for the action! Auth0 appear to hide rules for new tenants, so when enabling the extension you get the broken rule added and then are stuck as you can't disable it. I found you could still list the rules with the management API and then disable the rule with the update API. Hopefully auth0 will come up with something official for actions. |
Honestly I wish Auth0 made the feature native and not have to rely on extensions (and this one looks abandoned). I cannot use the New Universal Login Experience if I also want this feature simultaneously. It's terrible. |
Does it make sense to rework only the Rule leaving out the rest? As I understand it, the extension is based on the webtask that ties together the initial login, login to the primary account and error handling. Couldn't it be dropped in the future as well once the extension is completely abandoned? Also an Auth0 employee says the following in AMA:
|
@ADTC Where does this come from?
|
They stated that Account linking is available for actions but what they also tell you that you need to take care of showing user logins, authenticate them etc. Is not like they did a replacement solution with FE included as this extension. |
@glebignatieff if you need to insert custom logic you're forced to switch to the classic login. The "New Universal Login Experience" didn't allow custom code when I last checked. |
Hello @EgilSandfeld, Could you just help me with something: in our case we are using a custom domain, and I can't seem to be able to make your code work; do you reckon that anything should change in this scenario? Thank you for the help! |
Hello, I almost got my migration to an action working, but I ran into a scope issue and I can't figure it out. I think I used the wrong application for the The error I'm getting is this: {
"logs": "[ACTION_ACCOUNT_LINK] API call failed: {\n statusCode: 403,\n error: 'Forbidden',\n message: 'Insufficient scope, expected any of: update:users,update:current_user_identities',\n errorCode: 'insufficient_scope'\n}\n[ACTION_ACCOUNT_LINK] [object Object] Er"
} This error happens after I login to the primary account and it tries to complete the account linking. I can't really find any information on how to grant my application these scopes. I can find Also, does anyone know how I can get the full logs of the action execution? Or is that only possible when testing it in the action editor? Thank you for any help you can provide! I'm quite disappointed that this extension is abandoned. I wish I could just uninstall the extension and then install it from scratch and it would automatically use The New Thing. |
@stefansundin
You can also use this version |
Thank you, that seems to work! 🥳 I think what confused me the most was that it wasn't possible to grant |
Thank you a lot for writing it down! As far as I understand, |
Since Rules are getting deprecated, there should be a migration from using a rule to using an action.
Hopefully a guide can be provided to migrate existing installations, and perhaps this can be done already for new installs so that we don't have to manually do it.
PS: Hopefully this also resolves #163
The text was updated successfully, but these errors were encountered: