Listen to user inactivity with AWS Amplify / Cognito - amazon-cognito

Given that you can set access, refresh and ID token expiration time through the Amazon Cognito Console. How can I listen for the token expiring, so that I can redirect the user back to the login page and show an informational message when that happens?
I noticed that Amplify has a Hub utility where you can add listeners, but I'm not sure what to listen for.
https://docs.amplify.aws/lib/utilities/hub/q/platform/js/#state-management

Did quite a bit of resarch and found out that using the Amplify util called Hub, you can listen to the refresh token expiring, as well as other authentication related events.
https://docs.amplify.aws/lib/utilities/hub/q/platform/js/
Here's an example of a hook making use of it, React style:
export const useCognitoContextProvider = () => {
const [context, setContext] = useState({ isSignedIn: false });
useEffect(() => {
const authListener = ({ channel, payload: { event } }) => {
if (channel === 'auth') {
switch (event) {
case 'signIn':
setContext(prevState => (prevState.isSignedIn ? prevState : { ...prevState, isSignedIn: true }));
break;
case 'tokenRefresh_failure':
SignOutService.signOut();
break;
}
}
};
Hub.listen('auth', authListener);
return (): void => {
Hub.remove('auth', authListener);
};
}, []);
return context;
};
And usage:
// CognitoContext.js
export const CognitoContext = createContext({ isSignedIn: false });
// App.js
const cognitoContext = useCognitoContextProvider();
<Provider store={store}>
<CognitoContext.Provider value={cognitoContext}>
<Routes />
</CognitoContext.Provider>
</Provider>

Related

DeviceNotRegistered: "ExponentPushToken[***]" is not a registered push notification recipient

I'm trying to implement expo push notifications on react native app built with expo !
I did everything mentioned on their docs ! i'm getting the token successfully but when i try sending a push notification to that token using their api or the tool they provide i get this error
DeviceNotRegistered: "ExponentPushToken[***]" is not a registered push notification recipient
This is how i'm getting the token !
export const useNotifications = () => {
const registerForPushNotificationsAsync = async () => {
if (Device.isDevice) {
const { status: existingStatus } =
await Notifications.getPermissionsAsync();
let finalStatus = existingStatus;
if (existingStatus !== "granted") {
const { status } = await Notifications.requestPermissionsAsync();
finalStatus = status;
}
if (finalStatus !== "granted") {
alert("Failed to get push token for push notification!");
return;
}
const token = (await Notifications.getExpoPushTokenAsync()).data;
console.log("TOKEN------------", token);
alert(token);
} else {
alert("Must use physical device for Push Notifications");
}
if (Platform.OS === "android") {
Notifications.setNotificationChannelAsync("default", {
name: "default",
importance: Notifications.AndroidImportance.MAX,
vibrationPattern: [0, 250, 250, 250],
lightColor: "#FF231F7C",
});
}
};
const handleNotification = (notification = Notifications.Notification) => {
// could be useful if you want to display your own toast message
// could also make a server call to refresh data in other part of the app
};
// This listener is fired whenever a user taps on or interacts with a notification (works when app is foregrounded, backgrounded, or killed)
const handleNotificationResponse = (
response = Notifications.NotificationResponse
) => {
const data = ({ url } = response.notification.request.content.data);
if (data?.url) Linking.openURL(data.url);
};
return {
registerForPushNotificationsAsync,
handleNotification,
handleNotificationResponse,
};
};
useEffect(() => {
registerForPushNotificationsAsync();
Notifications.setNotificationHandler({
handleNotification: async () => ({
shouldShowAlert: true,
shouldPlaySound: false,
shouldSetBadge: true,
}),
});
const responseListener =
Notifications.addNotificationResponseReceivedListener(
handleNotificationResponse
);
return () => {
if (responseListener) {
Notifications.removeNotificationSubscription(responseListener);
}
};
}, []);
i run the eas build eas build -p android --profile preview so i can test it on a real device since push notifications works only on real devices and after that i pushed the cloud messaging server key that i got from my firebase project with this command expo push:android:upload --api-key <your-token-here>
As i said i successfully get the token but the i get the error when trying to send the notification!
am i missing a step or something ?
I tried run the build on two devices and both not working !

PWA fetch request in service worker sends "the site can't be reached" error on login with google the 2nd time

This error is really driving me crazy for the last 2 days. Please help.
So when I try to login with google the 1st time on my website, it doesn't cause any problem but when I try to do it the second time, with any account, it shows this error in the console:
The FetchEvent for "http://localhost:3000/auth/google/callback?code=4%2F0AX4somethingsomethingsomethingsomething&scope=profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile" resulted in a network error response: an object that was not a Response was passed to respondWith().
and the webpage shows this error:
This site can’t be reached The web page at http://localhost:3000/auth/google/callback?code=4%2F0AX4somethingsomethingsomethingsomething&scope=profile+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile might be temporarily down or it may have moved permanently to a new web address.
I am quite new to pwa and don't understand some of the code in the service worker file (I have copy pasted the 'fetch' part of the code from this webiste: blog.bitsrc.io) so that might be the reason I am not able to identify the error in the code. But you might identify it, this is my service worker code:
const staticCacheName = "site-static-v2";
const dynamicCacheName = "site-dynamic-v2";
const assets = ["/", "/stories", "/groups", "offline.html"];
// cache size limit function
const limitCacheSize = (name, size) => {
caches.open(name).then((cache) => {
cache.keys().then((keys) => {
if (keys.length > size) {
cache.delete(keys[0]).then(limitCacheSize(name, size));
}
});
});
};
// install event
self.addEventListener("install", (evt) => {
//console.log('service worker installed');
evt.waitUntil(
caches.open(staticCacheName).then((cache) => {
console.log("caching shell assets");
cache.addAll(assets);
})
);
});
// activate event
self.addEventListener("activate", (evt) => {
//console.log('service worker activated');
evt.waitUntil(
caches.keys().then((keys) => {
//console.log(keys);
return Promise.all(
keys
.filter((key) => key !== staticCacheName && key !== dynamicCacheName)
.map((key) => caches.delete(key))
);
})
);
});
// fetch events
self.addEventListener("fetch", function (event) {
event.respondWith(
fetch(event.request)
.catch(function () {
return caches.match(event.request);
})
.catch("offline.html")
);
});
This is my script in main.hbs (just like index.html).
if('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/serviceworker.js', { scope: '/' })
.then((reg) => console.log('Success: ', reg.scope))
.catch((err) => console.log('Failure: ', err));
})
}
I am making my website using express by the way.
I have tried pretty much every solution on stackoverflow but none seem to work.
Just for Information, I have also tried this for the 'fetch' part:
self.addEventListener('fetch', evt => {
evt.respondWith(
caches.match(evt.request).then(cacheRes => {
return cacheRes || fetch(evt.request).then(fetchRes => {
return caches.open(dynamicCacheName).then(cache => {
cache.put(evt.request.url, fetchRes.clone());
// check cached items size
limitCacheSize(dynamicCacheName, 15);
return fetchRes;
})
});
}).catch(() => {
return caches.match('offline.html');
})
);
}
);
(The above code also lets me login only once but doesn't let me logout unlike the previous code)
I have copy pasted almost every 'fetch' code on the internet but all of them have a problem with google auth (I am using passport for google auth).
This is my auth.js code:
const express = require("express");
const router = express.Router();
const passport = require("passport");
//Authenticate with google
//GET /auth/google
router.get("/google", passport.authenticate("google", { scope: ["profile"] }));
//Google auth callback
//GET /auth/google/callback
router.get(
"/google/callback",
passport.authenticate("google", { failureRedirect: "/" }),
function (req, res) {
// Successful authentication, redirect home.
res.redirect("/stories");
}
);
router.get("/logout", (req, res) => {
req.logout();
res.redirect("/");
});
module.exports = router;
You can also suggest a workaround with workbox

Nuxt apollo authentication

I try to build an an authentication with nuxt and apollo.
The login and signup is pretty easy, also to set up the jwt token, but when my token expire and I try to get a refresh token or to logout my user I getting following error.
Invariant Violation
Store reset while query was in flight (not completed in link chain)
Because my Error handler which I define in my nuxt.config.js do not work I try to build my own client.
So I set at apollo.clientConfigs.default my ~/graphql/config.ts which looks like
export default ({ app }: Context) => {
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
graphQLErrors.map(async (err) => {
if (err?.extensions?.exception.status === 401) {
await app.$apolloHelpers.onLogout()
}
return err
})
return forward(operation)
}
if (networkError) {
console.log(networkError, 'and another one!')
return forward(operation)
}
return forward(operation)
})
return {
httpEndpoint: 'http://localhost:3001/graphql',
link: from([errorLink as any])
}
}

How do I configure Amplify to to use multiple AppSync endpoints?

I need to support authenticated and unauthenticated AppSync requests in a React Native app. Since AppSync only allows one authorization type per API, I am setting up two APIs: one for authenticated users (Cognito User Pools), and one for guests (API Key).
I think to make this work I need to have two distinct AWSAppSyncClient configs in the same app.
// authenticated user
const appSyncAuthenticatedClient = new AWSAppSyncClient({
url: Config.APPSYNC_AUTHENTICATED_ENDPOINT,
region: Config.APPSYNC_REGION,
auth: {
type: 'AMAZON_COGNITO_USER_POOLS',
jwtToken: async () =>
(await Auth.currentSession()).getAccessToken().getJwtToken()
}
});
// guest
const appSyncUnauthenticatedClient = new AWSAppSyncClient({
url: Config.APPSYNC_UNAUTHENTICATED_ENDPOINT,
region: Config.APPSYNC_REGION,
auth: {
type: 'API_KEY',
apiKey: Config.APPSYNC_API_ID
}
});
and then determine which to use based on whether or not they are logged in
Auth.currentAuthenticatedUser()
.then(user => this.appSyncRunningClient = appSyncAuthenticatedClient)
.catch(err => this.appSyncRunningClient = appSyncUnauthenticatedClient);
const App = props => {
return (
<ApolloProvider client={this.appSyncRunningClient}>
<Rehydrated>
<RootStack/>
</Root>
</Rehydrated>
</ApolloProvider>
);
};
export default App;
This fails because currentAuthenticatedUser returns a promise, and I'm stuck at how to resolve a promise at this top level instantiation of the app. I'll also need to swap out this config during auth events.
In what way can I dynamically select and change the ApolloProvider config at startup and authentication events?
This is currently not possible. Until top-level await is officially supported you should create two Apollo clients one for the API and one for the Cognito.
for example: in your App.js
export default function App(props) {
const [client, setClient] = useState(null);
useEffect(() => {
checkAuth()
}, []);
function checkAuth() {
Auth.currentSession().then(session => {
const token = session.getIdToken();
const jwtToken = token.getJwtToken();
if (typeof jwtToken == "string") {
const authClientConfig = {
url: awsmobile.aws_appsync_graphqlEndpoint,
region: awsmobile.aws_appsync_region,
disableOffline: true,
auth: {
type: AUTH_TYPE.AMAZON_COGNITO_USER_POOLS,
jwtToken: jwtToken
}
}
const link = ApolloLink.from([createAuthLink(authClientConfig), createSubscriptionHandshakeLink(authClientConfig)]);
const authClient = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }) });
setClient(authClient);
} else {
throw "error";
}
}).catch(e => {
console.log(e);
const config = {
url: awsmobile.aws_appsync_graphqlEndpoint,
region: awsmobile.aws_appsync_region,
disableOffline: true,
auth: {
type: AUTH_TYPE.API_KEY,
apiKey: awsmobile.aws_appsync_apiKey
}
}
const link = ApolloLink.from([createAuthLink(config), createSubscriptionHandshakeLink(config)]);
const authClient = new ApolloClient({ link, cache: new InMemoryCache({ addTypename: false }) });
setClient(authClient);
})
}
if (!client) {
return "Loading..."
}
return (
<ApolloProvider client={client}>
...
</ApolloProvider>
);
}`
Things may have moved on as AppSync now supports multiple authentication types per API; however providing an answer as to how to auth/unauth on same endpoint for prosperity. Doesn't answer the how-to multiple endpoints question which is what led me here, but that's no longer required in OPs scenario.
Note: This answer applies to typescript - I'm not overly familiar with react but I think it will work exactly the same...
Unauthenticated access uses AWS_IAM / i.e. CognitoIdentityPool
(configured to allow unauthenticated access)
Authenticated Access users AMAZON_COGNITO_USER_POOLS authentication.
To switch between unauthenticated and authenticated API.graphql() calls. You need to test the current authentication status and use that to override the authMode as in the arguments to the API.graphql() call.
As a prerequisite:
The types in graphql must be setup to allow access via both #aws_iam and #aws_cognito_user_pools (see sample below)
The AppSync API must be configured to allow both authentication types (The code below assumes the API is configured for AWS_IAM by default, but allowed CognitoUserPools as an additional authentication type). This can be configured in console, or via cloudFormation.
Sample code for API call
let authMode;
try {
authMode = (await Auth.currentUserPoolUser()) ? GRAPHQL_AUTH_MODE.AMAZON_COGNITO_USER_POOLS : undefined;
} catch (err) { }
const result = await API.graphql({
...graphqlOperation(statement, gqlAPIServiceArguments),
authMode
});
Example grqphql type
type Profile #aws_iam #aws_cognito_user_pools {
username: ID!
stuff: String!
}
My Amplify Configuration
{
aws_project_region: 'VALUE_HERE',
aws_appsync_graphqlEndpoint: 'https://VALUE_HERE/graphql',
aws_appsync_region: 'VALUE_HERE',
aws_appsync_authenticationType: 'AWS_IAM',
aws_appsync_apiKey: 'XXXXXXXXXXXXXXXXXXXXXXXXXX', // This field seems to be required, but the value is ignored.
Auth: {
identityPoolId: 'VALUE_HERE',
region: 'VALUE_HERE',
userPoolId: 'VALUE_HERE',
userPoolWebClientId: 'VALUE_HERE',
oauth: {
domain: 'VALUE_HERE',
redirectSignIn: 'VALUE_HERE',
redirectSignOut: 'VALUE_HERE',
scope: ['email', 'openid', 'profile', 'aws.cognito.signin.user.admin'],
responseType: 'code'
}
}
};

react-native-linkdin-login is not working in ios?

I am using react-native-linkdin-login library to support linkding sigin in my application.It is working properly in android but in iOS it always ask to download an application rather than application already exist in device and redirect to the APP store. When I open and login to linkdin account, I can't come back to my react-native application, with user profile details.
Give me any suggestion as soon as possible.
async componentWillMount() {
LinkedinLogin.init(
[
'r_emailaddress',
'r_basicprofile'
]
);
}
async handleLinkedinLogin(){
LinkedinLogin.login().then((user) => {
alert("linkdin");
this.setState({ user : user });
this.getUserProfile();
}).catch((e) => {
var err = JSON.parse(e.description);
alert("ERROR: " + err.errorMessage);
alert('Error', e);
});
return true;
}
getUserProfile(user) {
LinkedinLogin.getProfile().then((data) => {
const userdata = Object.assign({}, this.state.user, data);
this.setState({ user: userdata });
const Email = userdata.emailAddress;
const Fullname = userdata.firstName+' '+userdata.lastName;
const SocialAppId = userdata.id;
const SignupType = 'Linkedin';
alert("Please wait....")
this.socialLogin(Fullname,Email,'null',SignupType);
}).catch((e) => {
alert(e);
});
}