Nuxt apollo authentication - vue.js

I try to build an an authentication with nuxt and apollo.
The login and signup is pretty easy, also to set up the jwt token, but when my token expire and I try to get a refresh token or to logout my user I getting following error.
Invariant Violation
Store reset while query was in flight (not completed in link chain)
Because my Error handler which I define in my nuxt.config.js do not work I try to build my own client.
So I set at apollo.clientConfigs.default my ~/graphql/config.ts which looks like
export default ({ app }: Context) => {
const errorLink = onError(({ graphQLErrors, networkError, operation, forward }) => {
if (graphQLErrors) {
graphQLErrors.map(async (err) => {
if (err?.extensions?.exception.status === 401) {
await app.$apolloHelpers.onLogout()
}
return err
})
return forward(operation)
}
if (networkError) {
console.log(networkError, 'and another one!')
return forward(operation)
}
return forward(operation)
})
return {
httpEndpoint: 'http://localhost:3001/graphql',
link: from([errorLink as any])
}
}

Related

SvelteKit + Hooks and MSAL.js using Azure AD B2C results in non_browser_environment

I am new to SvelteKit and i am trying to use MSAL.js with SvelteKit, the issue is i want to implement something similar to an AuthGuard/HttpInterceptor which checks to see if the user is still logged in as they navigate around the SPA or call the external API.
I am using the OAuth 2.0 authorization code flow in Azure Active Directory B2C
within in my auth.ts file i have the following code
let accountId: string = "";
const signIn = () => {
try {
msalInstance.loginRedirect(loginRequestWithApiReadWrite);
} catch (error) {
isAuthenticated.set(false);
console.warn(error);
}
}
// This captures the response from using the redirect flow to login
await msalInstance.handleRedirectPromise()
.then(response => {
if (response) {
if (response.idTokenClaims['tfp'].toUpperCase() === b2cPolicies.names.signUpSignIn.toUpperCase()) {
handleResponse(response);
}
}
})
.catch(error => {
console.log(error);
});
async function handleResponse(response: msal.AuthenticationResult) {
if (response !== null) {
user.set(response);
isAuthenticated.set(true);
setAccount(response.account);
} else {
selectAccount();
}
}
function selectAccount() {
const currentAccounts = msalInstance.getAllAccounts();
if (currentAccounts.length < 1) {
return;
} else if (currentAccounts.length > 1) {
const accounts = currentAccounts.filter(account =>
account.homeAccountId.toUpperCase().includes(b2cPolicies.names.signUpSignIn.toUpperCase())
&&
account.idTokenClaims?.iss?.toUpperCase().includes(b2cPolicies.authorityDomain.toUpperCase())
&&
account.idTokenClaims.aud === msalConfig.auth.clientId
);
if (accounts.length > 1) {
if (accounts.every(account => account.localAccountId === accounts[0].localAccountId)) { console.log("Multiple accounts belonging to the user detected. Selecting the first one.");
setAccount(accounts[0]);
} else {
console.log("Multiple users accounts detected. Logout all to be safe.");
signOut();
};
} else if (accounts.length === 1) {
setAccount(accounts[0]);
}
} else if (currentAccounts.length === 1) {
setAccount(currentAccounts[0]);
}
}
// in case of page refresh
selectAccount();
function setAccount(account: msal.AccountInfo | null) {
if (account) {
accountId = account.homeAccountId;
}
}
const authMethods = {
signIn,
getTokenRedirect
}
In a +page.svelte file i can then import the authMethods no problem, MSAL redirects me to the microsoft sign in page, i get redirected back and can then request an access token and call external API, great all is well.
<script lang='ts'>
import authMethods from '$lib/azure/auth';
<script>
<button on:click={authMethods.signIn}>Sign In</button>
However, the issue i am having is trying to implement this so i can check to see if the user is logged in against Azure B2C using a hook.server.ts file automatically. I would like to check a variable to see if the user is authenticated and if they arnt the hooks.server will redirect them to signUp by calling the authMethod within the hook, and the user will be automatically redirected to the sign in page.
In the hooks.server.ts i have the following code:
export const handle: Handle = (async ({ event, resolve }) => {
if (isAuthenticated === false) {
authRedirect.signIn();
msalInstance.handleRedirectPromise().then((response) => {
if (response) {
console.log('login with redirect succeeded: ', response)
isAuthenticated = true;
}
}).catch((error) => {
console.log('login with redirect failed: ', error)
})
}
const response = await resolve(event);
return response;
}) satisfies Handle;
When i navigate around the SvelteKit SPA, MSAL.js keeps throwing the error below, which i know is because i am running the code from the server flow rather than in the browser, so it was my understanding that if i implement the handleRedirectPromise() in both the auth.ts file and hooks.server.ts this would await the response from the signIn event and so long as i got a response i can then set isAuthenticated to true.
errorCode: 'non_browser_environment',
errorMessage: 'Login and token requests are not supported in non-browser environments.',
subError: ''
Are you required to use the MSAL library? I have got it working with https://authjs.dev/. I was using Active Directory -https://authjs.dev/reference/oauth-providers/azure-ad but there is also a flow for B2C https://authjs.dev/reference/oauth-providers/azure-ad-b2c which I haven't tried.
Then in the hooks.server.js you can do something like the below.
import { sequence } from '#sveltejs/kit/hooks';
import { redirect } from '#sveltejs/kit';
import { SvelteKitAuth } from '#auth/sveltekit';
import AzureADProvider from '#auth/core/providers/azure-ad';
import {
AZURE_AD_CLIENT_ID,
AZURE_AD_CLIENT_SECRET,
AZURE_AD_TENANT_ID
} from '$env/static/private'
const handleAuth = SvelteKitAuth({
providers: [
AzureADProvider({
clientId: AZURE_AD_CLIENT_ID,
clientSecret: AZURE_AD_CLIENT_SECRET,
tenantId: AZURE_AD_TENANT_ID
})
]
});
async function isAuthenticatedUser({ event, resolve }) {
const session = await event.locals.getSession();
if (!session?.user && event.url.pathname !== '/') {
throw redirect(302, '/');
} else if (session?.user && event.url.pathname === '/') {
throw redirect(302, '/dashboard');
}
const response = await resolve(event);
return response;
}
export const handle = sequence(handleAuth, isAuthenticatedUser);

node_modules/#tanstack/query-core/build/lib/mutation.js:153:10 in Mutation#execute react native expo

I'm buiding React Native Expo App with external rest api.
I have created reusable axios api call:
// axiosAPi.js
export const axiosApi = async (method, url, obj = {}) => {
try {
switch (method) {
case 'GET':
return await axios
.get(`${baseUrl}/${url}`, config)
.then((res) => res.data)
case 'POST':
return await axios
.post(`${baseUrl}/${url}`, obj, config)
.then((res) => res.data)
case 'PUT':
return await axios
.put(`${baseUrl}/${url}`, obj, config)
.then((res) => res.data)
case 'DELETE':
return await axios
.delete(`${baseUrl}/${url}`, config)
.then((res) => res.data)
}
} catch (error) {
throw error?.response?.data?.error
}
}
I have created a hook with login instance using react-query:
// api/index.js
export default function useApiHook() {
const login = useMutation((obj) => axiosApi('POST', `auth/login`, obj))
return { login }
}
Here is the implementation of login screen
// screens/login.js
const loginPostMutation = useApiHook()?.login
const submitHandler = (data) => {
loginPostMutation
?.mutateAsync(data)
?.then((res) => res)
.catch((err) => err)
}
When I send correct credentials is returns the data with no errors, but when I send incorrect credentials it returns the error + this warning in the console:
Invalid credentials
at node_modules/#tanstack/query-core/build/lib/mutation.js:153:10 in Mutation#execute
The line in question points towards react-query logging the error to the console in development mode. Invalid credentials is thus just an error that is returned from axios, very likely, a 401 - Unauthorized error.
You would very likely get the same error when making the axios request without react-query.

react admin and AuthProvider no redirect after login

I've notice (using some console.log) that the method checkAuth is called some times only before the login methods and not after, so
when the login is accomplished correctly, the token is stored in the browser's local storage and at the end the method login returns the resolved promise,
the checkAuth is not anymore invoked and the page redirect is not performed by the dashboard
If then i change manually the page, it works correctly because the token is in the localstorage and the checkAuth method is able to check it normally
This is my AuthProvider
import axios from "axios";
export default {
// called when the user attempts to log in
login: ({ username, password }) => {
username = encodeURIComponent(username);
password = encodeURIComponent(password);
const tokenUrl = "https://myendpoint/profili/token";
const headers = {
'Content-Type': 'application/x-www-form-urlencoded'
}
const config = {
headers
};
const data = `username=${username}&password=${password}&grant_type=password`;
axios.post(tokenUrl,data, config)
.then(response => {
if (response.status < 200 || response.status >= 300) {
throw new Error(response.statusText);
}
return response;
})
.then(response => {
localStorage.setItem('token', response.data);
})
.catch((error) => {
throw new Error('Network error');
});
console.log("LOGIN");
return Promise.resolve();
},
// called when the user clicks on the logout button
logout: () => {
localStorage.removeItem('token');
return Promise.resolve();
},
// called when the API returns an error
checkError: ({ status }) => {
if (status === 401 || status === 403) {
console.log("passato in checkError");
localStorage.removeItem('token');
return Promise.reject();
}
return Promise.resolve();
},
// called when the user navigates to a new location, to check for authentication
checkAuth: () => {
console.log("CHECK AUTH");
return localStorage.getItem('token')
? Promise.resolve()
: Promise.reject();
},
// called when the user navigates to a new location, to check for permissions / roles
getPermissions: () => Promise.resolve(),
};
I think you did the wrong returns.
Your login function returns Promise.resolve() right at the moment it emits the axios HTTP POST request, long before it gets the response from the authentication server.
Instead replace the return by of the function by :
return axios.post(tokenUrl,data, config)
.then(response =>
{
if (response.status < 200 || response.status >= 300)
{
throw new Error(response.statusText);
}
return response;
}).then(response =>
{
localStorage.setItem('token', response.data);
})
.catch((error) => {
throw new Error('Network error');
});
This way the Promise resolution will only happens after your authentication server responds.

response of axios retry in main.js in vue js

I have a method named getUsers and it is in created hook in Users Component and I have access token and refresh token in my local storage.
I want that when my token expires, I use refresh token and get new access token and retry last request that was failed because of expired access token.
My problem is I want get response of second try of axios call in first axios call point (in Users component in created hook) because I fill table from response of it.
How can I do that?
main.js:
axios.interceptors.request.use((config) => {
config.headers['Content-Type'] = `application/json`;
config.headers['Accept'] = `application/json`;
config.headers['Authorization'] = `Bearer ${localStorage.getItem('access_token')}`;
return config;
}, (err) => {
return Promise.reject(err);
});
let getRefreshError = false
axios.interceptors.response.use((response) => {
return response
},
(error) => {
const originalRequest = error.config;
if (!getRefreshError && error.response.status === 401) {
axios.post(process.env.VUE_APP_BASE_URL + process.env.VUE_APP_REFRESH_TOKEN,
{refresh_token: localStorage.getItem("refresh_token")})
.then(res => {
localStorage.setItem("access_token", res.data.result.access_token);
localStorage.setItem("refresh_token", res.data.result.refresh_token);
originalRequest.headers['Authorization'] = localStorage.getItem("access_token");
return axios(originalRequest)
.then((res) => {
return Promise.resolve(res);
}, (err) => {
return Promise.reject(err);
});
}).catch(error => {
getRefreshError = true;
router.push('/pages/login')
return Promise.reject(error);
})
}
return Promise.reject(error);
});
Users:
created() {
this.getUsers();
}
You can return a new Promise from error handler of response interceptor. Refresh token there, perform the original request and resolve promise based on the result of actions (refreshing and re-fetching). Here is a general sketch of what you should do.
axios.interceptors.response.use(
(res => res),
(err => {
return new Promise(resolve, reject) => {
// refresh token
// then save the token
// then reperform original request
// and resolve with the response of the original request.
resolve(resOfSecondRequest)
// in case of any error, reject with the error
// and catch it where original call was performed just like the normal flow
reject(errOfSecondRequest)
}
})
)

How can I verify token google recaptcha 3 on adonis js?

I using vue as my front end. I send token from my front end like this :
let payload = {
token: tokenCaptcha
}
axios.post(`http://127.0.0.1:3333/api/v1/category`, payload)
.then(response => {
return response.data
}).catch(
error => {
console.log(error)
})
The token will used to verify on the backend. My backend using adonis.js
The script of controller like this :
'use strict'
class CategoryController {
async store ({ request, response }) {
return request.input('token')
}
}
module.exports = CategoryController
My routes like this :
Route.group(()=>{
Route.post('category', 'CategoryController.store')
}).prefix('api/v1')
How can I verify the token on adonis.js(backend)?
I had search reference. But I don't find it
You need to use axios. Something like:
const axios = use('axios')
const Env = use('Env')
const querystring = use('querystring')
async store({ request, response }) {
const data = request.only(['token'])
try {
const data_request = await axios.post('https://www.google.com/recaptcha/api/siteverify', querystring.stringify({ secret: Env.get('RECAPTCHA_PRIVATE_KEY'), response: data['token'], remoteip: '172.217.23.110' }))
if (!data_request.data.success) {
//If the recaptcha check fails
...
}
} catch (error) {
...
}
}
Google documentation - Verifying the user's response
This code is made for v2. But the verification is the same : https://developers.google.com/recaptcha/docs/v3#site_verify_response