Skip to content

feat: let an account omit its DID until after sign-in - #2

Open
AdrianMSS wants to merge 13 commits into
nextfrom
feature/did-onboarding
Open

AdrianMSS wants to merge 13 commits into
nextfrom
feature/did-onboarding

Conversation

@AdrianMSS

@AdrianMSS AdrianMSS commented Oct 15, 2026 •

Copy link
Copy Markdown

📝 Description

Let an account exist before it has a DID, then attach a self-controlled DID after sign-in. The account id is allocated at create and stays the token subject. This is the api-auth half of trade-data issue 7, on feature/did-onboarding, targeting next.

ContextIdKeys.Account carries the account id. ContextIdKeys.User is the DID, and only once one is stored. Password change and adoption read the account key and set requiresAuthorization: false. They stay authenticated. Logout records the account id, and its route still requires a DID. Adoption calls identityCreate(undefined, undefined, { selfControlled: true }) on the in-process identity service (@3sixty/identity-models 0.20.0-next.3), not the identity HTTP route. A failed save removes the document and its signing key. No organisation credential is issued. Node does not register the account key. On IOTA, self-controlled create needs a gas station. Entity storage does not.

Stored scope stays removed, the password-policy route stays, and the escalated-privilege guard stays. When a create supplies a DID, that guard runs before the identity-uniqueness check.

🔄 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • ⚡ Performance improvement
  • ♻️ Code refactoring
  • 📦 Dependencies update

🛠️ Changes Made

  • Admin create allocates userId with RandomHelper.generateUuidV7() after the email, password, and organisation checks. The request has no account id field. A supplied userIdentity is trimmed and stored. A blank value is rejected before a row is written. Omitting it leaves identity empty. organizationIdentity falls back to the request organisation, and a missing organisation is rejected before anything is written. A DID that is already stored is rejected. Two accounts with no DID each receive their own userId. userId is required. userIdentity is optional and is returned only when a DID is stored. Update may replace an explicit DID and leaves userId unchanged.
  • Login and refresh load the account by userId. That id is the token sub. The email is the login name. IAuthTokenContext.userIdentity stays the account id.
  • The header sets ContextIdKeys.Account whenever the token verifies, and sets ContextIdKeys.User only when the stored identity starts with did:. No DID leaves User unset. A value that is not a DID is not written there.
  • Password change and adoption load the account from ContextIdKeys.Account. Logout records that key as the audit actor. The escalated-privilege guard still reads ContextIdKeys.User, so a caller with no DID is not treated as escalated.
  • PUT /authentication/password and POST /authentication/identity set requiresAuthorization: false and stay authenticated. Every other route still requires the DID.
  • AuthenticationUser is entity version 2: userId required, identity optional. Version 1 remains the authorization shape, with a required identity and no account id. The version 1 to 2 migration allocates userId with RandomHelper.generateUuidV7() and keeps identity. A version 0 row drops its stored scope on the way through. Tokens whose sub is an email or a DID no longer verify.
  • POST /authentication/identity returns 204 and leaves the session cookie in place for a bearer caller and a cookie caller. The identity component type stays identity-service until the engine passes identityComponentType. The local IIdentityAdoption slice is gone. A missing account returns not found. An identity that already starts with did: conflicts and leaves the row unchanged. Otherwise the new document id is saved and an account-updated audit entry records the account id as the actor. A later refresh keeps the same sub.
  • Adoption is rate limited as identity-adopt, 5 attempts per 15 minutes, checked before a document is created. The key is the account id. If the save fails, the new DID is removed with removeKeys, removeDocumentKey, and itself as the controller.
  • @3sixty/identity-models is a new dependency, at next (0.20.0-next.3). Resolving next for this package also moves @3sixty/context to 0.20.0-next.4.
  • GET /authentication/policy stays on the same route group.
  • The copy of these packages under the api repository is unchanged. A node does not pick this up until api-auth publishes it.

🧪 Testing

  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • I have tested this change manually
  • Testing not required for changes

Specs cover create with and without a DID, a blank identity, a missing organisation, two accounts that each receive an id, and escalated-privilege checks. Header specs cover a DID on both context keys, an account with no DID on the account key only, and a stale User value cleared when there is no DID. Password change and adoption load the account when User is absent. Adoption specs cover a stored DID, a DID that is already stored, a failed identity create, a failed save that removes the document key, the audit entry, the rate check, and a refresh that keeps the same account id. The password and adoption routes assert requiresAuthorization: false and do not skip authentication. Schema specs migrate a version 1 row and a version 0 row, and a login spec signs in with the id the migration allocated. The entity-storage service package builds, and its 150 tests pass. Repository pnpm run quality has not been re-run since this change.

@AdrianMSS
AdrianMSS requested a review from psyhedeliq October 15, 2026 11:32
@AdrianMSS
AdrianMSS requested a review from obany October 19, 2026 07:37
@obany

obany commented Oct 20, 2026

Copy link
Copy Markdown
Contributor

The api-auth packages will live in here, they have been separated from api to allow the authorization packages to sit between, but we have not yet merged the authorization branches which is why they are still in api

Comment thread packages/api-auth-entity-storage-models/src/models/IAuthenticationComponent.ts Outdated
@AdrianMSS
AdrianMSS requested a review from obany October 21, 2026 06:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants