Is there a way to do role based permissions in middleware?
I'm asking this because I was hoping that there would be a way to call getSession in middleware.
Otherwise I would need to implement a helper function hasPermissions(req), import it and call it in every single file and I'm trying to avoid that
Thanks
if you do not want to use third party authenticatio server and you want to do on your own, you could write this with using cookeis and jwt.
1- when users logins set a jwt token. In next.js when user logins, make a request to api function and api function should handle the login process:
import jwt from "jsonwebtoken";
const handleLoginWithEmail = async (e) => {
e.preventDefault();
if (email) {
try {
//... add your logic, set state
// make a
const response = await fetch("/api/login", {
method: "POST",
headers: {
// pass this if you are using passwdrdless or other service that needs token
Authorization: `Bearer ${ifToken}`,
"Content-Type": "application/json",
},
});
} catch (error) {
console.error("Something went wrong logging in", error);
}
}
};
your api function should create a jwt
export default async function login(req, res) {
if (req.method === "POST") {
try {
// if there is
const auth = req.headers.authorization;
// add your logic
const token = jwt.sign(
{
...metadata,
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000 + 7 * 24 * 60 * 60),
// I just create a custom object
"roles": {
"allowed-roles": ["user", "admin"],
"default-role": "user",
// you could decide the role based on email or userId. write your logic
"role":"user"
},
},
process.env.JWT_SECRET
);
// set the token
setTokenCookie(token, res);
res.send({ done: true });
} catch (error) {
console.error("Something went wrong logging in", error);
res.status(500).send({ done: false });
}
} else {
res.send({ done: false });
}
}
2- write a set cookie function used in above api function
import cookie from "cookie";
const MAX_AGE = 7 * 24 * 60 * 60;
export const setTokenCookie = (token, res) => {
const setCookie = cookie.serialize("token", token, {
maxAge: MAX_AGE,
expires: new Date(Date.now() + MAX_AGE * 1000),
secure: process.env.NODE_ENV === "production",
path: "/",
});
res.setHeader("Set-Cookie", setCookie);
};
3- write a verify token function
import jwt from "jsonwebtoken";
export async function verifyToken(token) {
if (token) {
const decodedToken = jwt.verify(token, process.env.JWT_SECRET);
console.log("decodedToken",decodedToken)
// get the token role from decodedToken
const userId = decodedToken?.issuer;
return {userId,role};
}
return null;
}
4- Finally in _middleware.js function:
export async function middleware(req, ev) {
console.log("ev in middleware", ev);
const token = req ? req.cookies?.token : null;
const {userId,role} = await verifyToken(token);
// got the role and add logic for role
}
Related
I am controlling user authentification in my next app with next-auth library
I am using the credentials provider. First I call the login endpoint which returns the user informations then I take the access token and put it inside the token given by next-auth callback.
this is my code in [...nextauth].js
const authOptions = {
session: {
strategy: "jwt",
},
providers: [
CredentialsProvider({
type: "credentials",
credentials: {},
async authorize(credentials, req) {
const { email, password } = credentials;
const result = await axios.post(
`http://127.0.0.1:5000/user/login`,
{
email,
password,
},
{
headers: { "Content-Type": "application/json" },
withCredentials: true,
}
);
return {
accessToken: result.data.accessToken,
};
},
}),
],
callbacks: {
async jwt({ user, token }) {
if (user?.accessToken) {
token.value = user.accessToken;
}
console.log(token); //<-- output below
return token;
},
},
};
output :
{
value: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjYzOTZiMTlhYTczMmUzMzYwMjU2ZjBlMiIsImlhdCI6MTY3NTAyMzEwNSwiZXhwIjoxNjc1MTA5NTA1fQ.5kdPmeLCpwbJBjtzKMhe5QMNEx75ThiDKm75PN0vjoc',
iat: 1675023106,
exp: 1675109506,
jti: 'd9108700-1b5f-4bd3-8d31-0c36f38d9fcb'
}
Now in getServerSideProps I can get it from the request because it is sent in Cookie
export async function getServerSideProps(context) {
console.log(context.req.cookies["next-auth.session-token"]); // <-- output in Blockquote
return {
// does not matter
};
}
I get this :
eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..6ryJ60GPcDLq9aWG.4oWlJbecyWUnbZYJiv6z0eAuFmRFSfEn4fQSlh1FTjlPiiDGZASA4UwqXNEHRpRMG6HRPRDcsUUCHBBzaV8JwCEetgSYJcSrZ5CK_AhyvFKUlKY-TpHSNDnmCI8ZS4y2nV_Xl0NqvMU3vA-D8gXtT5UcOrJLlN5dMe7S9xZo8vhr-gpohcEhKOefUgDjTmMYmBf190OLl0TY599FkJwpoeSFozAwavwbOZGQOxYVbsj3KTibsfE37juyqnDaiV_t59bWroGjz2d5kHLxfkpQB0IKYRnAH8sXbG7dDZUVLT1UQUN_FrjYpkFrQgxC7MmWZtCccQs-FsBXY7EbiYmJKIddpOeN1Q.1kas8bGE_O7IkEDiilxiZw
Now I want to decrypt this token to get its property 'value' (which is the accessToken) and use it.
is it possible to decrypt it with javascript ? Thank you for your attention !
this funciton is inside next-auth/jwt module:
async function decode(params) {
const {
token,
secret
} = params;
if (!token) return null;
const encryptionSecret = await getDerivedEncryptionKey(secret);
const {
payload
} = await (0, _jose.jwtDecrypt)(token, encryptionSecret, {
clockTolerance: 15
});
return payload;
}
since this is not exported you have to export all module
import jwt from "next-auth/jwt"
// since you reached the token inside getServerSidePrps passed here
jwt.decode(token,passYourSecret)
May jwt.decode(token,secret) works as montioned in #Yilmaz answer but there is a built-in helper method getToken() for doing that
For convenience, this helper function is also able to read and decode tokens passed from the Authorization: 'Bearer token' HTTP header.
import { getToken } from "next-auth/jwt";
const secret = 'MY_SECRET';
export default async function handler(req, res) {
const token = await getToken({ req, secret })
console.log("JSON Web Token", token)
res.end()
}
If using NEXTAUTH_SECRET env variable, we detect it, and you won't actually need to secret
export default async function handler(req, res) {
const token = await getToken({ req })
console.log("JSON Web Token", token)
res.end()
}
This question extends my last question and is primarily for me who might get stuck again. I'm trying to rebuild an app in Next.js that has this login form using JWT tokens. On the previous app, I store the access token in memory using context API and the refresh token in an httpOnly cookie. However, I discovered in Next.js that you can use something called _middleware that runs on every request. I thought of just storing both in an httpOnly cookie then verify each request. I'm still testing it out for bugs but it runs well for the time being. Answer below.
In this code I'm using a library called jose instead of jsonwebtoken because Native Node.js APIs are not supported and you will get the following error . Using Jose comes from this stack overflow answer.
_Middlesware.tsx
import { NextResponse } from "next/server";
import { generateAccessToken, verifyRefreshToken } from "../_operations/jwt/jwt";
export default async function (
req: {
url?: any;
cookies?: any;
}
): Promise<NextResponse | void> {
const { cookies } = req;
const url: string = req.url;
const refreshToken: string | undefined = cookies?.refresh_token_extreme;
const accessToken: string | undefined = cookies?.access_token_extreme;
const baseUrl: string = "http://localhost:3000";
// vercel.svg is used in /login
const unprotectedPaths: string[] = [
`${baseUrl}/login`,
`${baseUrl}/signup`,
`${baseUrl}/favicon.ico`,
`${baseUrl}/vercel.svg`,
`${baseUrl}/_next/webpack-hmr`,
`${baseUrl}/attachables/campus-images/image1.jpg`,
`${baseUrl}/attachables/mnhs-images/logos/login_logo.png`,
`${baseUrl}/attachables/mnhs-images/logos/mnhs_favicon_og.ico`,
];
if (unprotectedPaths.includes(url)) return void 0;
if (!refreshToken && url === `${baseUrl}/api/login`)
return NextResponse.next();
if (!accessToken && !refreshToken)
return NextResponse.redirect(`${baseUrl}/login`);
if (!accessToken && refreshToken) {
const verifiedToken = await verifyRefreshToken(refreshToken)
const newToken = await generateAccessToken(verifiedToken as any)
return NextResponse.next().cookie('access_token_extreme', newToken, {
secure: true,
sameSite: "strict",
httpOnly: true,
path: "/",
expires: new Date(Date.now() + 1000 * 60 * 10) // 10 minutes
});
}
return NextResponse.next();
}
jwt.ts
For why do I have uprotectedPaths, refer to my last question
import * as jose from "jose";
export function generateAccessToken(user: {}): Promise<string> {
return new Promise(async (resolve) => {
const accessToken = await new jose.SignJWT({ user })
.setProtectedHeader({ alg: "HS256" })
.setExpirationTime("10m")
.sign(new TextEncoder().encode(process.env.ACCESS_TOKEN_SECRET));
return resolve(accessToken);
});
}
export function generateRefreshToken(user: {}): Promise<string> {
return new Promise(async (resolve) => {
const refreshToken = await new jose.SignJWT({ user })
.setProtectedHeader({ alg: "HS256" })
.sign(new TextEncoder().encode(process.env.REFRESH_TOKEN_SECRET));
return resolve(refreshToken);
});
}
export function verifyRefreshToken(token: string) {
return new Promise(async (resolve) => {
const { payload: newToken } = await jose.jwtVerify(
token,
new TextEncoder().encode(process.env.REFRESH_TOKEN_SECRET)
);
return resolve(newToken);
});
}
I have this authentication issue
The registration works perfectly and the servers take the registration data: User password email and number. After this step, I have OTP verification
I got the pin code and run the verification mutation.
On the verification, I got the error :
You are not authenticated
And the all process stops because I am not verified
Here is the code for the react-native part
const VERIFY = gql
mutation($token: String!, $kind: TokenKind!) {
verify(token: $token, kind: $kind)
}
const VerificationScreen: React.FC < any > = (props) => {
const token = (props as any).route.params.token;
const [loading, setLoading] = React.useState < boolean > (false)
const [pin, setPin] = useState < string > ('')
const [veryfy] = useMutation(VERIFY)
const verifyPin = () => {
if (!pin) {
alert('Please TYPE Valid PIN')
return;
}
//veryfy
setLoading(true);
veryfy({
variables: {
token: pin,
kind: 'PHONE'
}
}).then(({
data
}) => {
setLoading(false)
console.log(data);
if (data) {
props.navigation.navigate('Intro', {
token: token
});
}
}).catch((e) => {
setLoading(false)
console.log(e);
})
}
The below code is an example showing how you can use the Apollo middle-ware [1] and context [2] to add headers(auth) at runtime or testing.
First we create a middle-ware block, then an inner context block.
In the context block we can use the line below to add external parameters, this is to configure the request
const { isAuth, Authorization } = headers;
A boolean(Auth) can be set to allow a token to be embedded in a Authorization header, or an existing Authorization header can be passed in directly, this can be usefull for testing for example.
const getClient = () => {
// create link middleware see [1]
const authMiddleware = new ApolloLink((operation, forward) => {
// code block below assumes there exists an auth token in globals
// add headers with the client context [2]
operation.setContext(({ headers = {} }) => {
// auth header using global token as a bearer token
const authHeaders = {
Authorization: global.access_token
? `Bearer ${global.access_token}`
: "",
};
// here an Authorization header can be passed in thru the context,
// which can be useful, eg for testing
const { isAuth, Authorization } = headers;
// if we have an Auth.. header we can just add that and return
if (Authorization) {
return {
headers: { ...publicHeaders, ...{ Authorization } },
};
}
const header = isAuth
? { ...publicHeaders, ...authHeaders }
: publicHeaders;
return {
headers: header,
};
});
return forward(operation);
}); // end create middleware
// create the graphql endpoint [1]
const httpLink = new HttpLink({ uri: '/graphql' });
// create client with the middleware and return the client
// code block below assumes there exists a globalCache
return new ApolloClient({
cache: globalCache,
link: concat(authMiddleware, httpLink),
});
}
use
// add/configure the appropriate headers in the context block
// for the client
client
.mutate({
mutation: <some mutation>,
variables: <some variables>,
context: {
headers: {
isAuth: false, // or true for authenticated operation
},
},
})
.then((result) => ....)
.catch((err) => {
console.log(....);
});
use in a hook
const [runMutation, { data }] =
useMutation(<gql>, {
context: {
headers: { isAuth: true },
variables: <some vars>,
onCompleted: (data) => callback(data),
onError: (error) => console.error("Error ", error),
},
});
links
1 middleware
2 context
Hi everyone!
I have my own custom strategy to get token, and all is good, but when a refresh page I lose user data and fetchUser does not works. It doesn´t send the params to API to get again the user data.
the workflow is next:
1- send params to token api and get token
2- send params to login API to get the user
//nuxt.config.js
customStrategy: {
_scheme: '~/schemes/customScheme',
endpoints: {
login: {
url: '/api/v1/token',
method: 'post',
propertyName: 'token',
headers: {'x-channel-id': 1}
},
user: {
url: '/api/v1/login',
method: 'post',
propertyName: false,
headers: {'x-channel-id': 1}
},
logout: null
}
}
customScheme.js
import LocalScheme from '#nuxtjs/auth/lib/schemes/local'
export default class CustomScheme extends LocalScheme {
_setToken (token) {
if (this.options.globalToken) {
// Set Authorization token for all axios requests
this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, token)
}
}
_clearToken () {
if (this.options.globalToken) {
// Clear Authorization token for all axios requests
this.$auth.ctx.app.$axios.setHeader(this.options.tokenName, false)
}
}
mounted () {
if (this.options.tokenRequired) {
const token = this.$auth.syncToken(this.name)
this._setToken(token)
}
return this.$auth.fetchUserOnce()
}
async login (endpoint) {
if (!this.options.endpoints.login) {
return
}
// Get token
const result = await this.$auth.request({
...endpoint
},
this.options.endpoints.login
)
// Set token
if (this.options.tokenRequired) {
const token = this.options.tokenType
? this.options.tokenType + ' ' + result
: result
this.$auth.setToken(this.name, token)
this._setToken(token)
}
// If result I get and set user
if (result) {
const user = await this.$auth.request({
...endpoint
},
this.options.endpoints.user
)
this.$auth.setUser(user);
}
}
async fetchUser (endpoint) {
// User endpoint is disabled.
if (!this.options.endpoints.user) {
this.$auth.setUser({})
return
}
// Token is required but not available
if (this.options.tokenRequired && !this.$auth.getToken(this.name)) {
return
}
// Try to fetch user and then set
try{
const user = await this.$auth.requestWith(
this.name,
endpoint,
this.options.endpoints.login
)
this.$auth.setUser(user)
} catch (error){
console.log(error)
}
}
}
When I set this.$auth.setUser(user) in login() method all is fine and app redirect me to /dashboard page and the user information (like role and email) is displayed on navBar but when I refresh page I lose user data. The app try to fetchUser but it give me a 400 error because user and password not sent.
Another thing I don´t understand is Why endpoint parameter is undefined in async fetchUser (endpoint) ??? . I think there is an issue in this part.
I hope u can help me
Regards
I just remove all this library and did my own custom Nuxt authentication
https://nemanjadragun92.medium.com/nuxt-js-custom-authentication-245d2816c2f3
I have a SPA which uses the solution provided here to authenticate with Azure AD and everything works as expected. Now I want to migrate this to use MSAL.js.
I use below for login:
import * as MSAL from 'msal'
...
const config = {
auth: {
tenantId: '<mytenant>.com',
clientId: '<myclientid>',
redirectUri: <redirecturi>,
},
cache: {
cacheLocation: 'localStorage',
}
};
const tokenRequest = {
scopes: ["User.Read"]
};
export default {
userAgentApplication: null,
/**
* #return {Promise}
*/
initialize() {
let redirectUri = config.auth.redirectUri;
// create UserAgentApplication instance
this.userAgentApplication = new MSAL.UserAgentApplication(
config.auth.clientId,
'',
() => {
// callback for login redirect
},
{
redirectUri
}
);
// return promise
return new Promise((resolve, reject) => {
if (this.userAgentApplication.isCallback(window.location.hash) || window.self !== window.top) {
// redirect to the location specified in the url params.
}
else {
// try pull the user out of local storage
let user = this.userAgentApplication.getUser();
if (user) {
resolve();
}
else {
// no user at all - go sign in.
this.signIn();
}
}
});
},
signIn() {
this.userAgentApplication.loginRedirect(tokenRequest.scopes);
},
And then I use below to get the token:
getCachedToken() {
var token = this.userAgentApplication.acquireTokenSilent(tokenRequest.scopes);
return token;
}
isAuthenticated() {
// getCachedToken will only return a valid, non-expired token.
var user = this.userAgentApplication.getUser();
if (user) {
// get token
this.getCachedToken()
.then(token => {
axios.defaults.headers.common["Authorization"] = "Bearer " + token;
// get current user email
axios
.get('<azureapi-endpoint>' + '/GetCurrentUserEmail')
.then(response => { })
.catch(err => { })
.finally(() => {
});
})
.catch(err => { })
.finally(() => { });
return true;
}
else {
return false;
}
},
}
but after login I get below error:
Access to XMLHttpRequest at 'https://login.windows.net/common/oauth2/authorize?response_type=code+id_token&redirect_uri=<encoded-stuff>' (redirected from '<my-azure-api-endpoint>') from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
Also the token that I get seems to be invalid as I get 401 errors trying to call api using the token. Upon checking the token against https://jwt.io/ I get an invalid signature.
I really appreciate anyone's input as I've already spent good few days and haven't got anywhere yet.
I'm not sure if this is your issue. however, for msal.js, in the config, there is no tenantId parameter, it's supposed to be authority. Here is a sample for graph api using msal.js
https://github.com/Azure-Samples/active-directory-javascript-graphapi-v2
specifically: the config is here: https://github.com/Azure-Samples/active-directory-javascript-graphapi-v2/blob/quickstart/JavaScriptSPA/authConfig.js
as per here, https://learn.microsoft.com/en-us/azure/active-directory/develop/msal-js-initializing-client-applications it is supposed to be hitting login.microsoftonline.com not login.windows.net