Portal Community

User Identity

Octopus users are identified via their tenant authentication system. The ExternalUserId is the identity claim from the auth provider (BizFirstGO SSO, Azure AD, etc.). Octopus does not manage passwords — it trusts the external identity provider:

public class UserComposite
{
    public Guid Id { get; init; }               // Octopus internal ID
    public string ExternalUserId { get; set; }  // from auth token (sub claim)
    public string TenantId { get; init; }
    public string DisplayName { get; set; }
    public string Email { get; set; }
    public UserPreferences Preferences { get; set; }
    public IReadOnlyList<Guid> AccessibleAgentIds { get; set; }
    public IReadOnlyList<Guid> ConversationIds { get; set; }
    public DateTimeOffset CreatedAt { get; init; }
    public DateTimeOffset LastActiveAt { get; set; }
}

Agent Access Control

A user can only interact with agents that appear in their AccessibleAgentIds list. This list is managed by tenant administrators via the agents-app. Agent access can be granted at three levels:

LevelScopeHow to Set
IndividualOne user, one agentUser record in agents-app
Role-basedAll users with a given roleAgent access policy in agents-app
Tenant-wideAll users in the tenantAgent marked as public within tenant

User Preferences

public class UserPreferences
{
    public string PreferredLanguage { get; set; }   // "en", "es", "fr"
    public string Timezone { get; set; }             // "America/New_York"
    public bool StreamingEnabled { get; set; }       // SSE streaming vs. full response
    public bool ShowToolCallDetails { get; set; }    // show tool call steps in UI
    public Guid? DefaultAgentId { get; set; }        // agent to open by default
}

Multi-Tenant Scoping

The TenantId on the UserComposite is the hard boundary. A user from Tenant A can never access agents, conversations, or memory belonging to Tenant B. The repository layer enforces this:

// All user queries are scoped to the tenant
public async Task<UserComposite?> GetByExternalIdAsync(
    string externalUserId, string tenantId)
{
    return await _db.OctopusUsers
        .Where(u => u.ExternalUserId == externalUserId
                 && u.TenantId == tenantId)
        .Select(u => MapToComposite(u))
        .FirstOrDefaultAsync();
}

Auto-Provisioning

When a user first accesses the Octopus chat for a tenant, a UserComposite is auto-provisioned from the auth token claims if it does not exist. The default accessible agents are set from the tenant's default agent access policy:

// In the chat API controller, before routing to agent:
var user = await _userRepository.GetOrProvisionAsync(
    externalUserId: HttpContext.User.FindFirst("sub")!.Value,
    tenantId: tenantId,
    displayName: HttpContext.User.FindFirst("name")?.Value ?? "User",
    email: HttpContext.User.FindFirst("email")?.Value);

if (!user.AccessibleAgentIds.Contains(agentId))
    return Forbid("User does not have access to this agent");
System Users

Octopus supports system users — non-human identities used by other services to interact with agents programmatically. For example, a Flow Studio workflow can act as a system user to call an agent's reasoning capability. System users have their own UserComposite with service-specific preferences.