IBeam.Identity 2.9.1

dotnet add package IBeam.Identity --version 2.9.1
                    
NuGet\Install-Package IBeam.Identity -Version 2.9.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="IBeam.Identity" Version="2.9.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="IBeam.Identity" Version="2.9.1" />
                    
Directory.Packages.props
<PackageReference Include="IBeam.Identity" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add IBeam.Identity --version 2.9.1
                    
#r "nuget: IBeam.Identity, 2.9.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package IBeam.Identity@2.9.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=IBeam.Identity&version=2.9.1
                    
Install as a Cake Addin
#tool nuget:?package=IBeam.Identity&version=2.9.1
                    
Install as a Cake Tool

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:

  1. 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.
  1. Contract Layer (IBeam.Identity)
  • Interfaces, models, options, events, authorization attributes.
  • No storage or provider-specific logic.
  1. 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.
  1. 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.
  1. 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:
    • IIdentityAuthService
    • IIdentityOtpAuthService
    • IIdentityOAuthAuthService
    • ITokenService
  • store contracts:
    • IIdentityUserStore, IOtpChallengeStore, IExternalLoginStore
    • ITenantMembershipStore, ITenantProvisioningService, IAuthSessionStore
    • ITenantRoleStore for tenant-scoped role CRUD and assignment
    • ITenantInviteStore for tenant invitation persistence
  • service contracts:
    • ITenantRoleService
    • ITenantInviteService
    • ITenantInviteUrlBuilder
    • ITenantInviteMessageFactory
    • IRoleAccessAuthorizer
    • IPermissionAccessAuthorizer
    • IPermissionCatalogProvider
    • IIBeamAccessControlService
    • IIBeamAccessCatalogProvider
    • IIBeamAccessCatalogItemProvider
    • IIBeamAccessCatalogOverrideStore
    • IIBeamAccessRuleProvider
  • 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

  1. Models
  • Defined in IBeam.Identity.Models.
  • Used by API and services (requests/responses/domain contracts).
  • Examples: AuthResultResponse, RegisterUserRequest, TenantInfo.
  1. 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" + AspNetUsersIBeamAspNetUsers
  • TablePrefix = "Acme" + TenantUsersAcmeTenantUsers

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.

  1. Identity AzureTable provider (IBeam.Identity.Repositories.AzureTable)
    1. IBeam:Identity:AzureTable:StorageConnectionString
    1. IBeam:AzureTables
    1. IBeam:Repositories:ConnectionString
    1. IBeam:ConnectionString
    1. ConnectionStrings:AzureTables
    1. ConnectionStrings:AzureStorage
    1. ConnectionStrings:IBeam
    1. ConnectionStrings:DefaultConnection
    1. ConnectionStrings:IdentityAzureTable
  1. Generic AzureTables repository provider (IBeam.Repositories.AzureTables)
    1. IBeam:Repositories:AzureTables:ConnectionString
    1. IBeam:AzureTables
    1. IBeam:Repositories:ConnectionString
    1. IBeam:ConnectionString
    1. ConnectionStrings:AzureTables
    1. ConnectionStrings:AzureStorage
    1. ConnectionStrings:IBeam
    1. ConnectionStrings:DefaultConnection
  1. Identity EntityFramework provider (IBeam.Identity.Repositories.EntityFramework)
    1. {configSectionPath}:ConnectionString (default IdentityEf)
    1. IBeam:Identity:EntityFramework:ConnectionString
    1. IBeam:Repositories:EntityFramework:ConnectionString
    1. IBeam:Repositories:ConnectionString
    1. IBeam:ConnectionString
    1. ConnectionStrings:IdentityEf
    1. ConnectionStrings:IdentityEntityFramework
    1. ConnectionStrings:IBeam
    1. ConnectionStrings:DefaultConnection

Configuration Models Exposed

  • IBeam:Identity:Jwt
  • IBeam:Identity:Otp
  • IBeam:Identity:OAuth
  • IBeam:Identity:Features
  • IBeam:Identity:Events
  • IBeam:Identity:TenantProvisioning
  • IBeam:Identity:EmailTemplates
  • IBeam:Identity:PermissionAccess
  • IBeam:Identity:RoleManagement
  • IBeam: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, or UseDefaultTenant.
  • DefaultTenantId: tenant ID used by UseDefaultTenant, and optionally by RequireExistingTenant.
  • AutoLinkUserToDefaultTenant: when true, UseDefaultTenant links authenticated users to DefaultTenantId if 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:

  • TenantId
  • Name
  • NormalizedName
  • Status
  • CreatedAt
  • UpdatedAt

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:

  • IIdentityTenantExtension
  • ITenantExtensionStore<TTenant>
  • ITenantExtensionResolver<TTenant>
  • ITenantExtensionCoordinator
  • ITenantLifecycleHook
  • ITenantMetadataProvider
  • IIdentityTenantStore
  • IIdentityTenantService

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:

  • IIdentityUserExtension
  • IIdentityUserProfileExtension
  • IIdentityUserContactProjection
  • IIdentityUserExtensionStore<TUserExtension>
  • IIdentityUserExtensionResolver<TUserExtension>
  • IIdentityUserExtensionCoordinator
  • UserExtensionContext

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:

  • IdentityEmail and IdentityPhoneNumber are read-only projections from IBeam identity and can be refreshed from IdentityUser.Email / IdentityUser.PhoneNumber.
  • ContactEmail and ContactPhoneNumber are 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:

  • ITenantInviteService
  • ITenantInviteStore
  • ITenantInviteUrlBuilder
  • ITenantInviteMessageFactory
  • TenantInviteCreateRequest
  • TenantInviteAcceptRequest
  • tenant invite lifecycle events in IBeam.Identity.Events

Create request highlights:

  • DestinationType: email or sms
  • Email or PhoneNumber
  • 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:

  1. Looks up the invite by secure token hash.
  2. Checks status and expiration.
  3. Verifies the recipient controls the invited email or phone number.
  4. Resolves the existing identity user or creates a new one.
  5. Links the user to the invite tenant.
  6. Applies invite roles through ITenantRoleService or creates a role-less membership through ITenantProvisioningService.
  7. Calls IIdentityUserExtensionCoordinator with Operation = "invite-accepted" and tenant-scoped profile hints.
  8. Applies optional IBeam.AccessControl resource grants when an IResourceAccessService is registered.
  9. 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:AccessControl to decide which tenant roles and permission claims can manage invites. Defaults recognize Owner, Administrator, and Admin roles, plus configurable operation-style permission names such as identity.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

Agents should treat Identity as auth/account infrastructure and keep application domain behavior in the consuming application's services.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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 120 7/23/2026
2.9.0 134 7/21/2026
2.8.2 135 7/21/2026
2.8.1 125 7/21/2026
2.8.0 131 7/21/2026
2.7.0 129 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
Loading failed