Amplify PubSub javascript subscribe and publish using cognito authorization: how to? - websocket

i want a cognito authorized user to be able to publish and subscribe to AWS IoT using Amplify PubSub but am having difficulty doing so.
i use javascript (inside a Vue app)
i have successfully managed to publish and subscribe to AWS IoT using the aws-iot-device-sdk so all the policies, etc are working.
here's the code i'm using with PubSub:
const region = "eu-west-1";
const endpoint = "blahBlah-ats.iot.eu-west-1.amazonaws.com";
const currentlySubscribedTopic = "myTopic";
Amplify.addPluggable(
new AWSIoTProvider({
aws_pubsub_region: region,
// aws_pubsub_endpoint: "xxxxxxzbx-ats.iot.eu-west-1.amazonaws.com"
// aws_pubsub_endpoint: 'wss://xxxxxxxxxxxxx.iot.<YOUR-IOT-REGION>.amazonaws.com/mqtt',
aws_pubsub_endpoint: "wss://" + endpoint + "/mqtt"
})
);
to subscribe:
PubSub.subscribe(currentlySubscribedTopic).subscribe({
next: data => console.log("Message received", data),
error: error => console.error(error),
close: () => console.log("Done")
});
to publish I call this method:
async publishMessage() {
console.log("publishing message...");
await PubSub.publish(currentlySubscribedTopic, {
msg: "Hello to all subscribers!"
});
console.log("published..");
}
unfortunately nothing happens (not even errors), so I am obviously doing something wrong.
how do i actually make the connection? is there a connect() function?
how do i incorporate the cognito credentials? i have tried this inside Auth.currentSession():
AWS.config.update({
region: "eu-west-1",
credentials: new AWS.CognitoIdentityCredentials({
IdentityPoolId: "eu-west-1:xxxx-7be8-487a-9e0b-91980xxx976e",
Logins: {
// optional tokens, used for authenticated login
"cognito-idp.eu-west-1.amazonaws.com/eu-west-1_xxxx4OFmI":
session.idToken.jwtToken
}
})
});
and can use AWS.config.credentials.get() to get the credentials but don't know how to use them to make the connection.
i'd appreciate some help as to how to do this.
thanks

I was toiling with this for quite awhile today and finally got it working. Pasting my code below. Though its not shown, the PubSub.publish() also worked fine too.
# App.js
import React from 'react';
import './App.css';
import Amplify from 'aws-amplify';
import PubSub from '#aws-amplify/pubsub';
import { AWSIoTProvider } from '#aws-amplify/pubsub/lib/Providers';
import awsconfig from './aws-exports';
import { withAuthenticator, AmplifySignOut } from '#aws-amplify/ui-react';
import EventViewer from './event-viewer.js';
Amplify.Logger.LOG_LEVEL = 'VERBOSE';
Amplify.addPluggable(new AWSIoTProvider({
aws_pubsub_region: 'us-west-2',
aws_pubsub_endpoint: 'wss://MY_IOT_ENDPOINT-ats.iot.us-west-2.amazonaws.com/mqtt',
}));
Amplify.configure(awsconfig);
PubSub.configure();
PubSub.subscribe('myTopic1').subscribe({
next: data => console.log('Message received', data),
error: error => console.error(error),
close: () => console.log('Done'),
});
function App() {
return (
<div className="App">
<AmplifySignOut />
<EventViewer/>
</div>
);
}
export default withAuthenticator(App);
Keep in mind, this also requires that:
You have created a proper AWS IoT policy (e.g. for example, having iot:Connect, and having iot:Subscribe and iot:Receive scoped to the appropriate topics)
You have attached the IoT Policy to your user's Cognito Federated Identity Id

Related

Iron session is not updating req.session object

I have a page where users can give themselves a "role" like member or admin. They can go to another route to create messages. I am trying to update user's role from "user" to "admin". It updates req.session to admin role in the admin.js file, but when I go to messages/create.js and try to log req.session, it shows that user still has the "user" role. I am saving the changes I make by calling req.session.save(), but it is not working.
admin.js
import { withIronSessionApiRoute } from "iron-session/next";
import nc from "next-connect";
import { session_config } from "../../lib/config";
import Users from "../../models/user";
import { connectToDatabase } from "../../util/mongodb";
const handler = nc()
handler.post(async (req) => {
if (req.body.password === process.env.ADMIN_PASSWORD) {
await connectToDatabase()
await Users.findOneAndUpdate({ name: req.session.user.name }, { role: "admin" })
const updated_user = { name: req.session.user.name, role: "admin" }
req.session.user = updated_user
await req.session.save()
}
})
export default withIronSessionApiRoute(handler, session_config);
messages/create.js
import { withIronSessionApiRoute } from "iron-session/next";
import nc from "next-connect";
import { session_config } from "../../../lib/config";
const handler = nc()
handler.post(async (req) => {
console.log(req.session.user)
console.log(req.body)
})
export default withIronSessionApiRoute(handler, session_config)
Please let me know what the issue is and how I can fix it. Thank you
The first thing I noticed from looking at the code is that you're not sending a response back to the client. Iron session uses cookies to manage stateless authentication and the way it manages is by setting the response header. Because you're not sending a response, it can't update the session.
Looking further into the API documentation, session.save() - "Saves the session and sets the cookie header to be sent once the response is sent."
Not knowing your full implementation or having a working code example from something like codesandbox.io, I suggest the following code to see if this solves your problem.
// please make sure that `res` is a parameter on the `.post()` function
// on your original code. I've already set it as shown below.
handler.post(async (req, res) => {
if (req.body.password === process.env.ADMIN_PASSWORD) {
await connectToDatabase()
await Users.findOneAndUpdate({ name: req.session.user.name }, { role: "admin" })
const updated_user = { name: req.session.user.name, role: "admin" }
req.session.user = updated_user
await req.session.save()
// response below
res.send({ ok: true })
// or if you don't want to send custom data back, comment the line above,
// and then uncomment the line below
// res.status(200).end()
}
})
Attempt 2
I made an iron session demo on Codesandbox using some of the demo code from the iron session repo NextJs example.
The code example shows:
login
log out
setting a user as an admin
fetching user data from server-side
fetching user data from client-side
fetching using SWR
Some side notes to be aware of: if you are doing something like
const sessionData = req.session.user, then trying to mutate the req.session.user, and then sending the data back, it won't work because the session object will be recreated per request and node cannot store req.session as a reference.
If my demo doesn't help you, then you're going to have to share more info and code, and maybe create a Codesandbox to reproduce what is happening to you.

Getting NextAuth.js user session in Apollo Server context

My web app is using:
NextJS
NextAuth.js
Apollo Server
I have a NextAuth set up in my app, and I am able to log in just fine.
The problem is coming from trying to get access to the user's session in the Apollo context. I want to pass my user's session into every resolver. Here's my current code:
import { ApolloServer, AuthenticationError } from "apollo-server-micro";
import schema from "./schema";
import mongoose from "mongoose";
import dataloaders from "./dataloaders";
import { getSession } from "next-auth/client";
let db;
const apolloServer = new ApolloServer({
schema,
context: async ({ req }) => {
/*
...
database connection setup
...
*/
// get user's session
const userSession = await getSession({ req });
console.log("USER SESSION", userSession); // <-- userSession is ALWAYS null
if (!userSession) {
throw new AuthenticationError("User is not logged in.");
}
return { db, dataloaders, userSession };
},
});
export const config = {
api: {
bodyParser: false,
},
};
export default apolloServer.createHandler({ path: "/api/graphql" });
The problem is, the session (userSession) is always null, even if I am logged in (and can get a session just fine from a proper NextJS API route). My guess is that because the NextAuth function used to get the session, getSession({ req }) is being passed req--which is provided from Apollo Server Micro, and not from NextJS (which NextAuth is expecting). I've done a lot of searching and can't find anyone who's had this same problem. Any help is much appreciated!
I had exactly this issue and I found it was because of the Apollo GraphQL playground.
The playground does not send credentials without "request.credentials": "include".
My NextAuth / GraphQL API looks like this:
import { ApolloServer } from "apollo-server-micro";
import { getSession } from "next-auth/client";
import { typeDefs, resolvers } "./defined-elsewhere"
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
context: async ({ req }) => {
const session = await getSession({ req });
return { session };
},
playground: {
settings: {
"editor.theme": "light",
"request.credentials": "include",
},
},
});
Hope this works for you!
I just ran into something similar. I'm not 100% sure because it's hard to know the exact details since your example code above doesn't show how you're interacting with apollo from the client before the session is coming through as null. I believe however that you're probably making an API call from inside the getStaticProps which causes static code generation and gets run at build time - ie when no such user context / session could possibly exist.
See https://github.com/nextauthjs/next-auth/issues/383
The getStaticProps method in Next.js is only for build time page generation (e.g. for generating static pages from a headless CMS) and cannot be used for user specific data such as sessions or CSRF Tokens.
Also fwiw I'm not sure why you got downvoted - seems like a legit question to ask imo even if the answer is mostly a standard rtm :). Has happened to me here before too - you win some you lose some :) Cheers

"Must provide query string" error with Apollo Client on React and Nexus Graphql Server

I'm starting to work with GraphQL and the new Nexus Framework GraphQL server, which is a great product.
On my server-side, I defined my schema, I can query my database with Prisma and everything runs smoothly. I can query data also from the Nexus GraphQL playground and also with Postman.
Now, I want to make things work on the client-side. I see that Apollo Client is the best solution to integrate React with GraphQL, but I just can't make things work. I read tons of docs but I'm missing something that I can't figure out.
GraphQL and the client part will be hosted on the same server, on separate node applications.
I'm configuring Apollo based on its documentations. The example below is for the new 3.0 Beta Version of Apollo which I'm testing, but the same scenario happens on the last stable version. I believe that I need to do something else to integrate Apollo and Nexus.
Every query returns: "Must Provide Query String".
The same query inside the playground works perfectly.
Here is my basic testing code:
apollo.js:
import { ApolloClient, HttpLink, InMemoryCache } from '#apollo/client'
const client = new ApolloClient({
cache: new InMemoryCache(),
link: new HttpLink({
uri: 'http://localhost:4000/graphql',
fetchOptions: {
mode: 'no-cors',
}
})
})
export default client
App.js:
import React from 'react'
import { ApolloProvider } from '#apollo/client';
import client from './database/apollo'
import Home from './components/Home'
const App = () => {
return (
<ApolloProvider client={client}>
<Home />
</ApolloProvider>
)
}
export default App;
Home.js:
import React, { useState, useEffect, useReducer } from 'react'
import { useQuery, gql } from '#apollo/client'
const PUBLICATIONS = gql`
{
albumreviews(last: 1) {
title
}
}
`
const Home = () =>{
const { loading, error, data } = useQuery(PUBLICATIONS)
if (loading) return <p>Loading...</p>
if (error) return <p>Error :(</p>
return data.albumreviews.map(({ review }) => (
<div>{JSON.parse(review)}</div>
))
}
export default Home
On the client-side: "Error" is displayed.
On the server-side: "Must provide query string"
Believe me, I've tried to adjust the query thousands of times trying to get a different answer.
Could some help me to move forward with this? Should I provide the Nexus schema to the apollo client? What is the better way of doing this?
You should pretty much never use no-cors. Off hand, I'm not sure why that option would cause your request to be malformed, but it will make it impossible for your response to be read anyway. Remove fetchOptions and whitelist your client URL in your CORS configuration on the server-side. CORs usage with Nexus is shown here in the docs.

GraphQL subscription using server-sent events & EventSource

I'm looking into implementing a "subscription" type using server-sent events as the backing api.
What I'm struggling with is the interface, to be more precise, the http layer of such operation.
The problem:
Using the native EventSource does not support:
Specifying an HTTP method, "GET" is used by default.
Including a payload (The GraphQL query)
While #1 is irrefutable, #2 can be circumvented using query parameters.
Query parameters have a limit of ~2000 chars (can be debated)
which makes relying solely on them feels too fragile.
The solution I'm thinking of is to create a dedicated end-point for each possible event.
For example: A URI for an event representing a completed transaction between parties:
/graphql/transaction-status/$ID
Will translate to this query in the server:
subscription TransactionStatusSubscription {
status(id: $ID) {
ready
}
}
The issues with this approach is:
Creating a handler for each URI-to-GraphQL translation is to be added.
Deploy a new version of the server
Loss of the flexibility offered by GraphQL -> The client should control the query
Keep track of all the end-points in the code base (back-end, front-end, mobile)
There are probably more issues I'm missing.
Is there perhaps a better approach that you can think of?
One the would allow a better approach at providing the request payload using EventSource?
Subscriptions in GraphQL are normally implemented using WebSockets, not SSE. Both Apollo and Relay support using subscriptions-transport-ws client-side to listen for events. Apollo Server includes built-in support for subscriptions using WebSockets. If you're just trying to implement subscriptions, it would be better to utilize one of these existing solutions.
That said, there's a library for utilizing SSE for subscriptions here. It doesn't look like it's maintained anymore, but you can poke around the source code to get some ideas if you're bent on trying to get SSE to work. Looking at the source, it looks like the author got around the limitations you mention above by initializing each subscription with a POST request that returns a subscription id.
As of now you have multiple Packages for GraphQL subscription over SSE.
graphql-sse
Provides both client and server for using GraphQL subscription over SSE. This package has a dedicated handler for subscription.
Here is an example usage with express.
import express from 'express'; // yarn add express
import { createHandler } from 'graphql-sse';
// Create the GraphQL over SSE handler
const handler = createHandler({ schema });
// Create an express app serving all methods on `/graphql/stream`
const app = express();
app.use('/graphql/stream', handler);
app.listen(4000);
console.log('Listening to port 4000');
#graphql-sse/server
Provides a server handler for GraphQL subscription. However, the HTTP handling is up to u depending of the framework you use.
Disclaimer: I am the author of the #graphql-sse packages
Here is an example with express.
import express, { RequestHandler } from "express";
import {
getGraphQLParameters,
processSubscription,
} from "#graphql-sse/server";
import { schema } from "./schema";
const app = express();
app.use(express.json());
app.post(path, async (req, res, next) => {
const request = {
body: req.body,
headers: req.headers,
method: req.method,
query: req.query,
};
const { operationName, query, variables } = getGraphQLParameters(request);
if (!query) {
return next();
}
const result = await processSubscription({
operationName,
query,
variables,
request: req,
schema,
});
if (result.type === RESULT_TYPE.NOT_SUBSCRIPTION) {
return next();
} else if (result.type === RESULT_TYPE.ERROR) {
result.headers.forEach(({ name, value }) => res.setHeader(name, value));
res.status(result.status);
res.json(result.payload);
} else if (result.type === RESULT_TYPE.EVENT_STREAM) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
Connection: 'keep-alive',
'Cache-Control': 'no-cache',
});
result.subscribe((data) => {
res.write(`data: ${JSON.stringify(data)}\n\n`);
});
req.on('close', () => {
result.unsubscribe();
});
}
});
Clients
The two packages mentioned above have companion clients. Because of the limitation of the EventSource API, both packages implement a custom client that provides options for sending HTTP Headers, payload with post, what the EvenSource API does not support. The graphql-sse comes together with it client while the #graphql-sse/server has companion clients in a separate packages.
graphql-sse client example
import { createClient } from 'graphql-sse';
const client = createClient({
// singleConnection: true, use "single connection mode" instead of the default "distinct connection mode"
url: 'http://localhost:4000/graphql/stream',
});
// query
const result = await new Promise((resolve, reject) => {
let result;
client.subscribe(
{
query: '{ hello }',
},
{
next: (data) => (result = data),
error: reject,
complete: () => resolve(result),
},
);
});
// subscription
const onNext = () => {
/* handle incoming values */
};
let unsubscribe = () => {
/* complete the subscription */
};
await new Promise((resolve, reject) => {
unsubscribe = client.subscribe(
{
query: 'subscription { greetings }',
},
{
next: onNext,
error: reject,
complete: resolve,
},
);
});
;
#graphql-sse/client
A companion of the #graphql-sse/server.
Example
import {
SubscriptionClient,
SubscriptionClientOptions,
} from '#graphql-sse/client';
const subscriptionClient = SubscriptionClient.create({
graphQlSubscriptionUrl: 'http://some.host/graphl/subscriptions'
});
const subscription = subscriptionClient.subscribe(
{
query: 'subscription { greetings }',
}
)
const onNext = () => {
/* handle incoming values */
};
const onError = () => {
/* handle incoming errors */
};
subscription.susbscribe(onNext, onError)
#gaphql-sse/apollo-client
A companion package of the #graph-sse/server package for Apollo Client.
import { split, HttpLink, ApolloClient, InMemoryCache } from '#apollo/client';
import { getMainDefinition } from '#apollo/client/utilities';
import { ServerSentEventsLink } from '#graphql-sse/apollo-client';
const httpLink = new HttpLink({
uri: 'http://localhost:4000/graphql',
});
const sseLink = new ServerSentEventsLink({
graphQlSubscriptionUrl: 'http://localhost:4000/graphql',
});
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
sseLink,
httpLink
);
export const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
});
If you're using Apollo, they support automatic persisted queries (abbreviated APQ in the docs). If you're not using Apollo, the implementation shouldn't be too bad in any language. I'd recommend following their conventions just so your clients can use Apollo if they want.
The first time any client makes an EventSource request with a hash of the query, it'll fail, then retry the request with the full payload to a regular GraphQL endpoint. If APQ is enabled on the server, subsequent GET requests from all clients with query parameters will execute as planned.
Once you've solved that problem, you just have to make a server-sent events transport for GraphQL (should be easy considering the subscribe function just returns an AsyncIterator)
I'm looking into doing this at my company because some frontend developers like how easy EventSource is to deal with.
There are two things at play here: the SSE connection and the GraphQL endpoint. The endpoint has a spec to follow, so just returning SSE from a subscription request is not done and needs a GET request anyway. So the two have to be separate.
How about letting the client open an SSE channel via /graphql-sse, which creates a channel token. Using this token the client can then request subscriptions and the events will arrive via the chosen channel.
The token could be sent as the first event on the SSE channel, and to pass the token to the query, it can be provided by the client in a cookie, a request header or even an unused query variable.
Alternatively, the server can store the last opened channel in session storage (limiting the client to a single channel).
If no channel is found, the query fails. If the channel closes, the client can open it again, and either pass the token in the query string/cookie/header or let the session storage handle it.

How to use Apollo Client with AppSync?

AppSync uses MQTT over WebSockets for its subscription, yet Apollo uses WebSockets. Neither Subscription component or subscribeForMore in Query component works for me when using apollo with AppSync.
One AppSync feature that generated a lot of buzz is its emphasis on
real-time data. Under the hood, AppSync’s real-time feature is powered
by GraphQL subscriptions. While Apollo bases its subscriptions on
WebSockets via subscriptions-transport-ws, subscriptions in GraphQL
are actually flexible enough for you to base them on another messaging
technology. Instead of WebSockets, AppSync’s subscriptions use MQTT as
the transport layer.
Is there any way to make use of AppSync while still using Apollo?
Ok, here is how it worked for me. You'll need to use aws-appsync SDK (https://github.com/awslabs/aws-mobile-appsync-sdk-js) to use Apollo with AppSync. Didn't have to make any other change to make subscription work with AppSync.
Configure ApolloProvider and client:
// App.js
import React from 'react';
import { Platform, StatusBar, StyleSheet, View } from 'react-native';
import { AppLoading, Asset, Font, Icon } from 'expo';
import AWSAppSyncClient from 'aws-appsync' // <--------- use this instead of Apollo Client
import {ApolloProvider} from 'react-apollo'
import { Rehydrated } from 'aws-appsync-react' // <--------- Rehydrated is required to work with Apollo
import config from './aws-exports'
import { SERVER_ENDPOINT, CHAIN_ID } from 'react-native-dotenv'
import AppNavigator from './navigation/AppNavigator';
const client = new AWSAppSyncClient({
url: config.aws_appsync_graphqlEndpoint,
region: config.aws_appsync_region,
auth: {
type: config.aws_appsync_authenticationType,
apiKey: config.aws_appsync_apiKey,
// jwtToken: async () => token, // Required when you use Cognito UserPools OR OpenID Connect. token object is obtained previously
}
})
export default class App extends React.Component {
render() {
return <ApolloProvider client={client}>
<Rehydrated>
<View style={styles.container}>
<AppNavigator />
</View>
</Rehydrated>
</ApolloProvider>
}
Here is how the subscription in a component looks like:
<Subscription subscription={gql(onCreateBlog)}>
{({data, loading})=>{
return <Text>New Item: {JSON.stringify(data)}</Text>
}}
</Subscription>
Just to add a note about the authentication as it took me a while to work this out:
If the authenticationType is "API_KEY" then you have to pass the apiKey as shown in #C.Lee's answer.
auth: {
type: config.aws_appsync_authenticationType,
apiKey: config.aws_appsync_apiKey,
}
If the authenticationType is "AMAZON_COGNITO_USER_POOLS" then you need the jwkToken, and
if you're using Amplify you can do this as
auth: {
type: config.aws_appsync_authenticationType,
jwtToken: async () => {
const session = await Auth.currentSession();
return session.getIdToken().getJwtToken();
}
}
But if your authenticationType is "AWS_IAM" then you need the following:
auth: {
type: AUTH_TYPE.AWS_IAM,
credentials: () => Auth.currentCredentials()
}

Resources