I am trying to create a Sveltekit app where users can log in. The login process is handled by a self-created API, so I would like to use the Auth.js Credentials Provider.
When I call the SignIn method as the FormAction in the +page.server.ts file, I get the error message 'window is not defined', which makes sense. Does this mean that Auth.js is not compatible with server-side rendering, or is there something else that I can adjust?
My Code:
//hooks.server.ts
SvelteKitAuth({
providers: [
// #ts-ignore
Credentials({
name: "credentials",
async authorize(credentials, req) {
// TODO: Call Api
const user = { id: "1", name: "J Smith", email: "jsmith#example.com" }
if (user) {
return user
} else {
return null
}
}
})
],
});
//+page.server.ts
export const actions = {
default: async ({ cookies, request }: { cookies: any, request: any }) => {
const data = await request.formData();
const credentials = {
username: data.get('username'),
password: data.get('password')
};
signIn('credentials', credentials)
.then(response => {
console.log('Success');
})
.catch(error => {
console.error('error', error);
});
}
};
Related
How do I mock authenticate with Nuxt and Cypress?
I have a FastAPI backend that issues a JWT to a frontend NuxtJS application. I want to test the frontend using Cypress. I am battling to mock authenticate.
Here is a simple Cypress test:
// cypress/e2e/user_authentication_test.cy.js
describe("A User logging in", () => {
it.only("can login by supplying the correct credentials", () => {
cy.mockLogin().then(() => {
cy.visit(`${Cypress.env("BASE_URL")}/dashboard`)
.window()
.its("$nuxt.$auth")
.its("loggedIn")
.should("equal", true);
});
});
});
The test above fails at the should assertion, and the user is not redirected.
The mockLogin command is defined as:
// cypress/support/commands.js
Cypress.Commands.add(
'mockLogin',
(username = 'someone', password = 'my_secret_password_123') => {
cy.intercept('POST', 'http://localhost:5000/api/v1/auth/token', {
fixture: 'auth/valid_auth_token.json',
}).as('token_mock')
cy.visit(`${Cypress.env('BASE_URL')}/login`)
cy.get('#login-username').type(username)
cy.get('#login-password').type(`${password}{enter}`)
cy.wait('#token_mock')
}
)
Where valid_auth_token.json contains a JWT.
The actual login is done as follows:
<!-- components/auth/LoginForm.vue -->
<template>
<!-- Login form goes here -->
</template>
<script>
import jwt_decode from 'jwt-decode' // eslint-disable-line camelcase
export default {
name: 'LoginForm',
data() {
return {
username: '',
password: '',
}
},
methods: {
async login() {
const formData = new FormData()
formData.append('username', this.username)
formData.append('password', this.password)
try {
await this.$auth
.loginWith('cookie', {
data: formData,
})
.then((res) => {
const decode = jwt_decode(res.data.access_token) // eslint-disable-line camelcase
this.$auth.setUser(decode)
this.$router.push('/')
})
} catch (error) {
// error handling
}
},
},
}
</script>
I'm using laravel passport for API's and nuxt.js for frontend after a successful login if I refresh the page the user is not authenticated anymore and loggedIn returns false, its my first nuxt.js project so I have no idea how to deal with that, any advise is appreciated
login.vue
<script>
import { mapActions } from 'vuex'
export default {
data() {
return {
email: "",
password: ""
}
},
methods:{
async login(){
const succesfulLogin = await this.$auth.loginWith('local', {
data: {
email: this.email,
password: this.password
},
})
this.$store.commit("saveUser",succesfulLogin.data)
this.$store.commit("saveToken", succesfulLogin.data.token)
if (succesfulLogin) {
await this.$auth.setUser({
email: this.email,
password: this.password,
})
this.$router.push('/profile')
}
}
}
}
</script>
store/index.js
export const state = () => ({
user:{},
token: ""
})
export const mutations = {
saveUser(state, payload) {
state.user=payload;
},
saveToken(state, token) {
state.token= token
}
}
export const actions = {
saveUserAction({commit}, UserObject){
commit('saveUser');
},
logoutUser({commit}){
commit('logout_user')
}
}
export const getters = {
getUser: (state) => {
return state.user
},
isAuthenticated(state) {
return state.auth.loggedIn
},
loggedInUser(state) {
return state.user.user
}
}
after a successful login
after refreshing the page
We do use a global middleware right after my auth module authentication
/middleware/global.js
export default async ({ app, store }) => {
if (store?.$auth?.$state?.loggedIn) {
if (!app.$cookies.get('gql.me_query_expiration')) {
// do some middleware logic if you wish
await app.$cookies.set('gql.me_query_expiration', '5min', {
// maxAge: 20,
maxAge: 5 * 60,
secure: true,
})
}
}
}
nuxt.config.js
router: {
middleware: ['auth', 'global'],
},
We're using cookie-universal-nuxt for handling secure cookies quickly, working great!
While accessing or refreshing the webapp (we do redirect to the /login page if not authenticated) and we use this basic GraphQL configuration where the cookie is needed.
/plugins/nuxt-apollo-config.js
export default ({ app }) => {
const headersConfig = setContext(() => ({
credentials: 'same-origin',
headers: {
Authorization: app.$cookies.get('auth._token.local'), // here
},
}))
[...]
}
Checking gql.me_query_expiration allows us to see if the user has authenticated lately/is currently authenticated or if he needs to refresh his token.
And auth._token.local is our actual JWT token, provided by the auth module.
As told above, it is more secure to have a secure cookie than some localStorage, this is also why we are not using it
nuxt.config.js
auth: {
localStorage: false, // REALLY not secure, so nah
...
}
You can just use localStorage and implement it yourself e.g.:
saveToken(state, token) {
localStorage.setItem("authToken", token);
state.token= token
},
saveUser(state, payload) {
localStorage.setItem("authUser", payload);
state.user=payload;
},
And then retrieving the localStorage when initializing your store you need to do something like this:
export const state = () => {
const localUser = localStorage.getItem("authToken")
const localToken = localStorage.getItem("authUser")
let user = {}
let token = ""
if (localUser) user = localUser
if (localToken) token = localToken
return {
user: user,
token: token
}
}
As #mbuechmann pointed out, be aware of the security risk when storing sensitive information in localStorage. Better to use cookies for tokens, but localStorage is the 'simple' solution.
or use a package like nuxt-vuex-localstorage
I cannot correctly set my jwt token from my cookie to my Headers for an authenticaed gql request using apollo client.
I believe the problem is on my withApollo.js file, the one that wraps the App component on _app.js. The format of this file is based off of the wes bos advanced react nextjs graphql course. What happens is that nextauth saves the JWT as a cookie, and I can then grab the JWT from that cookie using a custom regex function. Then I try to set this token value to the authorization bearer header. The problem is that on the first load of a page with a gql query needing a jwt token, I get the error "Cannot read property 'cookie' of undefined". But, if I hit browser refresh, then suddenly it works and the token was successfully set to the header.
Some research led me to adding a setcontext link and so that's where I try to perform this operation. I tried to async await setting the token value but that doesn't seem to have helped. It just seems like the headers don't want to get set until on the refresh.
lib/withData.js
import { ApolloClient, ApolloLink, InMemoryCache } from '#apollo/client';
import { onError } from '#apollo/link-error';
import { getDataFromTree } from '#apollo/react-ssr';
import { createUploadLink } from 'apollo-upload-client';
import withApollo from 'next-with-apollo';
import { setContext } from 'apollo-link-context';
import { endpoint, prodEndpoint } from '../config';
import paginationField from './paginationField';
const getCookieValue = (name, cookie) =>
cookie.match(`(^|;)\\s*${name}\\s*=\\s*([^;]+)`)?.pop() || '';
let token;
function createClient(props) {
const { initialState, headers, ctx } = props;
console.log({ headers });
// console.log({ ctx });
return new ApolloClient({
link: ApolloLink.from([
onError(({ graphQLErrors, networkError }) => {
if (graphQLErrors)
graphQLErrors.forEach(({ message, locations, path }) =>
console.log(
`[GraphQL error]: Message: ${message}, Location: ${locations}, Path: ${path}`
)
);
if (networkError)
console.log(
`[Network error]: ${networkError}. Backend is unreachable. Is it running?`
);
}),
setContext(async (request, previousContext) => {
token = await getCookieValue('token', headers.cookie);
return {
headers: {
authorization: token ? `Bearer ${token}` : '',
},
};
}),
createUploadLink({
uri: process.env.NODE_ENV === 'development' ? endpoint : prodEndpoint,
fetchOptions: {
credentials: 'include',
},
headers,
}),
]),
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
// TODO: We will add this together!
// allProducts: paginationField(),
},
},
},
}).restore(initialState || {}),
});
}
export default withApollo(createClient, { getDataFromTree });
page/_app.js
import { ApolloProvider } from '#apollo/client';
import NProgress from 'nprogress';
import Router from 'next/router';
import { Provider, getSession } from 'next-auth/client';
import { CookiesProvider } from 'react-cookie';
import nookies, { parseCookies } from 'nookies';
import Page from '../components/Page';
import '../components/styles/nprogress.css';
import withData from '../lib/withData';
Router.events.on('routeChangeStart', () => NProgress.start());
Router.events.on('routeChangeComplete', () => NProgress.done());
Router.events.on('routeChangeError', () => NProgress.done());
function MyApp({ Component, pageProps, apollo, user }) {
return (
<Provider session={pageProps.session}>
<ApolloProvider client={apollo}>
<Page>
<Component {...pageProps} {...user} />
</Page>
</ApolloProvider>
</Provider>
);
}
MyApp.getInitialProps = async function ({ Component, ctx }) {
let pageProps = {};
if (Component.getInitialProps) {
pageProps = await Component.getInitialProps(ctx);
}
pageProps.query = ctx.query;
const user = {};
const { req } = ctx;
const session = await getSession({ req });
if (session) {
user.email = session.user.email;
user.id = session.user.id;
user.isUser = !!session;
// Set
nookies.set(ctx, 'token', session.accessToken, {
maxAge: 30 * 24 * 60 * 60,
path: '/',
});
}
return {
pageProps,
user: user || null,
};
};
export default withData(MyApp);
api/auth/[...nextAuth.js]
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import axios from 'axios';
const providers = [
Providers.Google({
clientId: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
}),
Providers.Credentials({
name: 'Credentials',
credentials: {
username: { label: 'Username', type: 'text', placeholder: 'jsmith' },
password: { label: 'Password', type: 'password' },
},
authorize: async (credentials) => {
const user = await axios
.post('http://localhost:1337/auth/local', {
identifier: credentials.username,
password: credentials.password,
})
.then((res) => {
res.data.user.token = res.data.jwt;
return res.data.user;
}) // define user as res.data.user (will be referenced in callbacks)
.catch((error) => {
console.log('An error occurred:', error);
});
if (user) {
return user;
}
return null;
},
}),
];
const callbacks = {
// Getting the JWT token from API response
async jwt(token, user, account, profile, isNewUser) {
// WRITE TO TOKEN (from above sources)
if (user) {
const provider = account.provider || user.provider || null;
let response;
let data;
switch (provider) {
case 'google':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/google/callback?access_token=${account?.accessToken}`
);
data = await response.json();
if (data) {
token.accessToken = data.jwt;
token.id = data.user._id;
} else {
console.log('ERROR No data');
}
break;
case 'local':
response = await fetch(
`${process.env.NEXT_PUBLIC_API_URL}/auth/local/callback?access_token=${account?.accessToken}`
);
data = await response.json();
token.accessToken = user.token;
token.id = user.id;
break;
default:
console.log(`ERROR: Provider value is ${provider}`);
break;
}
}
return token;
},
async session(session, token) {
// WRITE TO SESSION (from token)
// console.log(token);
session.accessToken = token.accessToken;
session.user.id = token.id;
return session;
},
redirect: async (url, baseUrl) => baseUrl,
};
const sessionPreferences = {
session: {
jwt: true,
},
};
const options = {
providers,
callbacks,
sessionPreferences,
};
export default (req, res) => NextAuth(req, res, options);
I am taking data from api, how to send get request with token after login?
It shows error createError.js?2d83:16 Uncaught (in promise) Error: Request failed with status code 401
export default new Vuex.Store({
state: {
users: []
},
mutations: {
setUsers(state, args){
state.users = args;
}
},
actions: {
login({ }, arg) {
axios.post('login/', { username: arg.username, password: arg.password })
.then((response) => {
console.log(response);
let accessToken = response.token.data;
localStorage.setItem('token', accessToken);
localStorage.setItem('user', response.data.user);
axios.defaults.headers.common['Authorization'] = accessToken;
window.isSignedIn = true;
router.push('/');
})
.catch((error) => {
console.log(error);
})
},
getUsers({ commit }){
let { data } = axios.get('v1/user/list/');
commit('setUsers', data);
}
}
})
Depends which authentication you are using. Try with:
axios.defaults.headers.common['Authorization'] = `Bearer ${accessToken}`;
Other HTTP authentication schemes.
I call the react-navigation NavigationService within a redux action.
Testing the action I need to mock the navigate function.
/app/utils/NavigationService.js
import { NavigationActions } from 'react-navigation';
let navigator;
function setTopLevelNavigator(navigatorRef) {
navigator = navigatorRef;
}
function navigate(routeName, params) {
navigator.dispatch(NavigationActions.navigate({
type: NavigationActions.NAVIGATE,
routeName,
params,
}));
}
// add other navigation functions that you need and export them
export default {
navigate,
setTopLevelNavigator,
};
I created a __mock__ folder immediately adjacent to the NavigationService.js file.
app/utils/__mocks__/NavigationService.js UPDATED
const navigate = jest.fn();
const setTopLevelNavigator = jest.fn();
export default {
navigate,
setTopLevelNavigator,
};
Why doesn´t jest auto-mock the navigate function when the test is run?
https://jestjs.io/docs/en/manual-mocks
__tests__/actions/AuthActions.test.js UPDATED
jest.mock('../../app/utils/NavigationService'); //at the top directly behind other imports
it('should call firebase on signIn', () => {
const user = {
email: 'test#test.com',
password: 'sign',
};
const expected = [
{ type: types.LOGIN_USER },
{ payload: 1, type: types.DB_VERSION },
{ payload: 'prod', type: types.USER_TYPE },
{ payload: { name: 'data' }, type: types.WEEKPLAN_FETCH_SUCCESS },
{ payload: { name: 'data' }, type: types.RECIPELIBRARY_FETCH_SUCCESS },
{
payload: { user: { name: 'user' }, userVersionAndType: { dbVersion: 1, userType: 'prod' } },
type: types.LOGIN_USER_SUCCESS,
},
];
return store.dispatch(actions.loginUser(user)).then(() => {
expect(store.getActions()).toEqual(expected);
});
});
app/actions/AuthActions.js
export const loginUser = ({ email, password }) => (dispatch) => {
dispatch({ type: LOGIN_USER });
return firebase
.auth()
.signInWithEmailAndPassword(email, password)
.catch((signInError) => {
dispatch({ type: CREATE_USER, payload: signInError.message });
return firebase
.auth()
.createUserWithEmailAndPassword(email, password)
.then(async (user) => {
const userVersionAndType = await dispatch(initUser());
await dispatch(initWeekplan(userVersionAndType));
await dispatch(initRecipeLibrary(userVersionAndType));
return user;
});
})
.then(async (user) => {
saveCredentials(email, password);
const userVersionAndType = await dispatch(getUserVersionAndType());
await dispatch(weekplanFetch(userVersionAndType));
await dispatch(recipeLibraryFetch(userVersionAndType));
dispatch(loginUserSuccess({ user, userVersionAndType }));
NavigationService.navigate('Home');
})
.catch(error => dispatch(loginUserFail(error.message)));
};
You've create a manual mock for a user module.
Activating a manual mock of a user module for a particular test file requires a call to jest.mock.
For this particular case add this line to the top of __tests__/actions/AuthActions.test.js and the mock will be used for all tests within that test file:
jest.mock('../../app/utils/NavigationService'); // use the manual mock in this test file
Note that manual mocks for user modules and Node core modules (like fs, path, util, etc.) both have to be activated for a particular test file by a call to jest.mock, and that this behavior is different than manual mocks for Node modules which are automatically applied to all tests.