Is there a way to detect server side cookie for all pages in nextjs? [duplicate] - axios

So I'm creating authentication logic in my Next.js app. I created /api/auth/login page where I handle request and if user's data is good, I'm creating a httpOnly cookie with JWT token and returning some data to frontend. That part works fine but I need some way to protect some pages so only the logged users can access them and I have problem with creating a HOC for that.
The best way I saw is to use getInitialProps but on Next.js site it says that I shouldn't use it anymore, so I thought about using getServerSideProps but that doesn't work either or I'm probably doing something wrong.
This is my HOC code:
(cookie are stored under userToken name)
import React from 'react';
const jwt = require('jsonwebtoken');
const RequireAuthentication = (WrappedComponent) => {
return WrappedComponent;
};
export async function getServerSideProps({req,res}) {
const token = req.cookies.userToken || null;
// no token so i take user to login page
if (!token) {
res.statusCode = 302;
res.setHeader('Location', '/admin/login')
return {props: {}}
} else {
// we have token so i return nothing without changing location
return;
}
}
export default RequireAuthentication;
If you have any other ideas how to handle auth in Next.js with cookies I would be grateful for help because I'm new to the server side rendering react/auth.

You should separate and extract your authentication logic from getServerSideProps into a re-usable higher-order function.
For instance, you could have the following function that would accept another function (your getServerSideProps), and would redirect to your login page if the userToken isn't set.
export function requireAuthentication(gssp) {
return async (context) => {
const { req, res } = context;
const token = req.cookies.userToken;
if (!token) {
// Redirect to login page
return {
redirect: {
destination: '/admin/login',
statusCode: 302
}
};
}
return await gssp(context); // Continue on to call `getServerSideProps` logic
}
}
You would then use it in your page by wrapping the getServerSideProps function.
// pages/index.js (or some other page)
export const getServerSideProps = requireAuthentication(context => {
// Your normal `getServerSideProps` code here
})

Based on Julio's answer, I made it work for iron-session:
import { GetServerSidePropsContext } from 'next'
import { withSessionSsr } from '#/utils/index'
export const withAuth = (gssp: any) => {
return async (context: GetServerSidePropsContext) => {
const { req } = context
const user = req.session.user
if (!user) {
return {
redirect: {
destination: '/',
statusCode: 302,
},
}
}
return await gssp(context)
}
}
export const withAuthSsr = (handler: any) => withSessionSsr(withAuth(handler))
And then I use it like:
export const getServerSideProps = withAuthSsr((context: GetServerSidePropsContext) => {
return {
props: {},
}
})
My withSessionSsr function looks like:
import { GetServerSidePropsContext, GetServerSidePropsResult, NextApiHandler } from 'next'
import { withIronSessionApiRoute, withIronSessionSsr } from 'iron-session/next'
import { IronSessionOptions } from 'iron-session'
const IRON_OPTIONS: IronSessionOptions = {
cookieName: process.env.IRON_COOKIE_NAME,
password: process.env.IRON_PASSWORD,
ttl: 60 * 2,
}
function withSessionRoute(handler: NextApiHandler) {
return withIronSessionApiRoute(handler, IRON_OPTIONS)
}
// Theses types are compatible with InferGetStaticPropsType https://nextjs.org/docs/basic-features/data-fetching#typescript-use-getstaticprops
function withSessionSsr<P extends { [key: string]: unknown } = { [key: string]: unknown }>(
handler: (
context: GetServerSidePropsContext
) => GetServerSidePropsResult<P> | Promise<GetServerSidePropsResult<P>>
) {
return withIronSessionSsr(handler, IRON_OPTIONS)
}
export { withSessionRoute, withSessionSsr }

Related

Why my authentificationService is undefined?

I've one question. I don't understand why, but my services is undefined. Someone could help me to clarify this ?
I've a component "FormLogin" with the call of this service
<script>
import { authenticationService } from '#/container.js'
import { ref } from '#vue/reactivity'
export default {
emits: ['successfullyLogged'],
setup (props, context) {
const errors = ref([])
const email = ref(null)
const password = ref(null)
const submit = () => {
errors.value = []
authenticationService()
.login(email.value, password.value)
.then(() => {
context.emit('successfullyLogged')
})
.catch(error => {
errors.value = [error.response.data.message]
})
}
return {
email,
password,
errors,
submit
}
}
}
</script>
Then i've the "global injection"
import api from '#/clients/api.js'
import TokenRepository from '#/repositories/TokenRepository.js'
import AuthenticationService from '#/services/AuthenticationService.js'
export function tokenRepository () {
return new TokenRepository()
}
export function authenticationService () {
return new AuthenticationService(api, tokenRepository)
}
And after that, the service itself
import store from "#/store"
export default (client, tokenRepository) => {
const login = (email, password) => {
return client.post('/oauth/token', {
grant_type: 'password',
client_id: process.env.VUE_APP_CLIENT_ID,
client_secret: process.env.VUE_APP_CLIENT_SECRET,
username: email,
password: password
})
.then(response => {
tokenRepository().store(response.data.access_token)
store.dispatch('account/loadUser')
})
}
const logout = () => {
tokenRepository().destroy()
store.commit('account/setUser', {})
}
return {
login,
logout
}
}
But when i run this code, fill my form fields and hit the button "submit", i've this error in console, and i don't undestand why. (And when i try to use the debugger, it appear that authenticationService in FormLogin is undefined.
Thanks in advance for your help,
Christophe
So the answer of the problem was to remove the word "new" in my container.js
Bad answer for me
export function authenticationService () {
return new AuthenticationService(api, tokenRepository)
}
Good answer for me
export function authenticationService () {
return AuthenticationService(api, tokenRepository)
}

Axios response interceptor for refreshing token keeps firing in Vue 3

I'm trying to implement a refresh token with Vue 3 and Java for backend. It is working but interceptor keeps firing.
The logic: On every request there's a JWT Authorization header that authenticates the user. If that expires, there's a cookie endpoint in place ready to refresh the JWT.
I am using axios and interceptor response to check if the client gets a 401 to try and refresh the JWT. The cookie may be valid or not.
The problem is that the interceptor to refresh the JWT never stops firing, and I think I have something wrong with the synchronization of the requests. Below is my code:
Api.js:
import axios from "axios";
const instance = axios.create({
baseURL: "MY_URL",
});
export default instance;
token.service.js:
class TokenService {
getLocalRefreshToken() {
const user = JSON.parse(localStorage.getItem("user"));
return user?.refreshToken;
}
getLocalAccessToken() {
const user = JSON.parse(localStorage.getItem("user"));
return user?.accessToken;
}
updateLocalAccessToken(token) {
let user = JSON.parse(localStorage.getItem("user"));
user.accessToken = token;
localStorage.setItem("user", JSON.stringify(user));
}
getUser() {
return JSON.parse(localStorage.getItem("user"));
}
setUser(user) {
// eslint-disable-next-line no-console
console.log(JSON.stringify(user));
localStorage.setItem("user", JSON.stringify(user));
}
removeUser() {
localStorage.removeItem("user");
}
}
export default new TokenService();
setupInterceptors.js:
import axiosInstance from "./api";
import TokenService from "./token.service";
const setup = (store) => {
axiosInstance.interceptors.request.use(
(config) => {
const token = TokenService.getLocalAccessToken();
if (token) {
config.headers["Authorization"] = 'Bearer ' + token;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
axiosInstance.interceptors.response.eject()
axiosInstance.interceptors.response.use(
(res) => {
return res;
},
async (err) => {
const originalConfig = err.config;
if (originalConfig.url !== "/auth/login" && err.response) {
// Access Token was expired
if (err.response.status === 401 && !originalConfig._retry) {
originalConfig._retry = true;
try {
const rs = await axiosInstance.post("/auth/refreshtoken", {
refreshToken: TokenService.getLocalRefreshToken(),
});
const { accessToken } = rs.data;
store.dispatch("auth/refreshToken", accessToken);
TokenService.updateLocalAccessToken(accessToken);
return axiosInstance(originalConfig);
} catch (_error) {
return Promise.reject(_error);
}
}
}
return Promise.reject(err);
}
);
};
export default setup;
try this out and make sure you use another instance of Axios for the refresh token request
// to be used by the interceprot
firstAxiosInstance = axios.create({ baseURL: MY_URL });
//to be used by the refresh token API call
const secondAxiosInstance = axios.create({ baseURL: MY_URL});
firstAxiosInstance.interceptors.response.use(
(res) => {
return res;
},
async (err) => {
// this is the original request that failed
const originalConfig = err.config;
// decoding the refresh token at this point to get its expiry time
const decoded = jwt.decode(localStorage.getItem('refreshToken'));
// check if the refresh token has expired upon which logout user
if (decoded.exp < Date.now() / 1000) {
store.commit('logout');
router.push('/');
}
// get new access token and resend request if refresh token is valid
if (decoded.exp > Date.now() / 1000) {
if (err.response.status === 401) {
originalConfig._retry = true;
try {
const rs = await requestService.post('/api-v1/token/refresh/', {
refresh: localStorage.getItem('refreshToken'),
});
store.commit('update_aceess_token', rs.data);
err.config.headers.Authorization = `Bearer ${rs.data.access}`;
return new Promise((resolve, reject) => {
requestService
.request(originalConfig)
.then((response) => {
resolve(response);
})
.catch((e) => {
reject(e);
});
});
} catch (_error) {
return Promise.reject(_error);
}
}
}
return Promise.reject(err);
},
);
try clean el token authorization before send request refresh, by example
in mutations(vuex)
clearAccessToken(state) {
state.access_token = ''
TokenService.removeAccessTokenApi();
},
For me it was fixed by not using the same axios instance for the refresh token request.

Get User ID from session in next-auth client

I'm using next-auth with Prisma and Graphql, I followed these instructions to set up the adapter and my models accordingly:
https://next-auth.js.org/adapters/prisma
Authentication works but when I inspect session object from here :
const { data: session, status } = useSession()
I don't see ID
The reason I need the ID is to make further GraphQL queries. I'm using email value for now to fetch the User by email, but having ID available would be a better option.
Here's the quickest solution to your question:
src/pages/api/auth/[...nextAuth].js
export default NextAuth({
...
callbacks: {
session: async ({ session, token }) => {
if (session?.user) {
session.user.id = token.uid;
}
return session;
},
jwt: async ({ user, token }) => {
if (user) {
token.uid = user.id;
}
return token;
},
},
session: {
strategy: 'jwt',
},
...
});
This worked for me.
callbacks: {
async jwt({token, user, account, profile, isNewUser}) {
user && (token.user = user)
return token
},
async session({session, token, user}) {
session = {
...session,
user: {
id: user.id,
...session.user
}
}
return session
}
}
Here's the quickest solution that worked for me
import NextAuth from "next-auth"
import { MongoDBAdapter } from "#next-auth/mongodb-adapter"
import clientPromise from "../../../lib/mongodb"
export const authOptions = {
providers: [
<...yourproviders>
],
callbacks: {
session: async ({ session, token, user }) => {
if (session?.user) {
session.user.id = user.id;
}
return session;
},
},
adapter: MongoDBAdapter(clientPromise),
}
I just referred to the NextAuth docs (this page) and finally got it working the right way
callbacks: {
jwt({ token, account, user }) {
if (account) {
token.accessToken = account.access_token
token.id = user?.id
}
return token
}
session({ session, token }) {
// I skipped the line below coz it gave me a TypeError
// session.accessToken = token.accessToken;
session.user.id = token.id;
return session;
},
}
If you use TypeScript, add this to a new file called next-auth.d.ts
import NextAuth from 'next-auth';
declare module 'next-auth' {
interface Session {
user: {
id: string;
} & DefaultSession['user'];
}
}
I believe you can change the callbacks so it includes the user's ID in the session: https://next-auth.js.org/configuration/callbacks.
You will need to change the JWT callback so it also include the userId and the session callback so the id is also persisted to the browser session.

REST service exception handling in Angular2

First, I must mention that I'm a beginner in Angular and I'm kind of stucked with my sample code.
I created some simple login app which prompts for username and password, calls login REST service (written in Java) that returns some token at login success or throws an exception at login failure.
Here's some of my code.
Login component:
import { Component, OnInit } from '#angular/core';
import { Router } from '#angular/router';
import { AuthenticationService } from '../_services/index';
#Component({
moduleId: module.id,
templateUrl: 'login.component.html'
})
export class LoginComponent implements OnInit {
model: any = {};
error = '';
constructor(
private router: Router,
private authenticationService: AuthenticationService) { }
ngOnInit() {
// reset login status
this.authenticationService.logout();
}
login() {
this.authenticationService.login(this.model.username, this.model.password)
.subscribe(result => {
if (result === true) {
this.router.navigate(['/']);
} else {
this.error = 'Login failed!';
}
},
err => {
this.error = 'Login failed!';
});
}
}
Authentication service:
import { Injectable } from '#angular/core';
import { Http, Headers, RequestOptions, Response } from '#angular/http';
import { Observable } from 'rxjs';
import { CookieService } from 'angular2-cookie/core';
import { CookieOptionsArgs } from 'angular2-cookie/services/cookie-options-args.model';
import 'rxjs/add/operator/map';
#Injectable()
export class AuthenticationService {
public token: string;
constructor(private http: Http, private cookieService: CookieService) {
// set token if saved in cookie
this.token = cookieService.get('token');
}
login(username, password): Observable<boolean> {
return this.http.post('http://localhost:9081/MyApp/login?username=' + username + '&password=' + password, new RequestOptions({}))
.map((response: Response) => {
// login successful if there's a token in the response
let token = response.text();
if (token !== '-1') {
// set token
this.token = token;
// store token in cookie to keep user logged
let opts: CookieOptionsArgs = {
path: '/'
};
this.cookieService.put('token', token, opts);
// return true to indicate successful login
return true;
} else {
// return false to indicate failed login
return false;
}
});
}
logout(): void {
// clear token, remove cookie to log user out
this.token= null;
this.cookieService.remove('token');
}
}
Everything works as expected. When login is successful, token is returned and I'm redirected to a "home" page. Otherwise, a "Login falied" message appears on a login page and no redirection occurs. What bothers me is that I don't exactly know why login fails: is it because username doesn't exist or is it maybe because password is wrong. What is the proper way to handle exceptions thrown by REST service? I assume that authentication service is the right place but I don't exactly know how to do it. I tried to extract some info from request object but request mapping doesn't happen if exception is thrown.
Thanks for help!
It seems you're looking for catching the exception occuring on error login in AuthenticationService . If it's the case add .catch section after .map, like in this subject :
best practives catching error Angualr 2
.catch((error: any) => { //catch Errors here using catch block
if (error.status === 500) {
// Display your message error here
}
else if (error.status === 400) {
// Display your message error here
}
});
i have implemented my code this way :
login(email: string, password: string): Observable<boolean> {
return new Observable(observer => {
var data = { email: email, password: password };
this.http.post(this.server_url + '/auth/authenticate', data).subscribe(x => {
var result = {
email: x.json().email,
token: x.json().token,
roles: x.json().roles.map(x => x.name)
}
localStorage.setItem(this._userKey, JSON.stringify(result));
observer.next(true);
observer.complete();
}, er => {
if (er.status == 401) {
observer.next(false);
observer.complete();
} else {
console.log(er);
observer.error(er);
observer.complete();
}
});
});
}
so it handle three possibilities :
if cridential is OK it returns true
if credential is wrong return false (remember your server must
return 401 status !)
otherwise there is problem in server and throw error
and in handler i got :
login() {
this.loading = true;
this.authenticationService.login(this.model.username, this.model.password)
.subscribe(result => {
if (result == true) {
this.router.navigate(['/home']);
} else {
this.error = 'Username or password is incorrect';
this.loading = false;
}
}, err => {
this.error = 'Unexpected error occured.. please contact the administrator..';
this.loading = false;
});
}

Angular 2 data service

I'm building an observable data service based on the following article: https://coryrylan.com/blog/angular-2-observable-data-services
In the article he used an array as an example, here I will use the user object since I'm developing the user service.
Here's what I got:
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Events, SqlStorage, Storage } from 'ionic-angular';
import { Subject } from 'rxjs/Subject';
export interface DataStore {
user: Object
}
#Injectable()
export class UserService {
private baseUrl: string;
private storage: Storage;
private _user$: Subject<Object>;
private dataStore: DataStore;
constructor(
private http: Http
) {
this.baseUrl = 'http://localhost:3000';
this.storage = new Storage(SqlStorage);
this._user$ = <Subject<Object>>new Subject();
this.dataStore = {
user: { name: '' }
};
}
set user$(user: Object) {
this.storage.set('user', JSON.stringify(user));
this.dataStore.user = user;
this._user$.next(this.dataStore.user);
}
get user$() {
return this._user$.asObservable();
}
loadUser() {
return this.storage.get('user').then(
((user: string): Object => {
this.dataStore.user = JSON.parse(user);
this._user$.next(this.dataStore.user);
return this.dataStore.user;
})
);
}
login(accessToken: string) {
return this.http
.post('http://localhost:3000/login', { access_token: accessToken })
.retry(2)
.map((res: Response): any => res.json());
}
logout(): void {
this.storage.remove('user');
}
}
To authenticate I call the login() function and set the user data if everything ok.
this.userService.login(this.data.accessToken)
.subscribe(
(user: Object) => {
this.userService.user$ = user;
this.nav.setRoot(EventListComponent);
},
(error: Object) => console.log(error)
);
I feel it is better set the user data inside the service. I could do the following:
login(accessToken: string) {
return this.http
.post('http://localhost:3000/login', {
access_token: accessToken
})
.retry(2)
.map((res: Response): any => res.json())
.subscribe(
(user: Object) => {
this.userService.user$ = user;
this.nav.setRoot(EventListComponent);
},
(error: Object) => console.log(error)
);
}
But I won't be able to subscribe to the login() function in the component since it's already subscribed. How could I redirect the user if everything ok or show an alert if anything goes wrong in the component but setting the user inside the service?
In the main component I load the user data and set the rootPage:
this.userService.loadUser().then(
(user: Object) => this.rootPage = EventListComponent,
(error: Object) => this.rootPage = LoginComponent
);
I thought that calling the loadUser() function at this time I would not have to call it again, but I have to call it in all components that I need the user data:
this.user = this.userService.user$;
this.userService.loadUser();
I don't think the service is the way it should, what could I improve? Is there any better way to achieve what I want? Any example or idea?