NextAuth Callbacks Not Running - next.js

I'm designing an application with Next.js and using NextAuth for authentication(using Google OAuth). In order to use other Google APIs once authenticated, I want to persist the accessToken. The accessToken gets set in the session() callback. However, it seems like the callback never runs. Could someone help me out with this? Thanks!
Here's my [...nextauth].js file
import GoogleProvider from "next-auth/providers/google"
import NextAuth from "next-auth/next"
export default NextAuth(
{
// Configure one or more authentication providers
providers: [
GoogleProvider({
clientId: PROCESS.ENV.GOOGLE_CLIENT_ID',
clientSecret: PROCESS.ENV.GOOGLE_CLIENT_SECRET,
authorization: {
params: {
prompt: "consent",
access_type: "offline",
response_type: "code",
scope: 'openid email profile https://www.googleapis.com/auth/calendar'
},
},
callbacks: {
async session({ session, token, user }) {
session.user.id = token.id;
session.accessToken = token.accessToken;
// Not printed
console.log('In here');
return session;
},
async jwt({ token, user, account, profile, isNewUser }) {
console.log(token);
if (user) {
token.id = user.id;
}
if (account) {
token.accessToken = account?.access_token;
}
return token;
},
},
}),
// ...add more providers here
],
sercet: PROCESS.ENV.JWT_SECRET,
session: {
strategy: "jwt",
},
}
)
Here's my login component:
import React, { useState } from 'react';
import {useSession, signIn, signOut} from 'next-auth/react';
import axios from 'axios';
const Login = () => {
const x = useSession();
const {data: session} = x
const [calendar, setCalendar] = useState({});
const getCalendarData = async () => {
console.log(session.accessToken);
console.log(x);
const options = {
method: 'GET',
headers: {
Authorization: `Bearer ${session.accessToken}`,
}
};
const url = "https://www.googleapis.com/calendar/v3/calendars/primary";
const data = null
try{
data = await axios.get(url, options);
setCalendar(data);
} catch(error){
console.log(error);
}
}
if(session){
return (
<div>
<div> Welcome, {JSON.stringify(session.user)} </div>
<div>{JSON.stringify(calendar)}</div>
<div><button onClick={() => signOut()}>Sign Out</button></div>
<div><button onClick={async () => await getCalendarData()}>Get Calendar Data</button></div>
</div>
);
}
else{
return(
<div>
<div> You are not signed in </div>
<div><button onClick={() =>signIn()}> Sign in</button></div>
</div>
)
}
}

Related

Next-Auth who to deal with backend access token

I am using Django and Next.js (Version 13 with the app dir enabled). Now I have two questions:
What is the best practice to deal with the access token I receive after I do the authorize call to the django backend? Is it correct how I put it into the callbacks?
export const authOptions = {
secret: process.env.NEXTAUTH_SECRET,
providers: [
CredentialsProvider({
name: 'Django',
credentials: {
username: { label: "Username", type: "text", placeholder: "mail#domain.com" },
password: { label: "Password", type: "password" }
},
async authorize(credentials, req) {
// Do access call
const resToken = await fetch(process.env.AUTH_ENDPOINT, {
method: 'POST',
body: JSON.stringify(credentials),
headers: { "Content-Type": "application/json" }
})
const jwt_token = await resToken.json()
// fetching user data
const resUser = await fetch(`${process.env.BACKEND_URL}/auth/users/me/`, {
method: 'GET',
headers: { "Content-Type": "application/json",
"Authorization": `JWT ${jwt_token.access}` }
})
const user = await resUser.json()
if (resUser.ok && jwt_token.access) {
user.access_token = jwt_token.access
user.refresh_token = jwt_token.refresh
return user
}
// Return null if user data could not be retrieved
return null
}
})
],
session: {
strategy: "jwt",
},
jwt: { encryption: true, },
callbacks: {
async jwt({ token, user }) {
if (user) {
token.access_token = user.access_token
token.refresh_token = user.refresh_token
console.log("if executed")
}
return token
},
async session({ session, token, user }) {
if (!session) {
session.access_token = user.access_token
session.refresh_token = user.refresh_token
session.user = user
}return session;
},
}
}
export default NextAuth(authOptions)
I have the provider wrapped in the provider.js file as shown below. Now I was wondering if I need to passt the session as <SessionProvider session={session}> in the code below? And if yes - could you tell me how?
'use client'
import { SessionProvider } from 'next-auth/react'
export function Providers({ children }) {
return (
<SessionProvider>
{children}
</SessionProvider>
);
}
Thank you!

Refactoring reducers and actions in a feature based architecture style

This is a purely hypothetical example while I am trying to learn NextJs and I added Redux following the documentation from the official link
but I want to format the code in a certain way, similar to feature based architecture where each feature is encapsulated in a separate folder.
Please feel free to add, remove and suggest any changes, they are all most welcoming.
This is my general folder structure
components
public
pages
|-accounts
| |-index.tsx
|-_app.tsx
|-index.tsx
store
|-account
| |-accountsApi.ts
| |-actions.ts
| |-index.ts
| |-reducer.ts
| |-types.ts
|-index.ts
styles
package.json
...
And this is the Accounts component under accounts folder at pages.
I am dispatching the login action from action creators, is there any way to make the action accept an object with the credentials?
//pages/account/index.tsx
import Link from 'next/link'
import { useState } from 'react'
import Layout from '../../components/Layout'
import { useDispatch } from "react-redux"
import type { Account } from '../../store/account/types'
import { accoutActions } from '../../store/account'
type Props = {
accounts: Account[]
}
const Accounts: React.FunctionComponent<Props> = ({ accounts }) => {
const dispatch = useDispatch();
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = (event: any) => {
let obj = {
username: username,
password: password
}
dispatch(accoutActions.login(obj)); // how to set it up, so that the action accepts an object with credentials
}
return (
<Layout title="Accounts | Demo">
<h1>Accounts</h1>
<p>You are currently on: /accounts</p>
<p>
<Link href="/">
<a>Go home</a>
</Link>
</p>
<div>
<p>Login</p>
<div>
<label htmlFor="username">Username:</label>
<input onChange={e => setUsername(e.target.value)} type="text" id="username" name="username" value={username} />
<label htmlFor="password">Last name:</label>
<input onChange={e => setPassword(e.target.value)} type="text" id="password" name="password" value={password} />
<button type="submit" onClick={handleSubmit}>Login</button>
</div>
</div>
</Layout>
)
}
export default Accounts
Below is my account store with its respective files:
accountsAPI.ts where the api calls to the server are made, I will try to refactor later in a singleton pattern using axios
however I am learning what NextJs allows and what not.
import { CreateAccountInputModel, LoginInputModel } from "./types"
const url = 'https://127.0.0.1:8888/api/accounts'
export const createAccount = async (objModel: CreateAccountInputModel): Promise<any> => {
const response = await fetch(`${url}/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(objModel),
})
const result = await response.json()
return result
}
export const loginAccount = async(objModel: LoginInputModel): Promise<any> => {
const response = await fetch(`${url}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(objModel),
})
const result = await response.json()
return result
}
export const logoutAccount = async () : Promise<any> => {
const response = await fetch(`${url}/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
})
const result = await response.json()
return result
}
actions.ts contains account actions
import { createAction } from '#reduxjs/toolkit'
import { AccountActionTypes } from './types'
const create = createAction(AccountActionTypes.CREATE)
const login = createAction(AccountActionTypes.LOGIN)
const logout = createAction(AccountActionTypes.LOGOUT)
export const actionCreators = {
create,
login,
logout,
};
index.ts where I combine reducer from the reducer file and make a slice so that I can join all reducers to the store
I want to be able the reducers in a separate file
import { createAsyncThunk, createSlice } from '#reduxjs/toolkit'
import { CreateAccountInputModel } from './types'
import type { AppState, AppThunk } from '..'
import { createAccount, loginAccount, logoutAccount } from './accountsAPI'
import { reducer, initialState } from './reducer'
export const createAsync = createAsyncThunk(
'account/create',
async (objModel: CreateAccountInputModel) => {
const response = await createAccount(objModel)
// The value we return becomes the `fulfilled` action payload
return response.data
}
)
export const accountSlice = createSlice({
name: 'account',
initialState,
reducers: reducer, // error: Type 'ReducerWithInitialState<AccountState>' is not assignable to type 'ValidateSliceCaseReducers<AccountState, SliceCaseReducers<AccountState>>'.
extraReducers: (builder) => {
builder
.addCase(createAsync.pending, (state) => {
state.status = 'loading'
})
.addCase(createAsync.fulfilled, (state, action) => {
state.status = 'idle'
})
},
})
export { actionCreators as accoutActions } from './actions';
export const isAuthenticated = (state: AppState) => state.account.isAuthenticated
export default accountSlice.reducer
Here are reducers reducer.ts and the initial state of the given reducer
import { createReducer, PayloadAction } from '#reduxjs/toolkit'
import { actionCreators } from './actions';
import { Account, AccountState } from './types';
export const initialState: AccountState = {
isAuthenticated: false,
token: '',
status: 'idle',
accounts: [] as Account[],
}
export const reducer = createReducer(initialState, (builder) => {
builder
.addCase(actionCreators.login, (state, action: PayloadAction<any>) => {
state.isAuthenticated = action.payload.isAuthenticated
state.token = action.payload.token
// can I do, return { ...state, ...} so that the previous state is preserved?
})
.addCase(actionCreators.logout, (state, action) => {
state.isAuthenticated = false
})
.addCase(actionCreators.create, (state, action: PayloadAction<any>) => {
state.accounts = [...state.accounts, action.payload]
}).addDefaultCase((state, action) => {
state.isAuthenticated = false
state.token = ''
})
})
All the types and models are stored in types.ts file.
I can write types.ts if it is required, but you can assume a general case of an account.
is there any way to make the action accept an object with the credentials?
const login = createAction<TypeOfObjectWithCredentials>(AccountActionTypes.LOGIN)
Calling login with an object will put it inside the payload automatically.

How to pass query params to a redirect in NextJS

I redirect users to the login page, when they try to access a page without authentication.
I then wanna show them a Message.
But I am not able to pass parameters in the redirect. What causes the issue and how to do it properly?
// PAGE NEEDS AUTH / REDIRECT TO LOGIN WITH MESSAGE
// application
import { GetServerSideProps } from 'next';
import SitePageProducts from '../../components/site/SitePageProducts';
import axios from 'axios';
import { getSession } from 'next-auth/react';
import url from '../../services/url';
import { ProductFields } from '../../lib/ebTypes';
function Page() {
return <SitePageProducts />;
}
export default Page;
export const getServerSideProps: GetServerSideProps = async (context) => {
const session = await getSession(context)
if (session) {
const products = await axios.get(`${process.env.NEXT_PUBLIC_API_URL}/products`, {
}).then(res => {
console.log('res :>> ', res);
return res.data.products as ProductFields[]
}).catch(err => console.log(err));
console.log('products :>> ', products);
return {
props: {
loading: true,
token: session.user.token,
}
}
} else {
return {
redirect: {
permanent: false,
destination: url.accountSignIn().href,
props: { test: "Message from inside Redirect" }
},
props: {
params: { message: "Message from inside props" },
query: {
message: 'Message from inside props query'
},
message: 'Message from inside props root'
},
};
}
}
// LOGIN PAGE, SHOULD CONSUME AND SHOW MESSAGE WHY LOGIN IS NEEDED
import { GetServerSideProps } from 'next';
import AccountPageLogin from '../../components/account/AccountPageLogin';
import url from '../../services/url';
import { getSession } from "next-auth/react"
function Page(props: any) {
return <AccountPageLogin {...props} />;
}
export default Page;
export const getServerSideProps: GetServerSideProps = async (ctx) => {
// ALL CTX queries / props are empty?????
// CURRENT: query:{} --- EXPECTING: query: {message: "MY MESSAGE"}
console.log('ctx accountpagelogin::: :>> ', ctx);
const session = await getSession(ctx)
if (session) {
return {
redirect: {
destination: url.accountDashboard().href,
permanent: false,
},
};
}
return {
props: {},
};
};

Next auth credentials

I'm trying to do a credentials auth with next-auth. I have to use a custom sign-in page and I absolutely can't make it work for approximately one entire week.
I have :
// [...nextauth.js]
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import axios from '#api/axios';
const options = {
providers: [
Providers.Credentials({
async authorize(credentials) {
const { data: user, status } = await axios.post('/users/authentication', credentials);
if (user && status === 200) {
return user;
} else {
throw new Error('error message');
}
}
})
],
pages: {
signIn: '/profil/authentication/login',
error: '/profil/authentication/login'
},
session: {
jwt: true,
maxAge: 30 * 24 * 60 * 60 // 30 days
},
debug: true
};
export default (req, res) => NextAuth(req, res, options);
and :
// profil/authentication/login
import { signOut, useSession } from 'next-auth/client';
import AuthenticationForm from '#components/auth/authenticationForm';
import Layout from '#components/layout';
const Login = () => {
const [session] = useSession();
const intl = useIntl();
return (
<Layout>
{!session && <AuthenticationForm />}
{session && (
<>
Signed in as {session.user.email}
<br />
<button onClick={signOut}>Sign out</button>
</>
)}
</Layout>
);
};
export default Login;
// authenticationForm.js
import { signIn, csrfToken } from 'next-auth/client';
import { useRouter } from 'next/router';
import { useEffect, useState } from 'react';
import PasswordInput from '#components/auth/passwordInput';
import Button from '#components/form/button';
import TextInput from '#components/form/textInput';
const AuthenticationForm = ({ csrf }) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
const [error, setError] = useState('');
const router = useRouter();
const handleChangeUsername = ({ target: { value } }) => setUsername(value);
const handleChangePassword = ({ target: { value } }) => setPassword(value);
const handleLogin = () => {
signIn('credentials', {
username,
password,
callbackUrl: `${window.location.origin}/profil`
})
.then((res) => {
console.log('form::res -> ', res);
router.back();
})
.catch((e) => {
console.log('form::e -> ', e);
setError('login error');
});
};
useEffect(() => {
if (router.query.error) {
setError(router.query.error);
setUsername(router.query.username);
}
}, [router]);
return (
<form onSubmit={handleLogin}>
<TextInput
name="username"
value={username}
onChange={handleChangeUsername}
/>
<PasswordInput handleChange={handleChangePassword} />
{error && <div>{error}</div>}
<Button type="submit">
connexion
</Button>
<input name="csrfToken" type="hidden" defaultValue={csrf} />
</form>
);
};
AuthenticationForm.getInitialProps = async (context) => {
return {
csrf: await csrfToken(context)
};
};
export default AuthenticationForm;
And for sure a NEXTAUTH_URL=http://localhost:3000 in .env.local.
If I go on /profil/authentication/login, I see my form and when I click connect, I always some errors like : "Failed to fetch", nothing more, or :
[next-auth][error][client_fetch_error] (2) ["/api/auth/csrf", TypeError: Failed to fetch]
https://next-auth.js.org/errors#client_fetch_error
Even if I try to delete all the csrf handling in my form and let sign-in "do it alone yea".
I'm really stuck with this lib and I most likely will change for another one but I would like to know what am I doing wrong? Is there a FULL example with custom sign-in page and errors handled on the same sign-in page. This is so basic that I can't understand why I don't find one easily.
#Tralgar
I think that problem is related to CSRF policy on your backend, if you are on localhost then localhost:3000 and localhost:2000 is like two different domains. Just make sure if you have your frontend domain in your backend cors policy (if on localhost it must be with a port)
I was able to fix the error by deleting the .next build folder and creating a new build by running npm run build

How can I set the user object from the firebase auth request to my redux state?

I have a splashscreen in which appears the logo of my app and the title, and I give it a few seconds before doing anything with a setTimeout. Whithin that time I'm trying to check if the user already logged in before in the app or it's new by using this function that firebase has firebase.auth().onAuthStateChanged() which should return an user object with the information about the logged user, or nothing if the user didn't logged in. It detects that I was logged in, for that case I want my app to redirect to the main scene, which was working fine, but now I want to have that user's information in the main scene as well so I tried using this.props.user = user;, but my compiler gave me an error saying this:
TypeError: TypeError: TypeError: null is not an object (evaluating 'elements.props')
What can I do? I thought I could access to my props from componentDidMount function.
The code of my SplashScreen.js
import React, { Component } from 'react';
import { View, Image } from 'react-native';
import TypeWriter from 'react-native-typewriter';
import firebase from 'firebase';
import { Actions } from 'react-native-router-flux';
import { connect } from 'react-redux';
import { reloginUser } from '../actions';
class SplashScreen extends Component {
componentDidMount() {
// Your web app's Firebase configuration
const firebaseConfig = {
apiKey: "AIzaSyDSV1Ozhe4M6p9Q9MdUGSCAqhW53DyUNYo",
authDomain: "idionative.firebaseapp.com",
databaseURL: "https://idionative.firebaseio.com",
projectId: "idionative",
storageBucket: "idionative.appspot.com",
messagingSenderId: "924806570373",
appId: "1:924806570373:web:a3f5b6dc190d8039"
};
// Initialize Firebase
firebase.initializeApp(firebaseConfig);
setTimeout(function() {
firebase.auth().onAuthStateChanged((user) => {
if (user) { // Here I check whether the user was logged in or not
this.props.user = user;
Actions.main();
} else {
Actions.auth();
}
});
}, 2000);
}
render() {
const { sceneStyle, logoStyle, titleStyle } = styles;
return(
<View style={sceneStyle}>
<Image source={require('../assets/images/logo.png')} style={logoStyle} />
<TypeWriter typing={1} style={titleStyle}>Idionative{this.props.user}</TypeWriter>
</View>
);
}
}
const styles = {
sceneStyle: {
backgroundColor: '#9ED0E6',
flex: 1,
justifyContent: 'center',
alignItems: 'center'
},
logoStyle: {
width: 120,
height: 120
},
titleStyle: {
fontFamily: 'NotoSans-Regular',
fontSize: 50,
color: 'black',
marginTop: 20
}
};
const mapStateToProps = ({ auth }) => {
const { user } = auth;
return { user };
};
export default connect(mapStateToProps, { reloginUser })(SplashScreen);
The code of my actions file:
import firebase from 'firebase';
import { Actions } from 'react-native-router-flux';
import { EMAIL_CHANGED, PASSWORD_CHANGED, LOGIN_USER_SUCCESS, LOGIN_USER_FAIL, LOGIN_USER, RELOGIN_USER } from './types';
export const emailChanged = (text) => {
return {
type: EMAIL_CHANGED,
payload: text
};
};
export const passwordChanged = (text) => {
return {
type: PASSWORD_CHANGED,
payload: text
};
};
export const loginUser = ({ email, password }) => {
return (dispatch) => {
dispatch({ type: LOGIN_USER });
firebase.auth().signInWithEmailAndPassword(email, password)
.then(user => loginUserSuccess(dispatch, user))
.catch(() => loginUserFail(dispatch));
}
}
export const reloginUser = (user) => {
return {
type: RELOGIN_USER,
payload: user
};
};
const loginUserSuccess = (dispatch, user) => {
dispatch({
type: LOGIN_USER_SUCCESS,
payload: user
});
Actions.main();
};
const loginUserFail = (dispatch) => {
dispatch({
type: LOGIN_USER_FAIL
});
};
My reducer file:
import { EMAIL_CHANGED, PASSWORD_CHANGED, LOGIN_USER_SUCCESS, LOGIN_USER_FAIL, LOGIN_USER, RELOGIN_USER } from '../actions/types';
const INITIAL_STATE = { email: '', password: '', user: null, error: '', loading: false };
export default (state = INITIAL_STATE, action) => {
switch (action.type) {
case EMAIL_CHANGED:
return { ...state, email: action.payload };
case PASSWORD_CHANGED:
return { ...state, password: action.payload };
case LOGIN_USER:
return { ...state, loading: true, error: '' }
case LOGIN_USER_SUCCESS:
return { ...state, ...INITIAL_STATE, user: action.payload, };
case LOGIN_USER_FAIL:
return { ...state, error: 'This User Does Not Exist', password: '', loading: false }
case RELOGIN_USER:
return { ...state, user: action.payload }
default:
return state;
}
};

Resources