Serverless lambda function failes in react with CORS - reactjs

Hi all I have written a serverless lambda API using API gateway, the API works as I expect it to do so while i use POSTMAN. the api used PATCH method to update a record in dynamoDB. I have then implemented axios in my in my React app to make the call to my API. The call fails due to CORS fine, i went over to API gateway and i enabled CORS for that end point by pressing on to the method PATCH going into Method Response and Integration Method added all the response headders
Access-Control-Allow-Headers
Access-Control-Allow-Origin
Access-Control-Allow-Credentials
Access-Control-Allow-Methods
deployed the API and does not work. I then tried in my serverless.yml to add cors: true and deploy this didnt work either.
Here is my cod:
Serverless.yml:
setBookVotes:
handler: src/handlers/setBookVotes.handler
events:
- http:
method: PATCH
path: /book/{id}/vote
React:
export const updateVoteCount = (id, vote) => {
return axios.patch(`book/${id}`, {
vote: vote,
});
};
Lambda:
import AWS from "aws-sdk";
import middy from "#middy/core";
import httpJsonBodyParser from "#middy/http-json-body-parser";
import httpEventNormalizer from "#middy/http-event-normalizer";
import httpErrorHandler from "#middy/http-event-normalizer";
const dynamodb = new AWS.DynamoDB.DocumentClient();
async function setBookVotes(event, context) {
const { id } = event.pathParameters;
const { vote } = event.body;
const params = {
TableName: process.env.BOOK_TABLE_NAME,
Key: { id },
UpdateExpression: "set vote = vote + :vote",
ExpressionAttributeValues: {
":vote": vote,
},
ReturnValues: "ALL_NEW",
};
let updatedVotes;
try {
const result = await dynamodb.update(params).promise();
updatedVotes = result.Attributes;
} catch (error) {
console.error(error);
throw new createError.InternalServerError(error);
}
return {
statusCode: 200,
body: JSON.stringify(updatedVotes),
};
}
export const handler = middy(setBookVotes)
.use(httpJsonBodyParser())
.use(httpEventNormalizer())
.use(httpErrorHandler());

Related

Logout from next-auth with keycloak provider not works

I have a nextjs application with next-auth to manage the authentication.
Here my configuration
....
export default NextAuth({
// Configure one or more authentication providers
providers: [
KeycloakProvider({
id: 'my-keycloack-2',
name: 'my-keycloack-2',
clientId: process.env.NEXTAUTH_CLIENT_ID,
clientSecret: process.env.NEXTAUTH_CLIENT_SECRET,
issuer: process.env.NEXTAUTH_CLIENT_ISSUER,
profile: (profile) => ({
...profile,
id: profile.sub
})
})
],
....
Authentication works as expected, but when i try to logout using the next-auth signOut function it doesn't works. Next-auth session is destroyed but keycloak mantain his session.
After some research i found a reddit conversation https://www.reddit.com/r/nextjs/comments/redv1r/nextauth_signout_does_not_end_keycloak_session/ that describe the same problem.
Here my solution.
I write a custom function to logout
const logout = async (): Promise<void> => {
const {
data: { path }
} = await axios.get('/api/auth/logout');
await signOut({ redirect: false });
window.location.href = path;
};
And i define an api path to obtain the path to destroy the session on keycloak /api/auth/logout
export default (req, res) => {
const path = `${process.env.NEXTAUTH_CLIENT_ISSUER}/protocol/openid-connect/logout?
redirect_uri=${encodeURIComponent(process.env.NEXTAUTH_URL)}`;
res.status(200).json({ path });
};
UPDATE
In the latest versions of keycloak (at time of this post update is 19.*.* -> https://github.com/keycloak/keycloak-documentation/blob/main/securing_apps/topics/oidc/java/logout.adoc) the redirect uri becomes a bit more complex
export default (req, res) => {
const session = await getSession({ req });
let path = `${process.env.NEXTAUTH_CLIENT_ISSUER}/protocol/openid-connect/logout?
post_logout_redirect_uri=${encodeURIComponent(process.env.NEXTAUTH_URL)}`;
if(session?.id_token) {
path = path + `&id_token_hint=${session.id_token}`
} else {
path = path + `&client_id=${process.env.NEXTAUTH_CLIENT_ID}`
}
res.status(200).json({ path });
};
Note that you need to include either the client_id or id_token_hint parameter in case that post_logout_redirect_uri is included.
So, I had a slightly different approach building upon this thread here.
I didn't really like all the redirects happening in my application, nor did I like adding a new endpoint to my application just for dealing with the "post-logout handshake"
Instead, I added the id_token directly into the initial JWT token generated, then attached a method called doFinalSignoutHandshake to the events.signOut which automatically performs a GET request to the keycloak service endpoint and terminates the session on behalf of the user.
This technique allows me to maintain all of the current flows in the application and still use the standard signOut method exposed by next-auth without any special customizations on the front-end.
This is written in typescript, so I extended the JWT definition to include the new values (shouldn't be necessary in vanilla JS
// exists under /types/next-auth.d.ts in your project
// Typescript will merge the definitions in most
// editors
declare module "next-auth/jwt" {
interface JWT {
provider: string;
id_token: string;
}
}
Following is my implementation of /pages/api/[...nextauth.ts]
import axios, { AxiosError } from "axios";
import NextAuth from "next-auth";
import { JWT } from "next-auth/jwt";
import KeycloakProvider from "next-auth/providers/keycloak";
// I defined this outside of the initial setup so
// that I wouldn't need to keep copying the
// process.env.KEYCLOAK_* values everywhere
const keycloak = KeycloakProvider({
clientId: process.env.KEYCLOAK_CLIENT_ID,
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET,
issuer: process.env.KEYCLOAK_ISSUER,
});
// this performs the final handshake for the keycloak
// provider, the way it's written could also potentially
// perform the action for other providers as well
async function doFinalSignoutHandshake(jwt: JWT) {
const { provider, id_token } = jwt;
if (provider == keycloak.id) {
try {
// Add the id_token_hint to the query string
const params = new URLSearchParams();
params.append('id_token_hint', id_token);
const { status, statusText } = await axios.get(`${keycloak.options.issuer}/protocol/openid-connect/logout?${params.toString()}`);
// The response body should contain a confirmation that the user has been logged out
console.log("Completed post-logout handshake", status, statusText);
}
catch (e: any) {
console.error("Unable to perform post-logout handshake", (e as AxiosError)?.code || e)
}
}
}
export default NextAuth({
secret: process.env.NEXTAUTH_SECRET,
providers: [
keycloak
],
callbacks: {
jwt: async ({ token, user, account, profile, isNewUser }) => {
if (account) {
// copy the expiry from the original keycloak token
// overrides the settings in NextAuth.session
token.exp = account.expires_at;
token.id_token = account.id_token;
}
return token;
}
},
events: {
signOut: ({ session, token }) => doFinalSignoutHandshake(token)
}
});
signOut only clears session cookies without destroying user's session on the provider.
Year 2023 Solution:
hit GET /logout endpoint of the provider to destroy user's session
do signOut() to clear session cookies, only if step 1 was successful
Implementation:
Assumption: you are storing user's idToken in the session object returned by useSession/getSession/getServerSession
create an idempotent endpoint (PUT) on server side to make this GET call to the provider
create file: pages/api/auth/signoutprovider.js
import { authOptions } from "./[...nextauth]";
import { getServerSession } from "next-auth";
export default async function signOutProvider(req, res) {
if (req.method === "PUT") {
const session = await getServerSession(req, res, authOptions);
if (session?.idToken) {
try {
// destroy user's session on the provider
await axios.get("<your-issuer>/protocol/openid-connect/logout", { params: id_token_hint: session.idToken });
res.status(200).json(null);
}
catch (error) {
res.status(500).json(null);
}
} else {
// if user is not signed in, give 200
res.status(200).json(null);
}
}
}
wrap signOut by a function, use this function to sign a user out throughout your app
import { signOut } from "next-auth/react";
export async function theRealSignOut(args) {
try {
await axios.put("/api/auth/signoutprovider", null);
// signOut only if PUT was successful
return await signOut(args);
} catch (error) {
// <show some notification to user asking to retry signout>
throw error;
}
}
Note: theRealSignOut can be used on client side only as it is using signOut internally.
Keycloak docs logout

Rails API + React Frontend - how to make CSRF cookie NOT httponly?

I have a Rails 6 API and a React frontend and I would like to keep verify_authenticity_token on all controller actions.
Backend sets CSRF token as follows in application_controller.rb:
class ApplicationController < ActionController::Base
...
include ActionController::Cookies
after_action :set_csrf_cookie
...
protected
def verified_request?
super || request.headers['X-CSRF-Token'] === cookies['X-CSRF-Token']
end
def set_csrf_cookie
if protect_against_forgery? && current_user
cookies['X-CSRF-Token'] = {
value: form_authenticity_token,
httponly: false
}
end
end
end
Frontend is attempting to use js-cookie to retrieve cookies. I have the following in a cookies.js file:
import Cookies from 'js-cookie'
const getCSRFToken = () => {
window.Cookies = Cookies;
const token = Cookies.get('X-CSRF-Token')
return token
}
export default getCSRFToken
and I call this function when I create an Axios request. The function to build the request takes params like method, url, data, etc.:
export const newAxiosIns = params => {
// params will be a hash of various headers
const defaultParams = {
baseURL: baseUrl,
withCredentials: true,
headers: {
common: {
'X-CSRF-TOKEN': getCSRFToken()
}
}
}
const axiosIns = axios.create(defaultParams)
return axiosIns(params)
}
But the cookies end up being httponly in Chrome:
I wondered if it had to do with the form_authenticity_token, so I made a fake token with a value of 'faker' but that was also not httponly.
Thanks!

Safe way to initialize Apollo Client upon asynchronous token

I'm writing an app that uses Apollo Client to make graphql requests against a MongoDB Realm database.
This is the abstract structure of it:
<MongoContext>
<ApolloContext>
<App/>
</ApolloContext>
</MongoContext>
The top level component handles user authentication and provides a context. The next component down initiates Apollo Client and the caching logic and sets the context to the whole app.
The expected data flow is shown in the diagram on this page. The default behavior for a useQuery is that Apollo:
Tries to fetch from cache;
Tries to fetch from server; If successful, saves to cache.
My goal is to achieve offline capability. So, referring to the diagram again, the very first query should be resolved by the cache when it holds the data. Apollo's default cache mechanism is in memory, so I'm using apollo-cache-persist to cache it to localStorage.
More specifically, these are the conditions required:
The app should be responsive upon start.
At first render, the app is either offline or hasn't authenticated yet
Therefore it must read from cache, if available
If there's no cache, don't make any requests to the server (they'd all fail)
If the user is online, the app should get the authentication token for the requests
The token is requested asynchronously
While the token is unknown, read from the cache only (As 1.2 above)
When the token is known, use the data flow described above
My main problems are specifically with 1.2 and 2.2 above. I.e. preventing Apollo from making requests to the server when we already know it will fail.
I was also looking for a global solution, so modifying individual queries with skip or useLazyQuery aren't options. (And I'm not even sure that would work - I still needed the queries to be executed against the cache.)
Code:
ApolloContext component:
import * as React from 'react';
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
createHttpLink,
NormalizedCacheObject,
} from '#apollo/client';
import { setContext } from '#apollo/client/link/context';
import { persistCache } from 'apollo-cache-persist';
import { PersistentStorage } from 'apollo-cache-persist/types';
const ApolloContext: React.FC = ({ children }) => {
// this hook gets me the token asynchronously
// token is '' initially but eventually resolves... or not
const { token } = useToken();
const cache = new InMemoryCache();
const [client, setClient] = React.useState(createApolloClient(token, cache))
// first initialize the client without the token, then again upon receiving it
React.useEffect(() => {
const initPersistCache = async () => {
await persistCache({
cache,
storage: capacitorStorageMethods,
debug: true,
});
};
const initApollo = async () => {
await initPersistCache();
setClient(createApolloClient(token, cache));
};
if (token) {
initApollo();
} else {
initPersistCache();
}
}, [token]);
console.log('id user', id, user);
return <ApolloProvider client={client}>{children}</ApolloProvider>;
};
function createApolloClient(
token: string,
cache: InMemoryCache
) {
const graphql_url = `https://realm.mongodb.com/api/client/v2.0/app/${realmAppId}/graphql`;
const httpLink = createHttpLink({
uri: graphql_url,
});
const authorizationHeaderLink = setContext(async (_, { headers }) => {
return {
headers: {
...headers,
Authorization: `Bearer ${token}`,
},
};
});
return new ApolloClient({
link: authorizationHeaderLink.concat(httpLink),
cache,
});
}
What I've tried:
After attempting many different things. I found something that works, but it looks terrible. The trick is to give Apollo a custom fetch that rejects all requests when the user is not logged in:
const customFetch = (input: RequestInfo, init?: RequestInit | undefined) => {
return user.isLoggedIn
? fetch(input, init)
: Promise.reject(new Response());
};
const httpLink = createHttpLink({
uri: graphql_url,
fetch: customFetch,
});
Another way to prevent outbound requests is to just omit the link property:
return new ApolloClient({
link: user.isLoggedIn
? authorizationHeaderLink.concat(httpLink)
: undefined,
cache,
});
}
That looks way cleaner but now the problem is that make queries that can't be fulfilled by the cache to hang on loading forever.(related issue)
I'm looking for a cleaner and safer way to do this.

Combining a ExpressJS Router endpoint with a fetch call to an external enpoint

I am trying to create an Express Router endpoint that will return the CSV file from an external API (Jenkins in this case)
In more detail, what I am trying to achieve is to have a React Frontend call this route on the Express backend and download a CSV file.
BACKEND
The Express route is has this structure:
router.get('/path/latestCsvTestReport', async (req, res) => {
const { channel } = req.params;
return fetch(
`${jenkinsHost}/job/${channel}/${jenkinsPath}/lastSuccessfulBuild/artifact/test_result/report_test.csv`, {
...fetchOptions,
headers: { Authorization: jenkinsAuth},
},
)
.then(r => {
console.log('====== DATA =====', r);
res.setHeader('Content-type', 'text/csv');
res.setHeader('Cache-Control', 'no-cache');
res.send(r)
})
.catch((err) => {
// console.log(err);
res.status(404);
res.send('report not found');
});
});
and the URL called in the fetch returns a CSV file.
FRONTEND
I am calling the Express endpoint from a method on the React frontend using the following function, which utilised the file-saver library:
async function triggerReportDownload(chlId) {
console.log('===== CSV Request ====')
const resource = `/api/jenkins/${chlId}/latestCsvTestReport`;
saveAs(resource, "report.csv")
}
which is triggered by the click of a button on the FrontEnd.
At the moment, the button, triggers a download but the csv downloaded only contains:
{"size":0 timeout:0}
I am certain I am doing something completely wrong on the way the backend returns the CSV from the fetch call, but for the life of me I do not seem to be able to find the way to formulate the response. Any help/direction towards fixing this would be greatly appreciated.
The solution to this is to simply things as possible (being a newbie I had overcomplicated things). So here we are:
Backend
Import the utils library and then create a stream:
import util from 'util';
const streamPipeline = util.promisify(require('stream').pipeline);
This is then called from the Express router:
router.get('/jenkins/:channel/latestCsvTestReport.csv', async (req, res) => {
const { channel } = req.params;
const response = await fetch(
`${jenkinsHost}/job/${channel}/${jenkinsPath}/lastSuccessfulBuild/artifact/test_result/report_test.csv`, {
...fetchOptions,
headers: { Authorization: jenkinsAuth },
},
);
res.setHeader('Content-disposition', `attachment; filename=report_test_${Date.now()}.csv`);
res.set('Content-Type', 'text/csv');
return streamPipeline(response.body, res);
});
Frontend
Use windows.open to get the download file
async function triggerReportDownload(chlId) {
window.open(`/api/jenkins/${chlId}/latestCsvTestReport.csv`);
}

Gmail API with React

I try to create app with Gmail API and with React. For authorization I use lib
react-google-login
export default class App extends Component {
service = new Service();
render() {
const responseGoogle = (response) => {
this.service.getMessagesList();
}
return (
<div>
<GoogleLogin
clientId="YOUR_ID.apps.googleusercontent.com"
buttonText="Login"
onSuccess={responseGoogle}
/>
After authorization I send fetch request to gmail API and try to get my emails list. But server return me 401. Why? What I did wrong?
My fetch:
export default class Service {
_apiBase = "https://www.googleapis.com/gmail/v1/users/me";
async getResource(url) {
const res = await fetch(`${this._apiBase}${url}`);
if (!res.ok) {
throw new Error(`Could not fetch ${url}, received ${res.status}`);
}
const body = await res.json();
return body;
}
getMessagesList = async () => {
const list = await this.getResource(`/messages`);
return list;
};
Error 401 means Invalid Credentials. If you're getting this error, then most likely, the credentials you're using is invalid. You might be using a wrong type of credential.
Try going to console and checking your credentials again. You may refer to this documentation if you are looking for more information regarding implementing authorization.

Resources