Open revmischa opened 2 years ago
@revmischa This is not currently supported in Amplify. This is marked as a feature request. Could you brief more on your use case? Could you provide example of need for amplify in browser extension?, why would the code run in a service worker?
I am unfamiliar with browser extensions, any sample code or usage could help us understand the use case better and help with getting official support for more cross platforms
I've created a browser extension that requires users to log in to our service so that we can save their personal data. We use Cognito with OIDC and LinkedIn. It's the same flow one would use to sign in to a website except that at the end we transmit the credentials to the extension to save them there after logging in on the website so that the extension can make authenticated calls to AppSync as the user.
The service worker is the standard way to interface with other sites, acting as a persistent host to send and receive events and make API calls and store credentials. In there we call Auth.currentAuthenticatedUser()
.
Sending and receiving credentials is a challenge with Cognito as well. I realize the vision of having credential storage being abstracted away but I have to do some very gnarly hacks to authenticate users in the extension once they log in via Cognito in our website.
I would of course prefer a cleaner solution to this.
import {
CognitoAccessToken,
CognitoIdToken,
CognitoRefreshToken,
CognitoUser,
CognitoUserPool,
CognitoUserSession,
ICognitoUserData,
} from "amazon-cognito-identity-js";
import { Auth } from "aws-amplify";
import browser from "webextension-polyfill";
import {
COGNITO_USER_POOL_CLIENT_ID,
COGNITO_USER_POOL_ID,
} from "../api/cognito/config";
// see: https://stackoverflow.com/questions/60244048/login-to-chrome-extension-via-website-with-aws-amplify
// this is in-memory storage backed by browser extension storage
const memStorage = new Map<string, string>();
// TODO: don't hog all of browser storage; store everything inside a single key or something
export class AmplifyAuthStorage {
static syncPromise: Promise<void> | undefined = undefined;
static locked: boolean = false;
static keyPrefix = process.env.ENV_NAME || "development" + "_amplify_";
static setItem(key_: string, value: string) {
const key = AmplifyAuthStorage.keyPrefix + key_;
if (!AmplifyAuthStorage.locked) browser.storage.local.set({ [key]: value }); // ->>> content script
memStorage.set(key, value);
return memStorage.get(key);
}
// get item with the key
static getItem(key_: string) {
const key = AmplifyAuthStorage.keyPrefix + key_;
return memStorage.get(key) ?? null;
}
// remove item with the key
static removeItem(key_: string) {
const key = AmplifyAuthStorage.keyPrefix + key_;
if (!AmplifyAuthStorage.locked) memStorage.delete(key);
return browser.storage.local.remove(key);
}
// clear out the storage
static clear() {
console.debug("Clearing auth storage");
memStorage.clear();
if (!AmplifyAuthStorage.locked) browser.storage.local.clear();
}
// sync extension storage to local storage
// https://docs.amplify.aws/lib/auth/manageusers/q/platform/js/#managing-security-tokens
static async sync(): Promise<void> {
if (!AmplifyAuthStorage.syncPromise) {
AmplifyAuthStorage.syncPromise = new Promise((res, rej) => {
// load all items from our storage
browser.storage.local.get(null).then((saved) => {
memStorage.clear(); // reset storage first to account for deleted items
// sync to in-memory store (for synchronous access in getItem())
Object.entries(saved).forEach(([key, value]) => {
memStorage.set(key, value);
});
res();
});
});
}
return AmplifyAuthStorage.syncPromise;
}
static reset(): Promise<void> {
AmplifyAuthStorage.syncPromise = undefined;
return AmplifyAuthStorage.sync();
}
}
// used to persist a serialized cognito session
// received from elsewhere (platform login page)
export const persistCognitoSession = async ({
session,
userPoolId,
showNotification,
}: {
showNotification: boolean;
session: any;
userPoolId?: string;
}) => {
if (!session) {
console.warn("Got asked to persist empty session");
return;
}
// make sure we got credentials for the userpool we're talking to
if (!userPoolId) {
console.warn("missing userPoolId from persist session message");
} else {
// ensure we're getting authenticated for the correct user pool!
if (userPoolId !== COGNITO_USER_POOL_ID) {
console.warn(
`Asked to save cognito credentials for user pool ${userPoolId} but we are configured for pool ${COGNITO_USER_POOL_ID}`
);
return;
}
}
let idToken = new CognitoIdToken({
IdToken: session.idToken.jwtToken,
});
let accessToken = new CognitoAccessToken({
AccessToken: session.accessToken.jwtToken,
});
let refreshToken = new CognitoRefreshToken({
RefreshToken: session.refreshToken.token,
});
let clockDrift = session.clockDrift;
const sessionData = {
IdToken: idToken,
AccessToken: accessToken,
RefreshToken: refreshToken,
ClockDrift: clockDrift,
};
// Create the session
let userSession = new CognitoUserSession(sessionData);
const userData: ICognitoUserData = {
Username: userSession.getIdToken().payload["cognito:username"],
Pool: new CognitoUserPool({
UserPoolId: COGNITO_USER_POOL_ID!,
ClientId: COGNITO_USER_POOL_CLIENT_ID!,
}),
};
if (!window.STORYBOOK_MODE) userData.Storage = AmplifyAuthStorage;
// Make a new cognito user
const cognitoUser = new CognitoUser(userData);
// Attach the session to the user
cognitoUser.setSignInUserSession(userSession);
// Check to make sure it works
cognitoUser.getSession(async (err: any, session: CognitoUserSession) => {
if (err || !session) {
console.error("Failed to validate new cognito session", err);
return;
}
// we just saved a new session successfully
setCurrentCognitoSession(session);
const currentUser = await Auth.currentAuthenticatedUser({
bypassCache: true,
});
console.debug("Current user is now:", currentUser);
});
};
// no idea of any other way to override + store session
export const setCurrentCognitoSession = (session: CognitoUserSession) =>
(Auth.currentSession = async () => session);
hey @revmischa, this is interesting. As the service worker is a separate window that run in the background it does not have access to the tokens stored. One method we can utilize is the chrome messaging capabilities, we can get the tokens using the currentCredentials
call and then pass the credentials to the service worker using the sendMessage
call.
That's exactly what I'm doing; sending creds via message to the service worker. I then need to tell the worker to save those credentials for itself.
I understand, I did come across a workaround. Since Amplify is not using chrome.storage.local to store cookies in a service worker/background script, the workaround would be to tell amplify which storage call to use local data and auth cookies.
For example.
we can use the script example from here: https://gitlab.com/kmiyashita/chrome-extension-amplify-auth/-/blob/main/packages/chrome-ext/src/common/SharedAuthStorage.ts
then import the BrowserStorage
and configure amplify as following.
Amplify.configure({
...awsExports,
Auth: {storage: BrowserStorage}
});
this should enable us to create a shared storage and enable us to use amplify resources. For reference this article provides additional information: https://betterprogramming.pub/developing-chrome-extensions-with-amplify-authentication-be9b9e496a06
Before opening, please confirm:
JavaScript Framework
React, Not applicable
Amplify APIs
Authentication
Amplify Categories
auth
Environment information
Describe the bug
I am building a chrome extension that uses Cognito to authenticate AppSync requests. I want to sync auth state inside my extension with the state on my website. I'm more or less able to do this with web extension manifest v2.
I'm unable to build a "modern" extension with Amplify Auth though because in manifest v3 you must use Service Workers.
When I import Amplify Auth into a Service Worker it throws this error:
"Not supported"
From here: https://github.com/aws-amplify/amplify-js/blob/df95ea3724eb6406f64b03f25086cd3e8644cb5f/packages/auth/src/urlListener.ts#L24The reason is because the Service Worker context is not a web page nor node. It doesn't have
window.location
Now I'm sure Amplify can do most of what it needs to do without
window.location
. I am able to work around this if I set:Also in my webpack config I need to set:
Expected behavior
Don't explode if window.location is unavailable.
Ideally use
self
instead ofwindow
orthis
- see https://developer.mozilla.org/en-US/docs/Web/API/Window/selfReproduction steps
Create manifest v3 browser extension and
import { Auth } from "aws-amplify"
Code Snippet
Log output
aws-exports.js
No response
Manual configuration
No response
Additional configuration
No response
Mobile Device
No response
Mobile Operating System
No response
Mobile Browser
No response
Mobile Browser Version
No response
Additional information and screenshots
No response