FeathersJS + Auth0: Authenticated but user not populated - authentication

I installed the Auth0 lock and can login on my client side, with an idToken in my localStorage.
I send this idToken to my API server - a FeathersJS server, which is basically an extension to an Express server. I get authenticated correctly using JWT, but the user is empty in my req object (it's called student here):
{ authenticated: true,
query: {},
provider: 'rest',
headers: { (truncated...) },
student: {},
payload:
{ iss: 'https://mydomain.eu.auth0.com/',
sub: 'myusername',
aud: 'myaudience',
exp: 1494125072,
iat: 1494089072 } }
The 5 last lines are the payload contained inside Auth0's idToken.
The fact that my student user object is empty is kind of normal when I think about it, because the app doesn't know how to link a Auth0 user to one of my database users. It's done by the username property. But how do I tell my Feathers app that? Is there a populateUser or something similar?
I remembered such a function in the old Feathers, but the new one uses the common hook's populate function: see here:
populateUser -> use new populate hook in feathers-hooks-common
So I tried this new populate hook, but unfortunately it's only an After hook, which doesn't make sense since I want to populate the user before making the request.
And now I'm stuck. I mean, I could write my own before hook which populates the user, but I bet there's some nicer way to achieve what I want.
Below is some relevant code:
authentication.js
const authentication = require('feathers-authentication');
const jwt = require('feathers-authentication-jwt');
const oauth2 = require('feathers-authentication-oauth2');
const Auth0Strategy = require('passport-auth0').Strategy;
module.exports = function () {
const app = this;
const config = app.get('authentication');
// Set up authentication with the secret
app.configure(authentication(config));
app.configure(jwt());
app.configure(oauth2({
name: 'auth0',
Strategy: Auth0Strategy,
}));
// The `authentication` service is used to create a JWT.
// The before `create` hook registers strategies that can be used
// to create a new valid JWT (e.g. local or oauth2)
app.service('authentication').hooks({
before: {
create: [
authentication.hooks.authenticate(config.strategies)
],
remove: [
authentication.hooks.authenticate('jwt')
]
}
});
};
config file
{
...
"authentication": {
"entity": "student",
"service": "students",
"secret": "SAME_SECRET_AS_BELOW",
"strategies": [
"jwt"
],
"path": "/authentication",
"jwt": {
"header": {
"type": "access"
},
"audience": "SAME_AUDIENCE_AS_BELOW",
"subject": "anonymous",
"issuer": "https://mydomain.eu.auth0.com/",
"algorithm": "HS256",
"expiresIn": "1d"
},
"auth0": {
"clientID": "SAME_AUDIENCE_AS_ABOVE",
"clientSecret": "SAME_SECRET_AS_ABOVE",
"domain": "https://mydomain.eu.auth0.com/"
}
}
}

Related

Store Ability in Express Session?

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(...);
})

vuejs - using JWT as param in vue-router

I'm trying to build a vuejs app using quasar framework.
When a user enters his email account and clicks the reset password link that include a JWT as param (http://localhost:8080/reset-password/eyJhbGciOiJIUz...), he gets this error:
Cannot GET /reset-password/eyJhbGciOiJIUz....
This is the relevant code in routes.js:
{
path: '/reset-password/:token',
component: () => import('pages/profile/ResetPassword.vue')
}
When I remove the 2 dots in the JWT param, the VUE page is being loaded.
For example, this link doesn't work:
http://localhost:8080/reset-password/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX2lkIjoxLCJrZXkiOiJjMjljZDNmNjYwZDNlMWI4NjRhM2JmNjNkODQxZTc2MiIsImlhdCI6MTYxMzIzMTM3MCwiZXhwIjoxNjEzMjMxNDMwfQ.IEchqNWEGAzVZEQhQQIVl9bnbGcu3I_kCXhG8nmKv2k
But this one does:
http://localhost:8080/reset-password/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9eyJ1c2VyX2lkIjoxLCJrZXkiOiJjMjljZDNmNjYwZDNlMWI4NjRhM2JmNjNkODQxZTc2MiIsImlhdCI6MTYxMzIzMTM3MCwiZXhwIjoxNjEzMjMxNDMwfQIEchqNWEGAzVZEQhQQIVl9bnbGcu3I_kCXhG8nmKv2k
How can I solve it without changing the JWT token by removing the 2 dots?
the JWT token is composed of 3 parts (header, payload, verify signature) separated by periods
HEADER: ALGORITHM & TOKEN TYPE
in base 64
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
decode base 64
{
"alg": "HS256",
"typ": "JWT"
}
PAYLOAD: DATA
in base 64
eyJ1c2VyX2lkIjoxLCJrZXkiOiJjMjljZDNmNjYwZDNlMWI4NjRhM2JmNjNkODQxZTc2MiIsImlhdCI6MTYxMzIzMJNTM3MCwiZXhzwjMxNjE
decode base 64
{
"user_id": 1,
"key": "c29cd3f660d3e1b864a3bf63d841e762",
"iat": 1613231370,
"exp": 1613231430
}
VERIFY SIGNATURE
IEchqNWEGAzVZEQhQQIVl9bnbGcu3I_kCXhG8nmKv2k
what matters is the payload, this is sent through the url, and use it.
now if you want to recreate the complete jwt, the header will always be the same, the signature can be generated like this
HMACSHA256 (
base64UrlEncode (header) + "." +
base64UrlEncode (payload),
your-256-bit-secret
)
usually password recovery services only use payload
https://jwt.io
I was fighting with the same problem and solved it by adding the token as a query parameter. That means someone would call my url as
domain.com/my-path?token=a.b.c
The example is written for vue3
const myRoute = {
path: "my-path",
name: "My Page",
component: import("./App.vue"),
beforeEnter( to, from, next ) {
const { token } = to.query;
if (typeof token === "string") {
const jwtRegEx = /(^[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*$)/;
const match = token.match(jwtRegEx);
if (match === null) {
next(false);
} else {
next();
}
} else {
next(false);
}
},
};
The beforeEach function is there to check that the token is indeed a jwt.

Auth not accessible in vuex-module after page reload or direct access

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
}
}

Appsync Resolver no email in claim

I have created a resolver that uses the email address ($context.identity.claims.email). I tested my query in the AWS Console "Queries" section and all worked fine as $context.identity.claims looked as expected;
{
sub: 'xxx-xxx-xxx-xxx-xxx',
aud: 'xxxxxxxxx',
email_verified: true,
sub: 'xxx-xxx-xxx-xxx-xxx',
token_use: 'id',
auth_time: 1563643503,
iss: 'https://cognito-idp.ap-southeast-1.amazonaws.com/ap-southeast-1_xxxxx',
'cognito:username': 'xxxx',
exp: 1563647103,
iat: 1563643503,
email: 'xxx#xxx.xxx'
}
All looks good so lets use it in my React App that uses the AWS Amplify code for authentication. Its not working now and that is because there is no "email" in the claim section! It looks like this;
{
sub: 'xxx-xxx-xxx-xxx-xxx',
event_id: 'xxx-xxx-xxx-xxx-xxx',
token_use: 'access',
scope: 'aws.cognito.signin.user.admin',
auth_time: 1563643209,
iss: 'https://cognito-idp.ap-southeast-1.amazonaws.com/ap-southeast-1_xxxx',
exp: 1563646809,
iat: 1563643209,
jti: 'xxx-xxx-xxx-xxx-xxx',
client_id: 'xxxx',
username: 'xxxx'
}
Can anyone help me out as to why the email shows in the AWS Console Query but not when I call it from my own client?
Amplify can be configured to include the current ID Token for each graphql request by passing in a function. Two configuration options are shown in the following:
import { Auth } from 'aws-amplify';
const getIdToken = async () => ({
Authorization: (await Auth.currentSession()).getIdToken().getJwtToken()
});
const aws_exports = {
aws_appsync_graphqlEndpoint: 'https://****.appsync-api.us-east-2.amazonaws.com/graphql',
aws_appsync_region: 'us-east-2',
aws_appsync_authenticationType: 'AMAZON_COGNITO_USER_POOLS',
// OPTION 1
graphql_headers: getIdToken,
// OPTION 2
// API: {
// graphql_headers: getIdToken
// },
Auth: {
identityPoolId: 'us-east-2:********-****-****-****-************',
region: 'us-east-2',
userPoolId: 'us-east-2_*********',
userPoolWebClientId: '*************************',
type: 'AMAZON_COGNITO_USER_POOLS'
}
};
export default aws_exports;
Amplify.configure(awsconfig);
Note the different claims available to the resolver between Access & ID tokens.
Access tokens will provide claims such as client_id, jti, and scope, while ID token claims provide email, phone_number, etc., along with others like aud, cognito:roles and cognito:username.
Access Token
{
"claims": {
"auth_time": 1581438574,
"client_id": "*************************",
"cognito:groups": [
"Admin"
],
"event_id": "ec70594c-b02b-4015-ad0b-3c207a18a362",
"exp": 1581442175,
"iat": 1581438575,
"iss": "https://cognito-idp.us-east-2.amazonaws.com/us-east-2_*********",
"jti": "351d2d5f-13c3-4de8-ba7c-b3c5a9e46ca6",
"scope": "aws.cognito.signin.user.admin",
"sub": "********-****-****-****-************",
"token_use": "access",
"username": "********-****-****-****-************"
},
...
}
ID Token
{
"claims": {
"address": {
"formatted": "1984 Newspeak Dr"
},
"aud": "....",
"auth_time": 1581438671,
"birthdate": "1984-04-04",
"cognito:groups": [
"Admin"
],
"cognito:roles": [
"arn:aws:iam::012345678901:role/us-east-2-ConsumerRole"
],
"cognito:username": "********-****-****-****-************",
"email": "winston.smith#oceania.gov",
"email_verified": true,
"event_id": "e3087488-bfc8-4d08-a44c-089c4ae7d8ec",
"exp": 1581442271,
"gender": "Male",
"iat": 1581438672,
"iss": "https://cognito-idp.us-east-2.amazonaws.com/us-east-2_*********",
"name": "WINSTON SMITH",
"phone_number": "+15551111984",
"phone_number_verified": false,
"sub": "********-****-****-****-************",
"token_use": "id"
},
...
}
Tested with amplify-js#2.2.4
Source: https://github.com/aws-amplify/amplify-js/blob/aws-amplify%402.2.4/packages/api/src/API.ts#L86-L107
Guessing that inside your React App, you are retrieving the user attributes with something to the effect of
import { Auth } from 'aws-amplify';
async componentDidMount() {
const currentUser = await Auth.currentUserInfo();
const claims = currentUser.attributes;
// verification logic here, and here you cannot find claims['email']
}
One thing to check is whether the specific React App client can access the 'email' attribute. The client may have been disallowed to specific attributes.
Inside the AWS Cognito Console > User Pools > General Settings > App Clients you should see something like the screen shot below.
Find the specific app client (match the Id). Click on 'Set attribute read and write permissions' - underlined red. There you should be able to select the email attribute as Readable by this client.
I have had a request for a feature in Amplify and I got the following brilliant solution suggestion
TLDR : Update your Auth Provider to create a "pre-token generation" Lambad, and inside your lambda you add another 'fake' group to the claim, as the groups are part of the token passed to AppSync
More details on the solution in this repo
https://github.com/dantasfiles/AmplifyMultiTenant
Ok, so I think they is in the "token_use" element. My original code used this function;
import {API, graphqlOperation} from 'aws-amplify';
import * as queries from '../../graphql/queries';
async function makeCall() {
let resp = await API.graphql(graphqlOperation(queries.getMeta));
return resp.data.getMeta;
}
That code produces the observed above. If I use the following (very dirty but works) code I get the above expected result;
import {Auth, API, graphqlOperation} from 'aws-amplify';
import axios from 'axios';
import * as queries from '../../graphql/queries';
async function makeCall() {
const curSesh = await Auth.currentSession();
const token = curSesh.idToken.jwtToken;
const resp = await axios({
method: 'post',
url: API._options.aws_appsync_graphqlEndpoint,
data: graphqlOperation(queries.getMeta),
headers: {
authorization: token
}
});
return resp.data.data.getMeta;
}
I am not going mark this as solved quite yet as I am sure there is a far cleaner way to get this working. If anyone can shed light on it I would love to learn.

How to setup redis on loopback

I tried Redis on my node.js server before, the script looks like this:
//...
redisClient = redis.createClient();
redisClient.on("connect", function (err) {
console.log("redis terkoneksi");
});
redisClient.on("error", function (err) {
console.log("Redis Error " + err);
});
//...
//redisClient.set(...
//redisClient.get(...
At this time I want to try to install the redis loopback using the 'loopback-connector-redis' plugin. I have installed NPM, but I don't know how to use it. I have tried writing like this:
var DataSource = require('loopback-datasource-juggler').DataSource;
var ds = new DataSource('redis');
ds = redis.createClient(); //script error (createClient is not function)
ds.on("connect", function (err) {
console.log("redis terkoneksi");
});
ds.on("error", function (err) {
console.log("Redis Error " + err);
});
there are two questions I have:
1. how to use redis on loopback?
2. How to write correctly so that it can refer to the redis 'set', 'get', etc?
I will receive all the answers and suggestions, thank you.
best regards.
LoopBack provides two flavors of Redis connector.
Community-maintained loopback-connector-redis which is implementing CRUD-like storage API using Redis as the "database".
loopback-connector-kv-redis providing a key-value API that's closer to how Redis is typically used.
Since you are mentioning set and get commands, my recommendation is to use the KV connector. See https://github.com/strongloop/loopback-example-kv-connectors/tree/master/redis.lb3x for a full working example.
(1)
Create a datasource backed by the Redis KV connector.
Example: server/datasources.json
{
"db": {
"host": "127.0.0.1",
"port": 6379,
"name": "db",
"connector": "kv-redis"
}
}
(2)
Create a new model to represent the KeyValue data and operations.
Example: common/models/color.json
{
"name": "Color",
"base": "KeyValueModel",
"idInjection": true,
"options": {
"validateUpsert": true
},
"properties": {},
"validations": [],
"relations": {},
"acls": [],
"methods": {}
}
(3)
Attach the model to the datasource.
Example: server/model-config.json
{
"Color": {
"dataSource": "db",
"public": true
}
}
Now you can access all KeyValueModel methods on your model, either via REST API or from JavaScript:
const Color = app.models.Color;
await Color.set('my key', 'my value', {/*options*/});