I'm using ember-simple-auth and a custom authenticator for an HTTP basic login with CSRF protection. Everything is working fine except sometimes my restore method resolves when it should be failing, like when the session expires.
When authentication succeeds I resolve with the csrf token, but then when the token or session expires and I refresh the page, the resolve method still succeeds because all I'm doing is checking if the token is still there (not if it's valid). I know this is wrong, so I guess my question would be what is the proper way to handle this? Should I also be resolving with the session id? Should I be sending an AJAX request in the restore method with the stored token to see if it is still valid and returns success? I'm interested in hearing about any other improvements I could make as well.
Here is my authenticator code:
import Ember from 'ember';
import ENV from 'criteria-manager/config/environment';
import Base from 'ember-simple-auth/authenticators/base';
export default Base.extend({
restore(data) {
return new Ember.RSVP.Promise((resolve, reject) => {
if (data.token) {
Ember.$.ajaxSetup({
headers: {
'X-XSRF-TOKEN': data.token
}
});
resolve(data);
}
else {
reject();
}
});
},
authenticate(credentials) {
let csrfToken = this.getCookie('XSRF-TOKEN');
return new Ember.RSVP.Promise((resolve, reject) => {
Ember.$.ajax({
beforeSend: function(xhr) {
xhr.setRequestHeader("Authorization", "Basic " + btoa(credentials.username + ":" + credentials.password));
xhr.setRequestHeader("X-XSRF-TOKEN", csrfToken);
},
url: ENV.host + "/api/users/login",
method: 'POST'
}).done(() => {
//A new CSRF token is issued after login, add it to future AJAX requests
Ember.$.ajaxSetup({
headers: {
'X-XSRF-TOKEN': this.getCookie('XSRF-TOKEN')
}
});
Ember.run(() => {
resolve({
token: this.getCookie('XSRF-TOKEN')
});
});
}).fail((xhr) => {
Ember.run(() => {
if(xhr.status === 0) {
reject("Please check your internet connection!");
}
else if (xhr.status === 401) {
reject("Invalid username and/or password.");
}
else {
reject("Error: Http Status Code " + xhr.status);
}
});
});
});
},
invalidate() {
return new Ember.RSVP.Promise((resolve, reject) => {
let csrfToken = this.getCookie('XSRF-TOKEN');
Ember.$.ajax({
beforeSend: function(xhr) {
xhr.setRequestHeader("X-XSRF-TOKEN", csrfToken);
},
url: ENV.host + '/logout',
method: 'POST'
}).done(() => {
Ember.run(() => {
resolve();
});
}).fail(() => {
Ember.run(() => {
reject();
});
});
});
},
getCookie(name) {
let alLCookies = "; " + document.cookie;
let cookieArray = alLCookies.split("; " + name + "=");
if (cookieArray.length === 2) {
return cookieArray.pop().split(";").shift();
}
}
});
Should I also be resolving with the session id? Should I be sending an
AJAX request in the restore method with the stored token to see if it
is still valid and returns success?
It all depends on your project's needs. In my opinion it's good to check if token is still valid. For example, oauth2-password-grant stores expiring date in session and when restoring simply compares it with current time. You may do this too. Or, if your backend has some token validation endpoint, you may send request to be sure if token is valid.
Related
Hi and thanks in advance,
I've successfully setup JWT authentication using django-rest-framework-simplejwt and React but I'm still very confused about the advantages and specifically database hits.
I'm using simplejwt with ROTATE_REFRESH_TOKENS': True 'BLACKLIST_AFTER_ROTATION': True, when my access_token expire I ask for a new one through /api/token/refresh and it blacklist old tokens, I'm using axios interceptors to perform that automatically.
But in my understanding the benefits of JWt is that they are stateless, meaning I don't have to hit the user database table everytime I want to make an a request that needs authentication permission.
The problem is even with a simple view like this :
class IsConnecteddAPI(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, *args, **kwargs):
data = "You seem to be connected"
return Response(data, status=status.HTTP_200_OK)
using django-silk I see that it still performs 1 query to my user table when I call it with a valid access token, is that normal ? If so why do we say that JWT are stateless ? I'm really confused.
That's my axios code if needed :
import axios from "axios";
const baseURL = "http://localhost:5000";
const axiosInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
Authorization: localStorage.getItem("accesstoken")
? "JWT " + localStorage.getItem("accesstoken")
: null,
"Content-Type": "application/json",
accept: "application/json",
},
});
const axioAnonymousInstance = axios.create({
baseURL: baseURL,
timeout: 5000,
headers: {
"Content-Type": "application/json",
accept: "application/json",
},
});
axiosInstance.interceptors.response.use(
(response) => {
return response;
},
async function (error) {
const originalRequest = error.config;
if (typeof error.response === "undefined") {
alert(
"A server/network error occurred. " +
"Looks like CORS might be the problem. " +
"Sorry about this - we will get it fixed shortly."
);
return Promise.reject(error);
}
if (
error.response.status === 401 &&
originalRequest.url === baseURL + "token/refresh/"
) {
window.location.href = "/login/";
return Promise.reject(error);
}
if (
error.response.data.code === "token_not_valid" &&
error.response.status === 401 &&
error.response.statusText === "Unauthorized"
) {
const refreshToken = localStorage.getItem("refreshtoken");
if (refreshToken) {
const tokenParts = JSON.parse(atob(refreshToken.split(".")[1]));
// exp date in token is expressed in seconds, while now() returns milliseconds:
const now = Math.ceil(Date.now() / 1000);
console.log(tokenParts.exp);
if (tokenParts.exp > now) {
return axioAnonymousInstance
.post("/api/token/refresh/", { refresh: refreshToken })
.then((response) => {
localStorage.setItem("accesstoken", response.data.access);
localStorage.setItem("refreshtoken", response.data.refresh);
axiosInstance.defaults.headers["Authorization"] =
"JWT " + response.data.access;
originalRequest.headers["Authorization"] =
"JWT " + response.data.access;
return axiosInstance(originalRequest);
})
.catch((err) => {
// redirect ro /login here if wanted
console.log("axios Safe Instance error");
console.log(err);
// window.location.href = "/login/";
});
} else {
console.log("Refresh token is expired", tokenParts.exp, now);
window.location.href = "/login/";
}
} else {
console.log("Refresh token not available.");
window.location.href = "/login/";
}
}
// specific error handling done elsewhere
return Promise.reject(error);
}
);
export { axiosInstance, axioAnonymousInstance };
( I know I shouldn't use localStorage but whatever )
and I would typically just call this function to make the simple request to the view written above :
const IsConnected = () => {
axiosInstance
.get("/api/is_connected/")
.then((response) => {
if (response.status === 200) {
console.log(response.data);
console.log("Is connected : CONNECTED ");
} else {
console.log("IS connected : not connected");
}
})
.catch((error) => {
console.log("Is connected : NOT CONNECTED");
console.log(error);
});
};
Without the specifics of the exact query hit your db, it's hard to tell what is happening (the db query must have originated from a middleware because there's nothing in your code that does it, and I suspect it's django's CsrfViewMiddleware). However, as for your question of JWT being stateless, I suggest you to take a look at the official introduction.
Basically, what happens with a JWT is that your server performs a signature verification on the token using your server's secret key (please beware of some problems). If the verification passes, then the data stored inside the JWT is trusted and read as is, which is why no database query is necessary. Of course, this does mean that your user will know exactly what is stored inside their token because the data is a simple base64 encoded JSON object.
I have the following set up in my nuxt.config.js file:
auth: {
redirect: {
login: '/accounts/login',
logout: '/',
callback: '/accounts/login',
home: '/'
},
strategies: {
local: {
endpoints: {
login: { url: 'http://localhost:8000/api/login2/', method: 'post' },
user: {url: 'http://localhost:8000/api/user/', method: 'get', propertyName: 'user' },
tokenRequired: false,
tokenType: false
}
}
},
localStorage: false,
cookie: true
},
I am using django sessions for my authentication backend, which means that upon a successful login, i will have received a session-id in my response cookie. When i authenticate with nuxt however, i see the cookie in the response, but the cookie is not saved to be used in further requests. Any idea what else i need to be doing?
This is how I handled this, which came from a forum post that I cannot find since. First get rid of nuxt/auth and roll your own with vuex store. You will want two middleware, one to apply to pages you want auth on, and another for the opposite.
This assumes you have a profile route and a login route that returns a user json on successful login.
I'm also writing the user to a cookie called authUser, but that was just for debugging and can be removed if you don't need it.
store/index
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: {},
};
store/state
export default () => ({
user: null,
isAuthenticated: false,
});
store/actions
export async function nuxtServerInit({ commit }, { _req, res }) {
await this.$axios
.$get("/api/users/profile")
.then((response) => {
commit("setUser", response);
commit("setAuthenticated", true);
})
.catch((error) => {
commit("setErrors", [error]); // not covered in this demo
commit("setUser", null);
commit("setAuthenticated", false);
res.setHeader("Set-Cookie", [
`session=false; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
`authUser=false; expires=Thu, 01 Jan 1970 00:00:00 GMT`,
]);
});
}
store/mutations
export const setUser = (state, payload) => (state.user = payload);
export const setAuthenticated = (state, payload) =>
(state.isAuthenticated = payload);
store/getters
export const getUser = (state) => state.user;
export const isAuthenticated = (state) => state.isAuthenticated;
middleware/redirectIfNoUser
export default function ({ app, redirect, _route, _req }) {
if (!app.store.state.user || !app.store.state.isAuthenticated) {
return redirect("/auth/login");
}
}
middleware/redirectIfUser
export default function ({ app, redirect, _req }) {
if (app.store.state.user) {
if (app.store.state.user.roles.includes("customer")) {
return redirect({
name: "panel",
params: { username: app.store.state.user.username },
});
} else if (app.store.state.user.roles.includes("admin")) {
return redirect("/admin/dashboard");
} else {
return redirect({
name: "panel",
});
}
} else {
return redirect("/");
}
}
pages/login- login method
async userLogin() {
if (this.form.username !== "" && this.form.password !== "") {
await this.$axios
.post("/api/auth/login", this.form)
.then((response) => {
this.$store.commit("setUser", response.data);
this.$store.commit("setAuthenticated", true);
this.$cookies.set("authUser", JSON.stringify(response.data), {
maxAge: 60 * 60 * 24 * 7,
});
if (this.$route.query.redirect) {
this.$router.push(this.$route.query.redirect);
}
this.$router.push("/panel");
})
.catch((e) => {
this.$toast
.error("Error logging in", { icon: "error" })
.goAway(800);
The cookie is sent by the server but the client won't read it, until you set the property withCredentials in your client request (about withCredentials read here)
To fix your problem you have to extend your auth config with withCredentials property.
endpoints: {
login: {
url: 'http://localhost:8000/api/login2/',
method: 'post'
withCredentials: true
}
}
Also don't forget to set CORS policies on your server as well to support cookie exchange
Example from ExpressJS
app.use(cors({ credentials: true, origin: "http://localhost:8000" }))
More information about this issue on auth-module github
We are using ember-simple-auth with cookie authentication and we want to redirect to the last accessed route after we login again when the cookie expires. We manage to do the redirection for the following scenarios:
Not authenticated and try to access a route from url
Not authenticated and select an item from the navigation menu
Both, after successful authentication, we redirected to the requested route.
But, we want when our session cookie expired and the user tries to access a route to invalidate the session and redirect the user back to authentication page. When the user log in back we want to redirect him to the requested route. For now we store the previous transition so we can do the redirection but after we invalidate the session the data are lost.
What is the best way to do this?
Our code looks like:
Custom Authenticator
import Ember from 'ember';
import Base from 'ember-simple-auth/authenticators/base';
export default Base.extend({
restore() {
return new Ember.RSVP.Promise(function(resolve, reject) {
let sessionCookie = window.Cookies.get('beaker.session.id');
if(!window.isUndefined(sessionCookie)) {
resolve(true);
}else{
reject();
}
});
},
authenticate(data) {
return new Ember.RSVP.Promise(function (resolve, reject) {
Ember.$.ajax({
type: 'post',
url: '/core/authentication/basic/login',
data: data
}).then((response) => {
resolve({
responseText: response
});
}, (error) => {
reject(error);
});
});
},
invalidate() {
return new Ember.RSVP.Promise(function (resolve, reject) {
Ember.$.ajax({
type: 'post',
url: '/core/authentication/basic/logout'
}).then(() => {
resolve(true);
}, () => {
reject();
});
});
}
});
Application Route:
import Ember from 'ember';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
export default Ember.Route.extend(ApplicationRouteMixin, {
session: Ember.inject.service('session'),
beforeModel(transition) {
if(!this.get('session.isAuthenticated') && transition.targetName !== 'core.authentication') {
this.set('previousTransition', transition);
this.transitionTo('core.authentication');
}
},
actions: {
willTransition(transition) {
if (!this.get('session.isAuthenticated')) {
this.set('previousTransition', transition);
} else {
let previousTransition = this.get('previousTransition');
if (previousTransition) {
this.set('previousTransition', null);
previousTransition.retry();
}
}
}
}
});
Authentication Route
import Ember from 'ember';
export default Ember.Route.extend({
session: Ember.inject.service('session'),
actions: {
login() {
let that = this;
let { username, password } = this.controller.getProperties('username', 'password');
let data = {username: username, password: password};
if(this.get('session.isAuthenticated')) {
this.get('session').invalidate();
}
this.get('session').authenticate('authenticator:basic', data).then(() => {
let data = that.get('session.data.authenticated');
// show response message
}, (error) => {
// show error
});
}
}
});
You can add the previous transition inside the session data, like this
this.get('session').set('data.previousTransition', transition.targetName);
because that is still persisted after the session is invalidated.
And then get it back from the store, and do the transition:
this.get('session.store').restore().then(data => {
if (data.previousTransition !== null) {
this.transitionTo(data.previousTransition)
}
})
I solved it by using invalidationSucceded here.
this.get('session').on('invalidationSucceeded', () => this.transitionToRoute('dashboard'))
I've been trying to implement some authentication component in my app for a few hours now, and I still don't understand some of the things that are happening.
Basically, I'd like to send a POST request containing some credentials to my API, which sends me a cookie back with a token if the credentials worked. Then, the cookie should be included in the headers of all future requests to my API (which I believed was automatic).
server.js (my API is a mockup for now, with JSON files)
...
app.post('/api/login', jsonParser, (req, res) => {
fs.readFile(ACCOUNTS_FILE, (err, data) => {
if (err) {
console.error(err);
process.exit(1);
}
const accounts = JSON.parse(data);
const credentials = {
email: req.body.email,
password: req.body.password,
};
var token = null;
for (var i = 0; i < accounts.length; ++i) {
const account = accounts[i];
if (account.email === credentials.email
&& account.password === credentials.password) {
token = account.token;
break;
}
}
if (token) {
res.setHeader('Set-Cookie', `access_token=${token}; Secure; HttpOnly;`);
res.json({ token });
} else {
res.json({ token: null });
}
});
});
...
app.js
...
handleConnection(e) {
e.preventDefault();
const email = this.state.email.trim();
const password = this.state.password.trim();
if (!email && !password) {
return (false);
}
fetch(loginUrl, {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
credentials: 'include',
},
body: JSON.stringify(this.state),
})
.then((response) => response.json())
.then((data) => {
console.log(data);
})
.catch((error) => {
console.warn(error);
});
return (true);
}
...
Now the console.log(data) always displays my token (or null if my credentials are wrong), but the cookie thing doesn't work...
See, I receive the Set-Cookie header, but I still have no cookie on my page.
And even if I managed to get the cookie, when I try to create a cookie using document.cookie = "access_token=123"; and then send the request again, my cookie doesn't go in my header like it would with a jQuery Ajaxcall :
I read here that adding credentials: 'include' would save the day, but unfortunately it didn't.
What am I missing here?
Thanks in advance!
I had the same problem and I found the answer in Peter Bengtsson's comment here: https://davidwalsh.name/fetch
If I understood, in your case the fetch should be:
fetch(loginUrl, {
credentials: 'same-origin',
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json'
},
body: JSON.stringify(this.state),
})
I've created a clean ember app, installed simple-auth and implemented a custom authenticator for facebook.
https://github.com/prule/ember-auth-spike
I can see that I'm successfully getting the access token from FB and its put in the session (inspecting the container session via chrome ember extension shows me the session is authenticated and the access token is visible).
But when I reload the page in the browser, the session state is lost. Is this expected behaviour? Have I done something wrong in my custom authenticator? The authenticator code is a straight copy and paste (plus some console.logs) from https://github.com/simplabs/ember-simple-auth/blob/master/examples/7-multiple-external-providers.html
Thanks, I appreciate any help.
import Ember from 'ember';
import Base from 'simple-auth/authenticators/base';
export default Base.extend({
restore: function (data) {
return new Ember.RSVP.Promise(function (resolve, reject) {
console.log('restore');
if (!Ember.isEmpty(properties.accessToken)) {
console.log('found access token '+properties.accessToken);
resolve(properties);
}
else {
console.log('no token found');
reject();
}
});
},
authenticate: function (options) {
return new Ember.RSVP.Promise(function (resolve, reject) {
console.log('1');
FB.getLoginStatus(function (fbResponse) {
console.log('2');
console.log(fbResponse);
if (fbResponse.status === 'connected') {
Ember.run(function () {
console.log(fbResponse.authResponse.accessToken);
resolve({accessToken: fbResponse.authResponse.accessToken});
});
}
else if (fbResponse.status === 'not_authorized') {
reject();
}
else {
FB.login(function (fbResponse) {
if (fbResponse.authResponse) {
Ember.run(function () {
console.log(fbResponse.authResponse.accessToken);
resolve({accessToken: fbResponse.authResponse.accessToken});
});
}
else {
reject();
}
});
}
});
});
},
invalidate: function (data) {
return new Ember.RSVP.Promise(function (resolve, reject) {
FB.logout(function (response) {
Ember.run(resolve);
});
});
}
});
The argument to the authenticator's restore method is called data but you're checking for properties.accessToken. This should actually raise an error anyway as properties is undefined there.