Electron, contextIsolation and contextBridge - javascript

I recently had to revamp an old Electron app. I found out that it had contextIsolation set to false, so I went ahead and set it to true (and btw set nodeIntegration to false).
As expected, it broke the ipc communications
So I used, as advised in many places, a preload script that enables some communications channels
Here is my preload.js:
const {
contextBridge,
ipcRenderer
} = require("electron");
contextBridge.exposeInMainWorld(
"api", {
send: (channel, data) => {
console.log("Send on channel " + channel)
// whitelist channels
let validChannels = [];
if (validChannels.includes(channel)) {
ipcRenderer.send(channel, data);
}
},
receive: (channel, func) => {
console.log("Receive on channel " + channel)
let validChannels = [
"set-auth-token",
"set-window-name",
"get-window-name",
"send-message-to-one-drive",
"update-badge",
"is-hidden",
"open-google-sign-in"
];
if (validChannels.includes(channel)) {
// Deliberately strip event as it includes `sender`
ipcRenderer.on(channel, (event, ...args) => func(...args));
}
}
}
);
My issue is the "api" parameter, I don't know what to put in there.
Knowing that the website that's loaded by that app sends messages this way:
let electron = false;
if ('require' in window) {
electron = window.require('electron');
}
const isAppDesktop = !!electron;
const DesktopHelper = {
isAppDesktop() {
return isAppDesktop;
},
updateBadge(badgeCount) {
return isAppDesktop ? electron.ipcRenderer.send('update-badge', badgeCount) : undefined;
},
setAuthToken(token) {
return electron.ipcRenderer.send('set-auth-token', token);
},
isHidden() {
return isAppDesktop ? electron.ipcRenderer.sendSync('is-hidden') : undefined;
},
};
export default DesktopHelper;
Can someone help?

contextBridge.exposeInMainWorld(apiKey, api) - the apiKey argument is the name of the object that will be exposed as window[apiKey].
In your example, the apiKey is set to the string "api", so the exposed methods are available in the window.api object. You can access them as window.api.send and window.api.receive.
The methods in DesktopHelper should look like this:
updateBadge(badgeCount) {
return isAppDesktop ? window.api.send('update-badge', badgeCount) :
undefined;
},
Or simply api.send('update-badge', badgeCount), since window is the global. You may obviously want to pick a more descriptive name for the API.

Related

Service worker activate and push event don't get triggered even though the registration is successful and it's activated

I have built a React progressive web application that makes use of service workers.
The service worker gets registered and is activated:
I have been trying to detect the "activate" event using this:
service-worker.js
navigator.serviceWorker.addEventListener("activate", function (event) {
console.log("service worker activated");
});
I added that at the end of the service-worker file. But, this event never gets triggered and I have no idea why.
I also tried to implement push notifications and trigger the from the backend. For this, I needed a "push" event listener that would listen to these events from the server:
navigator.serviceWorker.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(showPushNotification(title, description, image));
});
This is how showPushNotification is defined:
export function showPushNotification(title, description, image) {
if (!("serviceWorker" in navigator)) {
console.log("Service Worker is not supported in this browser");
return;
}
navigator.serviceWorker.ready.then(function (registration) {
registration.showNotification(title, {
body: description,
icon: image,
actions: [
{
title: "Say hi",
action: "Say hi",
},
],
});
});
}
I tested calling that function manually and it successfully triggerss a push notification.
This is the server code that triggers the push notification:
const sendPushNotification = async (user_id, title, description, image) => {
const search_option = { user: user_id };
const users_subscriptions = await PushNotificationSubscription.find(
search_option
);
const number_of_users_subscriptions = users_subscriptions.length;
const options = {
vapidDetails: {
subject: "mailto:xxxx#xxxx.com",
publicKey: VAPID_PUBLIC_KEY,
privateKey: VAPID_PRIVATE_KEY,
},
};
let push_notif_sending_results = {};
for (let i = 0; i < number_of_users_subscriptions; i++) {
const user_subscription = users_subscriptions[i];
await webPush
.sendNotification(
user_subscription,
JSON.stringify({
title,
description,
image,
}),
options
)
.then((notif_send_result) => {
push_notif_sending_results[i] = { success: notif_send_result };
})
.catch((error) => {
push_notif_sending_results[i] = { error: error };
});
}
return push_notif_sending_results;
};
This is the part responsible for sending the push notification:
webPush
.sendNotification(
user_subscription,
JSON.stringify({
title,
description,
image,
}),
options
)
And it's successfully executed as it returns a 201 HTTP response.
So the "push" event listener is supposed to detect it and trigger a push notification.
I think everything regarding the push notification has been successfully implementing and the problem is how the "push" event listener is added since the "activate" event listener also doesn't work.
So I tried moving the two event listeners here right after the registration of the service worker is successful:
function registerValidSW(swUrl, config) {
navigator.serviceWorker.register(swUrl).then((registration) => {
registration.addEventListener("activate", (event) => {
console.log(
"๐Ÿš€ ~ file: serviceWorker.js:159 ~ navigator.serviceWorker.register ~ event",
event
);
});
registration.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(
showPushNotification(title, description, image)
);
});
});
}
But, it's still the same result.
Neither the "push" nor the "activate" event listeners get triggered.
Any idea what's going on?
Here's the whole service-worker file:
service-worker.js
import axios from "axios";
const isLocalhost = Boolean(
window.location.hostname === "localhost" ||
// [::1] is the IPv6 localhost address.
window.location.hostname === "[::1]" ||
// 127.0.0.0/8 are considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export function register(config) {
if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets;
return;
}
window.addEventListener("load", () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Let's check if a service worker still exists or not.
checkValidServiceWorker(swUrl, config);
// Add some additional logging to localhost, pointing developers to the
// service worker/PWA documentation.
} else {
// Is not localhost. Just register service worker
console.log(
"Is not localhost. Just register a service worker, by calling registerValidSW"
);
registerValidSW(swUrl, config);
}
});
}
}
async function subscribeToPushNotifications(serviceWorkerReg) {
let subscription = await serviceWorkerReg.pushManager.getSubscription();
if (subscription === null) {
const dev_public_vapid_key = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const prod_public_vapid_key =
"xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const public_vapid_key = isLocalhost
? dev_public_vapid_key
: prod_public_vapid_key;
subscription = await serviceWorkerReg.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: public_vapid_key,
});
axios
.post("/api/push_notif_subscription/subscribe", subscription)
.then((response) => {})
.catch((error) => {});
}
}
export function showPushNotification(title, description, image) {
if (!("serviceWorker" in navigator)) {
console.log("Service Worker is not supported in this browser");
return;
}
navigator.serviceWorker.ready.then(function (registration) {
registration.showNotification(title, {
body: description,
icon: image,
actions: [
{
title: "Say hi",
action: "Say hi",
},
],
});
});
}
function registerValidSW(swUrl, config) {
navigator.serviceWorker.register(swUrl).then((registration) => {
subscribeToPushNotifications(registration);
registration.onupdatefound = () => {
const installingWorker = registration.installing;
if (!installingWorker) {
return;
}
installingWorker.onstatechange = () => {
if (installingWorker.state === "installed") {
if (navigator.serviceWorker.controller) {
// At this point, the updated precached content has been fetched,
// but the previous service worker will still serve the older
// content until all client tabs are closed.
// Execute callback
if (config && config.onUpdate) {
config.onUpdate(registration);
}
} else {
// At this point, everything has been preached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
// Execute callback
if (config && config.onSuccess) {
config.onSuccess(registration);
}
}
}
};
};
});
}
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { "Service-Worker": "script" },
}).then((response) => {
// Ensure the service worker exists, and that we really are getting a JS file.
const contentType = response.headers.get("content-type");
if (
response.status === 404 ||
(!!contentType && contentType.indexOf("javascript") === -1)
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then((registration) => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
console.log("Service worker found, calling registerValidSW");
registerValidSW(swUrl, config);
}
});
}
export function unregister() {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.ready.then((registration) => {
registration.unregister();
});
}
}
navigator.serviceWorker.addEventListener("activate", function (event) {
console.log("service worker activated");
});
navigator.serviceWorker.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(showPushNotification(title, description, image));
});
The events "push" and "activate" are part of the ServiceWorkerGlobalScope as within the Service Worker API.
Push notifications must be handled within the service worker itself.
Therefore only the service worker can register an "activate" event listener.
The same applies for a "push" listener.
Specially in terms of the "push" listener this makes sense.
The idea of push events is to receive them, even if the main app (in this case the website) has been closed.
The service worker is an exception, as it even runs without the page being loaded.
Therefore move the "push" event into your service worker.
Your code (within the service worker) may look like this:
this.addEventListener("push", async function (event) {
const message = await event.data.json();
let { title, description, image } = message;
await event.waitUntil(showPushNotification(title, description, image));
});
function showPushNotification(title, description, image) {
registration.showNotification(title, {
body: description,
icon: image,
actions: [
{
title: "Say hi",
action: "Say hi",
},
],
});
}
The rest seems fine to me.
Update (Some more explanation)
I took a more careful look at your service-worker.js and it seems it contains general methods for registering the service worker.
As mentioned above the main app and the service worker are two completely separate chunks of code, running in different spaces. So this means everything which is not supposed to run in the service worker itself must be put outside of the service-worker.js. The service worker (in your case) should only contain the code for handling push notifications. It's important that you do not include the "service-worker.js" within your application.
In your case, you may seperate these functions into service-worker-register.js which contain all functions which are for managing the service worker registration but should not be executed within the service worker itself (isLocalhost, register, subscribeToPushNotifications, registerValidSW, checkValidServiceWorker, and unregister). Please note the code snippet from above and make changes accordingly to the code left within the service worker.
MDN has a pretty in depth tutorial on service workers (and there are a lot more) I recommend having a look at:
developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API/Using_Service_Workers

Vue created can not force to run non async

I have a Vue.js application and on the /callback route I am trying to have it do a few things. So far I am not having any luck with it because I am seeing things run async. I understand that it is normally how Vue/Javascript works however I am trying to force it to not be async.
The main issue I am having is sometimes the this.$store... are running before the items are set. This is an issue because of how things run on other Vuex actions. Mainly the getCompany one requires the loadToken one to complete as it is pulling the values from the local storage which is being set above.
I don't want to change this and how it works because of how the Vue router is set up to pull the token from local storage on each page reload. This token is used to connect to the backend so it needs to be pulled from local storage each reload as I don't want a user to log in just because they reload the page.
Code:
created() {
setTimeout(() => {
localStorage.setItem('token', this.$auth.token)
localStorage.setItem('user_data', JSON.stringify(this.$auth.user))
// Load company data
this.$store.dispatch('loadToken')
this.$store.dispatch('getCompany')
if(this.$auth == null || this.$auth.id_token['https://hello.io/account_signup_type/is_new']) {
this.$router.push('/setup')
} else {
// Load user data from Auth0
// Go to chat page
this.$router.push('/chat')
}
}, 500)
}
edit main.js code
import { Auth0Plugin } from '#/auth/auth0-plugin';
// Install the authentication plugin
Vue.use(Auth0Plugin, {
domain,
clientId,
audience,
onRedirectCallback: (appState) => {
router.push(
appState && appState.targetUrl
? appState.targetUrl
: window.location.pathname,
);
},
});
auth0-plugin
/**
* External Modules
*/
import Vue from 'vue';
import createAuth0Client from '#auth0/auth0-spa-js';
/**
* Vue.js Instance Definition
*/
let instance;
export const getInstance = () => instance;
/**
* Vue.js Instance Initialization
*/
export const useAuth0 = ({
onRedirectCallback = () =>
window.history.replaceState({}, document.title, window.location.pathname),
redirectUri = `${window.location.origin}/callback`,
...pluginOptions
}) => {
if (instance) return instance;
instance = new Vue({
data() {
return {
auth0Client: null,
isLoading: true,
isAuthenticated: false,
user: {},
error: null,
token: null,
id_token: null
};
},
methods: {
async handleRedirectCallback() {
this.isLoading = true;
try {
await this.auth0Client.handleRedirectCallback();
this.user = await this.auth0Client.getUser();
this.isAuthenticated = true;
} catch (error) {
this.error = error;
} finally {
this.isLoading = false;
}
},
loginWithRedirect(options) {
return this.auth0Client.loginWithRedirect(options);
},
logout(options) {
return this.auth0Client.logout(options);
},
getTokenSilently(o) {
return this.auth0Client.getTokenSilently(o);
},
getIdTokenClaims(o) {
return this.auth0Client.getIdTokenClaims(o);
}
},
async created() {
this.auth0Client = await createAuth0Client({
...pluginOptions,
// responseType: 'id_token',
domain: pluginOptions.domain,
client_id: pluginOptions.clientId,
audience: pluginOptions.audience,
redirect_uri: redirectUri,
});
try {
if (
window.location.search.includes('code=') &&
window.location.search.includes('state=')
) {
const { appState } = await this.auth0Client.handleRedirectCallback();
onRedirectCallback(appState);
}
} catch (error) {
this.error = error;
} finally {
this.isAuthenticated = await this.auth0Client.isAuthenticated();
this.user = await this.auth0Client.getUser();
this.$auth.getTokenSilently().then(token => this.token = token)
this.$auth.getIdTokenClaims().then(id_token => this.id_token = id_token)
this.isLoading = false;
}
},
});
return instance;
};
/**
* Vue.js Plugin Definition
*/
export const Auth0Plugin = {
install(Vue, options) {
Vue.prototype.$auth = useAuth0(options);
},
};
edit - updated router.beforeEach
router.beforeEach(async (to, from, next) => {
const auth = getInstance()
if(to.path == '/callback' && auth != null) {
console.log('Callback')
console.log(`Token: ${auth.token}`)
console.log(`User: ${JSON.stringify(auth.user)}`)
localStorage.setItem('token', auth.token)
localStorage.setItem('user_data', JSON.stringify(auth.user))
await store.dispatch('loadToken')
await store.dispatch('getCompany')
return next()
}
if(to.path != '/login' && to.path != '/setup') {
await store.dispatch('loadToken')
await store.dispatch('getCompany')
.then(() => {
return next()
})
} else {
return next()
}
})
edit - adding guide that I followed from Auth0 to get the code I have now - mostly
https://auth0.com/blog/complete-guide-to-vue-user-authentication/
The problem is that there is race condition because dispatch calls return promises that weren't chained before accessing the result of their work.
A good practice is to chain every promise, unless proven other wise.
The code that created contains actually belongs to the router in general because authentication logic is application-wide.
It's unnecessary to access global dependencies on this component instance. This is done for historical reasons because Vue originally was used in non-modular environment. In order to use outside components, global dependencies such as store need to be explicitly imported. In case this cannot be done, this needs to be fixed.
In this case auth instance is available through getInstance. In case the authentication shouldn't be done on each navigation, this needs to be done on condition, e.g.:
import { getInstance } from '.../auth';
import store from '.../store';
...
router.beforeEach(async (to, from, next) => {
const auth = getInstance();
if (...) {
...
await store.dispatch('loadToken')
await store.dispatch('getCompany')
...
next('/setup')
...
} else {
next()
}
})
getInstance doesn't serve a good purpose because it just exposes a variable. Instead, instance could be exported and imported directly, the behaviour would be the same due to how ES modules work.
Also global store already holds application logic and commonly used to handle authentication, including local storage operations.

Remix: middleware pattern to run code before loader on every request?

Is there a recommended pattern in Remix for running common code on every request, and potentially adding context data to the request? Like a middleware? A usecase for this might be to do logging or auth, for example.
The one thing I've seen that seems similar to this is loader context via the getLoadContext API. This lets you populate a context object which is passed as an arg to all route loaders.
It does work, and initially seems like the way to do this, but the docs for it say...
It's a way to bridge the gap between the adapter's request/response API with your Remix app
This API is an escape hatch, itโ€™s uncommon to need it
...which makes me think otherwise, because
This API is explicitly for custom integrations with the server runtime. But it doesn't seem like middlewares should be specific to the server runtime - they should just be part of the 'application' level as a Remix feature.
Running middlewares is a pretty common pattern in web frameworks!
So, does Remix have any better pattern for middleware that runs before every loader?
Instead of middleware, you can call a function directly inside the loader, this will also be more explicit. If you want to early return a response from those "middlewares" Remix let you throw the response object.
For example, if you wanted to check the user has a certain role you could create this function:
async function verifyUserRole(request: Request, expectedRole: string) {
let user = await getAuthenticatedUser(request); // somehow get the user
if (user.role === expectedRole) return user;
throw json({ message: "Forbidden" }, { status: 403 });
}
And in any loader call it this way:
let loader: LoaderFunction = async ({ request }) => {
let user = await verifyUserRole(request, "admin");
// code here will only run if user is an admin
// and you'll also get the user object at the same time
};
Another example could be to require HTTPS
function requireHTTPS(request: Request) {
let url = new URL(request.url);
if (url.protocol === "https:") return;
url.protocol = "https:";
throw redirect(url.toString());
}
let loader: LoaderFunction = async ({ request }) => {
await requireHTTPS(request);
// run your loader (or action) code here
};
There is no way inside Remix to run code before loaders.
As you found out, there is the loader context but it runs even before remix starts to do its job (so you won't know which route modules are matched for example).
You can also run arbitrary code before handing the request to remix in the JS file where you use the adapter for the platform you're deploying to (this depend on the starter you used. This file doesn't exist if you've chosen remix server as your server)
For now it should work for some use cases, but I agree this is a missing feature in remix for now.
Inside app/root.tsx
export let loader: LoaderFunction = ({ request }) => {
const url = new URL(request.url);
const hostname = url.hostname;
const proto = request.headers.get("X-Forwarded-Proto") ?? url.protocol;
url.host =
request.headers.get("X-Forwarded-Host") ??
request.headers.get("host") ??
url.host;
url.protocol = "https:";
if (proto === "http" && hostname !== "localhost") {
return redirect(url.toString(), {
headers: {
"X-Forwarded-Proto": "https",
},
});
}
return {};
};
Source: https://github.com/remix-run/remix-jokes/blob/8f786d9d7fa7ea62203e87c1e0bdaa9bda3b28af/app/root.tsx#L25-L46
here is my middlewares implementation for remix with typescript,it's works well
ctx.return(something)=== useLoaderData()
import compose from '#utils/compose';
export default function Index() {
const ctx = useLoaderData();
return <div>{ctx.name}</div>;
}
type DefaultCtx = {
name: string;
} & Request;
export const loader =(...args)=>compose<DefaultCtx>(
async (ctx, next) => {
ctx.name = 'first';
await next();
},
async (ctx, next) => {
ctx.name = 'secnod';
await next();
},
async (ctx, next) => {
ctx.name = 'third';
ctx.return(ctx);
await next();
}
)(args);
compose is same as koa;
here is the compose's implementation
type Next = () => Promise<void>;
type Context = {};
type Middle<T = {}> = (ctx: Context & T, next: Next) => void;
const compose = <T>(...middlewares: Middle<T>[]) => {
return middlewares.reverse().reduce(
(dispatch, middleware) => {
return async ctx =>
middleware(ctx, async () => dispatch(ctx, async () => {}));
},
async () => {}
);
};
export type Middleware<T = {}, P = unknown> = (
ctx: Context & T & { return: (param: P) => void },
next: Next
) => void;
const returnEarly: Middleware = async (ctx, next) => {
return new Promise<any>(async resolve => {
ctx.return = resolve;
await next();
});
};
const componseWithReturn = <T>(...middlewares: Middleware<T>[]) =>
compose(returnEarly, ...middlewares) as (ctx: T) => void;
export default componseWithReturn;

How to download styles via Chrome debugger API

I need to get styles with exact, not computed values for my extension. I can use document.styleSheets in some cases, but in case when css styles hosted on other domain, I am getting CORS error. I found a way to get those styles with help of chrome.debugger API, but having difficulties with implementation:
chrome.debugger.attach(debuggeeId, "1.3", () => {
chrome.debugger.sendCommand(debuggeeId, "Page.enable", null, (r) => {
chrome.debugger.sendCommand(debuggeeId, "Page.getResourceTree", null, (res) => {
// get style URLs from resourceTree object
const cssResources = getCSSResources(res.frameTree.resources);
for(let url of cssResources) {
chrome.debugger.sendCommand(debuggeeId, "Page.getResourceContent", {frameId: toString(tabId), url: url}, (resp) => {
console.log(resp) /// return undefined
})
}
})
})
})
For some reasons I am getting undefined from Page.getResourceContent. Just to clarify, getting undefined because of CORS(also works here??) or because of incorrect response to chrome.debugger API?
Code below cause the same - no data from request was returned.
The only problem I see is that frameId is an internal id of the frame which you can get from res.frameTree.frame.id, it's not related to tabId.
Might process all frames recursively, at least of the same origin, and use the modern syntax:
chrome.debugger.attach(debuggeeId, '1.3', async () => {
const send = (cmd, params = null) =>
new Promise(resolve =>
chrome.debugger.sendCommand(debuggeeId, cmd, params, resolve));
await send('Page.enable');
const {frameTree} = await send('Page.getResourceTree');
const frameQueue = [frameTree];
const results = [];
for (const {frame, childFrames, resources} of frameQueue) {
frameQueue.push(...childFrames);
const frameId = frame.id;
for (const {url, type} of resources) {
if (type === 'Stylesheet') {
results.push({
url,
frameUrl: frame.url,
...await send('Page.getResourceContent', {frameId, url}),
});
}
}
}
console.log(results);
});

Creating a Cognito UserPool with lambda's configured as triggers

I am attempting to create a Cognito user pool programmatically from a script using the JavaScript SDK.
I have successfully created the user-pool and defined a pre-signup and post-confirmation trigger by specifying the arn's of the relevant lambdas in my config. (as per the docs)
My script looks like this:
const aws = require('aws-sdk');
const awsConfig = require('../config/config');
aws.config.update({ region: awsConfig.REGION });
const provider = new aws.CognitoIdentityServiceProvider();
// user provided args
const stage = process.argv[2];
if (!stage) {
process.stdout.write('Please provide stage as argument\n');
process.exit(1);
}
// generate arns for pre and post cognito triggers
const getArn = (lambdaName) => {
return `arn:aws:lambda:${awsConfig.REGION}:${awsConfig.AWS_ACCOUNT_ID}` +
`:function:my-project-name-${stage}-${lambdaName}`;
};
const preSignUp = getArn('preSignUp');
const postConfirmation = getArn('postConfirmation');
const userPoolConfig = {
PoolName: `mypool-${stage}`,
AutoVerifiedAttributes: ['email'],
Schema: [
{
"StringAttributeConstraints": {
"MaxLength": "2048",
"MinLength": "0"
},
"Mutable": true,
"Required": true,
"AttributeDataType": "String",
"Name": "email",
"DeveloperOnlyAttribute": false
}
],
LambdaConfig: {
PostConfirmation: postConfirmation,
PreSignUp: preSignUp
}
};
const callback = (err, resp) => {
if (err) {
process.stdout.write(`${err}\n`);
} else {
process.stdout.write(resp.UserPool.Id);
}
};
provider.createUserPool(userPoolConfig, callback);
When I run this script it successfully creates the user pool, an and when I inspect it in the console the triggers are set correctly.
When I try to register a user on my user pool I get the error:
AccessDeniedException { code: 'UnexpectedLambdaException', ... }
If I go into the console and set the trigger manually it works just fine.
This bug has been reported - but I see no confirmation, nor solution:
https://github.com/aws/aws-cli/issues/2256
Desperately unable to fix or find a workaround.
If you need to add the permission in a serverless.yml file then this is what worked for us. Add it to your Resources section:
UserPoolLambdaInvokePermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:invokeFunction
Principal: cognito-idp.amazonaws.com
FunctionName: <function-name>
SourceArn: arn:aws:cognito-idp:<your region>:<your account>:userpool/*
This gives all the user pools the ability to invoke your particular function.
You can get the function name to use by looking in .serverless/serverless-state.json and there against your lambda you'll see the FunctionName property.
If you have set up your user pool and lambda triggers in cloud formation you will need to add the appropirate permissions for the user pool to invoke the lambda function.
You would have to add something like this to your cloud formation template.
"UserPoolPreSignupLambdaInvokePermission" : {
"Type" : "AWS::Lambda::Permission",
"Properties" : {
"Action" : "lambda:invokeFunction",
"Principal" : "cognito-idp.amazonaws.com",
"FunctionName" :{ "Ref" : "AutoVerifyEmailPreSignupLambdaFunction" },
"SourceArn" : {
"Fn::Sub" : "arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool}"
}
}
}
I managed to solve this problem. The issue is that the lambda does not have the correct permissions to interact with cognito.
I found this snippet of information hidden away here
So the in the callback function for the create user pool I attached the correct permissions like this:
const callback = (err, resp) => {
if (err) {
process.stdout.write(`${err}\n`);
} else {
const userPoolId = resp.UserPool.Id;
// the lambdas must have a permission attached that allows them to interact
// directly with cognito
const generateLambdaPersmission = (userPoolName, lambdaName) => {
return {
Action: 'lambda:InvokeFunction',
Principal: 'cognito-idp.amazonaws.com',
SourceArn: `arn:aws:cognito-idp:${awsConfig.REGION}:${awsConfig.AWS_ACCOUNT_ID}:userpool/${userPoolId}`,
FunctionName: getArn(lambdaName),
StatementId: `${stage}1`
};
};
lambda.addPermission(generateLambdaPersmission(userPoolId, 'preSignUp'), (err, resp) => {
if (err) {
process.stdout.write(`error attaching permission to lambda: ${err}`);
}
});
lambda.addPermission(generateLambdaPersmission(userPoolId, 'postConfirmation'), (err, resp) => {
if (err) {
process.stdout.write(`error attaching permission to lambda: ${err}`);
}
});
process.stdout.write(userPoolId);
}
};
See the documentation on adding permissions via the JavaScript SDK here

Categories