I have an access_denied error that occurs only for some users in my web app when they try to login user username/password.
Using the auth0-js 9.3.3 plugin for a VueJS 2.0 SPA using the authorization extension.
I get the following response in the logs window in Auth0. How can I debug these kind of errors and see what's the cause?
This results in the access_token and id_token being empty.
{
"date": "2018-06-03T11:15:15.478Z",
"type": "f",
"description": "Unexpected token { in JSON at position 19",
"connection": null,
"connection_id": "",
"client_id": "1ySh5N0sOXxMkcAslnuhRfxO5BloY56t",
"client_name": "IRIS",
"ip": "80.57.245.139",
"user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/66.0.3359.181 Safari/537.36",
"details": {
"body": {
"wa": "wsignin1.0",
"wresult": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VyX2lkIjoiNWFlODM0ZTc5MjA4YjgwNThhNGEyMDFkIiwiZW1haWwiOiJtYXJnZXJ0aG8xQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJsYXN0X3Bhc3N3b3JkX3Jlc2V0IjoiMjAxOC0wNS0xMlQxNDo1Njo0My4xNzFaIiwic2lkIjoiVVZQSThRWG9RdHRlNzg4c2c0Yy14VHZqNnFRRHdQVHMiLCJpYXQiOjE1MjgwMjQ1MTMsImV4cCI6MTUyODAyNDU3MywiYXVkIjoidXJuOmF1dGgwOjUwMXN0OlVzZXJuYW1lLVBhc3N3b3JkLUF1dGhlbnRpY2F0aW9uIiwiaXNzIjoidXJuOmF1dGgwIn0.lnK7k568DtWiUUEQEqn1PIOAQGeGQ6kg2Y6cwZUyl655ae-9dA-uM4oijD3ByDwVBL8aqFxxAAZmdPOr8pSpehTgsI7WTYrZee1sT2i80zG2IaKb-0Ue8Yx_7aGNMzhXVZHdkdY13EL5gzNeV52IVlhQLmUtDL9C4LZqApjN7wk",
"wctx": "{\"strategy\":\"auth0\",\"auth0Client\":\"eyJuYW1lIjoibG9jay5qcyIsInZlcnNpb24iOiIxMC4xOC4wIiwibGliX3ZlcnNpb24iOiI4LjcuMCJ9\",\"tenant\":\"501st\",\"connection\":\"Username-Password-Authentication\",\"client_id\":\"1ySh5N0sOXxMkcAslnuhRfxO5BloY56t\",\"response_type\":\"token id_token\",\"scope\":\"openid profile email groups permissions roles\",\"protocol\":\"oauth2\",\"redirect_uri\":\"http://localhost:8080/callback\",\"state\":\"s31LeZ-DQZLfAX10cTZ4AcoP9E7-nl-w\",\"nonce\":\"0j2CVd8Aogz2sgh8MaetsgEEq-uKo0sN\",\"sid\":\"UVPI8QXoQtte788sg4c-xTvj6qQDwPTs\",\"audience\":\"https://iris.501st.nl\",\"realm\":\"Username-Password-Authentication\",\"session_user\":\"5b13cdc121652a131b057eb4\"}"
},
"qs": {},
"connection": null,
"error": {
"message": "Unexpected token { in JSON at position 19",
"oauthError": "access_denied",
"type": "oauth-authorization"
}
},
"hostname": "XXXX.eu.auth0.com",
"user_id": "auth0|5ae834e79208b8058a4a201d",
"user_name": "XXXXX#gmail.com",
"log_id": "90020180603111515478182853610644826347557693433397116978"
}
This is the code I use for the authentication:
import decode from 'jwt-decode'
import auth0 from 'auth0-js'
import Router from 'vue-router'
const ID_TOKEN_KEY = 'id_token'
const ACCESS_TOKEN_KEY = 'access_token'
const CLIENT_ID = process.env.VUE_APP_AUTH0_CLIENT_ID
const CLIENT_DOMAIN = process.env.VUE_APP_AUTH0_CLIENT_DOMAIN
const SCOPE = 'openid profile email groups permissions roles'
const AUDIENCE = process.env.VUE_APP_AUTH0_AUDIENCE
const auth = new auth0.WebAuth({
clientID: CLIENT_ID,
domain: CLIENT_DOMAIN
})
export function login () {
auth.authorize({
responseType: 'token id_token',
redirectUri: process.env.VUE_APP_AUTH0_REDIRECT,
audience: AUDIENCE,
scope: SCOPE
})
}
export function getProfile () {
const accessToken = localStorage.getItem('access_token')
if (!accessToken) {
console.log('Access token must exist to fetch profile')
}
if (accessToken) {
return new Promise((resolve, reject) => {
auth.client.userInfo(accessToken, function (err, profileData) {
if (err) {
if (err.stack) {
console.log(err.stack)
} else {
console.log(err)
}
}
if (!profileData) {
console.log('Logging out because cannot get profile data!')
logout()
reject(Error('Cannot get profile data'))
return false
}
if (profileData && !profileData.email_verified) {
alert('Je hebt een e-mail grekegen om je e-mailadres te valideren. Zodra je e-mail is gevalideerd kan je opnieuw inloggen.')
logout()
reject(Error('Email not verified'))
} else if (profileData['https://iris.501st.nl/app_metadata'].authorization.groups.length === 0) {
alert('Je account moet nog worden goedgekeurd door de GWM voordat je toegang krijgt tot IRIS. Neem contact op met de GWM.')
logout()
reject(Error('No groups configured'))
} else if (!profileData['https://iris.501st.nl/user_metadata'].costumes) {
alert('Je hebt nog geen kostuums aan je account gekoppeld, vraag de GWM om deze voor je in te regelen.')
logout()
reject(Error('No costumes configured'))
} else if (profileData) {
resolve(profileData)
} else {
console.log('Logging out because cannot get profile data!')
logout()
reject(Error('Cannot get profile data'))
}
})
})
}
}
const router = new Router({
mode: 'history'
})
export function logout () {
clearIdToken()
clearAccessToken()
router.go('/')
}
export function requireAuth (to, from, next) {
if (!isLoggedIn()) {
next({
path: '/',
query: { redirect: to.fullPath }
})
} else {
next()
}
}
export function getIdToken () {
return localStorage.getItem(ID_TOKEN_KEY)
}
export function getAccessToken () {
return localStorage.getItem(ACCESS_TOKEN_KEY)
}
function clearIdToken () {
localStorage.removeItem(ID_TOKEN_KEY)
}
function clearAccessToken () {
localStorage.removeItem(ACCESS_TOKEN_KEY)
}
// Helper function that will allow us to extract the access_token and id_token
function getParameterByName (name) {
let match = RegExp('[#&]' + name + '=([^&]*)').exec(window.location.hash)
return match && decodeURIComponent(match[1].replace(/\+/g, ' '))
}
// Get and store access_token in local storage
export function setAccessToken () {
let accessToken = getParameterByName('access_token')
if (accessToken) {
localStorage.setItem(ACCESS_TOKEN_KEY, accessToken)
}
}
// Get and store id_token in local storage
export function setIdToken () {
let idToken = getParameterByName('id_token')
if (idToken) {
localStorage.setItem(ID_TOKEN_KEY, idToken)
}
}
export function isLoggedIn () {
const idToken = getIdToken()
return !!idToken && !isTokenExpired(idToken)
}
function getTokenExpirationDate (encodedToken) {
const token = decode(encodedToken)
if (!token.exp) { return null }
const date = new Date(0)
date.setUTCSeconds(token.exp)
return date
}
function isTokenExpired (token) {
const expirationDate = getTokenExpirationDate(token)
return expirationDate < new Date()
}
Related
Summary
I have implemented next-auth to get the accessToken from the provider and pass the same as argument to my mutation, then use the token generated from our backend on consecutive API calls.
The token rotation has been implemented based on the next-auth doc refresh token rotation
Problem
After 1 min (my token expiry time) if the session is untouched/running in background then probably it calls the api and updates with the new refreshToken (works as intended) but when we refresh/reload the page after one minute it signs out of the session.
When I check the terminal, it looks like when I reload the page, the old refreshToken is not getting updated with new refreshToken. It still has the same refreshToken (the one session had before reload) in client but no the issue when we leave the app untouched 😕
Is there any work around for this? Or what am I doing wrong here?
Thanks in advance!
Snippet
here's the pages/api/[..nextauth].tsx
import NextAuth, { NextAuthOptions } from "next-auth"
import { JWT } from "next-auth/jwt"
import GithubProvider from "next-auth/providers/github"
import { Provider, RefreshToken } from "../../../generated/graphql"
const loginQuery = `mutation login($AccessToken: String!, $Provider: Provider!) {
login(accessToken: $AccessToken, provider: $Provider) {
refreshToken
token
tokenExpiry
}
}`
const refreshTokenQuery = `mutation refreshToken($RefreshToken: String!) {
refreshToken(refreshToken: $RefreshToken) {
refreshToken
token
tokenExpiry
}
}`
const authMutation = (query, variables) : Promise<RefreshToken | null> => {
return fetch(process.env.NEXT_PUBLIC_API_URL, {
method: 'POST',
body: JSON.stringify({
query: query,
variables: variables
})
})
.then(res => res.json())
.then(data => {
if (data.errors) {
return null;
} else {
let response = data.data.refreshToken || data.data.login;
if (response.refreshToken === undefined ||
response.tokenExpiry === undefined ||
response.token === undefined) return null
return <RefreshToken> response;
}
})
.catch(error => {
console.log(error);
return null;
});
};
async function login(account, user: any) {
let provider: Provider;
if (account?.provider === 'github') provider = Provider.GitHub
if(provider === undefined) return
const variables = {
AccessToken: account.access_token,
Provider: provider
};
const response = await authMutation(loginQuery, variables)
if (response === null) return
console.log("new refresh token..", response.refreshToken)
return {
accessToken: response.token,
accessTokenExpires: response.tokenExpiry * 1000,
refreshToken: response.refreshToken,
user,
}
}
async function refreshAccessToken(token: any) {
const variables = {
RefreshToken: token.refreshToken,
};
console.log("variables", variables)
const response = await authMutation(refreshTokenQuery, variables)
if (response === null) return
console.log("new refresh token..", response.refreshToken)
return {
...token,
accessToken: response.token,
accessTokenExpires: response.tokenExpiry * 1000,
refreshToken: response.refreshToken,
}
}
export const authOptions: NextAuthOptions = {
providers: [
GithubProvider({
clientId: process.env.NEXT_PUBLIC_GITHUB_OAUTH_CLIENT_ID,
clientSecret: process.env.NEXT_PUBLIC_GITHUB_OAUTH_CLIENT_SECRET,
}),
],
theme: {
colorScheme: "light",
},
callbacks: {
jwt: async ({ account, token, user }: any): Promise<JWT> => {
console.log("current refreshToken", token.refreshToken, token.accessTokenExpires)
// Initial Sign In
if (account && user) {
console.log("initial login", account)
return login(account, user)
}
// Return previous token if the access token has not expired yet
if (Date.now() < token.accessTokenExpires){
console.log("token not expired")
return token
}
// Rotate refresh_token and fetch new access_token if current one is expired
if (token){
console.log("token expired")
return refreshAccessToken(token)
}
},
session: async ({ session, token }: any) => {
if (!session?.user || !token?.accessToken) return
session.accessToken = token.accessToken as string
session.error = token.error as string | undefined
session.user = token.user
return session
},
},
}
export default NextAuth(authOptions)
and this how _app.tsx looks like,
import "../styles/globals.css"
import { SessionProvider as NextSessionProvider } from "next-auth/react"
import UrqlProvider from "../lib/provider"
import { AppProps } from "next/app"
function App({ Component, pageProps }: AppProps) {
const { session } = pageProps
return (
<>
<NextSessionProvider session={session}>
<UrqlProvider>
<Component {...pageProps} />
</UrqlProvider>
</NextSessionProvider>
</>
)
}
export default App
I want to set login, logout callback url.
So, I set the callback url like this.
//signIn
const signInResult = await signIn("credentials", {
message,
signature,
redirect: false,
callbackUrl: `${env.nextauth_url}`,
});
//signOut
signOut({ callbackUrl: `${env.nextauth_url}`, redirect: false });
But, When I log in, I look at the network tab.
api/auth/providers, api/auth/callback/credentials? reply with
callbackUrl(url) localhost:3000
It's api/auth/callback/credentials? reply.
It's api/auth/providers reply
and api/auth/session reply empty object.
When I run on http://localhost:3000, everything was perfect.
But, After deploy, the login is not working properly.
How can I fix the error?
I added [...next-auth] code.
import CredentialsProvider from "next-auth/providers/credentials";
import NextAuth from "next-auth";
import Moralis from "moralis";
import env from "env.json";
export default NextAuth({
providers: [
CredentialsProvider({
name: "MoralisAuth",
credentials: {
message: {
label: "Message",
type: "text",
placeholder: "0x0",
},
signature: {
label: "Signature",
type: "text",
placeholder: "0x0",
},
},
async authorize(credentials: any): Promise<any> {
try {
const { message, signature } = credentials;
await Moralis.start({
apiKey: env.moralis_api_key,
});
const { address, profileId } = (
await Moralis.Auth.verify({ message, signature, network: "evm" })
).raw;
if (address && profileId) {
const user = { address, profileId, signature };
if (user) {
return user;
}
}
} catch (error) {
console.error(error);
return null;
}
},
}),
],
pages: {
signIn: "/",
signOut: "/",
},
session: {
maxAge: 3 * 24 * 60 * 60,
},
callbacks: {
async jwt({ token, user }) {
user && (token.user = user);
return token;
},
async session({ session, token }: any) {
session.user = token.user;
return session;
},
async redirect({ url, baseUrl }) {
// Allows relative callback URLs
if (url.startsWith("/")) return `${baseUrl}${url}`;
// Allows callback URLs on the same origin
else if (new URL(url).origin === baseUrl) return url;
return baseUrl;
},
},
secret: env.nextauth_secret,
});
I'm building a website witch has some protected routes that I want to prevent non-authenticated users from accessing it, So I'm using Laravel Sanctum for that purpose. I was testing this using postman, but now I want to actually use it in production from backend, So how I suppose to do that token that was generated after login!
Thanks in advance.
I think the answer is late, but it may be useful to someone else
You will set your login routes
// Login Route
Route::middleware('guest')->group(function () {
Route::post('/login', LoginAction::class)->name('auth.login');
});
// Logout Route
Route::middleware('auth:sanctum')->group(function () {
Route::post('/logout', LogoutAction::class)->name('auth.logout');
});
public function __invoke(AuthRequest $request)
{
$username = $request->input('username');
$password = $request->input('password');
$remember = $request->input('remember');
// I use filter_var to know username is email or just the regular username
$field = filter_var($username, FILTER_VALIDATE_EMAIL) ? 'email' : 'username';
// User::where('username', $username)->first();
// User::where('email', $username)->first();
$user = User::where($field, $username)->first();
if (
!$user || !Hash::check($password, $user->password)
|| !Auth::attempt(
[
$field => $username,
'password' => $password
],
$remember
)
) {
// Return Error Message
return $this->error(['message' => trans('auth.failed')], Response::HTTP_UNAUTHORIZED);
}
// After Check Useranme And Password Create Token
$token = $user->createToken($request->device)->plainTextToken;
// Return Success Message With Token
return $this->success(['message' => trans('auth.login', ['user' => $user->username]), 'token' => $token, 'user' => $user]);
in vuex Actions
export const loginAction = ({ commit }, userInfo) => {
let urlParams = new URLSearchParams(window.location.search);
return new Promise((resolve, reject) => {
login(userInfo)
.then((response) => {
if (response.success) {
commit("SET_LOGIN_USER", response.payload.user);
// Set Token Mutaion
commit("SET_TOKEN", response.payload.token);
// Notify User for success logged in
Notify.create({
type: "positive",
message: `welcome ${response.payload.user.username}`,
});
// Redirect to dashboard
router.push(
urlParams.get("redirect") || { name: "dashboard" }
);
}
resolve(response);
})
.catch((error) => {
// For any error remove any auth user details and notify him
Notify.create({
type: "negative",
message: `${error.response?.data.payload}`,
});
commit("REMOVE_AUTH_DETAILS");
reject(error);
});
});
};
in vuex mutations 3 methods
// Helpers Fuctions
import { setLoginUser, setToken, removeToken, removeLoginUser } from '../../../utils/auth';
export const SET_LOGIN_USER = (state, user) => {
state.loginUser = user
setLoginUser(user)
}
export const SET_TOKEN = (state, token) => {
state.token = token
setToken(token)
}
export const REMOVE_AUTH_DETAILS = (state) => {
state.loginUser = null
state.token = null
removeLoginUser()
removeToken()
}
My Helper Functions
// I use Quasar Framework But you can use any cookie package like js-cookie
import { Cookies } from "quasar";
const TokenName = "TOKEN";
const LoginUser = "LOGIN_USER";
export function setToken(token) {
return Cookies.set(TokenName, token);
}
export function getToken() {
return Cookies.get(TokenName);
}
export function removeToken() {
return Cookies.remove(TokenName);
}
export function setLoginUser(loginUser) {
return Cookies.set(LoginUser, JSON.stringify(loginUser));
}
export function getLoginUser() {
return Cookies.get(LoginUser);
}
export function removeLoginUser() {
return Cookies.remove(LoginUser);
}
I use Vue js for frontend and axios for making requests
in axios file request.js
// It's helper function to get token from cookie
import { getToken } from "./auth";
import router from "../router";
import store from "../store";
import axios from "axios";
// Create axios instance
const service = axios.create({
baseURL: "/api/v1/",
timeout: 10000 // Request timeout
});
// Request intercepter
service.interceptors.request.use(
config => {
if (getToken()) {
config.headers["Authorization"] = "Bearer " + getToken(); // Set Token
}
return config;
},
error => {
// Do something with request error
console.log("error-axios", error); // for debug
Promise.reject(error);
}
);
// response pre-processing
service.interceptors.response.use(
response => {
return response.data;
},
error => {
// remove auth user informations from cookies and return to login page if unauthorized
if ([401].includes(error.response.status)) {
store.commit('auth/REMOVE_AUTH_DETAILS')
router.push({ name: "login" });
}
return Promise.reject(error);
}
);
export default service;
// Logout Fuction in controller or action
public function __invoke(Request $request)
{
if(! $request->user()) {
return $this->error(['message' => trans('auth.no_auth_user')]);
}
// Remove Token
$request->user()->tokens()->delete();
return $this->success([
'message' => trans('auth.logout', ['user' => auth()->user()->username])
], Response::HTTP_OK);
}
// Logout mutation (vuex)
export const logoutAction = ({ commit }) => {
return new Promise((resolve, reject) => {
logout()
.then(() => {
Notify.create({
type: "negative",
message: `see you later ${getLoginUser().username}`,
});
commit("REMOVE_AUTH_DETAILS");
router.push({ name: "login" });
resolve();
})
.catch((error) => {
router.push({ name: "login" });
commit("REMOVE_AUTH_DETAILS");
reject(error);
});
});
};
Any other help I will not be late
Token is null right after login, It stores only if I refresh the page. It should store just after login without need to reload.
src/stores/index.ts:
const initialUser = JSON.parse(sessionStorage.getItem('Project:token') || '{}');
const useProject = defineStore('project-store', {
state: () => ({
token: initialUser as string | null,
status: initialUser ? { loggedIn: false } : { loggedIn: true },
}),
actions: {
async login(user: User) {
const token = await grantAuth(user);
this.loginSuccess = token;
},
loginSuccess(token: string) {
this.token = token;
this.status.loggedIn = !!token;
},
src/services/entryPoint.js:
import store from '../stores/index';
export default async function entryPoint() {
const getStore = async () => {
if (!store) {
store = await import('../stores/index');
} else {
if (store) {
const { token } = store;
if (token) {
return token;
}
}
}
};
}
src/services/api.js:
import axios from 'axios';
import entryPoint from './entryPoint';
const api = axios.create({ baseURL: import.meta.env.VITE_APP_API_URL });
const entry = entryPoint();
if (entry) {
api.defaults.headers.Authorization = `Bearer ${entry}`;
}
export default api;
src/services/auth.js:
import api from './api';
const requestToken = encodedData => {
return api.post('/projects/login', null, {
headers: {
Authorization: `Basic ${encodedData}`,
},
});
}
const registerToken = data => {
if (data && data.token) {
sessionStorage.setItem('Project:token', JSON.stringify(data.token));
}
};
export const grantAuth = async user => {
const flatData = `${user.username.toUpperCase()}:${user.password}`;
const encodedData = btoa(flatData);
const response = await requestToken(encodedData);
registerToken(response.data);
return response.data ? response.data.token : null;
};
A solution would be to give the command window.location.reload(), however It is not viable for a SPA, It is preferable that the token to be available right after login
Why my jwt token doesn't expire after 1 hour?
I've noticed that it doesn't expire when I forgot to logout my account in my admin panel that I created in vuejs with vuex.
here is my API that I created in ExpressJS using bcrypt and express-jwt for token.
router.post('/login', (req, res) => {
let sql = "SELECT * FROM AUTHENTICATION WHERE email = ?";
myDB.query(sql, [req.body.email, req.body.password], function (err, results) {
if (err) {
console.log(err);
} else {
if (!results) {
res.status(404).send('No user found.')
} else {
try {
let passwordMatched = bcrypt.compareSync(req.body.password, results[0].password);
if (passwordMatched) {
// Passwords match
let token = jwt.sign({ id: results.id }, config.secret, {
expiresIn: '1h'
});
res.status(200).send({ auth: true, token: token, user: results });
} else {
//Password doesn't match
return res.status(401).send({ auth: false, token: null });
}
} catch (error) {
res.send({ Success: false })
}
}
}
})
});
here's my login in vuex where I received the token from my backend.
import axios from 'axios';
const state = {
status: '',
token: localStorage.getItem('token') || '',
user: {}
};
const getters = {
isLoggedIn: state => !!state.token,
authStatus: state => state.status,
};
const mutations = {
auth_request(state) {
state.status = 'loading'
},
auth_success(state, token, user) {
state.status = 'success'
state.token = token
state.user = user
},
auth_error(state) {
state.status = 'error'
},
logout(state) {
state.status = ''
state.token = ''
},
};
const actions = {
login({ commit }, user) {
return new Promise((resolve, reject) => {
commit('auth_request')
axios({ url: 'http://localhost:9001/login/login', data: user, method: 'POST' })
.then(resp => {
const token = resp.data.token
const user = resp.data.user
localStorage.setItem('token', token)
// Add the following line:
axios.defaults.headers.common['Authorization'] = token
commit('auth_success', token, user)
resolve(resp)
})
.catch(err => {
commit('auth_error')
localStorage.removeItem('token')
reject(err)
})
})
}
};
EDIT: Added vuejs code for login
thanks for the help guys!
Your JWT token is just an encoded + signed JSON with relevant fields such as expiresIn, iat.
While it may contain the expiresIn field, it does not mean that the backend server will honour it.
Logic needs to be written in the backend server to parse the timestamp, do comparison with the current time to determine whether it has expired. If it is expired, the backend should return a response code of 401 Unauthorized to tell the frontend (your Vue client) that the token is no longer valid.
What you can do is to put the expiry-checking logic in a middleware to look into the request headers' Authorization field.