Nuxt auth update auth on custom requests - javascript

From nuxt auth website I saw this:
setUserToken(token)
Returns: Promise
Set the auth token and fetch the user using the new token and current strategy.
TIP: This function can properly set the user after registration
this.$auth.setUserToken(token)
.then(() => this.$toast.success('User set!'))
Tried to use it and it said method is undefined, looked up in the source files and none of methods are like this one.
I am not very good with this but, how would I set user and token with nuxt/auth module after registration or anything but login/loginWith?
If there is no option for that why is it there on documentation?
I would also need to know if I need to create custom auth do I need to use both cookies and localstorage or just one of them?
It says that cookies are used for server side and storage for client side.
Can I use just cookies and on nuxtServerInit get cookie for token and set token and user data fetched by api within vuex store? Then use it from there if it is needed?

Nuxt/auth module hurt my brain so long and today I created custom module:
First I have this store structure:
store/
-- index.js
-- mutations.js
-- actions.js
-- state.js
-- getters.js
middleware/
-- redirectIfAuth.js
-- redirectIfNotAuth.js
layouts/
default.vue -> has redirectIfNotAuth.js
guest.vue -> has redirectIfAuth.js
pages/
-- login/
---- index.vue -> uses guest.vue as layout
-- dashboard/
----- index.vue -> uses default.vue as layout without declaration
Inside Index.js I have:
import state from './state'
import * as actions from './actions'
import * as mutations from './mutations'
import * as getters from './getters'
export default {
state,
getters,
mutations,
actions,
modules: {}
}
Inside State.js I have:
export default () => ({
user: null,
token: null,
headers: null
})
Inside Actions.js I have:
const cookieparser = process.server ? require('cookieparser') : undefined
// importing server based cookie library
export async function nuxtServerInit ({ commit }, { req, res }) {
// If we have any axios requests we need to add async/await
// And since this works on server mode, we don't need to check is it server
let token = null
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
token = parsed.authToken
} catch (e) {
console.log(e)
}
}
// If we have token within cookies we get user data from api and we pass Autorization headers with token
if (token !== null && token !== false) {
await axios.get('/api/auth/me', {
headers: {
'Authorization': token
}
}).then((response) => {
// If we get user data we set it to store
commit('setUser', response.data.data)
commit('setToken', token)
commit('setHeaders', token)
}).catch((error) => {
// If we get error, we should logout user by removing data within cookies and store
// Additionally you can create specific code error on backend to check if token is expired or invalid
// and then check for status code and then remove data
commit('setUser', null)
commit('setToken', null)
res.setHeader('Set-Cookie', [`authToken=false; expires=Thu, 01 Jan 1970 00:00:00 GMT`])
// This is only way I found useful for removing cookies from node server
console.warn(error)
})
}
}
Inside Mutations.js I have:
export const setToken = (state, payload) => state.token = payload
export const setUser = (state, payload) => state.user = payload
export const setHeaders = (state, payload) => {
state.headers = {
headers: {
'Authorization': payload
}
}
}
Inside Getters.js I have:
export const getUser = (state) => state.user
export const getToken = (state) => state.token
export const getHeaders = (state) => state.headers
Second I created two middlewares and it seems like nuxt middlewares work on both server and client sides, so I needed to require both libraries for server and client side Then I checked which side it is and then try to get token for further investigations If you include and don't check for server and client but use one of them, your templates wont render but show undefined errors for req on client instead and on server it wont show anything.
Inside redirectIfAuth.js I have:
const cookieparser = process.server ? require('cookieparser') : undefined
const Cookie = process.client ? require('js-cookie') : undefined
export default function ({ app, redirect, req }) {
let token = null
if (process.server) {
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
token = parsed.authToken
} catch (e) {
console.log(e)
}
}
} else if (process.client) {
token = Cookie.get('authToken')
}
if (token && token !== false) {
app.store.commit('setToken', token)
app.store.commit('setHeaders', token)
if (app.store.state.user) {
if (app.store.state.user.roles.includes('customer')) {
return redirect({
name: 'customer-slug',
params: { slug: app.store.state.user.username }
})
} else if (app.store.state.user.roles.includes('admin')) {
return redirect({
name: 'dashboard'
})
} else {
return redirect({
name: 'index'
})
}
} else {
return redirect({
name: 'index'
})
}
}
}
Inside redirectIfNotAuth.js I have:
const cookieparser = process.server ? require('cookieparser') : undefined
const Cookie = process.client ? require('js-cookie') : undefined
export default function ({ app, redirect, route, req }) {
let token = null
if (process.server) {
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
token = parsed.authToken
} catch (e) {
console.log(e)
}
}
} else if (process.client) {
token = Cookie.get('authToken')
}
if (token === null || token === false) {
return redirect({
name: 'login',
query: {
redirect: route.fullPath
}
})
}
}
Now we use these middlewares within pages or layouts as:
export default {
middleware: ['redirectIfAuth']
}
Or
export default {
middleware: ['redirectIfNotAuth']
}
Login:
async login () {
if (this.form.email !== '' && this.form.password !== '') {
await this.$axios.post('/api/auth/login', this.form).then((response) => {
this.$store.commit('setUser', response.data.data)
this.$store.commit('setToken', 'Bearer ' + response.data.meta.access_token)
this.$store.commit('setHeaders', 'Bearer ' + response.data.meta.access_token)
Cookie.set('authToken', 'Bearer ' + response.data.meta.access_token, { expires: 365 })
// Cookie.set('authUser', response.data.data, { expires: 365 }) if you need user data within cookies
if (this.$route.query.redirect) {
this.$router.push(this.$route.query.redirect)
}
this.$router.push('/')
})
}
}
Logout:
async logout () {
await this.$axios.post('/api/auth/logout', {}, this.headers)
// Cookie.remove('authUser') if exists
Cookie.remove('authToken')
this.$router.push('/')
}
I hope this helps someone or someone get idea from this to make something else. I had million problems with official nuxt auth and only this helped me sort things out...

Related

Remix Auth - `destroySession` returning [object Promise] instead of clearing the session

I'm trying to implement User Authentication in a Remix app and have been going around in circles trying to figure out why destroying the session is returning [object Promise] instead of clearing it.
My auth.server.js file contains functions that manage anything relating to creating, retrieving and destroying the Session Cookie, as well as a function to login.
import { hash, compare } from "bcryptjs";
import { prisma } from "./database.server";
import { createCookieSessionStorage, redirect } from "#remix-run/node";
export const sessionStorage = createCookieSessionStorage({
cookie: {
secure: process.env.NODE_ENV === "production",
path: "/",
secrets: [process.env.SESSION_SECRET],
sameSite: "lax",
maxAge: 30 * 24 * 60 * 60, // 30 days
httpOnly: true
}
});
async function createUserSession(userId, redirectPath) {
const session = await sessionStorage.getSession();
session.set("userId", userId);
return redirect(redirectPath, {
headers: {
"Set-Cookie": await sessionStorage.commitSession(session)
}
});
}
export async function getUserFromSession(request) {
const session = await sessionStorage.getSession(
request.headers.get("Cookie")
);
const userId = session.get("userId");
return userId ? userId : null;
}
export function destroyUserSession(request) {
const session = sessionStorage.getSession(request.headers.get("Cookie"));
return redirect("/", {
headers: {
"Set-Cookie": sessionStorage.destroySession(session)
}
});
}
export async function login({ email, password }) {
const existingUser = await prisma.user.findFirst({ where: { email } });
if (!existingUser) {
return throwError(
"Could not log you in, please check provided email.",
401
);
}
const passwordCorrect = await compare(password, existingUser.password);
if (!passwordCorrect) {
return throwError(
"Could not log you in, please check provided password.",
401
);
}
return createUserSession(existingUser.id, "/");
}
function throwError(text, code) {
const error = new Error(text);
error.status = code; // authentication error code
throw error; // this triggers ErrorBoundary, as it's not an error response
}
My logout function is in a separate .js file located inside the /routes folder.
import { json } from "#remix-run/node";
import { destroyUserSession } from "~/data/auth.server";
export function action({ request }) {
if (request.method !== "POST") {
throw json({ message: "Invalid request method" }, { status: 400 });
}
return destroyUserSession(request);
}
Finally, the Logout button is inside the navigation bar and contained within a <Form> element.
<Form method="post" action="/logout">
<button type="submit">Logout</button>
</Form>
SOLVED
I was somehow able to solve it by moving the logout function into auth.server.js and leaving the following in the logout.js route:
import { redirect } from "#remix-run/node";
import { logout } from "~/data/auth.server";
export const action = ({ request }) => logout(request);
export const loader = () => redirect("/auth");
The session functions are all async. The reason you had [object Promise] was that you were returning the promise directly. You need to use await.
export function destroyUserSession(request) {
const session = sessionStorage.getSession(request.headers.get("Cookie"));
return redirect("/", {
headers: {
// use await on session functions
"Set-Cookie": await sessionStorage.destroySession(session)
}
});
}
P.S. That's one of the benefits of TypeScript. It would have told you that headers required a string value and would not allow the Promise return.

I am losing user information (session) on refreshing and between tabs while using iron-session and next.js

I am currently working on a project using iron-session and next.js. I don't lose my user on clicking Link tags. But if I refresh user becomes undefined. Cookie is set and doesn't get deleted on refresh. I don't know what is wrong.
Here is my login.ts code:
export default withIronSessionApiRoute(loginRoute, sessionOptions);
async function loginRoute(req: NextApiRequest, res: NextApiResponse) {
try {
const {
data: {tcId, admin, userName},
} = await axios('http://localhost:8080/user/login', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
data: JSON.stringify(req.body),
});
const user = {tcId: tcId, userName: userName, admin: admin} as User;
req.session.user = user;
await req.session.save();
res.json(user);
} catch (error) {
res.status(500).json({message: (error as Error).message})
}}
Here is my session.ts code:
// this file is a wrapper with defaults to be used in both API routes and `getServerSideProps` functions
import type { IronSessionOptions } from 'iron-session'
import type { User } from '../pages/api/user'
export const sessionOptions: IronSessionOptions = {
password: process.env.SECRET_COOKIE_PASSWORD as string,
cookieName: 'eys-cookie',
// secure: true should be used in production (HTTPS) but can't be used in development (HTTP)
cookieOptions: {
secure: process.env.NODE_ENV === 'production',
},
};
// This is where we specify the typings of req.session.*
declare module 'iron-session' {
interface IronSessionData {
user?: User
}
}
As I said previously. I don't lose my user. While routing around using Link tags from next.
Refreshing causes to lose my user. Also other tabs don't reach my user.
I can show more code if wanted. But I think problem is here.
I had a similar issue and solved the tab switching by setting:
revalidateOnFocus = false,
for the useSWR function
sample
const fetcher = (url: any) => fetch(url).then((res) => res.json());
const options = {
revalidateOnFocus: false,
};
const {
data: session,
error,
mutate: mutateUser,
} = useSWR<Session>("/api/session", fetcher, options);
you can also explore other options here https://swr.vercel.app/docs/options
Probably you forgot to add SWRconfig on the index or app component:
import { AppProps } from 'next/app'
import { SWRConfig } from 'swr'
import fetchJson from 'lib/fetchJson'
function MyApp({ Component, pageProps }: AppProps) {
return (
<SWRConfig
value={{
fetcher: fetchJson,
onError: (err) => {
console.error(err)
},
}}
>
<Component {...pageProps} />
</SWRConfig>
)
}
export default MyApp

Understanding stores, context, sessions, and component hierarchy in Svelte/Sveltekit

I'm familiar with Svelte but completely new to Sveltekit. I'm trying to build a Sveltekit app from scratch using AWS Cognito as the authorization tool without using AWS Amplify, using the amazon-cognito-identity-js sdk. I've got all the functionality working as far as login, registration, and verification, but I can't seem to get a handle on how to store the session data for the structure I've built.
I've been trying to translate the strategies from this tutorial, based in React, to Sveltekit -- (AWS Cognito + React JS Tutorial - Sessions and Logging out (2020) [Ep. 3]) https://www.youtube.com/watch?v=R-3uXlTudSQ
and this REPL to understand using context in Svelte ([AD] Combining the Context API with Stores) https://svelte.dev/repl/7df82f6174b8408285a1ea0735cf2ff0
To elaborate, I've got my structure like so (only important parts shown):
src
|
|-- components
|-- ...
|-- status.svelte
|-- routes
|
|-- dashboard
|-- onboarding
|-- __layout.reset.svelte
|-- login.svelte
|-- signup.svelte
|-- verify.svelte
|-- ...
|-- settings
|-- __layout.svelte
|-- index.svelte
|-- styles
|-- utils
|-- cognitoTools.ts
|-- stores.ts
I wanted to have a separate path for my onboarding pages, hence the sub-folder. My cognito-based functions reside within cognitoTools.ts. An example of a few functions look like:
export const Pool = new CognitoUserPool(poolData);
export const User = (Username: string): any => new CognitoUser({ Username, Pool });
export const Login = (username: string, password: string): any => {
return new Promise ((resolve, reject) => User(username).authenticateUser(CognitoAuthDetails(username, password), {
onSuccess: function(result) {
console.log('CogTools login success result: ', result);
resolve(result)
},
onFailure: function(err) {
console.error('CogTools login err: ', err);
reject(err)
}
}))
}
I'm able to then use the methods freely anywhere:
// src/routes/onboarding/login.svelte
import { Login, Pool } from '#utils/cognitoTools'
import { setContext, getContext } from 'svelte'
let username;
let password;
let session = writeable({});
let currentSession;
// Setting our userSession store to variable that will be updated
$: userSession.set(currentSession);
// Attempt to retrieve getSession func defined from wrapper component __layout.svelte
const getSession = getContext('getSession');
const handleSubmit = async (event) => {
event.preventDefault()
Login(username, password, rememberDevice)
.then(() => {
getSession().then((session) => {
// userSession.set(session);
currentSession = session;
setContext('currentSession', userSession);
})
})
}
...
// src/routes/__layout.svelte
...
const getSession = async () => {
return await new Promise((resolve, reject) => {
const user = Pool.getCurrentUser();
if (user) {
user.getSession((err, session) => {
console.log('User get session result: ', (err ? err : session));
err ? reject() : resolve(session);
});
} else {
console.log('get session no user found');
reject();
}
})
}
setContext('getSession', getSession)
Then, I've been trying to retrieve the session in src/components/status.svelte or src/routes/__layout.svelte (as I think I understand context has to be set in the top level components, and can then be used by indirect child components) to check if the context was set correctly.
Something like:
let status = false;
const user = getContext('currentSession');
status = user ? true : false;
I'm running in circles and I know I'm so close to the answer. How do I use reactive context with my current file structure to accomplish this?
I don't know much about the sdk, so I can't help you with your code above. But I also built an app that uses cognito for auth, and I can share some snippets on how to do it from scratch.
Implement a login form. I have my basic app skeleton (navbar, footer, main slot) in _layout.svelte, and it is configured to show the Login.svelte component and not the main slot if the user is not logged in.
file: __layout.svelte
<script context="module">
export async function load({ session }) {
return {
props: {
user: session.user,
}
}
}
</script>
<script>
import "../app.css";
import Login from "$components/Login.svelte";
export let user
</script>
<svelte:head>
<title>title</title>
</svelte:head>
{#if user}
<header>
</header>
<main>
<slot />
</main>
{:else}
<Login />
{/if}
file: Login.svelte
<form action="/" method="GET">
<input type="hidden" name="action" value="signin" />
<button type="submit" >Sign in</button>
</form>
Handle the login
I choose to do this as a svelte endpoint paired with the index. It keeps the routing super simple. You could do separate login.js and logout.js endpoints if you prefer. Just change your url in the form above.
file: index.js
import { v4 as uuid } from '#lukeed/uuid'
import db from '$lib/db'
const domain = import.meta.env.VITE_COGNITO_DOMAIN
const clientId = import.meta.env.VITE_COGNITO_CLIENT_ID
const redirectUri = import.meta.env.VITE_COGNITO_REDIRECT_URI
const logoutUri = import.meta.env.VITE_COGNITO_LOGOUT_URI
export const get = async (event) => {
const action = event.url.searchParams.get('action')
if (action === 'signin') {
// Hard to guess random string. Used to protect against forgery attacks.
// Should add check in callback that the state matches to prevent forgery
const state = uuid()
return {
status: 302,
headers: {
location: `https://${domain}/oauth2/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=openid+email+profile&state=${state}`,
},
}
}
if (action === 'signout') {
// delete this session from database
if (event.locals.session_id) {
await db.sessions.deleteMany({
where: { session_id: event.locals.session_id }
})
}
return {
status: 302,
headers: {
location: `https://${domain}/logout?client_id=${clientId}&logout_uri=${logoutUri}`
}
}
}
return {}
}
Handle the callback from AWS cognito. Again, I have the callback simply point to the root url. All authentication for me is handled at "/". The heavy lifting is done by hooks.js. This is your SK middleware. It for me is the sole arbiter of the user's authentication state, just because I like to keep it easy for me to understand.
file: hooks.js
import { v4 as uuid } from '#lukeed/uuid'
import cookie from 'cookie'
import db from '$lib/db'
const domain = import.meta.env.VITE_COGNITO_DOMAIN
const clientId = import.meta.env.VITE_COGNITO_CLIENT_ID
const clientSecret = import.meta.env.VITE_COGNITO_CLIENT_SECRET
const redirectUri = import.meta.env.VITE_COGNITO_REDIRECT_URI
const tokenUrl = `https://${domain}/oauth2/token`
const profileUrl = `https://${domain}/oauth2/userInfo`
export const handle = async ({ event, resolve }) => {
const cookies = cookie.parse(event.request.headers.get('cookie') || '')
event.locals.session_id = cookies.session_id // this will be overwritten by a new session_id if this is a callback
if (event.locals.session_id) {
// We have a session cookie, check to see if it is valid.
// Do this by checking against your session db or session store or whatever
// If not valid, or if it is expired, set event.locals.session_id to null
// This will cause the cookie to be deleted below
// In this example, we just assume it's valid
}
if ( (!event.locals.session_id) && event.url.searchParams.get('code') && event.url.searchParams.get('state') ) {
// No valid session cookie, check to see if this is a callback
const code = event.url.searchParams.get('code')
const state = event.url.searchParams.get('state')
// Change this to try, catch for error handling
const token = await getToken(code, state)
if (token != null) {
let cognitoUser = await getUser(token)
event.locals.session_id = uuid()
// Add the value to the db
await db.sessions.create({
data: {
session_id: event.locals.session_id,
user_id: cognitoUser.username,
created: Date()
},
})
let user = await db.users.findUnique({
where: {
user_id: cognitoUser.username,
}
})
event.locals.user = user
event.locals.authorized = true
}
}
const response = await resolve(event);
// This will delete the cookie if event.locals.session_id is null
response.headers.set(
'set-cookie',
cookie.serialize('session_id', event.locals.session_id, {
path: '/',
httpOnly: true,
sameSite: 'strict',
maxAge: 60 * 60 * 24 * 7, // one week
})
)
return response;
}
export async function getSession(event) {
return {
user: event.locals.user,
}
}
const getToken = async (code, state) => {
let authorization = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
const res = await fetch(tokenUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
Authorization: `Basic ${authorization}`,
},
body: `grant_type=authorization_code&client_id=${clientId}&code=${code}&state=${state}&redirect_uri=${redirectUri}`
})
if (res.ok) {
const data = await res.json()
return data.access_token
} else {
return null
}
}
const getUser = async (token) => {
const res = await fetch(profileUrl, {
headers: {
Authorization: `Bearer ${token}`,
},
})
if (res.ok) {
return res.json()
} else {
return null
}
}
Lastly, getting the auth state. In a client-side route, this is done via the page load function. I put this in __layout to have it available on all .svelte routes. You can see it at the top of the __layout file above. For SSR endpoints, you can just access event.locals directly.
NOTE: All of the env vars are set in your .env file and will be imported by vite. This only happens when you start your app, so if you add/change them, you need to restart it.
I don't know if this helps at all since it is so different from your app structure, but maybe you will get some ideas from it.
I don't know what the problem is you run into exactly, but one thing that stands out to me is that you are calling setContext when it's "too" late. You can only call getContext/setContext within component initialization. See this answer for more details: Is there a way to use svelte getContext etc. svelte functions in Typescript files?
If this is the culprit and you are looking for a way how to get the session then: Use context in combination with stores:
<!-- setting the session -->
<script>
// ...
const session = writable(null);
setContext('currentSession', session);
// ...
Login...then(() => ...session.set(session));
</script>
<!-- setting the session -->
<script>
// ..
const user = getContext('currentSession');
// ..
status = $user ? true : false;
</script>
Another thing that stands out to me - but is too long/vague for a StackOverflow answer - is that you are not using SvelteKit's features to achieve this behavior. You could look into load and use stuff in __layout to pass the session down to all children. I'm no sure if this is of any advantage for you though since you are maybe planning to do a SPA anyway and therefore don't need such SvelteKit features.

vue-apollo 3.0.0 Beta configuration

Pretty new at this so any help much appreciated.
I know how to do Authentication with Apollo client but when I add to my Vue-cli-3 generated project the new vue-apollo-plugin (https://www.npmjs.com/package/vue-apollo). I don't understand how and where to configure my authMiddleware.
Here is the auto generated file form the the cli:
import Vue from 'vue'
import VueApollo from 'vue-apollo'
import { createApolloClient, restartWebsockets } from 'vue-cli-plugin-apollo/graphql-client'
// Install the vue plugin
Vue.use(VueApollo)
// Name of the localStorage item
const AUTH_TOKEN = 'apollo-token'
// Config
const defaultOptions = {
httpEndpoint: process.env.VUE_APP_GRAPHQL_HTTP || 'http://localhost:4000', // Use `null` to disable subscriptions
wsEndpoint: process.env.VUE_APP_GRAPHQL_WS || 'ws://localhost:4000',
// LocalStorage token
tokenName: AUTH_TOKEN,
// Enable Automatic Query persisting with Apollo Engine
persisting: false,
// Use websockets for everything (no HTTP)
// You need to pass a `wsEndpoint` for this to work
websocketsOnly: false,
// Is being rendered on the server?
ssr: false,
// Additional ApolloClient options
// apollo: { ... }
// Client local data (see apollo-link-state)
// clientState: { resolvers: { ... }, defaults: { ... } }
}
// Call this in the Vue app file
export function createProvider (options = {}) {
// Create apollo client
const { apolloClient, wsClient } = createApolloClient({
...defaultOptions,
...options,
})
apolloClient.wsClient = wsClient
// Create vue apollo provider
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
defaultOptions: {
$query: {
// fetchPolicy: 'cache-and-network',
},
},
errorHandler (error) {
// eslint-disable-next-line no-console
console.log('%cError', 'background: red; color: white; padding: 2px 4px; border-radius: 3px; font-weight: bold;', error.message)
},
})
return apolloProvider
}
// Manually call this when user log in
export async function onLogin (apolloClient, token) {
localStorage.setItem(AUTH_TOKEN, token)
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
try {
await apolloClient.resetStore()
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (login)', 'color: orange;', e.message)
}
}
// Manually call this when user log out
export async function onLogout (apolloClient) {
localStorage.removeItem(AUTH_TOKEN)
if (apolloClient.wsClient) restartWebsockets(apolloClient.wsClient)
try {
await apolloClient.resetStore()
} catch (e) {
// eslint-disable-next-line no-console
console.log('%cError on cache reset (logout)', 'color: orange;', e.message)
}
}
I have what I would previously use for authentication via the header here:
const authMiddleware = new ApolloLink((operation, forward) => {
// add the authorization to the headers
const token = localStorage.getItem(AUTH_TOKEN)
operation.setContext({
headers: {
authorization: token ? `Bearer ${token}` : null
}
})
return forward(operation)
})
It seems like when I dig a bit deeper into some of the imported objects from the vue-apollo package there is something like this already built in in the createApolloClient object it has this property:
authLink = setContext(function (_, _ref2) {
var headers = _ref2.headers;
return {
headers: _objectSpread({}, headers, {
authorization: getAuth(tokenName)
})
};
});
Does this mean I can simply destructure the property off the createApolloClient object? Any help or tips much appreciated.
Take a look at vue-cli-plugin-apollo
You can pass a link: authLink and\or getAuth:()=>{return "something"} in const defaultOptions = { ... } in /vue-apollo.js.
Or in main.js when you call createProvider
new Vue({
// router, store
apolloProvider: createProvider({
link: authLink,
getAuth: AUTH_TOKEN => localStorage.getItem(AUTH_TOKEN)
}),
// ...
})
using both if you adding header in authLink, getAuth is probably redundant.
if you plan to use more than one link, there is apollo-link package link: ApolloLink.from([ ... ])

How to use Redux to refresh JWT token?

Our React Native Redux app uses JWT tokens for authentication. There are many actions that require such tokens and a lot of them are dispatched simultaneously e.g. when app loads.
E.g.
componentDidMount() {
dispath(loadProfile());
dispatch(loadAssets());
...
}
Both loadProfile and loadAssets require JWT. We save the token in the state and AsyncStorage. My question is how to handle token expiration.
Originally I was going to use middleware for handling token expiration
// jwt-middleware.js
export function refreshJWTToken({ dispatch, getState }) {
return (next) => (action) => {
if (isExpired(getState().auth.token)) {
return dispatch(refreshToken())
.then(() => next(action))
.catch(e => console.log('error refreshing token', e));
}
return next(action);
};
}
The problem that I ran into was that refreshing of the token will happen for both loadProfile and loadAssets actions because at the time when they are dispatch the token will be expired. Ideally I would like to "pause" actions that require authentication until the token is refreshed. Is there a way to do that with middleware?
I found a way to solve this. I am not sure if this is best practice approach and there are probably some improvements that could be made to it.
My original idea stays: JWT refresh is in the middleware. That middleware has to come before thunk if thunk is used.
...
const createStoreWithMiddleware = applyMiddleware(jwt, thunk)(createStore);
Then in the middleware code we check to see if token is expired before any async action. If it is expired we also check if we are already are refreshing the token -- to be able to have such check we add promise for fresh token to the state.
import { refreshToken } from '../actions/auth';
export function jwt({ dispatch, getState }) {
return (next) => (action) => {
// only worry about expiring token for async actions
if (typeof action === 'function') {
if (getState().auth && getState().auth.token) {
// decode jwt so that we know if and when it expires
var tokenExpiration = jwtDecode(getState().auth.token).<your field for expiration>;
if (tokenExpiration && (moment(tokenExpiration) - moment(Date.now()) < 5000)) {
// make sure we are not already refreshing the token
if (!getState().auth.freshTokenPromise) {
return refreshToken(dispatch).then(() => next(action));
} else {
return getState().auth.freshTokenPromise.then(() => next(action));
}
}
}
}
return next(action);
};
}
The most important part is refreshToken function. That function needs to dispatch action when token is being refreshed so that the state will contain the promise for the fresh token. That way if we dispatch multiple async actions that use token auth simultaneously the token gets refreshed only once.
export function refreshToken(dispatch) {
var freshTokenPromise = fetchJWTToken()
.then(t => {
dispatch({
type: DONE_REFRESHING_TOKEN
});
dispatch(saveAppToken(t.token));
return t.token ? Promise.resolve(t.token) : Promise.reject({
message: 'could not refresh token'
});
})
.catch(e => {
console.log('error refreshing token', e);
dispatch({
type: DONE_REFRESHING_TOKEN
});
return Promise.reject(e);
});
dispatch({
type: REFRESHING_TOKEN,
// we want to keep track of token promise in the state so that we don't try to refresh
// the token again while refreshing is in process
freshTokenPromise
});
return freshTokenPromise;
}
I realize that this is pretty complicated. I am also a bit worried about dispatching actions in refreshToken which is not an action itself. Please let me know of any other approach you know that handles expiring JWT token with redux.
Instead of "waiting" for an action to finish, you could instead keep a store variable to know if you're still fetching tokens:
Sample reducer
const initialState = {
fetching: false,
};
export function reducer(state = initialState, action) {
switch(action.type) {
case 'LOAD_FETCHING':
return {
...state,
fetching: action.fetching,
}
}
}
Now the action creator:
export function loadThings() {
return (dispatch, getState) => {
const { auth, isLoading } = getState();
if (!isExpired(auth.token)) {
dispatch({ type: 'LOAD_FETCHING', fetching: false })
dispatch(loadProfile());
dispatch(loadAssets());
} else {
dispatch({ type: 'LOAD_FETCHING', fetching: true })
dispatch(refreshToken());
}
};
}
This gets called when the component mounted. If the auth key is stale, it will dispatch an action to set fetching to true and also refresh the token. Notice that we aren't going to load the profile or assets yet.
New component:
componentDidMount() {
dispath(loadThings());
// ...
}
componentWillReceiveProps(newProps) {
const { fetching, token } = newProps; // bound from store
// assuming you have the current token stored somewhere
if (token === storedToken) {
return; // exit early
}
if (!fetching) {
loadThings()
}
}
Notice that now you attempt to load your things on mount but also under certain conditions when receiving props (this will get called when the store changes so we can keep fetching there) When the initial fetch fails, it will trigger the refreshToken. When that is done, it'll set the new token in the store, updating the component and hence calling componentWillReceiveProps. If it's not still fetching (not sure this check is necessary), it will load things.
I made a simple wrapper around redux-api-middleware to postpone actions and refresh access token.
middleware.js
import { isRSAA, apiMiddleware } from 'redux-api-middleware';
import { TOKEN_RECEIVED, refreshAccessToken } from './actions/auth'
import { refreshToken, isAccessTokenExpired } from './reducers'
export function createApiMiddleware() {
const postponedRSAAs = []
return ({ dispatch, getState }) => {
const rsaaMiddleware = apiMiddleware({dispatch, getState})
return (next) => (action) => {
const nextCheckPostponed = (nextAction) => {
// Run postponed actions after token refresh
if (nextAction.type === TOKEN_RECEIVED) {
next(nextAction);
postponedRSAAs.forEach((postponed) => {
rsaaMiddleware(next)(postponed)
})
} else {
next(nextAction)
}
}
if(isRSAA(action)) {
const state = getState(),
token = refreshToken(state)
if(token && isAccessTokenExpired(state)) {
postponedRSAAs.push(action)
if(postponedRSAAs.length === 1) {
return rsaaMiddleware(nextCheckPostponed)(refreshAccessToken(token))
} else {
return
}
}
return rsaaMiddleware(next)(action);
}
return next(action);
}
}
}
export default createApiMiddleware();
I keep tokens in the state, and use a simple helper to inject Acess token into a request headers
export function withAuth(headers={}) {
return (state) => ({
...headers,
'Authorization': `Bearer ${accessToken(state)}`
})
}
So redux-api-middleware actions stays almost unchanged
export const echo = (message) => ({
[RSAA]: {
endpoint: '/api/echo/',
method: 'POST',
body: JSON.stringify({message: message}),
headers: withAuth({ 'Content-Type': 'application/json' }),
types: [
ECHO_REQUEST, ECHO_SUCCESS, ECHO_FAILURE
]
}
})
I wrote the article and shared the project example, that shows JWT refresh token workflow in action
I think that redux is not the right tool for enforcing the atomicity of token refresh.
Instead I can offer you an atomic function that can be called from anywhere and ensures that you will always get a valid token:
/*
The non-atomic refresh function
*/
const refreshToken = async () => {
// Do whatever you need to do here ...
}
/*
Promise locking-queueing structure
*/
var promiesCallbacks = [];
const resolveQueue = value => {
promiesCallbacks.forEach(x => x.resolve(value));
promiesCallbacks = [];
};
const rejectQueue = value => {
promiesCallbacks.forEach(x => x.reject(value));
promiesCallbacks = [];
};
const enqueuePromise = () => {
return new Promise((resolve, reject) => {
promiesCallbacks.push({resolve, reject});
});
};
/*
The atomic function!
*/
var actionInProgress = false;
const refreshTokenAtomically = () => {
if (actionInProgress) {
return enqueuePromise();
}
actionInProgress = true;
return refreshToken()
.then(({ access }) => {
resolveQueue(access);
return access;
})
.catch((error) => {
rejectQueue(error);
throw error;
})
.finally(() => {
actionInProgress = false;
});
};
Posted also here: https://stackoverflow.com/a/68154638/683763

Categories

Resources