14.77. DD 77: Merchant Multi-Tenancy and Self-Provisioning#

14.77.1. Summary#

A new requirement is planned that allows a self-provisioning feature of instances in the merchant backend.

14.77.2. Motivation#

We want to enable self-provisioning feature of instances in the merchant backend. A lot of if not most banks have an OpenID-Connect-based IdP that has all their customers already enrolled. Banks may choose to run merchant instances for their customers (Merchant-as-a-Service). In order to simplify enrollment and facilitate adoption, it should be possible to authenticate against such an IdP and allow users to create and use their own instances.

This conflates self-provisioning itself with OIDC integration on purpose: Self-provisioning without OIDC integration provides little benefit to users. Only if they are enabled to re-use their accounts/credentials of the Merchant-as-a-Service provider this feature becomes useful.

The limitation on OIDC is also on purpose: It is the de-facto standard for Identity Federation.

14.77.3. Requirements#

  1. Support OpenID-Connect authentication.

  2. Support self-provisioning of instances for users through the API/UI.

  3. Instances can be associated with users through the API/UI.

14.77.4. Proposed Solution#

The proposed solution is as follows:

14.77.4.1. User#

A user consists of the following properties:

  1. User ID

  2. Associated instances (may be empty)

  3. Password (optional)

  4. External IdP

Instance association can also be viewed as a property of the instance (associated users) but effectively it will be its own table mapping instances to users (n*m).

14.77.4.2. OIDC login:#

(Note that the following assumes that the user logged in before).

Two new endpoints must be implemented: /oidc-login and /oidc-callback. The /oidc-login endpoint is used when the user clicks on Login with OIDC button on the login page of the Merchant Backoffice. The endpoint will redirect the user to the IdP, startin the OIDC flow. The flow will be initiated with the /oidc-callback endpoint as redirect_uri, meaning that the user upon successful authentication will be redirected to /oidc-callback with an authorization code. The merchant backend will exchange the code for ID/access token, and return a cookie associating this state with the new user session. The SPA can then use the Cookie as a credential at the existing /token endpoint to receive a native merchant access token, at which point the session cookie expires.

The OIDC IdP is configured globally (not per instance) by the admin in merchant.conf.

14.77.4.3. Self provisioning:#

Once a user logs in with an external (OIDC) IdP for the first time, a new user entry is created in the merchant backend which is not associated with any instance. This user/token only has access to the self-service page of the Merchant backoffice UI. The user may create a new instance (and is immediately added as a user to the new instance as its creator). We may want to require that OIDC users have an email address (either as their external ID or as a property) and use this as our local User ID. Alternatively (or additionally), other users may add this new user to their instances. The authorization logic of the merchant backend must be modified such that any user that is not associated with an instance is not allowed to perform any operations on it. For now, all associated users have the same roles/rights and are effectively instance admins.

14.77.4.4. Migration:#

Currently, authentication is tied to the instance itself, which is protected by a password. The current design can be migrated by (automatically) creating a user for each existing instance and its password moved to the new user. The ID of the new user is then also immediately associated with the instance as a valid (admin) user.

Example:

BEGIN;

-- Check patch versioning is in place.
SELECT _v.register_patch('merchant-0028', NULL, NULL);

SET search_path TO merchant;

-------------------------- Users  ---------------------------

CREATE TABLE IF NOT EXISTS merchant_users
  (user_serial BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY
  ,user_id TEXT NOT NULL UNIQUE
  ,auth_hash BYTEA CHECK(LENGTH(auth_hash)=64)
  ,auth_salt BYTEA CHECK(LENGTH(auth_salt)=32)
  );
COMMENT ON TABLE merchant_users
  IS 'all the users enrolled in this backend';
COMMENT ON COLUMN merchant_users.user_id
  IS 'identifier of the user (required)';
COMMENT ON COLUMN merchant_users.auth_hash
  IS 'hash used for merchant back office authorization, may be NULL (unset)';
COMMENT ON COLUMN merchant_users.auth_salt
  IS 'salt to use when hashing password before comparing with auth_hash';


--- FIXME not sure if that is what we want...
CREATE TABLE IF NOT EXISTS merchant_instance_users
  (user_serial BIGINT
     REFERENCES merchant_users (user_serial) ON DELETE CASCADE,
  merchant_serial BIGINT
     REFERENCES merchant_instances (merchant_serial) ON DELETE CASCADE  );
COMMENT ON COLUMN merchant_instance_users.user_serial
  IS 'identifies an the admin user of the instance';

COMMENT ON COLUMN merchant_login_tokens.merchant_serial
  IS 'identifies the instance for which the user is admin';


INSERT INTO merchant_users (user_id, auth_hash, auth_salt)
SELECT merchant_id, auth_hash, auth_salt FROM merchant_instances;

ALTER TABLE merchant_instances
DROP COLUMN auth_hash;

ALTER TABLE merchant_instances
DROP COLUMN auth_salt;

COMMIT;

14.77.5. Test Plan#

(If this DD concerns a new or changed feature, describe how it can be tested.)

Locally, OIDC logins can be tested by running a local test OIDC server, e.g. “pipx run oidc-provider-mock” and configuring the endpoints accordingly.

14.77.6. Definition of Done#

(Only applicable to design documents that describe a new feature. While the DoD is not satisfied yet, a user-facing feature must be behind a feature flag or dev-mode flag.)

14.77.7. Alternatives#

  1. No user concept

It is theoretically possible to implement authentication and self-provisioning without adding the concept of a User to the merchant. This will require some gymnastics around the SPA such that first, OIDC users can log-in to an instance at all. This means adding a list of authorized OIDC IDs (careful: Must be unique/include the IdP ID) to the instance. This modification is also required in the proposed solution. This will allow OIDC federated users to log in to existing instances for which this login was pre-configured by an admin. In order to further support self-provisioning, we need a mechanism that allows OIDC to log in without an associated instance. This is tricky because the authentication is tightly integrated with the concept of having an instance behind it. The simplest solution would be to have the SPA do the OIDC flow browser side, and allow the self-provisioning API endpoint to accept the OIDC access token (or ID token) as credentials which will create an instance with this identity as authroized admin user. Note that this goes off-spec of OIDC as both the ID token and access token are not supposed to be presented to the Merchant backend API (ID token audience limited to the client = SPA, access token audience is limited to the IdP Userinfo endpoint).

14.77.8. Drawbacks#

Introducing the concept of a User is a rather big change to the authentication logic of the merchant. However, it will edge its architecture conceptually closer to common OIDC-based approaches. Meaning that if in the future the Merchant authentication is delegated completely to an OIDC IdP, this change becomes easier.

The alternatives do not provide this but also incur rather big changes that are kind of messy as elaborated above.

14.77.9. Discussion / Q&A#

  1. On the difficulty of not having a User

So after some drafting of an OIDC implementation I can see that IF you need some kind of self-service instance creation (via OIDC) I think we really really should add the concept of a user. As long as we only configure an IDP and an authorized user for an instance/instances, the implementation is trivial. Once we need the login before the instance even exists, it gets pretty ugly. I am not sure if I should continue with what I am doing rn until we know what exactly this self provisioning requirement is. Because if we currently authenticate against the instance, and then want to authenticate before the instance even exists, the whole concept falls apart.

Without the self-service instance creation and only loggin into an existing instance w/o password and with OIDC instead, I can have an implementation done pretty soon. But with self-service instance creation most of that implementation will become obsolete because conceptually, authentication will have to be tied to the user and not the instance.

What I mean with it gets ugly: What we could do is go completely off spec and have the SPA do the OIDC flow,aquiring the ID and access tokens. Then, we allow some kind of token exchange (OIDC token -> merchant token, kind of ugly as this is not really allowed) with the permission to create instances. Somehow we then have to limit the number of instances that can be created with that token (which is where it gets really ugly)