I have implemented authorization using cookies, when ApolloClient is initialized, an authorization link is specified, where there is a token in the headers. But when trying to load data with getServerSideProps I always get an Unauthorized error even though without getServerSideProps everything works. This is my first experience with apollo, so I don't quite understand how to fix it.
My ApolloClient:
import { parseCookies } from 'nookies'
import { setContext } from '#apollo/client/link/context';
import { ApolloClient, InMemoryCache, createHttpLink } from '#apollo/client'
const cookies = parseCookies()
const httpLink = createHttpLink({
uri: 'http://localhost:3001/graphql',
});
const authLink = setContext((_, { headers }) => {
const token = cookies.access_token
return {
headers: {
...headers,
authorization: token ? `Bearer ${token}` : "",
}
}
});
export let client = new ApolloClient({
cache: new InMemoryCache(),
credentials: 'same-origin',
link: authLink.concat(httpLink),
})
Component with getServerSideProps:
export default function Suggestions({ users }: any) {
return (<MainLayout>
{JSON.stringify(users)}
</MainLayout>)
}
export async function getServerSideProps(context: GetServerSidePropsContext) {
const { data } = await client.query({
query: GET_USERS
});
return {
props: { users: data.users },
}
}
Main.js file on the backend (backend on Nestjs)
async function bootstrap() {
const app = await NestFactory.create<NestExpressApplication>(AppModule, { cors: true });
app.useGlobalPipes(new ValidationPipe())
app.useGlobalGuards(new JwtAuthGuard(new Reflector()))
app.enableCors({
origin: 'http://localhost:3000',
credentials: true,
allowedHeaders: 'Origin,X-Requested-With,Content-Type,Accept,Authorization,authorization'
})
await app.listen(3001);
}
bootstrap();
And Graphql in App.module on backend
#Module({
imports: [
GraphQLModule.forRoot({
autoSchemaFile: path.join(process.cwd(), 'src/schema/gql'),
sortSchema: true,
driver: ApolloDriver,
context: ({ req, res }) => ({ req, res }),
cors: {
origin: 'http://localhost:3000',
credentials: true,
}
}),
Related
I have a website in production (https://socialvc.netlify.app/) but the queries on the page will only appear if the app is also running locally on localhost:3000.
I tried to configure ApolloClient 3 ways
locally only
with a conditional to see if the app is in production or local environment
exclusively the production ur - only the first code block works on prod and it only works if I am running locally as well
All 3 are demonstrated below:
const client = new ApolloClient({
uri: "http://localhost:3000/api/graphql",
cache: new InMemoryCache(),
});
so if im running my localhost:3000 I can see the queries in production
I tried
let uri = "http://localhost:3000/api/graphql";
if (process.env.NODE_ENV === "production") {
uri = "https://socialvc.netlify.app/api/graphql";
}
const client = new ApolloClient({
uri: uri,
cache: new InMemoryCache(),
});
and
const client = new ApolloClient({
uri: "https://socialvc.netlify.app/api/graphql",
cache: new InMemoryCache(),
});
Some more info: I am using Next.JS with graphql/ApolloClient/Prisma and Postgre
Edit: So I have cors enabled but am still getting that error. Below I show where I have set my headers and allowed for Cors. Could I be missing something there?
const apolloServer = new ApolloServer({
typeDefs,
resolvers,
});
const startServer = apolloServer.start();
export default async function handler(req: any, res: any) {
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader(
"Access-Control-Allow-Headers",
"Origin, X-Requested-With, Content-Type, Accept, Access-Control-Allow-Methods, Access-Control-Allow-Origin, Access-Control-Allow-Credentials, Access-Control-Allow-Headers"
);
res.setHeader(
"Access-Control-Allow-Methods",
"POST, GET, PUT, PATCH, DELETE, OPTIONS, HEAD"
);
if (req.method === "OPTIONS") {
res.end();
return false;
}
await startServer;
await apolloServer.createHandler({
path: "/api/graphql",
})(req, res);
}
Enable CORS for Apollo Server 4 and Next.js
src/allow-next-api-cors.ts
import { NextApiResponse, NextApiRequest, NextApiHandler } from "next";
const allowedHeaders = [
"access-control-allow-credentials",
"access-control-allow-origin",
"apollographql-client-name", // this might be key for you
"authorization",
"content-type",
];
export const allowNextApiCors = (handler: NextApiHandler) =>
async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Access-Control-Allow-Credentials", "true");
res.setHeader("Access-Control-Allow-Origin","*");// research having this set to *
res.setHeader("Access-Control-Allow-Methods", ["GET", "OPTIONS", "POST"]);
res.setHeader("Access-Control-Allow-Headers", allowedHeaders.join(","));
if (req.method === "OPTIONS") {
res.status(200).end();
return;
}
await handler(req, res);
};
api/graphql.ts
import { startServerAndCreateNextHandler } from "#as-integrations/next";
import { NextApiRequest, NextApiResponse } from "next";
import { ApolloServer } from "#apollo/server";
// set correct import paths
import { typeDefs, resolvers } from "your-types-and-resolvers";
import { allowNextApiCors } from "src/apollo-next-allow-cors";
type User = { email: string; id: string; name: string; role: string };
type Context = { currentUser: User; accessToken: string };
type CreateContext = (req: NextApiRequest, res: NextApiResponse) => Promise<Context>;
const getUser = (): Context => ({
currentUser: {
email: "test#example.com",
id: "1",
name: "Test User",
role: "USER",
},
accessToken: "super-secret-token",
});
const context: CreateContext = async () => {
const { currentUser, accessToken } = getUser();
return { currentUser, accessToken };
};
const context: CreateContext = async () => {
const { currentUser, accessToken } = getUser();
return { currentUser, accessToken };
};
const apolloServer = new ApolloServer({ typeDefs, resolvers });
const handler = startServerAndCreateNextHandler(apolloServer, { context });
export default allowNextApiCors(handler);
I am currently using NextAuth to signIn in my application, and want to add more scopes into it while the user is already signed in so I can use the Google Fit API.
I've been reading the documentation of NextAuth and doing some research but did not find anything helpful for the current NextAuth v4 in this scope situation.
My current Google configuration:
import NextAuth from 'next-auth';
import GoogleProvider from "next-auth/providers/google"
const GOOGLE_AUTHORIZATION_URL =
'https://accounts.google.com/o/oauth2/v2/auth?' +
new URLSearchParams({
prompt: 'consent',
access_type: 'offline',
response_type: 'code'
})
export default NextAuth({
// Configure one or more authentication providers
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
authorization: GOOGLE_AUTHORIZATION_URL,
}),
],
callbacks: {
async jwt({ token, user, account }) {
// Initial sign in
if (account && user) {
return {
accessToken: account.access_token,
accessTokenExpires: Date.now() + account.expires_in * 1000,
refreshToken: account.refresh_token,
user
}
}
// Return previous token if the access token has not expired yet
if (Date.now() < token.accessTokenExpires) {
return token
}
// Access token has expired, try to update it
return refreshAccessToken(token)
},
async session({ session, token }) {
session.user = token.user;
session.accessToken = token.accessToken
session.error = token.error
return session
}
},
jwt: {
secret: process.env.NEXTAUTH_JWT_SECRET,
},
secret: process.env.NEXTAUTH_SECRET,
})
async function refreshAccessToken(token) {
try {
const url =
"https://oauth2.googleapis.com/token?" +
new URLSearchParams({
client_id: process.env.GOOGLE_CLIENT_ID,
client_secret: process.env.GOOGLE_CLIENT_SECRET,
grant_type: "refresh_token",
refresh_token: token.refreshToken,
})
const response = await fetch(url, {
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
method: "POST",
})
const refreshedTokens = await response.json()
if (!response.ok) {
throw refreshedTokens
}
return {
...token,
accessToken: refreshedTokens.access_token,
accessTokenExpires: Date.now() + refreshedTokens.expires_at * 1000,
refreshToken: refreshedTokens.refresh_token ?? token.refreshToken, // Fall back to old refresh token
}
} catch (error) {
console.log(error)
return {
...token,
error: "RefreshAccessTokenError",
}
}
}
My current code is working just fine, so I just need the scopes to authorize and use the Google Fitness API.
Actually made it work, created a file called add_scopes.js inside pages/api/auth/
export default (req, res) => {
if (req.method === 'POST') {
// construct the authorize URL with additional scopes
const scopes = 'openid https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/fitness.activity.read https://www.googleapis.com/auth/fitness.location.read'
const redirectUri = process.env.GOOGLE_CALLBACK_URL
const clientId = process.env.GOOGLE_CLIENT_ID
const authorizationUrl = `https://accounts.google.com/o/oauth2/v2/auth?prompt=consent&access_type=offline&response_type=code&scope=${scopes}&redirect_uri=${redirectUri}&client_id=${clientId}`
// send the authorization URL to the client
res.json({ authorizationUrl });
} else {
res.status(405).end(); // Method Not Allowed
}
}
then made a button to call this api route:
import { useCallback } from 'react';
import { Button } from 'react-bootstrap';
const AddScopesButton = ({scopes=scopes}) => {
const isAuthorized = scopes.includes("https://www.googleapis.com/auth/fitness.activity.read") && scopes.includes("https://www.googleapis.com/auth/fitness.location.read")
const handleClick = useCallback(async () => {
try {
const res = await fetch("/api/auth/add_scopes", { method: "POST" });
const json = await res.json()
if (res.ok) {
window.location.href = json.authorizationUrl;
} else {
throw new Error(res.statusText);
}
} catch (error) {
console.error(error);
}
}, []);
return (
<>
{!isAuthorized && (
<Button className='mt-2' onClick={handleClick}>Add Scopes</Button>
)}
{isAuthorized && <span>Authorized</span>}
</>
);
};
export default AddScopesButton;
The only problem is if you signOut and signIn back in you need to get the authorization again, would really like to know if there is a way to save the accessToken/scopes that were authorized.
i am using next-auth with ldap to authenticate user name and password. i am able to log/authenticate the user using username and password. but when i can't create user using prisma as await is not allowed inside promise.
this is my [...next-auth].js
`
const ldap = require("ldapjs");
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
import { PrismaClient } from "#prisma/client";
const url = `ldap://${process.env.LDAP_SERVER}`;
const prisma = new PrismaClient();
export default NextAuth({
providers: [
CredentialsProvider({
name: "LDAP",
credentials: {
username: { label: "DN", type: "text", placeholder: "" },
password: { label: "Password", type: "password" },
},
authorize: async (credentials, req) => {
// You might want to pull this call out so we're not making a new LDAP client on every login attemp
const client = ldap.createClient({
url: url,
});
return new Promise((resolve, reject) => {
client.bind(
`${credentials.username}#${process.env.LDAP_DOMAIN}`,
credentials.password,
(error) => {
if (error) {
console.log("Wrong email or password.");
reject("Wrong email or password.");
} else {
console.log("Successfully Logged In");
resolve({
username: credentials.username,
password: credentials.password,
});
}
const filter = `(sAMAccountName=${credentials.username})`;
client.search(
process.env.LDAP_BASE_DN,
{
filter,
scope: "sub",
attributes: [
"mail",
"employeeid",
"title",
"name",
"division",
"department",
"section",
],
},
(err, results) => {
if (err) {
reject(`User ${username} LDAP search error`);
}
const entries = [];
results.on("searchEntry", (entry) => {
entries.push(entry.object);
});
results.on("error", (err) => {
reject("LDAP SEARCH error");
});
results.on("end", (result) => {
if (entries.length == 0) {
reject("Something went wrong. Please try again. (AD)");
}
console.log({ entries });
const searchResult = JSON.stringify(entries[0]);
const adEmployee = JSON.parse(searchResult);
const empId = adEmployee?.employeeID;
const name = adEmployee.name;
console.log(empId);
const newUser= await prisma.user.findUnique({
where:{
oracleId:oracleId
}
})
if(!newUser){
await prisma.user.create({
data:{
oracleId:empId,
fullName:name
}
})
}
});
}
);
}
);
});
},
}),
],
pages: {
signIn: "/auth/sign-in",
},
callbacks: {
jwt: async ({ token, user }) => {
if (user) {
token.username = user.username;
token.password = user.password;
}
return token;
},
session: async ({ session, token }) => {
if (token) {
session.id = token.id;
session.username = token.username;
}
// console.log(token);
return session;
},
},
debug: process.env.NODE_ENV === "development",
secret: process.env.NEXTAUTH_SECRET,
jwt: {
secret: process.env.NEXTAUTH_SECRET,
encryption: true,
},
});
`
await is not allowed inside promise, where should i call prisma.
Thanks
For this u need to use API endpoint (as prisma is used on server side and cannot be used on client side especially when you pass db url from env also not shown on frontend), your create for example /api/register where:
import { PrismaClient } from '#prisma/client';
import dotenv from 'dotenv'
dotenv.config();
const prisma = new PrismaClient();
const Handler = async (
req,
res
) => {
await prisma.$connect()
const users = await prisma.user.findMany()
//check if user u add is already in db
//if not then
try {
savedUser = await prisma.user.create({ data: new_user });
await prisma.$disconnect()
} catch (error: any) {
await prisma.$disconnect()
// show db error
return res.status(501).json({message: error.message})
}
res.status(200).json({ message: 'User added to db ' + savedUser.name });
}
this is just a simple explanation of what you need to do to make it work, you may add some safety:
const { username, password } = req.body
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
if (!username || !password) {
return res.status(400).json({ message: 'Username and password are required' });
}
then u call api endpoint:
const response = await axios.post(
LOGIN_URL,
JSON.stringify({ username, password }),
{
headers: { 'Content-Type': 'application/json' },
withCredentials: true
}
)
where LOGIN_URL could be /api/register
I'm creating a next js application, using next-auth to handle authentication.
I have an external backend api, so I'm using Credentials Provider.
The problem is that the backend sends httponly cookies, but those are not being attached to the browser when i make a request client side.
In /pages/api/[...auth].js
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import clientAxios from '../../../config/configAxios'
export default NextAuth({
providers: [
Providers.Credentials({
async authorize(credentials) {
try {
const login = await clientAxios.post('/api/login', {
username: credentials.username,
password: credentials.password,
is_master: credentials.is_master
})
const info = login.data.data.user
const token = {
accessToken: login.data.data.access_token,
expiresIn: login.data.data.expires_in,
refreshToken: login.data.data.refresh_token
}
// I can see cookies here
const cookies = login.headers['set-cookie']
return { info, token, cookies }
} catch (error) {
console.log(error)
throw (Error(error.response.data.M))
}
}
})
],
callbacks: {
async jwt(token, user, account, profile, isNewUser) {
if (token) {
// Here cookies are set but only in server side
clientAxios.defaults.headers.common['Cookie'] = token.cookies
}
if (user) {
token = {
user: user.info,
...user.token,
}
}
return token
},
async session(session, token) {
// Add property to session, like an access_token from a provider.
session.user = token.user
session.accessToken = token.accessToken
session.refreshToken = token.refreshToken
return session
}
},
session: {
jwt: true
}
})
my axios config file
import axios from 'axios';
const clientAxios = axios.create({
baseURL: process.env.backendURL,
withCredentials: true,
headers:{
'Accept' : 'application/json',
'Content-Type' : 'application/json'
}
});
export default clientAxios;
a page component
import { getSession } from "next-auth/client";
import clientAxios from "../../../config/configAxios";
import { useEffect } from "react"
export default function PageOne (props) {
useEffect(async () => {
// This request fails, cookies are not sent
const response = await clientAxios.get('/api/info');
}, [])
return (
<div>
<h1>Hello World!</h1>
</div>
)
}
export async function getServerSideProps (context) {
const session = await getSession(context)
if (!session) {
return {
redirect: {
destination: '/login',
permanent: false
}
}
}
// This request works
const response = await clientAxios.get('/api/info');
return {
props: {
session,
info: response.data
}
}
}
After time of researching I have figured it out.
I had to make a change in /pages/api/auth in the way I'm exporting NextAuth.
Instead of
export default NextAuth({
providers: [
...
]
})
Export it like this, so we can have access to request and response object
export default (req, res) => {
return NextAuth(req, res, options)
}
But to access them in the options object, we can make it a callback
const nextAuthOptions = (req, res) => {
return {
providers: [
...
]
}
}
export default (req, res) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
To send a cookie back to the frontend from the backed we must add a 'Set-Cookie' header in the respond
res.setHeader('Set-Cookie', ['cookie_name=cookie_value'])
The complete code would be
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
const nextAuthOptions = (req, res) => {
return {
providers: [
CredentialsProvider({
async authorize(credentials) {
try {
const response = await axios.post('/api/login', {
username: credentials.username,
password: credentials.password
})
const cookies = response.headers['set-cookie']
res.setHeader('Set-Cookie', cookies)
return response.data
} catch (error) {
console.log(error)
throw (Error(error.response))
}
}
})
]
}
}
export default (req, res) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
Update - Typescript example
Create a type for the callback nextAuthOptions
import { NextApiRequest, NextApiResponse } from 'next';
import { NextAuthOptions } from 'next-auth';
type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions
Combining everything
import { NextApiRequest, NextApiResponse } from 'next';
import NextAuth, { NextAuthOptions } from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import axios from 'axios'
type NextAuthOptionsCallback = (req: NextApiRequest, res: NextApiResponse) => NextAuthOptions
const nextAuthOptions: NextAuthOptionsCallback = (req, res) => {
return {
providers: [
CredentialsProvider({
credentials: {
},
async authorize(credentials) {
try {
const response = await axios.post('/api/login', {
username: credentials.username,
password: credentials.password
})
const cookies = response.headers['set-cookie']
res.setHeader('Set-Cookie', cookies)
return response.data
} catch (error) {
console.log(error)
throw (Error(error.response))
}
}
})
],
callbacks: {
...
},
session: {
...
}
}
}
export default (req: NextApiRequest, res: NextApiResponse) => {
return NextAuth(req, res, nextAuthOptions(req, res))
}
To remove cookie in nextAuth after signing out, I used the following block of code - set the cookie parameters to match what you have for the cookie to be expired - Use the SignOut event in [...nextauth].js file
export default async function auth(req, res) {
return await NextAuth(req, res, {
...
events: {
async signOut({ token }) {
res.setHeader("Set-Cookie", "cookieName=deleted;Max-Age=0;path=/;Domain=.techtenum.com;");
},
},
...
}
}
You need to configure clientAxios to include cookies that the server sends as part of its response in all requests back to the server. Setting api.defaults.withCredentials = true; should get you what you want. See the axios configuration for my vue application below:
import axios from "axios";
export default ({ Vue, store, router }) => {
const api = axios.create({
baseURL: process.env.VUE_APP_API_URL
});
api.defaults.withCredentials = true; ------> this line includes the cookies
Vue.prototype.$axios = api;
store.$axios = api;
};
As per instructions followed here, I'm trying to cache my endpoint URL and token from Auth0 before constructing my Apollo client:
import React from 'react';
import { ApolloClient, ApolloProvider, from, HttpLink, InMemoryCache } from '#apollo/client';
import { setContext } from '#apollo/link-context';
import { useAuth0 } from './auth/AuthContext';
const App: React.FC = () => {
const { isLoading, getTokenSilently, getIdTokenClaims } = useAuth0();
if (isLoading) return <Loader />;
let endpoint: string;
let token: string;
const contextLink = setContext(async () => {
if (!token) {
token = await getTokenSilently();
}
if (!endpoint) {
endpoint = await getIdTokenClaims()['https://example.com/graphql_endpoint'];
}
return { endpoint, token };
});
/**
* TODO: check for autorization error and remove token from cache
* See: https://www.apollographql.com/docs/react/v3.0-beta/api/link/apollo-link-context/
*/
const apolloClient = new ApolloClient({
cache: new InMemoryCache(),
link: from([
contextLink,
new HttpLink({
uri: endpoint || '',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${token}`
}
})
])
});
return (
<ApolloProvider client={apolloClient}>
<div />
</ApolloProvider>
);
};
export default App;
I'm getting the error TS2454 (variable is used before being assigned) for both endpoint and token above. Any idea how I can get around this?
You're declaring both endpoint and token as variables, but not initializing them to anything before checking them inside of setContext.
let endpoint: string;
let token: string;
const contextLink = setContext(async () => {
if (!token) {
token = await getTokenSilently();
}
if (!endpoint) {
endpoint = await getIdTokenClaims()['https://example.com/graphql_endpoint'];
}
return { endpoint, token };
});
Try setting default values:
let endpoint: string = "";
let token: string = "";