I have created a simple jwt auth application the same way its displayed here: https://github.com/raymondfeng/loopback4-example-auth0
The authentication part is working properly but the authorization does not work as expected.
I decorated my controller with following function and added a scope.
#authenticate({strategy: 'auth0-jwt', options: {scopes: ['greet']}})
In my authentication strategy I´m checking the scope via the AuthenticationMetadata class.
import {AuthenticationBindings, AuthenticationMetadata, AuthenticationStrategy} from '#loopback/authentication';
import {inject} from '#loopback/core';
import {ExpressRequestHandler, Request, Response, RestBindings} from '#loopback/rest';
import {UserProfile} from '#loopback/security';
import {JWT_SERVICE} from './types';
const jwtAuthz = require('express-jwt-authz');
export class JWTAuthenticationStrategy implements AuthenticationStrategy {
name = 'auth0-jwt';
constructor(
#inject(RestBindings.Http.RESPONSE)
private response: Response,
#inject(AuthenticationBindings.METADATA)
private metadata: AuthenticationMetadata,
#inject(JWT_SERVICE)
private jwtCheck: ExpressRequestHandler,
) {}
async authenticate(request: Request): Promise<UserProfile | undefined> {
return new Promise<UserProfile | undefined>((resolve, reject) => {
this.jwtCheck(request, this.response, (err: unknown) => {
if (err) {
console.error(err);
reject(err);
return;
}
console.log(this.metadata.options);
// If the `#authenticate` requires `scopes` check
if (this.metadata.options?.scopes) {
jwtAuthz(this.metadata.options!.scopes, {failWithError: true})(request, this.response, (err2?: Error) => {
if (err2) {
console.error(err2);
reject(err2);
return;
}
// eslint-disable-next-line #typescript-eslint/no-explicit-any
resolve((request as any).user);
});
} else {
// eslint-disable-next-line #typescript-eslint/no-explicit-any
resolve((request as any).user);
}
});
});
}
}
When trying to access
this.metadata.options
I´m always getting an undefined back.
How can I achieve to get the options and the scope out of metadata?
Thanks
For Loopback Authorization your class needs to implement the Provider<Authorizer> interface. In that interface it defines the 2 functions you need to implement
#injectable({scope: BindingScope.TRANSIENT})
class AuthorizationService implements Provider<Authorizer>{
value (): Authorizer {
return this.authorize.bind(this);
}
async authorize (
context: AuthorizationContext,
metadata: AuthorizationMetadata,
) {
// TODO implement authorization
}
}
The authorization metadata will be injected by loopback into that function automatically after you bind it with an AuthorizationTags.Authorizer
If you are having problems implementing Authentication then read my step by step guide on how we implemented Loopback Authentication using Firebase. That should be able to help you with the core ideas to get Authentication running.
Related
dI am currently trying to implement a stripe integration for our client-side to start checkout processes, manage subscriptions, etc... Altough the documentation seems pretty straight forward i have followed the steps for implementing the node SDK, but it does not work for whatever reason.
After calling a method (e.g list all customers) i dont see any request going out in the network tab, there is also no error it just seems that it does nothing at all. I have already tried using a different apiVersion, nothing changed.
This is what the shell class looks like:
import { StripeConfig } from 'shared/src/sharedConfigs/configs/stripeConfig';
import Stripe from 'stripe';
export class StripeShell {
private readonly publishableKey: string;
private readonly secretKey: string;
private readonly apiVersion: string;
private stripeApi: Stripe;
public constructor(config: StripeConfig) {
this.publishableKey = config.publishableKey;
this.secretKey = config.secretKey;
this.apiVersion = config.apiVersion;
this.stripeApi = new Stripe(this.secretKey, {
// #ts-ignore
apiVersion: '2020-08-01',
});
}
public getCustomer(customerId: string): Promise<Stripe.Customer | Stripe.DeletedCustomer> {
return this.stripeApi.customers.retrieve(customerId);
}
public listCustomers(): Promise<Stripe.ApiList<Stripe.Customer>> {
return this.stripeApi.customers.list();
}
public getSubscription(subscriptionId: string): Promise<Stripe.Subscription> {
return this.stripeApi.subscriptions.retrieve(subscriptionId);
}
public getInvoice(invoiceId: string): Promise<Stripe.Invoice> {
return this.stripeApi.invoices.retrieve(invoiceId);
}
}
And this is how i tried testing it:
const test = () => {
shell.stripe.listCustomers().then((res) => {
console.log(res);
}).catch(err => console.error(err));
};
I am using React as frontend Framework and Typescript.
I already tried requiring stripe instead of importing, also tried different apiVersions, also tried using curl which works fine, double checked the secret/publishable key and tried both.
I need to retrieve a customer connected to the storefront backend side to reward him in different ways.
I created a plugin that extends the plugin.class of the plugins system.
It fetches the customer on the store api using the route store-api/account/customer then it sends to my backend its identifier. I also resolve the shop_url of the admin api with window.location.protocol and window.location.hostname...
This seems to me not secured or accurate (the domain can be different from the sales channel to the admin api) and I would like to know if it would be possible to fetch a secured unique customer's token that would allow me to resolve both the shop_url and the customer's identifier.
I cannot find anything in the documentation that would help me securing that part of my app.
Thanks.
(Edit)
Here is my actual code to fetch the customer inside the plugin:
import Plugin from 'src/plugin-system/plugin.class';
import StoreApiClient from 'src/service/store-api-client.service';
const storeClient = new StoreApiClient();
const handleUser = (data, request) => {
let unsecuredUserId = null;
if (request.status === 200) {
try {
const user = JSON.parse(data);
unsecuredUserId = user.id || null;
} catch (e) {}
}
doSomethingWith(unsecuredUserId);
}
export default class SaylPlugin extends Plugin {
init() {
storeClient.get('store-api/account/customer', handleUser);
}
}
You have access to the customer object in the twig template, given the user is currently logged-in. Using this fact you can pass customer data to your plugin using data attributes. The plugin base offers automatic parsing of options based on the naming convention.
{% set myPluginData = {
customerId: context.customer.id
} %}
<div data-my-custom-plugin="true"
data-my-custom-plugin-options="{{ myPluginData|json_encode }}">
</div>
class MyCustomPlugin extends Plugin {
init() {
if (this.options.customerId) {
// do something when the customer is logged in
}
}
// ...
}
PluginManager.register('MyCustomPlugin', MyCustomPlugin, '[data-my-custom-plugin]');
I finally found a way to get the things more secured.
My new plugin code:
import Plugin from 'src/plugin-system/plugin.class';
import StoreApiClient from 'src/service/store-api-client.service';
const storeClient = new StoreApiClient();
const handleContext = (data, request) => {
if (request.status === 200) {
try {
const context = JSON.parse(data);
if (context instanceof Object) {
resolveCustomerBackendSide(
context.token,
context.salesChannel.id
);
}
} catch (e) {
console.error(e);
}
}
}
export default class SaylPlugin extends Plugin {
init() {
storeClient.get('store-api/context', handleContext);
}
}
With this context I can resolve the admin api credentials backend side using the sales channel identifier that I save during the app registration process (you will have to allow sales_channel read in the app's manifest). Therefore I fetch the sales channel backend side to retrieve the sw-access-key header and I can finally fetch the store-api backend side to retrieve the customer in a secured way (the token that you get after fetching the store-api/context can be used as sw-context-token header.
I have many services that all need to know the tenant ID from the request (kept in JWT auth token). The request is either GRPC (jwt stored in MetaData) or Graphql (jwt stored in context.headers.authorization).
I would like to be able to force myself not to forget to pass this tenant id when using the services. Ideally I dont want to even have to constantly write the same code to get the info from the request and pass it through. However the only ways I've managed to do it was using:
#Inject(REQUEST) for grpc in the service constructor. This doesn't work for the graphql requests. The only other way I saw was to only return service methods AFTER providing the data, which looks ugly as hell:
class MyService {
private _actions: {
myMethod1() { ... }
}
withTenantDetails(details) {
this._details = details;
return this._actions;
}
}
If I can somehow get the execution context within MyService that would be a good option, and make this easy using:
const getTenantId = (context: ExecutionContext) => {
if (context.getType() === 'rpc') {
logger.debug('received rpc request');
const request = context.switchToRpc().getContext();
const token = request.context.get("x-authorization");
return {
token,
id: parseTokenTenantInfo(token)
};
}
else if (context.getType<GqlContextType>() === 'graphql') {
logger.debug('received graphql request');
const gqlContext = GqlExecutionContext.create(context);
const request = gqlContext.getContext().request;
const token = request.get('Authorization');
return {
token,
id: parseTokenTenantInfo(token)
};
}
else {
throw new Error(`Unknown context type receiving in tenant param decorator`)
}
}
But I can't find any way to get the executioncontext across to the service without also having to remember to pass it every time.
It's possible to inject Request into injectable service.
For that, the Service will be Scope.Request, and no more Singleton, so a new instance will be created for each request. It's an important consideration, to avoid creating too many resources for performance reason.
It's possible to explicit this scope with :
#Injectable({ scope: Scope.REQUEST })
app.service.ts :
#Injectable({ scope: Scope.REQUEST })
export class AppService {
tenantId: string;
constructor(#Inject(REQUEST) private request: Request) {
// because of #Inject(REQUEST),
// this service becomes REQUEST SCOPED
// and no more SINGLETON
// so this will be executed for each request
this.tenantId = getTenantIdFromRequest(this.request);
}
getData(): Data {
// some logic here
return {
tenantId: this.tenantId,
//...
};
}
}
// this is for example...
const getTenantIdFromRequest = (request: Request): string => {
return request?.header('tenant_id');
};
Note that, instead of decode a JWT token in order to retrieve TENANT_ID for each request, and maybe for other service (one per service), an other approach could be to decode JWT one single time, and then add it in Request object.
It could be done with a global Guard, same as authorization guard examples of official docs.
Here just a simple example : (could be merged with a Auth Guard)
#Injectable()
export class TenantIdGuard implements CanActivate {
canActivate(
context: ExecutionContext,
): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();
request['tenantId'] = getTenantIdFromRequest(request);
return true; // or any other validation
}
}
For GraphQL applications, we should inject CONTEXT in place of REQUEST :
constructor(#Inject(CONTEXT) private context) {}
You have to set either request inside context, or directly TENANT_ID inside context in order to retrieve it after inside service.
Goal
I'm attempting to build a multi-tenant application with Feathers JS. On login, the tenant ID will be included in the request. From then on, the tenant ID will be gotten from the user field with the request parameters. Every user has this tenantId field.
In MongoDB, I have one unique collection of every data type per tenant. The collection names look like tenantId.documents and tenantId.users
Problem
The service generated via the feathers generate service CLI command looks like so:
export class Documents extends Service {
//eslint-disable-next-line #typescript-eslint/no-unused-vars
constructor(options: Partial<MongoDBServiceOptions>, app: Application) {
super(options);
const client: Promise<Db> = app.get('mongoClient');
client.then(db => {
this.Model = db.collection('documents');
});
}
}
As you can see, the generated Services seem to need their collection name ("documents" in this case) during instantiation. Normally, this makes sense since it saves time awaiting a call to app.get("mongoClient")
However, since I need to dynamically change which collection I read from based on the User's tenantId, this won't work for me.
I implemented something like the following:
export class Documents extends Service {
client: Promise<Db>
//eslint-disable-next-line #typescript-eslint/no-unused-vars
constructor(options: Partial<MongoDBServiceOptions>, app: Application) {
super(options);
this.client = app.get("mongoClient");
}
async create(data: IDocumentData, params: Params) {
const db: Db = await this.client;
this.Model = db.collection(`${params.user!!.organizationId}.documents`);
return super.create(data, params);
}
}
The problems are these:
I need to await this.client every request, even when the promise will probably already be fulfilled by the time a user actually makes a request to this service
I have to implement every method of the parent Service even though I barely need to add any real functionality.
Question
What is the most feathers-y way to solve this problem?
I don't want to override every method that I need in every service
I don't see a way to handle this with middleware or hooks.
I also don't think it's necessary to create one service instance per tenant in my application. It seems wasteful since I don't need to make any additional external requests based on the tenant ID, I just need to change the collection
Is there a good, pretty way to do this in Feathers?
Thanks to the helpful Feather community Slack channel, I think I came across a halfway-decent solution to this specific issue. It doesn't address all of my concerns, but at least it de-clutters my code.
First, I should create a new class that extends the built in Service class that implements the feature that I want. It could look something like this:
class DynamicMongoService extends Service {
client: Promise<Db>;
collectionName: string;
constructor(
options: Partial<MongoDBServiceOptions>,
app: Application,
collectionName: string
) {
super(options);
this.client = app.get("mongoClient");
this.collectionName = collectionName;
}
async getCollection(params: Params) {
const db: Db = await this.client;
this.Model = db.collection(
`${params!!.user!!.organizationId}.${this.collectionName}`
);
}
async find(params?: Params) {
await this.getCollection(params!!);
return super.create(params!!);
}
async get(id: Id, params?: Params) {
await this.getCollection(params!!);
return super.get(id, params);
}
async create(data: Partial<any> | Array<Partial<any>>, params?: Params) {
await this.getCollection(params!!);
return super.create(data, params);
}
async update(id: NullableId, data: any, params?: Params) {
await this.getCollection(params!!);
return super.update(id!!, data, params);
}
async patch(id: NullableId, data: Partial<any>, params?: Params) {
await this.getCollection(params!!);
return super.patch(id!!, data, params);
}
async remove(id: NullableId, params?: Params) {
await this.getCollection(params!!);
return super.patch(id!!, params!!);
}
}
The key elements are thus:
Pass collection name in the constructor
Get the collection name before each method
An implementation of this service would look like this:
export class Documents extends DynamicMongoService {
constructor(options: Partial<MongoDBServiceOptions>, app: Application) {
super(options, app, "documents");
}
}
Not the best, but easy enough!
I am creating an Aurelia web project that will consume an API. The API is housed as an Azure Mobile Service. I know I need to add the X-ZUMO Auth header to the request. But, when I instantiate my http client, that header never makes it to the request headers according to the browser dev tools.When I run this in my application, I am presented with a login screen I presume because the X-ZUMO header is not present so the app doesn't have permissions to run. I am running this using gulp which is setting up a local instance of the web application for me. I also tried the external IP address.
Here is my class:
import {HttpClient} from 'aurelia-http-client';
export class WebApi
{
static inject = [HttpClient];
constructor(http)
{
this.http = http;
this.baseUrl = 'https://myProject.azure-mobile.net/';
this.zumoHeader = 'X-ZUMO-APPLICATION';
this.zumoKey = 'zumoKey';
this.client = new HttpClient()
.configure(x => {
x.withBaseUrl(this.baseUrl);
x.withHeader(this.zumoHeader, this.zumoKey);
});
}
// requests will go here
testGet()
{
this.client.jsonp('api/logs?application_id=sessionID&__count=20')
.then(response =>
{
alert(response);
});
}
}
Turns out, you can't use the jsonp method in this instance. The get method(if you're making a get request) is the only way the headers will get added.
After this, I had to make sure the server could handle CORS as well.
import {HttpClient} from 'aurelia-http-client';
export class WebApi
{
static inject = [HttpClient];
constructor(http)
{
this.http = http;
this.baseUrl = 'https://myProject.azure-mobile.net/';
this.zumoHeader = 'X-ZUMO-APPLICATION';
this.zumoKey = 'zumoKey';
this.client = new HttpClient()
.configure(x => {
x.withBaseUrl(this.baseUrl);
x.withHeader(this.zumoHeader, this.zumoKey);
});
}
// requests will go here
testGet()
{
this.client.get('api/logs?application_id=sessionID&__count=20')
.then(response =>
{
alert(response);
});
}
}