Next-Auth with firebase Authentication - reactjs

just wanna have my custom credential provider which authenticate the entered username and password with Firebase Authentication on sign in page
pages/api/auth/[...nextauth].ts
import NextAuth from "next-auth"
import { getDatabase } from "firebase/database"
import { DB } from "../../../constants/firebase"
import { FirebaseAdapter } from "#next-auth/firebase-adapter"
import * as firestoreFunctions from "firebase/firestore"
import CredentialsProvider from "next-auth/providers/credentials"
export default NextAuth({
session: {
strategy: "database",
},
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
username: {
label: "Username",
type: "text",
placeholder: "somebody#gmail.com",
},
password: { label: "Password", type: "password" },
},
async authorize(credentials, req) {
const database = getDatabase()
console.log(database)
const user = {
id: 1,
usename: "j",
password: "123456789",
}
if (
credentials?.username === user.usename &&
credentials.password === "123456789"
) {
return user
}
return null
},
}),
],
adapter: FirebaseAdapter({
db: DB,
...firestoreFunctions,
}),
// pages: {
// signIn: "/auth/signin",
// signOut: "/auth/signout",
// error: "/auth/error", // Error code passed in query string as ?error=
// verifyRequest: "/auth/verify-request", // (used for check email message)
// newUser: "/auth/new-user", // New users will be directed here on first sign in (leave the property out if not of interest)
// },
callbacks: {
async jwt({ token, user }) {
if (user) {
token.email = user.email
}
return token
},
async session({ session, token, user }) {
if (token) {
session.user!.email = token.email
}
return session
},
redirect({ url, baseUrl }) {
if (url.startsWith(baseUrl)) return url
else if (url.startsWith("/"))
return new URL(url, baseUrl).toString()
return baseUrl
},
},
})
firebase.ts
import { initializeApp, getApp, getApps } from "firebase/app"
import { getAnalytics } from "firebase/analytics"
import { getFirestore } from "#firebase/firestore"
import { getStorage } from "#firebase/storage"
import getFirebaseObject from "./firebaseConfig"
const app = !getApps.length ? initializeApp(getFirebaseObject()) : getApp()
const DB = getFirestore(app)
const storages = getStorage()
const analytics = getAnalytics(app)
export { app, DB, analytics, storages }
as you see
const user = {
id: 1,
usename: "j",
password: "123456789",
}
in fact except of these static data wanna search and get right user info from the Firebase
I know there are a some other way of doing this but I like working with next-auth for last change wanna make sure there's a spot of light in this was ;)

i found this public repository where the author does something similar to what you want to achieve, which is create a custom token with your database credentials.
May be this repository can help you. It has a few errors, but it gave me a general idea about what to do, as I had a similar case.
try {
if (user !== null) {
await customTokenSignIn(user.id, user.email);
(await getUser(user.id)) ??
(await createUser(toReqUser(user, account)));
const data = await getUser(user.id);
setResUser(user, data as ResUser);
return true;
}
return false;
} catch (e) {
console.error(e);
return false;
}
const customTokenSignIn = async (id: string, email: string) => {
const hash = toHash(id);
const customToken = await adminAuth.createCustomToken(hash);
await auth.signInWithCustomToken(customToken).then((res) => {
res.user?.updateEmail(email);
});
await adminAuth.setCustomUserClaims(hash, { sid: id });
await createUserToken({ id: id, firebaseUid: hash });
};

Related

How to check if user is authenticated in React with express sessions and apollo on backend?

I have an app where I want to authenticate a user and show authenticated user pages which are unavailable to a non authenticated oneenter code here.
On backend I am using express-sessions and apollo-server.
Basically my idea was to set userId in the session, then check if it is empty or not and send from backend to frontend if user is authenticated or not.
My index.ts file is as following:
const { createClient } = require('redis');
let redisClient = createClient({ legacyMode: true });
async function main() {
const httpServer = http.createServer(app);
// check if userid exists
function isAuthenticated(req: any) {
const userId: string = req?.userId;
return !!userId;
}
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [ApolloServerPluginDrainHttpServer({ httpServer })],
});
await server.start();
await redisClient.connect();
app.use(
bodyParser.json(),
cors<cors.CorsRequest>(),
session({
store: new RedisStore({
host: host,
port: 6379,
client: redisClient,
}),
secret: process.env.SECRET,
resave: false,
saveUninitialized: false,
cookie: {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
},
}),
expressMiddleware(server, {
context: async ({ req, res }) => {
return { req, res, isAuthenticated: isAuthenticated(req) };
},
})
);
try {
await mongoose.connect(process.env.MONGO_DB_URI || '');
console.log(`Mongoose connected on port`);
} catch (error) {
console.log(error);
}
await new Promise<void>((resolve) =>
httpServer.listen({ port: 4000 }, resolve)
);
console.log(`🚀 Server ready at http://localhost:4000/`);
}
main();
my sign-in resolver is this:
signInUser: async (_, { user }, context) => {
const { username, password } = user;
const foundUser = await UserModel.findOne({ username });
if (foundUser) {
const match = await bcrypt.compare(password, foundUser.password);
if (match) {
if (context.req.session) {
context.req.session.userId = foundUser.id; // here I am setting user id to the session
return { message: 'Signed in successfully!', status: 'success' };
}
}
}
},
and
Query: {
isAuthenticated: (_, __, {isAuthenticated}) => {
return isAuthenticated;
},
},
And on frontend I created a hook like this, to use in my components.
import { useQuery, gql } from '#apollo/client';
export const IS_AUTHENTICATED = gql`
query IsAuthenticated {
isAuthenticated
}
`;
const useAuth = () => {
const { data } = useQuery(IS_AUTHENTICATED);
return {
isAuthenticated: data && data.isAuthenticated,
};
};
export default useAuth;
The problem is that I req which I check userId does not have a userId yet in the apollo context. So it is always false. So I am not sure what approach would be better there?
I don't really want to set the cookie as httpOnly: false and manipulate it on frontend, for example.

Is there a way to use ctx.session.$create in api using Blitz.js

I am trying to use blitz.js login API in a Flutter project. So I have created a /api/auth/login.ts file with the following code
import { getAntiCSRFToken, getSession, SecurePassword } from "#blitzjs/auth"
import { authenticateUser } from "app/auth/mutations/login"
import { AuthenticationError } from "blitz"
import db from "db"
import { Role } from "types"
const handler = async (req, res) => {
const session = await getSession(req, res)
const { email, password } = req.body
if (req.method !== "POST" || !req.body.data || !session.userId) {
res.status(401).json({ error: `Do not tamper with this route!` })
} else {
console.log("Create a new session for the user")
// Create a new session for the user
//login
const user = await authenticateUser(email, password)
const user = await db.user.findFirst({ where: { email } })
if (!user) return res.json({ data: "Hello", email, password })
const result = await SecurePassword.verify(user.hashedPassword, password)
const { hashedPassword, ...rest } = user
await req.session.$create({ userId: user.id, role: user.role as Role })
res.json({ rest })
}
export default handler
I also tried to use their docs but it was not clear enough and understandable
Can I use ctx.session.$create and insert it to db using blitz.js api
I have solved the problem using this code
import { Role } from "types"
import { authenticateUser } from "app/auth/mutations/login"
import { getSession } from "#blitzjs/auth"
export default async function customRoute(req, res) {
const session = await getSession(req, res)
const { email, password } = req.body
console.log(email, password)
console.log(session.$isAuthorized())
const user = await authenticateUser(email, password)
if (user.id === session.userId) {
return res.status(409).json({ error: `Already exist` })
}
await session.$create({ userId: user.id, role: user.role as Role })
// // res.setHeader("Content-Type", "application/json")
res.end(JSON.stringify({ userId: session.userId }))
}
At first, I was getting a CSRF mismatch error and then a localStorage is undefined and now somehow everything is working with this code.

Correct implementation of refreshtoken with ApolloClient

I am trying to refresh the authentication token when it is near the expiration time, however I end up with an endless loop. The expectation is that the code that checks the expiration time in App.js should work and reset the auth token and the refresh token, however is starts looping endlessly.
I have the following Auth helper functions:
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const { AuthenticationError } = require("apollo-server-express");
const config = require("config");
const jwtSecret = config.get("jwt_secret");
const jwtRefreshTokenSecret = config.get("jwt_refresh_token");
const authFunctions = {
checkSignedIn: async (req, requireAuth = true) => {
const header = req.headers.authorization;
if (header) {
const token = header.replace("Bearer ", "");
const decoded = jwt.verify(token, jwtSecret);
console.log(decoded);
let user = await User.findById(decoded.id);
if (!user) {
throw new AuthenticationError("Invalid user credentials.");
}
return user;
}
if (requireAuth) {
throw new AuthenticationError("You must be logged in.");
}
return null;
},
issueToken: async (user) => {
let token = "Bearer " + (await authFunctions.createToken(user));
let refreshToken = await authFunctions.createToken(user, 100);
return { token, refreshToken };
},
issueNewToken: async (req) => {
try {
const token = req.headers.refreshtoken;
if (token) {
const decoded = await jwt.verify(token, jwtRefreshTokenSecret);
let user = await User.findById(decoded.id);
console.log(user);
if (!user) {
throw new AuthenticationError("No user found.");
}
let tokens = await authFunctions.issueToken(user);
return { ...tokens, user };
}
} catch (err) {
throw new AuthenticationError("Invalid Refresh Token.");
}
},
createToken: async ({ id, address }, expiresIn = 60) => {
let secret = expiresIn === 60 ? jwtSecret : jwtRefreshTokenSecret;
return await jwt.sign({ id, address }, secret, { expiresIn });
},
};
module.exports = authFunctions;
The schema:
const { gql } = require("apollo-server");
const typeDefs = gql`
scalar Date
scalar MongoId
type User {
_id: MongoId!
address: String!
createdAt: Date
}
type Auth {
user: User!
token: String!
refreshToken: String!
}
input LoginInput {
address: String!
}
type Query {
refreshTokens: Auth!
}
type Mutation {
createOrGetUser(loginInput: LoginInput): Auth!
}
`;
module.exports = typeDefs;
The resolvers:
const User = require("../../models/User");
const {
issueToken,
issueNewToken,
checkSignedIn,
} = require("../../middleware/Auth");
const resolvers = {
Query: {
refreshTokens: async (root, args, { req }, info) =>
await issueNewToken(req),
},
Mutation: {
createOrGetUser: async (root, args, { req }, info) => {
try {
const existingUser = await User.findOne({
address: args.loginInput.address,
})
.populate({
path: "orders",
model: Order,
})
.exec();
if (existingUser) {
let tokens = await issueToken(existingUser);
return {
user: existingUser,
...tokens,
};
}
const user = await new User({
address: args.loginInput.address,
});
const result = await user.save();
let tokens = await issueToken(result);
return {
user,
...tokens,
};
} catch (err) {
throw err;
}
},
},
};
module.exports = resolvers;
And the App.js
import React, { Fragment } from "react";
import { BrowserRouter as Router, Route, Switch } from "react-router-dom";
import {
ApolloClient,
ApolloLink,
createHttpLink,
useReactiveVar,
concat,
} from "#apollo/client";
import { ApolloProvider } from "#apollo/client/react";
import { accessTokenVar, isLoggedInVar } from "./cache";
import cache from "./cache.js";
import jwtDecode from "jwt-decode";
// Styling
import "./styles/App.css";
// Components
import Routes from "./components/routing/Routes";
import Navbar from "./components/navbar/Navbar";
import { REFRESH_TOKENS } from "./queries/User";
const httpLink = createHttpLink({
uri: "/graphql",
});
const refreshTokens = () => {
return client.query({ query: REFRESH_TOKENS }).then((response) => {
console.log(response);
// Need to set the token to cache here, but the query doesn't get executed it seems
//accessTokenVar(response.data)
return;
});
};
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
let token = accessTokenVar();
if (token) {
token = token.replace("Bearer ", "");
const { exp } = jwtDecode(token);
console.log(exp);
// Refresh the token a minute early to avoid latency issues
const expirationTime = exp - 30;
if (Date.now() / 1000 >= expirationTime) {
refreshTokens(); // this keeps going on a loop
}
}
operation.setContext(({ headers = {} }) => ({
headers: {
...headers,
authorization: token ? token : "",
},
}));
return forward(operation);
});
const client = new ApolloClient({
cache,
link: concat(authMiddleware, httpLink),
connectToDevTools: true,
credentials: "include",
});
const App = () => {
const accessToken = useReactiveVar(accessTokenVar);
const isLoggedIn = useReactiveVar(isLoggedInVar);
//let isLoggedIn;
accessToken ? isLoggedInVar(true) : isLoggedInVar(false);
return (
<ApolloProvider client={client}>
<Router>
<Fragment>
<Navbar isLoggedIn={isLoggedIn} />
</Fragment>
</Router>
</ApolloProvider>
);
};
export default App;
Note: When createOrGetUser is executed the first time, the flow works. Then, I wait for 30 seconds and send a protected query, after which it gets in an endless loop.
Overall, I feel this flow is broken somehow, but I can't figure out what exactly. Would appreciate any help with this!

AWS Amplify and Next.JS with GraphQL Server Error No current user from getStaticPaths

I'm having trouble accessing data from Amplify's API Graphql, and it keeps returning
Server Error
Error: No current user
I've been following this tutorial: https://youtu.be/13nYLmjZ0Ys?t=2292
I know I'm signed into Amplify because if I go into different pages, I can grab user Auth and I can even display the SignOut button. But for whatever reason, I'm not sure why I'm getting this error
import { API } from "aws-amplify";
import { useRouter } from "next/router";
import { listActivations, getActivation } from "../../graphql/queries";
const Activation = ({ activation }) => {
const router = useRouter();
if (router.isFallback) {
return <div>Loading</div>;
}
return <div>{activation.title}</div>;
};
export default Activation;
export async function getStaticPaths() {
const SSR = withSSRContext();
console.log("static paths");
const activationData = await SSR.API.graphql({
query: listActivations,
});
console.log("activationData", activationData);
const paths = activationData.data.listActivations.items.map((activation) => ({
params: { id: activation.id },
}));
return {
paths,
fallback: true,
};
}
export async function getStaticProps({ params }) {
const SSR = withSSRContext(); // added SSR, but still getting error
console.log("static props");
const { id } = params;
const activationData = await SSR.API.graphql({
query: getActivation,
variables: { id },
});
return {
props: {
activation: activationData.data.getActivation,
},
};
}
The console log static paths appears, and then after that, I get errors.
Do you think it has anything to do with my GraphQL schema?
type User #model #auth(rules: [{ allow: owner, ownerField: "username" }]) {
id: ID!
username: String!
email: String!
userType: UserType
}
type Activation
#model
#key(
name: "activationsByStudentId"
fields: ["student"]
queryField: "activationsByStudentId"
)
#auth(
rules: [
{ allow: groups, groups: ["Admin"] }
{ allow: owner }
{
allow: owner
ownerField: "studentId"
operations: [create, update, delete]
}
{ allow: private, operations: [read] }
{ allow: public, operations: [read] }
]
) {
id: ID!
studentId: ID!
title: String!
student: Student #connection(fields: ["studentId"])
teachers: [TeachersActivations] #connection(name: "ActivationTeachers")
}
Edit: I've also added User model to see if this could be a cause too.
Since both getStaticProps and getStaticPaths are called during build time, and on the server when fallback is equal to true, you need to configure Amplify for SSR (Server-Side Rendering). Make sure to take a look at SSR Support for AWS Amplify JavaScript Libraries.
The solution: first, configure Amplify for SSR:
Amplify.configure({ ...awsExports, ssr: true });
Then you need to use withSSRContext, and add the the authMode parameter. As quoted from the link above:
For example, take an AppSync GraphQL API that is backed by an identity provider such as Amazon Cognito User pools, Okto, or Auth0. Some GraphQL types may require a user to be authenticated to perform certain requests. Using the API class, the user identity will now automatically be configured and passed into the API request headers:
const SSR = withSSRContext();
const activationData = await SSR.API.graphql({
query: listActivations,
authMode: "AMAZON_COGNITO_USER_POOLS"
});
Still, I couldn't figure out the issue why this can't work, so I decided to move my query into client-side
const [activation, setActivation] = useState(null);
const router = useRouter();
const { aid } = router.query;
useEffect(() => {
if (!aid) return;
async function activationDataFromClient() {
try {
const getActivationData = await API.graphql({
query: getActivation,
variables: {
id: aid,
},
});
setActivation(getActivationData.data.getActivation);
} catch (err) {
console.log("error fetching activation data: ", err);
}
}
activationDataFromClient();
}, [aid]);
I had the same problem. Changing the authMode to 'API_KEY' enabled it to work for me. See example below:
export async function getStaticPaths(context) {
const SSR = withSSRContext();
const { data } = await SSR.API.graphql({
query: listArticles,
authMode: 'API_KEY'
});
const paths = data.listArticles.items.map((article) => ({
params: { id: article.id },
}));
return {
paths,
fallback: true,
};
}
export async function getStaticProps({ params }) {
const SSR = withSSRContext();
const { data } = await SSR.API.graphql({
query: getArticle,
variables: {
id: params.id,
},
authMode: 'API_KEY'
});
return {
props: {
article: data.getArticle
}
}
}

Next Auth: Redirect page before success login

I want make a custom login use next Auth.
What I want is, after check my email and password true, I want redirect to google auth page and after google auth true I want set the token to session and redirect to home. Is it possible to make it like that?
//This is the flow
Login -> google Auth -> Home
This is login page
//Front End
const handler = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
setLoading(true);
const res = await signIn("credentials", {
redirect: false,
data: emailRef.current?.value,
password: passwordRef.current?.value,
});
if (res?.error) {
toast.error(res?.error);
setLoading(false);
} else {
Router.replace("/");
Router.events.on("routeChangeComplete", success);
}
};
This is the API Page
//API
export default NextAuth({
session: {
jwt: true,
},
providers: [
Providers.Credentials({
async authorize(credentials: credentialsData, req: NextApiRequest) {
let client;
try {
client = await ConnectDatabase();
} catch (error) {
throw new Error("Failed connet to database.");
}
const checkEmail = await client
.db()
.collection("users")
.findOne({ email: credentials.data });
const checkPhone = await client
.db()
.collection("users")
.findOne({ phone: credentials.data });
let validData = {
password: "",
email: "",
};
if (!checkEmail && !checkPhone) {
client.close();
throw new Error("Email atau No HP tidak terdaftar.");
} else if (checkEmail) {
validData = checkEmail;
} else if (checkPhone) {
validData = checkPhone;
}
const checkPassword = await VertifyPassword(
credentials.password,
validData.password
);
if (!checkPassword) {
client.close();
throw new Error("Password Salah.");
}
client.close();
return validData;
},
}),
],
callbacks: {
async session(session) {
const data = await getSelectedUser(session.user!.email!);
session.user = data.userData;
return Promise.resolve(session);
},
},
});
In above Example I don't use google auth, so after login success it immediately redirects to home

Resources