IBeam.Identity
2.9.1
dotnet add package IBeam.Identity --version 2.9.1
NuGet\Install-Package IBeam.Identity -Version 2.9.1
<PackageReference Include="IBeam.Identity" Version="2.9.1" />
<PackageVersion Include="IBeam.Identity" Version="2.9.1" />
<PackageReference Include="IBeam.Identity" />
paket add IBeam.Identity --version 2.9.1
#r "nuget: IBeam.Identity, 2.9.1"
#:package IBeam.Identity@2.9.1
#addin nuget:?package=IBeam.Identity&version=2.9.1
#tool nuget:?package=IBeam.Identity&version=2.9.1
IBeam.Identity
IBeam.Identity is the contract package for the IBeam identity domain.
Narrative Introduction
This package provides the shared language for identity workflows across API, services, and repository implementations. It contains interfaces, request/response models, options, and event contracts so higher-level packages can evolve independently behind stable abstractions.
Identity Architecture (Simple View)
IBeam identity is intentionally layered:
- API Layer (
IBeam.Identity.Api)
- HTTP endpoints, auth middleware, request validation, response mapping.
- Controllers include OTP/password/OAuth/token/session/tenant-role APIs.
- Controllers are intentionally thin. They translate HTTP input/output and let expected service exceptions return friendly messages.
- Contract Layer (
IBeam.Identity)
- Interfaces, models, options, events, authorization attributes.
- No storage or provider-specific logic.
- Service Layer (
IBeam.Identity.Services)
- Core auth orchestration: OTP, password, OAuth, token issuing, tenant selection.
- Uses only contracts (
IIdentityUserStore,IOtpChallengeStore, etc.). - Owns business rules, validation decisions, lifecycle logging/events, and error classification.
- Repository Layer (provider implementations)
- Azure Table provider (
IBeam.Identity.Repositories.AzureTable) currently ships complete implementations. - Entity Framework provider (
IBeam.Identity.Repositories.EntityFramework) exists for EF-based identity paths.
- Communications Layer
- Email/SMS abstractions and providers used by OTP/registration flows.
Service-Owned Rules and Error Handling
IBeam keeps business behavior in services, not controllers or repositories. Services are responsible for enforcing auth and tenant rules, deciding whether an error is expected, emitting lifecycle events/logging, and throwing typed identity exceptions with user-safe messages when a request cannot proceed.
Controllers should stay as transport adapters. They validate basic HTTP shape, call the service, and map expected errors such as validation failures, missing membership, invalid credentials, and not-found cases into friendly API responses.
Unexpected exceptions and system failures should not be turned into detailed user-facing messages. They bubble to the API exception pipeline, which writes operational details through IApiErrorSink. With the Azure Table provider, those records are stored in the SystemErrors table. The response remains generic unless detailed errors are explicitly enabled.
Auth Pattern Flexibility
IBeam treats a user as a stable UserId with one or more verified authentication identifiers bound to it. The user can start with SMS OTP, add email OTP later, set an email/password credential after that, and still remain the same identity.
The advantage for product teams is that onboarding can be low-friction without painting the account model into a corner:
- SMS-first onboarding for mobile or care-team workflows.
- Email OTP for magic-link or code-based sign-in.
- Email/password for users who need a traditional credential.
- 2FA using either verified email or verified SMS.
- OAuth links that point back to the same
UserId.
Provider implementations should resolve auth identifiers through an indexed lookup, then load the canonical user by UserId. They should not scan the full user table for login.
Features and Components
- auth service contracts:
IIdentityAuthServiceIIdentityOtpAuthServiceIIdentityOAuthAuthServiceITokenService
- store contracts:
IIdentityUserStore,IOtpChallengeStore,IExternalLoginStoreITenantMembershipStore,ITenantProvisioningService,IAuthSessionStoreITenantRoleStorefor tenant-scoped role CRUD and assignmentITenantInviteStorefor tenant invitation persistence
- service contracts:
ITenantRoleServiceITenantInviteServiceITenantInviteUrlBuilderITenantInviteMessageFactoryIRoleAccessAuthorizerIPermissionAccessAuthorizerIPermissionCatalogProviderIIBeamAccessControlServiceIIBeamAccessCatalogProviderIIBeamAccessCatalogItemProviderIIBeamAccessCatalogOverrideStoreIIBeamAccessRuleProvider
- options models (
JwtOptions,OtpOptions,OAuthOptions,FeatureOptions, etc.) - lifecycle event contracts and default no-op implementations
- role access attributes (service-safe, no MVC dependency):
[RoleAccess("owner", "billing")][RoleAccessId("3f7a4b4f-8fc5-49bb-b6fe-1f4a9b43a3e9")][AllowAllRoleAccess]
- dynamic permission attributes:
[PermissionAccess("SavePatient")][PermissionAccessId("6c76f166-b130-4c80-bf7e-99d38ea1a75f")][IBeamOperation("projects.delete")][IBeamResourceAccess("project", "projectId", "manage")][IBeamOperationTemplate("{permissionPrefix}.delete")]
Models vs Entities
- Models
- Defined in
IBeam.Identity.Models. - Used by API and services (requests/responses/domain contracts).
- Examples:
AuthResultResponse,RegisterUserRequest,TenantInfo.
- Entities
- Provider-specific persistence shapes (e.g., Azure Table entities).
- Include storage keys (
PartitionKey,RowKey) and persistence metadata. - Examples in Azure Table provider:
TenantEntity,UserTenantEntity,OtpChallengeEntity.
Azure Table Schema (Current Provider)
ElCamino identity tables
AspNetUsers: base user identities.AspNetRoles: role definitions.AspNetIndex: identity lookup/index support.
IBeam custom identity tables
Tenants: tenant master records.TenantUsers: tenant-to-user membership index.UserTenants: user-to-tenant membership index.TenantRoles: tenant-scoped roles.TenantInvites: tenant invitation records and token lookup rows.OtpChallenges: OTP lifecycle records (destination, hash, attempts, expiry, consume state).AuthIdentifiers: auth lookup bindings from email/SMS identifiers to canonical user ids.ExternalLogins: OAuth provider-user links.AuthSessions: refresh/session tracking and revocation.SystemLogs: operational log sink records.SystemErrors: operational API error sink records.Schema: schema version marker for bootstrap.
Access-control persistence for permission maps, resource grants, and service-operation rules is owned by IBeam.AccessControl.
Table Naming and Prefixing
For Azure Table identity provider, physical table name is:
{TablePrefix}{BaseTableName}
Examples:
TablePrefix = "IBeam"+AspNetUsers⇒IBeamAspNetUsersTablePrefix = "Acme"+TenantUsers⇒AcmeTenantUsers
Some consuming applications configure the ElCamino base table names to use Identity instead of
AspNet. For those hosts, use:
{
"IBeam": {
"Identity": {
"AzureTable": {
"TablePrefix": "IBeam",
"UserTableName": "IdentityUsers",
"RoleTableName": "IdentityRoles",
"IndexTableName": "IdentityIndex"
}
}
}
}
That produces physical tables such as IBeamIdentityUsers, IBeamIdentityRoles, and
IBeamIdentityIndex.
This applies to both ElCamino and custom IBeam identity tables.
If IBeam:Identity:AzureTable:TablePrefix is unset, the provider uses an empty prefix. IBeam does not derive a prefix from the environment name, application name, or connection string. Environment-specific names such as WellderlyTest must be configured explicitly as TablePrefix.
Operational tables follow the same rule:
- empty prefix:
SystemLogs,SystemErrors,Schema TablePrefix = "Acme":AcmeSystemLogs,AcmeSystemErrors,AcmeSchema
AcmeIdentitySystemLogs and AcmeIdentitySystemErrors are not default IBeam table names unless the app explicitly overrides SystemLogsTableName or SystemErrorsTableName.
The generic repository provider (IBeam.Repositories.AzureTables) uses IBeam:Repositories:AzureTables:TableNamePrefix. When that value is unset, repository tables are also unprefixed apart from Azure-safe normalization of the entity table name. This setting is separate from IBeam:Identity:AzureTable:TablePrefix.
Connection String Resolution Cascade
Current implemented behavior
Azure Table providers currently resolve connection strings with fallback precedence.
- Identity AzureTable provider (
IBeam.Identity.Repositories.AzureTable)
-
IBeam:Identity:AzureTable:StorageConnectionString
-
IBeam:AzureTables
-
IBeam:Repositories:ConnectionString
-
IBeam:ConnectionString
-
ConnectionStrings:AzureTables
-
ConnectionStrings:AzureStorage
-
ConnectionStrings:IBeam
-
ConnectionStrings:DefaultConnection
-
ConnectionStrings:IdentityAzureTable
- Generic AzureTables repository provider (
IBeam.Repositories.AzureTables)
-
IBeam:Repositories:AzureTables:ConnectionString
-
IBeam:AzureTables
-
IBeam:Repositories:ConnectionString
-
IBeam:ConnectionString
-
ConnectionStrings:AzureTables
-
ConnectionStrings:AzureStorage
-
ConnectionStrings:IBeam
-
ConnectionStrings:DefaultConnection
- Identity EntityFramework provider (
IBeam.Identity.Repositories.EntityFramework)
-
{configSectionPath}:ConnectionString(defaultIdentityEf)
-
IBeam:Identity:EntityFramework:ConnectionString
-
IBeam:Repositories:EntityFramework:ConnectionString
-
IBeam:Repositories:ConnectionString
-
IBeam:ConnectionString
-
ConnectionStrings:IdentityEf
-
ConnectionStrings:IdentityEntityFramework
-
ConnectionStrings:IBeam
-
ConnectionStrings:DefaultConnection
Configuration Models Exposed
IBeam:Identity:JwtIBeam:Identity:OtpIBeam:Identity:OAuthIBeam:Identity:FeaturesIBeam:Identity:EventsIBeam:Identity:TenantProvisioningIBeam:Identity:EmailTemplatesIBeam:Identity:PermissionAccessIBeam:Identity:RoleManagementIBeam:Identity:AccessControl
Tenant Provisioning Policy
Auth flows use IBeam:Identity:TenantProvisioning to decide what happens after a user is authenticated but no active tenant membership is available.
Configuration properties:
Mode:AutoCreateTenantForNewUser,RequireExistingTenant, orUseDefaultTenant.DefaultTenantId: tenant ID used byUseDefaultTenant, and optionally byRequireExistingTenant.AutoLinkUserToDefaultTenant: whentrue,UseDefaultTenantlinks authenticated users toDefaultTenantIdif no membership exists.AutoLinkRoleNames: optional role names to ensure/grant during default-tenant auto-link.
Default mode is AutoCreateTenantForNewUser, which preserves the original workspace-per-user behavior:
{
"IBeam": {
"Identity": {
"TenantProvisioning": {
"Mode": "AutoCreateTenantForNewUser"
}
}
}
}
Tenant Extension Pattern
IBeam owns IdentityTenant for identity/auth concerns, while an application can own its app/domain tenant entity such as Tenant, Organization, Workspace, Practice, or Business.
In many consuming applications, plain tables named Users and Tenants are not IBeam's packaged identity tables. They are app-owned extension/domain tables that should be linked to IBeam identity records:
| App Table | IBeam Link | Owns |
|---|---|---|
Users, AppUsers, or Profiles |
UserId, and often TenantId + UserId |
Profile fields, preferences, handles, onboarding, avatar, app display state. |
Tenants, Organizations, or Workspaces |
TenantId |
Slug, branding, billing/provider fields, address, lifecycle flags, app metadata. |
Do not treat extension tables as a request to add app fields to IBeam's packaged identity tables or auth DTOs. IBeam can coordinate creation/sync of extension rows, but the consuming app owns the schema, validation, update APIs, repositories, and migration scripts for those rows.
IdentityTenant stays minimal:
TenantIdNameNormalizedNameStatusCreatedAtUpdatedAt
Applications map their extended tenant row by the same TenantId and keep app/business fields there. For example, Hubbsly can keep Slug, DisplayName, StripeAppKey, IsActive, IsDeleted, CreatedUtc, and UpdatedUtc in Hubbsly.Tenants while IBeam keeps auth tenant metadata in IBeamIdentityTenants.
Core extension contracts:
IIdentityTenantExtensionITenantExtensionStore<TTenant>ITenantExtensionResolver<TTenant>ITenantExtensionCoordinatorITenantLifecycleHookITenantMetadataProviderIIdentityTenantStoreIIdentityTenantService
Service registration:
services.AddIBeamIdentityServices(configuration);
services.AddIBeamIdentityTenantExtension<Tenant, TenantExtensionStore>();
services.AddIBeamIdentityTenantMetadataProvider<TenantMetadataProvider>();
When configured, IBeam hydrates the app-owned tenant extension during tenant creation, tenant selection, tenant listing, and tenant membership bootstrap. If the identity tenant exists but the app tenant row does not, the app's ITenantExtensionStore<TTenant>.CreateAsync is called. If the app row exists, UpdateFromIdentityTenantAsync can keep display metadata in sync.
ITenantMetadataProvider lets an app project app-owned metadata back into IBeam tenant displays. For example, a Hubbsly provider can return DisplayName = Tenant.DisplayName and IsActive = Tenant.IsActive && !Tenant.IsDeleted; IBeam then uses that metadata when returning tenant selections and before issuing tenant-scoped tokens.
Tenant-user profile fields are projections from IdentityUser, not separate tenant-owned profile data. TenantUserInfo exposes DisplayName, Email, and PhoneNumber so tenant user lists align with IdentityUser.DisplayName, IdentityUser.Email, and IdentityUser.PhoneNumber.
User extensions
IBeam owns identity/security primitives only: identity user id, login identifiers, verification/auth state, passwords, OTP, sessions, refresh tokens, tenant membership, and role/token claims. Applications own extended user profile data such as first and last name, preferences, onboarding state, contact preferences, and tenant-scoped profile metadata. IdentityUser.DisplayName is the canonical identity display value; when a user is created without one, IBeam defaults it from explicit display name, then email, then phone number.
Register a host-owned user extension store when the app wants IBeam lifecycle events to project identity users into its own user table:
services.AddIBeamIdentityServices(configuration);
services.AddIBeamIdentityUserExtension<User, UserExtensionStore>();
Core contracts:
IIdentityUserExtensionIIdentityUserProfileExtensionIIdentityUserContactProjectionIIdentityUserExtensionStore<TUserExtension>IIdentityUserExtensionResolver<TUserExtension>IIdentityUserExtensionCoordinatorUserExtensionContext
When configured, IBeam invokes the user extension store during user creation and when auth resolves to a tenant-scoped login or tenant selection. If the app row does not exist, CreateAsync is called with the IdentityUser and UserExtensionContext; if it already exists, UpdateFromIdentityUserAsync can sync identity-owned values such as normalized email or phone. If no user extension store is registered, IBeam continues auth normally and does not create user profile rows.
IBeam does not expose built-in profile extension routes and does not persist app-specific user profile fields. For Hubbsly-style apps, the app-owned Users table should be keyed by selected IBeam TenantId plus IBeam UserId.
User profile data example
For profile details such as address, favorite color, gamer tag, theme, avatar URL, social media handle, onboarding flags, or user preferences, do not add fields to the packaged IBeam identity user table. Keep those fields in an app-owned entity and bind it back to the IBeam identity with UserId, and optionally TenantId when the value is tenant-specific.
Many apps also need email and phone values in their user/profile table. Keep identity contact and app contact separate by convention:
IdentityEmailandIdentityPhoneNumberare read-only projections from IBeam identity and can be refreshed fromIdentityUser.Email/IdentityUser.PhoneNumber.ContactEmailandContactPhoneNumberare app-owned contact preferences. They should not be used for login, verification, invite matching, recovery, or other auth decisions unless the app explicitly routes the change through an IBeam verification flow.
Example app-owned profile entity:
public sealed class AppUserProfile : IIdentityUserProfileExtension, IIdentityUserContactProjection
{
public Guid UserId { get; set; }
public Guid? TenantId { get; set; }
public string DisplayName { get; set; } = string.Empty;
public string FirstName { get; set; } = string.Empty;
public string LastName { get; set; } = string.Empty;
public string? IdentityEmail { get; set; }
public string? IdentityPhoneNumber { get; set; }
public string? ContactEmail { get; set; }
public string? ContactPhoneNumber { get; set; }
public string? Address { get; set; }
public string? FavoriteColor { get; set; }
public string? GamerTag { get; set; }
public string? Theme { get; set; }
public string? SocialHandle { get; set; }
public DateTimeOffset CreatedUtc { get; set; }
public DateTimeOffset UpdatedUtc { get; set; }
}
Example extension store:
public sealed class AppUserProfileStore : IIdentityUserExtensionStore<AppUserProfile>
{
public Task<AppUserProfile?> FindByUserIdAsync(
Guid userId,
Guid? tenantId,
CancellationToken ct = default)
{
// Look up the app-owned profile row by UserId and optional TenantId.
throw new NotImplementedException();
}
public Task<AppUserProfile> CreateAsync(
IdentityUser identityUser,
UserExtensionContext context,
CancellationToken ct = default)
{
var profile = new AppUserProfile
{
UserId = identityUser.UserId,
TenantId = context.TenantId,
DisplayName = context.DisplayName ?? string.Empty,
FirstName = context.FirstName ?? string.Empty,
LastName = context.LastName ?? string.Empty,
Theme = "dark-mode",
CreatedUtc = DateTimeOffset.UtcNow,
UpdatedUtc = DateTimeOffset.UtcNow
};
IdentityUserDefaults.SyncIdentityContact(profile, identityUser);
// Save the app-owned profile row.
throw new NotImplementedException();
}
public Task<AppUserProfile> UpdateFromIdentityUserAsync(
AppUserProfile profile,
IdentityUser identityUser,
UserExtensionContext context,
CancellationToken ct = default)
{
profile.DisplayName = context.DisplayName ?? profile.DisplayName;
IdentityUserDefaults.SyncIdentityContact(profile, identityUser);
profile.UpdatedUtc = DateTimeOffset.UtcNow;
// Save identity-owned display/contact sync without overwriting app-owned preferences.
throw new NotImplementedException();
}
}
Register it in the consuming app:
services.AddIBeamIdentityServices(configuration);
services.AddIBeamIdentityUserExtension<AppUserProfile, AppUserProfileStore>();
The extension hook ensures the profile row exists during identity lifecycle flows. The consuming app should still expose its own typed profile service/API for user-editable fields such as Address, FavoriteColor, GamerTag, Theme, and SocialHandle, because IBeam does not know the app's validation, privacy, or storage rules for those fields.
Typical end-to-end flow for updating a profile field:
PUT /api/me/profile/gamertag
|
v
MeProfileController
|
v
UserProfileService.UpdateGamerTagAsync(...)
|
v
IUserProfileRepository.SaveAsync(...)
|
v
App-owned table: Users or AppUsers
PartitionKey = TENANT|{tenantId:D}
RowKey = USER|{userId:D}
Example request DTO:
public sealed record UpdateGamerTagRequest(string GamerTag);
Example response DTO:
public sealed record AppUserProfileDto(
Guid UserId,
Guid? TenantId,
string? DisplayName,
string? IdentityEmail,
string? IdentityPhoneNumber,
string? ContactEmail,
string? ContactPhoneNumber,
string? GamerTag,
string? Theme,
string? SocialHandle);
Example app service:
[IBeamOperation("users.profile")]
public sealed class UserProfileService
{
private readonly IUserProfileRepository _profiles;
public UserProfileService(IUserProfileRepository profiles)
{
_profiles = profiles;
}
[IBeamOperation("users.profile.updateGamertag")]
public async Task<AppUserProfileDto> UpdateGamerTagAsync(
Guid tenantId,
Guid userId,
string gamerTag,
CancellationToken ct = default)
{
var profile = await _profiles.GetAsync(tenantId, userId, ct)
?? throw new InvalidOperationException("User profile was not found.");
profile.GamerTag = gamerTag.Trim();
profile.UpdatedUtc = DateTimeOffset.UtcNow;
var saved = await _profiles.SaveAsync(profile, ct);
return new AppUserProfileDto(
saved.UserId,
saved.TenantId,
saved.DisplayName,
saved.IdentityEmail,
saved.IdentityPhoneNumber,
saved.ContactEmail,
saved.ContactPhoneNumber,
saved.GamerTag,
saved.Theme,
saved.SocialHandle);
}
}
Example Azure Table shape for the app-owned profile row:
| Field | Purpose |
|---|---|
PartitionKey |
TENANT|{tenantId:D} for tenant-scoped profile data. |
RowKey |
USER|{userId:D} for point lookup by IBeam user id. |
TenantId |
Tenant scope for tenant-specific profile values. |
UserId |
Stable IBeam identity user id. |
DisplayName |
App display name, initialized from IBeam identity context. Identity-owned display changes should be explicit; app fields should not silently override IdentityUser.DisplayName. |
IdentityEmail |
Read-only projection from IdentityUser.Email. |
IdentityPhoneNumber |
Read-only projection from IdentityUser.PhoneNumber. |
ContactEmail |
App-owned contact email, not used for auth unless routed through an IBeam verification flow. |
ContactPhoneNumber |
App-owned contact phone, not used for auth unless routed through an IBeam verification flow. |
Address |
App-owned profile/contact field. |
FavoriteColor |
App-owned preference field. |
GamerTag |
App-owned profile field. |
Theme |
App-owned preference, such as dark-mode. |
SocialHandle |
App-owned social/contact display field. |
CreatedUtc |
Profile row creation timestamp. |
UpdatedUtc |
Profile row update timestamp. |
For single-tenant deployments, use UseDefaultTenant with an explicit tenant ID. Auth requests that omit tenant ID use this configured default; IBeam does not infer it from environment name or storage account.
{
"IBeam": {
"Identity": {
"TenantProvisioning": {
"Mode": "UseDefaultTenant",
"DefaultTenantId": "225925cc-995e-4584-a63b-4f2cb4f38f6f",
"AutoLinkUserToDefaultTenant": true,
"AutoLinkRoleNames": [ "Member" ]
}
}
}
}
Use RequireExistingTenant to disable tenant creation and automatic linking from auth flows. If the user is not already linked to the requested/configured tenant, auth fails with a validation error instead of creating Tenants, TenantUsers, UserTenants, or TenantRoles rows.
{
"IBeam": {
"Identity": {
"TenantProvisioning": {
"Mode": "RequireExistingTenant",
"DefaultTenantId": "225925cc-995e-4584-a63b-4f2cb4f38f6f"
}
}
}
}
Equivalent code configuration:
builder.Services.Configure<TenantProvisioningOptions>(options =>
{
options.Mode = TenantProvisioningMode.UseDefaultTenant;
options.DefaultTenantId = Guid.Parse("225925cc-995e-4584-a63b-4f2cb4f38f6f");
options.AutoLinkUserToDefaultTenant = true;
options.AutoLinkRoleNames.Add("Member");
});
Tenant Invitations
Tenant invitations are the reusable IBeam workflow for tenant-managed user onboarding. A tenant owner/admin can invite a recipient by email or SMS without knowing whether the recipient already has an IBeam account. The invite records the tenant, destination, role/access intent, profile hints, expiration, sender, and correlation metadata. Acceptance resolves or creates the canonical identity user and links that user to the invited tenant.
Core contracts:
ITenantInviteServiceITenantInviteStoreITenantInviteUrlBuilderITenantInviteMessageFactoryTenantInviteCreateRequestTenantInviteAcceptRequest- tenant invite lifecycle events in
IBeam.Identity.Events
Create request highlights:
DestinationType:emailorsmsEmailorPhoneNumber- profile hints:
DisplayName,FirstName,LastName,Metadata - initial tenant access:
RoleIds,RoleNames,SetAsDefaultTenant - optional resource grants:
ResourceType,ResourceId,AccessLevel,ExpirationUtc,Metadata - delivery/context:
ExpiresUtc,RedirectUrl,CorrelationId,CausationId
Acceptance modes:
otp: verify an email or SMS OTP challenge for the invited destination.sms-otp: SMS-specific OTP acceptance.email-password: verify/control the invited email and set or validate a password.existing-session: accept as the current authenticated user, only when that user owns the verified invited destination.
Successful acceptance:
- Looks up the invite by secure token hash.
- Checks status and expiration.
- Verifies the recipient controls the invited email or phone number.
- Resolves the existing identity user or creates a new one.
- Links the user to the invite tenant.
- Applies invite roles through
ITenantRoleServiceor creates a role-less membership throughITenantProvisioningService. - Calls
IIdentityUserExtensionCoordinatorwithOperation = "invite-accepted"and tenant-scoped profile hints. - Applies optional
IBeam.AccessControlresource grants when anIResourceAccessServiceis registered. - Marks the invite redeemed and returns a tenant-scoped token plus membership context.
Security notes:
- Create, list, get, resend, and revoke are tenant-admin operations. The API layer uses
IBeam:Identity:AccessControlto decide which tenant roles and permission claims can manage invites. Defaults recognizeOwner,Administrator, andAdminroles, plus configurable operation-style permission names such asidentity.tenantinvites.manage. - Preview and accept can be anonymous, but the tenant id used for acceptance comes from the stored invite, not from client query parameters.
- Invite tokens are generated with secure random bytes and stored only as SHA-256 hashes.
- The create and preview paths do not reveal whether the destination is already bound to a global identity user.
- Host apps own invite screens, branding, email/SMS templates, license-seat policy, and post-acceptance profile editing.
Override delivery behavior in host apps with DI:
builder.Services.AddIBeamIdentityServices(builder.Configuration);
builder.Services.AddScoped<ITenantInviteUrlBuilder, AppInviteUrlBuilder>();
builder.Services.AddScoped<ITenantInviteMessageFactory, AppInviteMessageFactory>();
Example service usage:
var created = await tenantInvites.CreateInviteAsync(
tenantId,
new TenantInviteCreateRequest(
DestinationType: TenantInviteDestinationTypes.Email,
Email: "ada@example.com",
DisplayName: "Ada Lovelace",
FirstName: "Ada",
LastName: "Lovelace",
RoleNames: ["Member"],
SetAsDefaultTenant: true,
RedirectUrl: "https://app.example.com/invites/accept",
Metadata: new Dictionary<string, string>
{
["source"] = "admin-users"
}),
invitedByUserId,
ct);
var accepted = await tenantInvites.AcceptInviteAsync(
new TenantInviteAcceptRequest(
InviteToken: created.InviteToken,
Mode: TenantInviteAcceptModes.ExistingSession),
authenticatedUserId,
ct);
Examples
1) API composition in a host app
builder.Services.AddIBeamIdentityApi(builder.Configuration);
builder.Services.AddIBeamAccessControl(options =>
{
options.Modules.AddRange(HubbslyModules.All);
options.ResourceCatalogProviders.Add<HubbslyAccessCatalogProvider>();
});
builder.Services.AddIBeamIdentityApiControllers();
2) Azure Table identity configuration with prefix and scoped connection
{
"IBeam": {
"Identity": {
"AzureTable": {
"StorageConnectionString": "UseDevelopmentStorage=true",
"TablePrefix": "Acme",
"UserTableName": "AspNetUsers",
"RoleTableName": "AspNetRoles",
"IndexTableName": "AspNetIndex"
}
}
}
}
3) Fallback-only configuration (top-level IBeam connection)
{
"IBeam": {
"ConnectionString": "UseDevelopmentStorage=true"
}
}
With AzureTable providers, this can be used when deeper scoped keys are not supplied.
4) Service role access example
[RoleAccess("SavePatient")]
public sealed class PatientService
{
private readonly IRoleAccessAuthorizer _roleAccess;
public PatientService(IRoleAccessAuthorizer roleAccess)
{
_roleAccess = roleAccess;
}
public Task SavePatientAsync(ClaimsPrincipal user, CancellationToken ct = default)
{
_roleAccess.EnsureAuthorizedForCurrentMethod(user, this);
return Task.CompletedTask;
}
}
5) Auth identifier contract example
IdentityUser? byEmail = await userStore.FindByEmailAsync("adam@test.com", ct);
IdentityUser? bySms = await userStore.FindByPhoneAsync("16145551212", ct);
// Both should return the same UserId after the identifiers are linked.
6) Add another auth pattern to the current user
await auth.StartEmailPasswordLinkAsync(
userId,
"adam@test.com",
resetUrlBase: "https://app.example.com/finish-email-link",
ct: ct);
await auth.CompleteEmailPasswordLinkAsync(
userId,
"adam@test.com",
challengeId,
verificationToken,
"new secure password",
ct);
7) Bootstrap a tenant membership and roles
await tenantRoles.EnsureTenantMembershipAndGrantRolesAsync(
new TenantMembershipRoleBootstrapRequest(
TenantId: configuredTenantId,
UserId: userId,
TenantName: "Wellderly",
RoleNames: new[] { "Member" },
SetAsDefault: true),
ct);
8) Declare modules and dynamic resource catalogs
using IBeam.Identity.Interfaces;
using IBeam.Identity.Models;
using IBeam.Identity.Services;
builder.Services.AddIBeamAccessControl(options =>
{
options.Modules.Add(new AccessModuleDefinition(
Key: "work",
Label: "Work",
Description: "Work board access.",
SupportedAccessLevels: ["view", "edit", "manage"],
ImpliedByRoleNames: ["Owner", "Administrator", "Work Viewer"],
ImpliedByPermissionNames: ["work.view"]));
options.ResourceCatalogProviders.Add<HubbslyAccessCatalogProvider>();
});
public sealed class HubbslyAccessCatalogProvider : IIBeamAccessCatalogProvider
{
public Task<IReadOnlyList<AccessCatalogResource>> GetResourcesAsync(
Guid tenantId,
CancellationToken ct = default)
{
IReadOnlyList<AccessCatalogResource> resources =
[
new(
ResourceType: "product",
ResourceId: "24e4785d-d558-4511-a879-b70d5c88cd51",
Label: "Qurvia",
Description: "Remote patient monitoring product.",
SupportedAccessLevels: ["view", "edit", "manage"])
];
return Task.FromResult(resources);
}
}
The effective access catalog is layered from IBeam defaults, host configuration, host providers, and tenant DB additions or overrides. Tenant roles are not part of the access-catalog contract; use the tenant roles API for the canonical role catalog and descriptions. Use IIBeamAccessCatalogProvider for resource rows such as products and projects. Use IIBeamAccessCatalogItemProvider for non-resource catalog entries such as tenant-specific agents or integrations:
public sealed class HubbslyAgentCatalogProvider : IIBeamAccessCatalogItemProvider
{
public Task<IReadOnlyList<AccessCatalogItem>> GetCatalogItemsAsync(
Guid tenantId,
CancellationToken ct = default)
=> Task.FromResult<IReadOnlyList<AccessCatalogItem>>(
[
new(
Key: "codex",
Label: "Codex",
Description: "Coding and repository automation agent.",
Category: AccessCatalogCategories.Agent,
Source: AccessCatalogSources.HostProvider,
IsAssignable: true,
IsMutable: false,
IsEnabled: true,
SubjectTypes: [AccessSubjectTypes.ApiCredential])
]);
}
Tenant catalog additions and allowed overrides are persisted through IIBeamAccessCatalogOverrideStore; the Azure Table provider stores them in AccessCatalogOverrides. Assignments and grants remain separate and database-backed.
Operation permissions are first-class catalog entries for business actions such as projects.delete, work.move, or apiCredentials.rotate. Use explicit service checks for enforcement and attributes for discovery:
[IBeamOperation(
"projects.delete",
Label = "Delete Project",
Module = "products",
ResourceType = "project",
RequiredAccessLevel = "manage",
Category = "projects",
IsDangerous = true)]
[IBeamResourceAccess("project", "projectId", "manage")]
public async Task DeleteProjectAsync(Guid projectId, CancellationToken ct)
{
await access.RequirePermissionAsync("projects.delete", ct);
await access.RequireResourceAccessAsync("project", projectId, "manage", ct);
}
Generic inherited methods can use templates with the resource registry:
options.Resources.Add<Project>("project", "projects", label: "Project", module: "products");
[IBeamOperationTemplate("{permissionPrefix}.delete", Operation = "delete", IsDangerous = true)]
[IBeamResourceAccessTemplate("{resourceKey}", "id", "manage")]
public virtual Task DeleteAsync<T>(Guid id, CancellationToken ct = default)
{
throw new NotImplementedException();
}
9) Check access from domain services
public sealed class ProductService
{
private readonly IIBeamAccessControlService _access;
public ProductService(IIBeamAccessControlService access)
{
_access = access;
}
public async Task UpdateProductAsync(
ClaimsPrincipal user,
Guid productId,
UpdateProductRequest request,
CancellationToken ct)
{
await _access.RequireResourceAccessAsync(
user,
resourceType: "product",
resourceId: productId.ToString("D"),
minimumAccessLevel: "edit",
ct);
// Apply app-owned product update rules here.
}
}
Extended Docs And Agent Guidance
- AI prompt:
.agent/prompt.md - Root implementation guide:
../.agent/implementation-guide.md - Azure Table schema inventory:
../docs/identity-azure-table-schema-inventory.md - Roles, permissions, and grants:
../docs/roles-permissions-and-grants.md - Service logging and audit:
../docs/service-logging-and-audit.md - Service operation permissions:
../docs/service-operation-permissions.md - Consuming API migration prompt:
../IBeam.AI.Enablement/examples/consuming-api-migration-prompt.md - Identity extension scan prompt:
../IBeam.AI.Enablement/examples/identity-extension-scan-prompt.md
Agents should treat Identity as auth/account infrastructure and keep application domain behavior in the consuming application's services.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- IBeam.AccessControl (>= 2.9.1)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on IBeam.Identity:
| Package | Downloads |
|---|---|
|
IBeam.Identity.Services
IBeam modular framework components for .NET APIs and services. |
|
|
IBeam.Identity.Repositories.EntityFramework
IBeam modular framework components for .NET APIs and services. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.9.1 | 67 | 7/23/2026 |
| 2.9.0 | 81 | 7/21/2026 |
| 2.8.2 | 82 | 7/21/2026 |
| 2.8.1 | 72 | 7/21/2026 |
| 2.8.0 | 78 | 7/21/2026 |
| 2.7.0 | 100 | 7/21/2026 |
| 2.6.0 | 133 | 7/20/2026 |
| 2.5.0 | 131 | 7/17/2026 |
| 2.4.2 | 127 | 7/16/2026 |
| 2.4.1 | 148 | 7/14/2026 |
| 2.4.0 | 151 | 6/24/2026 |
| 2.3.0 | 142 | 6/24/2026 |
| 2.2.0 | 154 | 6/23/2026 |
| 2.1.0 | 156 | 6/23/2026 |
| 2.0.68 | 142 | 6/23/2026 |
| 2.0.66 | 149 | 6/22/2026 |
| 2.0.65 | 144 | 6/22/2026 |
| 2.0.64 | 208 | 6/17/2026 |
| 2.0.63 | 155 | 6/16/2026 |
| 2.0.62 | 153 | 6/16/2026 |