Nextjs redirect in _app.js 'The page isn’t redirecting properly' - node.js

I am building out the user auth portion of a nextjs app. I have most of it completed save for the protected routes. I am attempting to handle the auth portion in _app.js. Here is the file:
import App from 'next/app'
import '../styles/globals.css'
import { parseCookies } from 'nookies'
import { redirectUser } from '../utils/auth'
import jwt from 'jsonwebtoken'
class MyApp extends App {
static async getInitialProps({ Component, ctx }) {
const { token } = parseCookies(ctx)
let pageProps = {}
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx)
}
if (!token) {
const isProtectedRoute = ctx.pathname === '/account'
if (isProtectedRoute) {
redirectUser(ctx, '/login')
}
} else {
try {
const verified = jwt.verify(token, process.env.JWT_SECRET)
} catch(err) {
console.error(err)
redirectUser(ctx, '/login')
}
}
return { pageProps }
}
render() {
const { Component, pageProps } = this.props;
return (
<Component {...pageProps} />
)
}
}
export default MyApp
And my redirectUser file:
import cookie from 'js-cookie';
import Router from 'next/router';
export function handleLogin(token) {
cookie.set('token', token)
Router.push('/account');
}
export function redirectUser(ctx, location) {
if (ctx.req) {
ctx.res.writeHead(302, { Location: location })
ctx.res.end()
} else {
Router.push(location)
}
}
The auth portion correctly redirects when a user doesn't have a token but if that token is altered to be invalid I get a redirect error:
The page isn’t redirecting properly
Firefox has detected that the server is redirecting the request for this address in a way that will never complete.
I've confirmed that it is the second redirectUser call that is causing this error. Even if I place it outside of the else conditional at the bottom of the getInitialProps function I get the same result.

You're getting that error because you end up triggering an infinite redirect cycle to the /login page.
Landing on any page with an invalid token will redirect to the /login page. When this redirect happens it will trigger another App.getInitialProps call which will end up causing yet another redirect to the /login page since the token is still invalid, and so on.
You could prevent this behaviour by adding checks when the current page is the /login page. This can be handled in several ways, but I'll leave an example here.
if (!token) {
const isProtectedRoute = ctx.pathname === '/account'
if (isProtectedRoute) {
redirectUser(ctx, '/login')
}
} else if (ctx.pathname !== "/login") { // To avoid redirect loop
try {
const verified = jwt.verify(token, process.env.JWT_SECRET)
} catch(err) {
console.error(err)
redirectUser(ctx, '/login')
}
}

Related

Im trying to implement authentication in next.js using next-auth and next.js middleware, but im getting an error when using it in every route

Im trying to implement auth logic using this example but I am trying to implement it in every route like so
export const config = {
matcher: ['/:path*'],
};
but my browser breaks because too many requests, but when I change the matcher array to specific routes, it works, like this:
matcher: ['/teacher','/student'],
with the matcher like this, auth works for the pages (student,teacher) but not other pages.
middleware.ts:
import { NextRequest, NextResponse } from 'next/server';
import { withAuth } from 'next-auth/middleware';
const PUBLIC_FILE = /\.(.*)$/;
export default withAuth(
async function middleware(req: NextRequest) {
if (
req.nextUrl.pathname.startsWith('/_next') ||
req.nextUrl.pathname.includes('/api/') ||
PUBLIC_FILE.test(req.nextUrl.pathname)
) {
return;
}
if (req.nextUrl.locale === 'default') {
const locale = req.cookies.get('NEXT_LOCALE') || 'en';
return NextResponse.rewrite(
new URL(
`/${locale}${req.nextUrl.pathname}${req.nextUrl.search}`,
req.url,
),
);
}
},
{
callbacks: {
authorized({ req, token }) {
return !!token;
},
},
},
);
export const config = {
matcher: ['/:path*', '/teacher'],
};

React role based authentication if the two users are from two different tables

I'm new to React/Node and working on a learning project. It's a platform that connects users (freelancers) with nonprofit companies. I would like users to sign up and login as A) user or B) company. I can't figure out how to do this, and all the guides I found are for when your users are all coming from the same table, but with different auth levels (eg. user, admin, etc..).
In my case, it's different. users and companies are two different resources. A user can view /companies and click a button to connect to that company. A user can view a page that lists all their connections. Likewise, a company can login and view a page that lists all the users that connected with them.
Right now, the backend is working successfully. Both users/companies can signup/login, and you get a token back as expected (tested in Insomnia). I'm using JSON Web Tokens.
On the frontend, users can signup, login, make connections, and view their connections successfully. Now I just want companies to do the same, but have no idea how. I made an attempt at doing it, but when a company tries to login, they're directed to the homepage and they're not logged in. No error messages show up.
Not sure what code to post, but I will keep this concise. This is all the relevant code (shortened). I would appreciate any help, or pointers.
schema
CREATE TABLE companies (
company_handle VARCHAR(25) PRIMARY KEY,
password TEXT NOT NULL,
company_name TEXT NOT NULL
role TEXT DEFAULT 'company'
);
CREATE TABLE users (
username VARCHAR(25) PRIMARY KEY,
password TEXT NOT NULL,
role TEXT DEFAULT 'user'
);
CREATE TABLE connections (
username VARCHAR(25)
REFERENCES users ON DELETE CASCADE,
company_handle VARCHAR(25)
REFERENCES companies ON DELETE CASCADE,
PRIMARY KEY (username, company_handle)
);
Frontend
App.js
function App() {
const [currentUser, setCurrentUser] = useState(null);
const [currentCompany, setCurrentCompany] = useState(null);
const [token, setToken] = useLocalStorage(TOKEN_LOCAL_STORAGE_ID);
const [connectionHandles, setConnectionHandles] = useState([]);
// Load user info from the API
useEffect(function loadUserInfo() {
async function getCurrentUser() {
if (token) {
try {
let { username } = jwt.decode(token);
let { companyHandle } = jwt.decode(token);
VolunteerApi.token = token;
if (username) {
let currentUser = await VolunteerApi.getCurrentUser(username);
setCurrentUser(currentUser);
}
if (companyHandle) {
let currentCompany = await VolunteerApi.getCurrentCompany(companyHandle);
setCurrentCompany(currentCompany);
}
} catch (err) {
console.error("Problem with the loadUserInfo function", err);
setCurrentUser(null);
setCurrentCompany(null);
}
}
}
getCurrentUser();
}, [token]);
// Login user function
async function loginUser(loginData) {
try {
let token = await VolunteerApi.loginUser(loginData);
setToken(token);
return {
success: true
};
} catch (err) {
console.error("Problem with the login function", err);
return {
success: false, err
};
}
}
// Login company function
async function loginCompany(loginData) {
try {
let token = await VolunteerApi.loginCompany(loginData);
setToken(token);
return {
success: true
};
} catch (err) {
console.error("Problem with the login function", err);
return {
success: false, err
};
}
}
return (
<BrowserRouter>
<UserContext.Provider value={{ connectionHandles, setConnectionHandles, currentUser, setCurrentUser, currentCompany, setCurrentCompany }}>
<div>
<Navigation />
<Routes loginUser={loginUser} loginCompany={loginCompany} />
</div>
</UserContext.Provider>
</BrowserRouter>
);
}
api.js
class VolunteerApi {
static token;
static async request(endpoint, data = {}, method = "get") {
console.debug("API Call:", endpoint, data, method);
const url = `${BASE_URL}/${endpoint}`;
const headers = { Authorization: `Bearer ${VolunteerApi.token}` };
const params = (method === "get")
? data
: {};
try {
return (await axios({ url, method, data, params, headers })).data;
} catch (err) {
console.error("API Error:", err.response);
let message = err.response.data.error.message;
throw Array.isArray(message) ? message : [message];
}
}
// Login company
static async loginCompany(data) {
let res = await this.request(`auth/login-company`, data, "post");
return res.token;
}
// Login user
static async loginUser(data) {
let res = await this.request(`auth/login-user`, data, "post");
return res.token;
}
}
Backend
auth.js
router.post("/login-company", async function (req, res, next) {
try {
const { companyHandle, password } = req.body;
const company = await Company.authenticate(companyHandle, password);
const token = createToken(company);
return res.json({ token });
} catch (err) {
return next(err);
}
});
router.post("/login-user", async function (req, res, next) {
try {
const { username, password } = req.body;
const user = await User.authenticate(username, password);
const token = createToken(user);
return res.json({ token });
} catch (err) {
return next(err);
}
});
token.js
function createToken(user) {
console.assert(undefined,
"createToken passed user with an undefined user");
let payload = {
username: user.username,
companyHandle: user.companyHandle
};
return jwt.sign(payload, SECRET_KEY);
}
If I understand correctly what you wish to achieve is that your same app can be viewed with 2 different perspectives (User view or Company view) using who logged in as your flag to show the correct data. Having different roles for the same page can be tricky but thankfully there are a number of ways to achieve this.
What I recommend as the simplest approach would be conditional rendering.
When someone logs in as a user or a company you can save that detail to the browsers local storage using localStorage.setItem("UserType", "Example"); and you can get this information using localStorage.getItem("UserType");
Then when the user or company is in your page using that detail you can render the right elements like so:
{condition == true && (<> <Module/> </>)}
Now since we are using react we can import whole js files as modules. so you can have something that looks like this:
import UserPage from 'somewhere/User.js'
import CompanyPage from 'somewhere/Company.js'
function MainApp() {
const userOrCompany = localStorage.getItem("UserType")
return(
<>
{userOrCompany === 'User' && (<> <UserPage/> </>)}
{userOrCompany === 'Company' && (<> <CompanyPage/> </>)}
</>
);
}
export default MainApp;
Also, I recommend handling tokens from the backend for security reasons. That way you can condition your backend data to needing a token before returning anything :D

Nestjs Interceptor how to catch http 401 error and resubmit original request

I need to write an http header interceptor to add Authorization header, if there is a 401 error, submit another request for a new token, then resubmit the original request with the new token.
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
return next.handle().pipe(
catchError(async error => {
if (error.response.status === 401) {
const originalRequest = error.config;
var authRes = await this.authenticationService.getAccessToken();
this.authenticationService.accessTokenSubject.next(authRes.access_token);
// I need to resubmit the original request with the new token from here
// but return next.handle(originalRequest) doesn't work
}
return throwError(error);
}),
);
}
But next.handle(originalRequest) doesn't work. How to resubmit the original request in the interceptor? Thank you very much in advance for your help.
I just encountered a similar problem, where I can catch the exception from exception filter but can't do so in interception layer.
So I looked up the manual and found it says:
Any exception thrown by a guard will be handled by the exceptions layer
(global exceptions filter and any exceptions filters that are applied to the current context).
So, if the exception is thrown from AuthGuard context(including the validate method in your AuthService), probably better to move the additional logic by extending the Authguard
like this:
export class CustomizedAuthGuard extends AuthGuard('strategy') {
handleRequest(err, user, info, context, status) {
if (err || !user) {
// your logic here
throw err || new UnauthorizedException();
}
return user;
}
}
or simply using customized exception filter.
It's been a while since the question but maybe it will help someone.
Ok, suppose that we need handle unauthorize exception out of route and guards, maybe service to service. So you can implement a interceptor like that and add some logic to get some data if needed, Ex: inject some Service in the interceptor.
So, throw an unauthorize exception and we are going to intercept it:
#Injectable()
export class UnauthorizedInterceptor implements NestInterceptor {
constructor(
private readonly authService: AuthService,
private readonly httpService: HttpService,
) {}
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
catchError((err) => {
const {
response: { status, config },
} = err;
// assuming we have a request body
const jsonData = JSON.parse(config.data);
if (status === HttpStatus.UNAUTHORIZED) {
// We can use some data in payload to find user data
// here for example the user email
if (jsonData?.email) {
return
from(this.authService.getByUserEmail(jsonData.email)).pipe(
switchMap((user: User) => {
if (user) {
// Ex: we can have stored token info in user entity.
// call function to refresh access token and update user data
// with new tokens
return from(this.authService.refreshToken(user)).pipe(
switchMap((updatedUser: User) => {
// now updatedUser have the new accessToken
const { accessToken } = updatedUser;
// set the new token to config (original request)
config.headers['Authorization'] = `Bearer ${accessToken}`;
// and use the underlying Axios instance created by #nestjs/axios
// to resubmit the original request
return of(this.httpService.axiosRef(config));
}),
);
}
}),
);
} else {
return throwError(() => new HttpException(err, Number(err.code)));
}
} else {
return throwError(() => new HttpException(err, Number(err.code)));
}
}),
);
}
}

MetaMask Web3 ethereum not defined

I know this issue exists already and people have posted before but I can't get this working so sorry for asking this.
I am using Heroku to build and deploy, this is not being done locally.
I am trying to get MetaMask to get recognized in my Dapp and I am using the code generated by MetaMask to fix their privacy mode breaking change but I cannot get past 'web3' 'Web3' and 'ethereum' undefined compile error. I don't understand where it needs to go within my app. Any assistance would be greatly appreciated. Beyond appreciated.
Here is my app.js:
import React, { Component } from 'react'
import './App.css'
import Navbar from './Navbar'
import Content from './Content'
import { connect } from 'react-redux'
import {
loadWeb3,
loadAccount,
loadToken,
loadExchange
} from '../store/interactions'
import { contractsLoadedSelector } from '../store/selectors'
window.addEventListener('load', async () => {
// Modern dapp browsers...
if (window.ethereum) {
window.web3 = new Web3(ethereum);
try {
// Request account access if needed
await ethereum.enable();
// Acccounts now exposed
web3.eth.sendTransaction({/* ... */});
} catch (error) {
// User denied account access...
}
}
// Legacy dapp browsers...
else if (window.web3) {
window.web3 = new Web3(web3.currentProvider);
// Acccounts always exposed
web3.eth.sendTransaction({/* ... */});
}
// Non-dapp browsers...
else {
console.log('Non-Ethereum browser detected. You should consider trying MetaMask!');
}
});
class App extends Component {
componentWillMount() {
this.loadBlockchainData(this.props.dispatch)
}
async loadBlockchainData(dispatch) {
const web3 = loadWeb3(dispatch)
await web3.eth.net.getNetworkType()
const networkId = await web3.eth.net.getId()
await loadAccount(web3, dispatch)
const token = await loadToken(web3, networkId, dispatch)
if(!token) {
window.alert('Token smart contract not detected on the current network. Please select another network with Metamask.')
return
}
const exchange = await loadExchange(web3, networkId, dispatch)
if(!exchange) {
window.alert('Exchange smart contract not detected on the current network. Please select another network with Metamask.')
return
}
}
render() {
return (
<div>
<Navbar />
{ this.props.contractsLoaded ? <Content /> : <div className="content"></div> }
</div>
);
}
}
export default connect(mapStateToProps)(App)
});
As of January 2021, Metmask has removed their injected window.web3
If you want to connect your dApp to Metamask, I'd try the following
export const connectWallet = async () => {
if (window.ethereum) { //check if Metamask is installed
try {
const address = await window.ethereum.enable(); //connect Metamask
const obj = {
connectedStatus: true,
status: "",
address: address
}
return obj;
} catch (error) {
return {
connectedStatus: false,
status: "🦊 Connect to Metamask using the button on the top right."
}
}
} else {
return {
connectedStatus: false,
status: "🦊 You must install Metamask into your browser: https://metamask.io/download.html"
}
}
};
If you'd like to learn how to also sign transactions with Metamask, I'd recommend you check out this super beginner-friendly NFT Minter tutorial. You got this!
over here someone says you can solve "ethereum is not defined" by writing
const { ethereum } = window
This works for me in React 18.1.0

FeathersJS authentication deactivate user

I am using FeathersJS and been happy with authentication it provides. I this case it is local JWT. A client requested user management with an ability to disable some. There is field isDisabled in Users model, but it's hard to figure out where the check should be performed and how to set it up.
"#feathersjs/feathers": "^3.0.2",
"#feathersjs/authentication": "^2.1.0",
"#feathersjs/authentication-jwt": "^1.0.1",
"#feathersjs/authentication-local": "^1.0.2",
It depends where you want to check. You can either customize the JWT verifier or create a hook on the users service for the get method:
app.service('users').hooks({
after: {
get(context) {
const user = context.result;
if(user.isDisabled) {
throw new Error('This user has been disabled');
}
}
}
});
I did this directly in my authenticate hook:
const { authenticate } = require('#feathersjs/authentication').hooks
const { NotAuthenticated } = require('#feathersjs/errors')
const verifyIdentity = authenticate('jwt')
function hasToken(hook) {
if (hook.params.headers == undefined) return false
if (hook.data.accessToken == undefined) return false
return hook.params.headers.authorization || hook.data.accessToken
}
module.exports = async function authenticate(context) {
try {
await verifyIdentity(context)
} catch (error) {
if (error instanceof NotAuthenticated && !hasToken(context)) {
return context
}
}
if (context.params.user && context.params.user.disabled) {
throw new Error('This user has been disabled')
}
return context
}
You see I did check the just loaded user record and throw an error in case. And as this hook is called in before:all the user is rejected before any action is done.
As for feathers 4 you can extend your auth strategies very easily. For example if we want to user only be able to login and verify their JWT we would do the following in authentication.ts (Typescript):
import { Id, Query, ServiceAddons } from '#feathersjs/feathers';
import { AuthenticationService, JWTStrategy } from '#feathersjs/authentication';
import { LocalStrategy } from '#feathersjs/authentication-local';
import { expressOauth } from '#feathersjs/authentication-oauth';
import { Application } from './declarations';
declare module './declarations' {
interface ServiceTypes {
'authentication': AuthenticationService & ServiceAddons<any>;
}
}
Extend the local strategy by alter getEntityQuery to only inlcude users which are active.
class CustomLocalStrategy extends LocalStrategy {
async getEntityQuery(query: Query) {
return {
...query,
active: true,
$limit: 1
};
}
}
Extend the JWT strategy by alter getEntity() to return null if the user is inactive
class CustomJWTStrategy extends JWTStrategy {
async getEntity(id: Id) {
const entity = await this.entityService.get(id);
if (!entity.active) {
return null;
}
return entity;
}
}
export default function(app: Application): void {
const authentication = new AuthenticationService(app);
authentication.register('jwt', new CustomJWTStrategy());
authentication.register('local', new CustomLocalStrategy());
app.use('/authentication', authentication);
app.configure(expressOauth());
}

Resources