I have seen the express example, where an ability is stored via middleware in the req object. It then uses the following method to evaluate the permissions:
ForbiddenError.from(req.ability).throwUnlessCan('read', article);
I want to achieve a similar thing. My idea is to save the ability inside an express session that is shared with socket io websockets. Through the sharing req.session = socket.handshake.session. My approach is the following, I make a request from the frontend application to get rules to update the ability on the frontend. The backend saves the ability inside the express session:
// abilities.js file
import { Ability } from '#casl/ability';
export const defineAbilitiesFor = (rules) => {
return new Ability(rules);
};
export default defineAbilitiesFor;
// handler for express route to get permissions from the frontend
export const getPermissions = async (req, res) => {
...
rules.push({
action: ['view'],
subject: views,
});
// manage all own processes
rules.push({
action: ['manage'],
subject: 'Process',
conditions: {
userId: req.kauth.grant.access_token.content.sub,
},
});
// store ability in session
req.session.rules = defineAbilitiesFor(rules);
const token = jwt.sign({ token: packRules(rules) }, 'secret');
if (token) {
return res.status(200).json(token);
} else {
return res.status(400).json('Error');
}
...
Then when a websocket request happens, I want to check in the backend if the user has the permissions to do that action:
ForbiddenError.from(socket.handshake.session.rules).throwUnlessCan('view', 'Process');
However, this throws the following error:
TypeError: this.ability.relevantRuleFor is not a function
at ForbiddenError.throwUnlessCan
The session object seems to have the correct ability object. When I console.log socket.handshake.session.rules, I get the following output:
{
h: false,
l: {},
p: {},
'$': [
{ action: [Array], subject: 'Process', conditions: [Object] },
{ action: [Array], subject: [Array] },
{ action: [Array], subject: 'Process', conditions: [Object] }
],
m: {}
}
Also the can function and everything else I tried wasn't working. I think storing the plain rules as an object inside the session and then updating the ability class before each request would work, but I don't want to do that. I want to store the ability right inside the session, so that I only have to execute the throwUnlessCan or can functions.
Is this even possible and if so, how would you do this?
Thanks so far.
Instead of storing the whole Ability instance, you need to store only its rules! rules is a plain js array of objects, so it can be easily serialized.So, change the code to this:
export const getPermissions = async (req, res) => {
...
rules.push({
action: ['view'],
subject: views,
});
// manage all own processes
rules.push({
action: ['manage'],
subject: 'Process',
conditions: {
userId: req.kauth.grant.access_token.content.sub,
},
});
// store ability RULES in session
req.session.rules = rules;
const token = jwt.sign({
token: packRules(rules) // packRules accepts an array of RawRule! not an Ability instance
}, 'secret');
if (token) {
return res.status(200).json(token);
} else {
return res.status(400).json('Error');
}
To use Ability in other handlers add a middleware:
function defineAbility(req, res, next) {
if (req.session.rules) {
req.ability = new Ability(req.session.rules);
next();
} else {
// handle case when there is no rules in session yet
}
}
// later
app.get('/api/users', defineAbility, (req, res) => {
req.ability.can(...);
// or
ForbiddenError.from(req.ability).throwUnlessCan(...);
})
Related
I have the following code in my HTML page
Gun.on('opt', function (ctx) {
if (ctx.once) {
return
}
this.to.next(ctx)
window.auth = ctx.opt.auth
ctx.on('get', function (msg) {
msg.auth = window.auth
this.to.next(msg)
})
ctx.on('put', function (msg) {
msg.put.auth = window.auth
this.to.next(msg)
})
})
var gun = Gun({
peers: ['http://localhost:8765/gun'],
auth: {
user: 'mroon',
password: 'titi'
}
})
On the server, I simply watch the requests
Gun.on('create', function(db) {
console.log('gun created')
this.to.next(db);
db.on('get', function(request) {
// this request contains the auth attribute from the client
this.to.next(request);
});
db.on('put', function(request) {
// this request does not contain the auth attribute from the client
this.to.next(request);
});
});
every time I query the graph with gun.get('someAttribute') the request on the server contains the auth attribute.
but when a gun.get('someAttribute').put({attribute: 'my new value'}) is called, the request on the server does not contain the auth attribute.
How can I add the auth attribute to the put request in such a way that all the peers will get it too?
#micha-roon you jumped straight to GUN's core/internal wire details, which is not the easiest thing to start with, but here is something I do that I'm guessing is what you are looking for:
(if not, please just comment & I'll update)
What this does is it adds a DEBUG flag to all outbound messages in GUN, you can change this to add other metadata or info
Gun.on('opt', function(root){
if(!root.once){
root.on('out', function(msg){
msg.DBG = msg.DBG || +new Date;
this.to.next(msg);
});
}
this.to.next(root);
})
Also another good reference: https://github.com/zrrrzzt/bullet-catcher
I have an authentication on my nuxt web-app, using the nuxt/auth module. I also use modular vuex stores to handle different states. After I login, everything is fine and I can navigate through the app normally. But when I try to reload the page or access it directly through a URL, the user is not accessible, thus, the whole web-app becomes unusable. I try to access the user object with this.context.rootState.auth.user, which is null after page-reload or direct access. Strangely enough, this only happens in production.
I already tried to add an if-guard, but sadly the getter is not reactive. Probably because it´s a nested object. This is my current getter:
get someGetter() {
if (!this.context.rootState.auth.user) {
return []
}
const userId = this.context.rootState.auth.user.id as string
const arr = []
for (const item of this.items) {
// Using userId to add something to arr
}
return arr
}
Is there a way to force nuxt to finish the authentication before initialising the vuex-modules, or to make this getter reactive, so it will trigger again, when the user object is accessible?
This is what my auth-config looks like in nuxt.config.ts:
auth: {
strategies: {
local: {
_scheme: '#/auth/local-scheme',
endpoints: {
login: {
url: '/api/authenticate',
method: 'post',
propertyName: false
},
logout: { url: '/api/logout', method: 'post' },
user: { url: '/api/users/profile', propertyName: false }
}
},
// This dummy setting is required so we can extend the default local scheme
dummy: {
_scheme: 'local'
}
},
redirect: {
logout: '/login'
}
}
EDIT
I resolved this by following Raihan Kabir´s answer. Using vuex-persistedstate in an auth-plugin, which is triggered every time the server renders the page. The plugin saves the userId in a cookie, so the store can use it as a fallback, if the auth-module isn´t ready.
The thing is, the vuex clears data on reload/refresh to keep credentials secure. That's what vuex is. If you want to store data for long time without being interrupted after reloading, you should use localstorage for that. But localstorage is not recommended for storing credentials.
If you need only user_id to keep in the vuex, use Cookie instead. And try something like this in your store's index.js file -
export const actions = {
// This one runs on the beginning of reload/refresh
nuxtServerInit ({ commit }, { req }) {
if (req.headers.cookie) {
const parsed = cookieparser.parse(req.headers.cookie)
try {
// get user id that you would set on auth as Cookie
user_id = parsed.uid
} catch (err) {
// error here...
}
}
// perform login and store info on vuex store
commit('authUserOnReload', user_id)
},
}
// Define Mutations
export const mutations = {
authUserOnReload (state, user_id) {
// perform login here and store user
}
}
I created a Vue component which exports an async function. This component acts as a wrapper for calling my API. It's based on axios with a caching component that relies on localforage for some short lived persistence.
import localforage from 'localforage'
import memoryDriver from 'localforage-memoryStorageDriver'
import { setup } from 'axios-cache-adapter'
export default {
async cache() {
// Register the custom `memoryDriver` to `localforage`
await localforage.defineDriver(memoryDriver)
// Create `localforage` instance
const store = localforage.createInstance({
// List of drivers used
driver: [
localforage.INDEXEDDB,
localforage.LOCALSTORAGE,
memoryDriver._driver
],
// Prefix all storage keys to prevent conflicts
name: 'tgi-cache'
})
// Create `axios` instance with pre-configured `axios-cache-adapter` using a `localforage` store
return setup({
// `axios` options
baseURL: 'https://my.api',
cache: {
maxAge: 2 * 60 * 1000, // set cache time to 2 minutes
exclude: { query: false }, // cache requests with query parameters
store // pass `localforage` store to `axios-cache-adapter`
}
})
}
}
Here is how I am importing and using this component in my views:
import api from '#/components/Api.vue'
export default {
data() {
return {
userId: this.$route.params.id,
userData: ''
}
},
methods: {
loadClient(userId) {
const thisIns = this;
api.cache().then(async (api) => {
const response = await api.get('/client/find?id='+userId)
thisIns.userData = response.data.data[0]
}).catch(function (error) {
console.log(error)
})
},
},
created() {
this.loadClient(this.userId)
},
}
I can import this component and everything appears to work. I get data back from my API. However, immediately after every call, I get an error:
TypeError: Cannot read property 'cache' of undefined
Which references this line:
api.cache().then(async (api) => {
I am unable to understand why this is happening, or what it means. The error itself indicates that the component I am importing is undefined, though that's clearly not the case; if it were, the API call would ultimately fail I would suspect. Instead, I am lead to believe that perhaps I am not constructing/exporting my async cache() function properly.
Upon further review, I don't actually understand why the author has implemented it the way he has. Why would you want to create an instance of localForage every single time you make an API call?
I've opted not to use a component and to only instantiate an instance of localForage once.
main.js
import localforage from 'localforage'
import memoryDriver from 'localforage-memoryStorageDriver'
import { setup } from 'axios-cache-adapter'
// Register the custom `memoryDriver` to `localforage`
localforage.defineDriver(memoryDriver)
// Create `localforage` instance
const localforageStore = localforage.createInstance({
// List of drivers used
driver: [
localforage.INDEXEDDB,
localforage.LOCALSTORAGE,
memoryDriver._driver
],
// Prefix all storage keys to prevent conflicts
name: 'my-cache'
})
Vue.prototype.$http = setup({
baseURL: 'https://my.api',
cache: {
maxAge: 2 * 60 * 1000, // set cache time to 2 minutes
exclude: { query: false }, // cache requests with query parameters
localforageStore // pass `localforage` store to `axios-cache-adapter`
}
})
the view
export default {
data() {
return {
userId: this.$route.params.id,
userData: ''
}
},
methods: {
loadClient(userId) {
const thisIns = this;
thisIns.$http.get('/client/find?id='+userId)
.then(async (response) => {
thisIns.userData = response.data.data[0]
})
.catch(function (error) {
console.log(error)
})
},
},
created() {
this.loadClient(this.userId)
},
}
I have multiple endpoints starting with /networks/{networkId}/*. I don't want to have logic to find a network and execute some extra validation on it, in every handler. Is there any way to solve this on a higher level? Ex. plugins/server method etc?
In every handler I have the following boilerplate code:
import networkRepo from 'common/repositories/network';
// handler.js
export default (req, reply) => {
return networkRepo.findById(req.params.networkId).then(network => {
// Logic to validate whether the logged user belongs to the network
// Logic where I need the network instance
});
}
The best situation would be:
// handler.js
export default (req, reply) => {
console.log(req.network); // This should be the network instance
}
Best way to achieve what you want is to either create a generic function that you can call first inside your handler or alternatively create an internal hapi route which will perform you look-up and return value to your other handler. Internal routes can then be accesses by server.inject from within your other handler, see options called allowInternals for more details, I can write pseudo-code to help!
[{
method: 'GET',
path: '/getNetworkByID/{id}',
config: {
isInternal: true,
handler: function (request, reply) {
return networkRepo.findById(req.params.networkId).then(network => {
// Logic to validate whether the logged user belongs to the network
// Logic where I need the network instance
reply(network.network);
});
}
}
},
{
method: 'GET',
path: '/api/networks/{id}',
config: {
isInternal: true,
handler: function (request, reply) {
request.server.inject({
method: 'GET',
url: '/getNetworkByID/' + request.params.id,
allowInternals: true
}, (res) => {
console.log(res.result.network) //network
});
}
}
}]
I need to do some additional authentication in a few of my handlers. Is there a way of doing that way in a composable way?
export async function handler(request) {
const user = request.auth.credentials;
const customer = FindCustomer(user);
if (!customer) {
throw Boom.forbidden('user is not a customer');
}
if (!customer.verified) {
throw Boom.forbidden('customer not validated');
}
// actual handler logic
}
Is there a way to wrap this so that some routes already provide the customer in the request object ?
You can make use of the extension points in the request life cycle. In your case, the 'onPostAuth' extension point would be ideal.
server.ext('onPostAuth', function (request, reply){
const user = request.auth.credentials;
const customer = FindCustomer(user);
if (!customer) {
return reply (Boom.forbidden('user is not a customer'));
}
if (!customer.verified) {
return reply(Boom.forbidden('customer not validated'));
}
reply.continue();
});
Complementing ZeMoon's answer, you can implement the onPostAuth like this:
server.ext('onPostAuth', function (request, reply) {
if(request.route.settings.plugins.verifyCustomer) {
const user = request.auth.credentials;
const customer = FindCustomer(user);
if (!customer) {
return reply (Boom.forbidden('user is not a customer'));
}
if (!customer.verified) {
return reply(Boom.forbidden('customer not validated'));
}
}
reply.continue();
});
And then add a configuration plugins.verifyCustomer to the route:
server.route({
method: 'get',
path: '/test1',
handler: function(request, reply) {
// Handler logic here
},
config: {
plugins: {
verifyCustomer: true
}
}
});
i think a more robust way would be to assign scope to the credentials when the customer is authenticated, and to require the scope in the routes you want it.