v0.6.5-beta
Multi-tenant SaaS support, end to end. Identity now manages companies and memberships and puts the active company in the token. Users can sign in with a phone code, verify their phone and e-mail, and reset their password over SMTP. Events carry the tenant to their consumers, and generated lists get range and multi-value filters, unique constraints and validation rules. Every Identity feature is off by default, so existing auth.yaml files behave as before.
Identity
Companies (multiTenancy)
Opening a company: self-service. The user who opens it becomes its admin (
adminRole). WithrequireApproval, the company stays pending until a platform role (approverRoles, e.g. SuperAdmin) approves it. Platform roles can also suspend and reactivate companies.Joining a company: two ways.
- The person asks with the company code, and a company admin approves them with roles.
- The company invites by e-mail or phone, and the person accepts.
An invitation to someone who has no account yet waits until a user with that verified e-mail or phone appears, so nobody can claim it by signing up with another person's address.
One person, many companies: the token carries
tenant_id,tenant_nameand the roles of the active company. Switch companies withtenant_idon a refresh; the chosen company stays on later refreshes. A user can set a default company.Rules:
- Company roles (e.g. TenantAdmin, Operator) come only from the membership and can never be granted globally.
- A company's last admin cannot leave or be demoted.
- A suspended company drops out of the token on the next refresh.
Company details: a company admin can update the name and tax number.
Existing databases: an existing
identity_dbgets the new tables automatically.
One-time codes (otp)
- Phone sign-in and sign-up through the grant
urn:baseforge:grant-type:otp. Addotpto the client's grants. The app can pass a self-assignablerole, e.g.Driver. - Verification: a user can verify a phone number or e-mail address on their account, and pending company invitations are attached when they do.
- Code security: codes are stored hashed and expire. There is an attempt limit, a resend cooldown, an hourly cap and a per-IP limit.
- Phone-only accounts: an account may have no e-mail address; if it has one, the address must still be unique.
E-mail and password reset
messaging: e-mail goes out through SMTP (Resend, Google Workspace, Brevo, SES…) or the developmentLogprovider. The SMTP password stays in.env(Auth__Messaging__Smtp__Password).- SMS: there is an
ISmsSenderinterface; for now the only provider isLog. passwordReset: the forgot-password flow.- The response is the same for every address, and each address gets at most one e-mail per cooldown.
- The link is time-limited.
- A successful reset confirms the e-mail, clears any lockout and signs the user out everywhere.
registration.selfAssignableRoles: roles an app may give a user at sign-up (POST /api/account/roles/{role}).accessTokenLifetimeMinutes: sets the access token lifetime.
Fixes
- External login rules are now in one place. The reference Razor login page could open an account while registration was closed, and could link to a password account by matching e-mail. Generated services did not include that page.
- Errors:
/api/*errors are ProblemDetails, the same shape the services use. A duplicate record returns 409. - Security:
System.Security.Cryptography.Pkcsis pinned to 10.0.14 (OpenIddict pulled in a vulnerable 10.0.7). - Client settings: grants and scopes added to
auth.yamllater now reach existing clients too.
Services
- Tenant in events: the outbox envelope carries the publishing tenant, and the consumer handles each event in that tenant. Handlers in multi-tenant services can read and write without hand-written tenant plumbing.
- Early TenantId: a new tenant entity gets its
TenantIdas soon as it is added, not only at SaveChanges. auth.crossTenantRoles: users in these roles whose token has no tenant read every tenant's records. This applies to GET only; writes still need a tenant.- RabbitMQ down at startup: the service keeps running and retries the connection with backoff. Before, the whole service stopped.
- List filters:
rangeFilters(?xFrom=&xTo=),inFilters(?xIn=A&xIn=B) anddefaultSort. unique: field groups that must be unique. In multi-tenant services uniqueness is per tenant, and deleted records are ignored.- Validation:
min,max,minLengthandpatternon props are checked on create and update. externalRefs.x.nullablemakes a reference to another service optional.ProgramHooks.cs: add your own services, middleware and RabbitMQ subscriptions (including non-CRUD events) inCustom/ProgramHooks.Custom.cs. They survive regeneration.
Designer
- New fields: the Identity panel covers companies, OTP, e-mail/SMTP, password reset, self-assignable roles and token lifetime. Entities get range/multi-value filters, default sort, unique fields, validation rules and optional external references. Services get cross-tenant read roles.
- Stale-form guard: Generate no longer silently overwrites a
spec.yaml/auth.yamlthat changed on disk after the page was opened. It asks first. - Port check: Generate refuses host ports that another service in the workspace already uses.
Breaking changes
sortByand new list filters: behavior is unchanged unless you use the new keys.- Production with OTP or password reset: a non-Development Identity with OTP or password reset turned on refuses to start while the e-mail or SMS provider is
Log. - Unique indexes:
EnsureCreatedcreates them only in new databases. For an existing database, add them yourself.
Upgrade
dotnet tool update -g BaseForge.CodeGen --prerelease
baseforge update <service> # opens the Designer with the spec loaded → Generate
baseforge new-identity --spec identity/auth.yaml --output identityGenerated services reference the BaseForge.* 0.6.5-beta packages. Hand-written code under Custom/ is untouched. Details are in the architecture decisions (§5.2.1, §5.5, §6.2, §6.6, §6.7).