Base solution for your next web application
Open Closed

Trigger authenticated ASP.NET Zero API endpoint from external service #12414


User avatar
0
[email protected] created

Hi,

We're trying to integrate our external service with the ASP.NET Zero API. Right now, we’re sending a POST request with a username and password to /api/TokenAuth/Authenticate to get a JWT token, and that part works fine.

However, we're a bit stuck on what happens if the account we use has 2FA enabled. Since our service runs on a schedule and can’t handle interactive prompts, we're not sure how to deal with the extra authentication step.

Could you advise on the best approach or workaround for authenticating when 2FA is enabled?

Markdown is supported
Copy & paste or drag & drop images (max 30 MB per image)

11 Answer(s)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @benjamin.edinger

    A request should be sent to the /api/TokenAuth/Authenticate endpoint using a username and password. If two-factor authentication (2FA) is enabled for the account, the response will include RequiresTwoFactorVerification = true, along with UserId and TwoFactorAuthProviders information.

    To retrieve the two-factor authentication providers (TwoFactorAuthProviders), the GetValidTwoFactorProvidersAsync method from the UserManager class must be used.

    If RequiresTwoFactorVerification = true is returned, a 2FA code must be generated and sent using one of the methods listed in TwoFactorAuthProviders. This can be done by making a request to the SendTwoFactorAuthCode endpoint.

    Next, the received 2FA code must be validated using the TwoFactorAuthenticateAsync method. This method should be implemented as a public API endpoint to handle the verification process.

    If the verification is successful, the system will generate a JWT token, and the user will be successfully authenticated.

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    [email protected] created

    Hi @oguzhanagir

    So basically what we are trying to do is to create an Activity for ELSA Workflows that authenticates users from our tenants to use the ASP.NET Zero API.

    It would be greatly appreciated if you could show us how to implement the functionality to authenticate a user in the code.

    using Elsa.ActivityResults;
    using Elsa.Attributes;
    using Elsa.Expressions;
    using Elsa.Services;
    using Elsa.Services.Models;
    using System.Threading.Tasks;
    
    public class SignInActivity : Activity
    {
        public SignInActivity()
        {
    
        }
    
        [ActivityInput(Hint = "The username or email of the user.", DefaultSyntax = SyntaxNames.Literal)]
        public string Username { get; set; }
    
        [ActivityInput(Hint = "The user's password.", DefaultSyntax = SyntaxNames.Literal)]
        public string Password { get; set; }
    
        [ActivityOutput(Hint = "The newly generated JWT token.")]
        public string JwtToken { get; set; }
    
        protected override async ValueTask<IActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
        {
            return null;
        }
    }
    

    The endpoint to trigger a workflow is secured and can only be triggered by authenticated users so no security issue there. However we don't know how to authenticate a user in the ELSA context

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @benjamin.edinger

    I understand that you want to create a custom Elsa Workflow Activity (SignInActivity) that authenticates a user against your ASP.NET Zero API using username/email and password and returns a JWT access token

    ASP.NET Zero exposes a standard login endpoint: POST /api/TokenAuth/Authenticate

    You’ll inject HttpClient into your Elsa SignInActivity and call this endpoint to authenticate the user.

    public class SignInActivity : Activity
    {
        private readonly IHttpClientFactory _httpClientFactory;
    
        public SignInActivity(IHttpClientFactory httpClientFactory)
        {
            _httpClientFactory = httpClientFactory;
        }
    
        [ActivityInput(Hint = "The username or email of the user.", DefaultSyntax = SyntaxNames.Literal)]
        public string Username { get; set; }
    
        [ActivityInput(Hint = "The user's password.", DefaultSyntax = SyntaxNames.Literal)]
        public string Password { get; set; }
    
        [ActivityOutput(Hint = "The newly generated JWT token.")]
        public string JwtToken { get; set; }
    
        protected override async ValueTask<ActivityExecutionResult> OnExecuteAsync(ActivityExecutionContext context)
        {
            var httpClient = _httpClientFactory.CreateClient();
    
            // Replace with your actual Host URL
            var tokenUrl = "https://your-api-url/api/TokenAuth/Authenticate";
    
            var payload = new
            {
                userNameOrEmailAddress = Username,
                password = Password,
                rememberClient = true
            };
    
            var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
    
            var response = await httpClient.PostAsync(tokenUrl, content);
            if (!response.IsSuccessStatusCode)
                throw new Exception("Authentication failed.");
    
            var responseJson = await response.Content.ReadAsStringAsync();
            var tokenResult = JsonSerializer.Deserialize<TokenAuthResponse>(responseJson, new JsonSerializerOptions
            {
                PropertyNameCaseInsensitive = true
            });
    
            JwtToken = tokenResult.AccessToken;
    
            return Done();
        }
    
        private class TokenAuthResponse
        {
            public string AccessToken { get; set; }
            public string EncryptedAccessToken { get; set; }
            public string RefreshToken { get; set; }
            public string RefreshTokenExpireInSeconds { get; set; }
            public int ExpireInSeconds { get; set; }
            public long UserId { get; set; }
        }
    }
    

    If you will log in as a tenant, the tenant ID information can be added as a Header when sending the request.

    httpClient.DefaultRequestHeaders.Add("Abp.TenantId", "1");

    You can handle this flexibly. If you prefer, you can simply send an HTTP request to the Authenticate endpoint to retrieve the token. Alternatively, you can implement the same logic that the Authenticate method performs directly within the activity and generate the token there.

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    [email protected] created

    Hi @oguzhanagir,

    I already got this but what if the user has 2FA enabled?

    Best regards

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @benjamin.edinger

    Sending the verification code must be done using SendTwoFactorAuthCode. Here, you need to retrieve the information about whether the user has Email or Phone active. Depending on which provider is active, the endpoint will send a confirmation code to the user's selected provider.

    Here, after completing this sign-in process, you will need to direct the Two Factor verification code to a page that will receive the Two Factor provider with returnUrl. After the user selects the Provider, the SendTwoFactorAuthCode endpoint will send a Two Factor code to the user with the relevant UserId and Provider. You need to perform the Authenticate process by receiving this from the user.

    If the user has a two-factor code, it will be sufficient to add it to the payload section of the Authenticate endpoint.

    var payload = new
    {
        userNameOrEmailAddress = Username,
        password = Password,
        rememberClient = true,
        twoFactorVerificationCode = "your-two-factor-code"
    };
    

    If Two Factor login is active and you want to log in to a single stream, you will need to disable Two Factor if it is active in Elsa Workflow.

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    [email protected] created

    Hi @oguzhanagir

    there should not be any user interaction because in the workflows. Workflows will run automatically every hour lets say. So the user cannot input the 2FA code.

    Best regards

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @benjamin.edinger

    It would be reasonable to disable 2FA during authentication for Elsa Workflow. Since 2FA requires additional verification beyond the username and password with different providers, and we do not have API access to these providers, automatic user interaction will not be possible in Elsa Workflows. Therefore, you can disable 2FA for requests coming from Elsa Workflows.

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    [email protected] created

    How can I disable two-factor authentication (2FA) exclusively for ELSA Workflows. As you outlined ELSA would send a request to the auth endpoint where a 2FA Code is required.

    Below is a proposed solution of mine along with an inquiry on its design and any potential security concerns.


    User-Triggered Workflows

    Proposed Approach:

    • Secure Endpoint:
      Create a protected endpoint in the ASP.NET Zero API that accepts a WorkflowId as a parameter. This endpoint will be accessible only to authenticated users.

    • Token Injection:
      Retrieve the JWT token from the user’s authentication cookie (specifically, "Abp.AuthToken") and pass it as a variable named "token" into the workflow. This ensures that the workflow is executed on behalf of a user with the appropriate permissions, thereby maintaining the integrity of the audit log.

    Code Example:

    public async Task TriggerWorkflowAsync(string workflowDefinitionId, CancellationToken cancellationToken = default)
    {
        // Retrieve the workflow blueprint.
        var blueprint = await _workflowRegistry.GetWorkflowAsync(
            workflowDefinitionId,
            VersionOptions.Published,
            cancellationToken);
    
        if (blueprint == null)
        {
            throw new Exception($"Workflow blueprint not found for id: {workflowDefinitionId}");
        }
    
        // Retrieve the current user.
        var user = await GetCurrentUserAsync();
    
        // Get JWT token from the HTTP context cookie.
        var context = _httpContextAccessor.HttpContext;
        var authToken = context?.Request.Cookies["Abp.AuthToken"];
    
        // Prepare workflow variables and inject the token.
        var variables = new Variables();
        if (!string.IsNullOrWhiteSpace(authToken))
        {
            variables.Set("token", authToken);
        }
    
        // Instantiate the workflow with the tenant id.
        var workflowInstance = await _workflowFactory.InstantiateAsync(blueprint, tenantId: blueprint.TenantId, cancellationToken: cancellationToken);
        workflowInstance.Variables = variables;
    
        // Run the workflow.
        var result = await _workflowRunner.RunWorkflowAsync(blueprint, workflowInstance, cancellationToken: cancellationToken);
    }
    

    Automatically-Triggered Workflows

    Proposed Approach:

    • Service User:
      Create a dedicated Service User with a very strong password and disable 2FA for this account. The Service User will be used within the SignInActivity to obtain the JWT token.

    • Token Handling:
      Whether the workflow is triggered manually or automatically, the JWT token is injected into the token variable via the SignInActivity. This ensures that the token is always populated properly.

    • Tenant Integration:
      Implement logic to automatically create a workflow Service User for each new tenant upon registration. Ensure that this Service User account is protected from deletion.


    Security Considerations

    • Encryption of Secrets:
      It is critical that all tokens and passwords are encrypted. I intend to have ELSA manage the encryption, ensuring that the handling of secrets remains secure. But at the moment I don't know if that is possible in ELSA...

    • Authentication & Authorization:
      The design guarantees that workflows triggered by users require a valid JWT token, linking the action to the proper user. This not only secures the workflow execution but also maintains a reliable audit log.

    • Service User Management:
      The Service User for automatic workflows must be safeguarded by a robust password policy and flagged as non-deletable to prevent accidental or malicious removal.


    Could you please review this approach and advise if it meets security best practices or if there are additional concerns that I should address?

    Best regards

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @benjamin.edinger

    Sorry for the late reply. I have done detailed research on this situation.

    Service User for Automatic Workflows: Having a dedicated, 2FA-disabled service account for machine-triggered workflows is a practical solution. Automatically creating a service user per tenant is a good multi-tenancy-aware design.

    2FA Bypass Scope Control:

    • Using a specific claim (e.g., "IsWorkflow") in the JWT token to distinguish these requests.
    • At the Authenticate stage, it would be logical to detect that the request comes from the workflow and adjust the flow to skip the 2FA stage. I think you can create a separate method similar to the Authenticate method and call it only from Elsa Workflow.

    ELSA currently supports some secret handling mechanisms but encryption at rest is something you might need to manage externally (e.g., via Azure Key Vault, HashiCorp Vault, or ASP.NET Core’s Data Protection API). Relying solely on ELSA for encryption without verifying its capabilities could be a security risk.

    Token Lifetime & Refresh Logic: If the JWT token is long-lived or reused in automated workflows, make sure to account for expiry, token revocation, and secure storage. Prefer short-lived tokens with refresh mechanisms where possible.

    Proposed Solution: Use Client Credentials Flow for ELSA Workflows

    To allow ELSA Workflows to authenticate without a username or password, you can enable and utilize the Client Credentials Flow. This flow is ideal for system-level authentication where no user context is required, and it is a secure and standards-compliant way to acquire access tokens.

    • You register a dedicated client in your OpenIddict configuration (which you already have).
    • ELSA sends a request to the /connect/token endpoint using the ClientId and ClientSecret.
    • OpenIddict issues an access token that ELSA can use to call protected APIs.

    Your current OpenIddict setup already includes support for client_credentials:

    "OpenIddict": {
      "IsEnabled": "true", // <- Make sure this is set to true
      "Applications": [
        {
          "ClientId": "elsa_workflow_client",
          "ClientSecret": "your-super-secure-secret",
          "DisplayName": ""Elsa Workflow Client",
          "ConsentType": "Explicit", // You can also mark it as "Systematic" according to your scenario.
          "RedirectUris": [ ],
          "PostLogoutRedirectUris": [],
          "Scopes": [
            "default-api"
          ],
          "Permissions": [
            "ept:token",
            "gt:client_credentials"
          ]
        }
      ]
    },
    

    ELSA Token Request

    POST /connect/token
    Content-Type: application/x-www-form-urlencoded
    
    grant_type=client_credentials&
    client_id=elsa_workflow_client&
    client_secret=your-super-secure-secret&
    scope=default-api
    

    The value from here can be used for the value that Elsa Workflow wants for the size.

    Limit the token scope to only what ELSA needs (e.g., default-api, or even a custom workflow-api scope).

    • Use Client Credentials Flow when:
    • You don’t need to impersonate a user.
    • The workflow performs background or system-level tasks.
    • You want a secure, stateless authentication mechanism.
    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    [email protected] created

    Hi @oguzhanagir,

    Thank you for your detailed response. I’d like to clarify the authentication mechanism further:

    1. With the client credentials flow, will the same ClientSecret be used for all tenants, or is it possible to assign a unique ClientSecret to each tenant?
    2. If the ClientSecret is shared among tenants, is tenant isolation then solely enforced through the TenantId claim, or are additional measures recommended to secure tenant data?

    I appreciate your guidance on this.

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)
  • User Avatar
    0
    oguzhanagir created
    Support Team

    Hi @benjamin.edinger

    By default, using a single ClientSecret is more common and easier to manage.

    Using a separate ClientSecret for each tenant is technically possible, but it requires creating and managing a separate application registration for each tenant in OpenIddict.

    When the ClientSecret is shared, isolation largely relies on the TenantId claim in the token and the automatic data filtering mechanisms provided by ASP.NET Zero/Abp. However, you may need to implement tenant-specific checks across all layers of your application.

    Markdown is supported
    Copy & paste or drag & drop images (max 30 MB per image)