elsa-workflows / elsa-core

A .NET workflows library
https://v3.elsaworkflows.io/
MIT License
6.53k stars 1.2k forks source link

Documentation for setting up authentication for server & dashboard #2681

Open sfmskywalker opened 2 years ago

sfmskywalker commented 2 years ago

We need to document how to configure the workflow server (ASP.NET Core) with authentication middleware and securing the Elsa API controllers and how to configure the dashboard with a plugin to send access tokens to the backend.

The documentation should be created as a guide and should describe the following:

Identity Provider

ASP.NET Core

Dashboard

Some sample snippets that can be used as input for the documentation:

In startup:

// ConfigureServices:
services
    .AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { ... });

services.AddAuthorization();

...

// Configure:

app
    .UseAuthentication()
    .UseAuthorization()
    .UseEndpoints(endpoints =>
    {
            endpoints
               .MapControllers()
                  .RequireAuthorization(); // Protects all controllers, including Elsa's API controllers. It's like adding `[AuthorizeAttribute]` to all controllers
    });

In the front-end, the following plugin can be created to read an access token from a locally stored cookie:

function AuthPlugin(elsaStudio) {
    const {eventBus} = elsaStudio;

    const getAccessToken = async () => {
        const httpClient = axios.create({
            baseURL: window.location.origin
        });

        try {
            const response = await httpClient.get('.auth/me');
            return response.data[0].id_token;
        } catch (e) {
            console.warn(e.response);
            return null;
        }
    };

    const configureAuthMiddleware = async (e) => {
        const token = await getAccessToken();

        if (!token)
            return;

        e.register({
            onRequest(request) {
                request.headers = {'Authorization': `Bearer ${token}`};
                return request;
            }
        });
    };

    // Handle the "http-client-created" event so we con configure the http client. 
    eventBus.on('http-client-created', configureAuthMiddleware);
}

To register a plugin, see: https://elsa-workflows.github.io/elsa-core/docs/next/extensibility/extensibility-designer-plugins#custom-plugins.

nickbeau commented 2 years ago

Hi All

This is superb work. I've got a Elsa Server with Secured API against Azure AD. However I'm having troble setting the bearer token for the designer, that I'm using as a blazor component. JS Code:

function AuthorizationMiddlewarePlugin(elsaStudio) {
    const eventBus = elsaStudio.eventBus;

    eventBus.on('http-client-created', e => {
        // Register Axios middleware.
        e.service.register({
            onRequest(request) {
                request.headers = { 'Authorization': '${token}' }
                return request;
            }
        });
    });
}

Registration in my page:

const elsaStudioRoot = document.querySelector('elsa-studio-root');

             elsaStudioRoot.addEventListener('initializing', e => {
            const elsaStudio = e.detail;
            elsaStudio.pluginManager.registerPlugin(AuthorizationMiddlewarePlugin);
 });

It just doesn't seem to work. My C# is good, but my Axios and JS are pretty lightweight.

Any ideas, and I'll write the auth documentation for you :)

The errors I'm getting in the console are, unsurprisingly:

VM1166:1          GET https://localhost:5001/v1/features 401
(anonymous) @ VM1166:1
(anonymous) @ p-7f9fc0e9.js:2

However:

  1. My bearer token is correct, I can use this in Blazor components and Swashbuckle to access the APIs
  2. All my other code (which is Blazor Server) can use the token to access the endpoints
  3. All I need is some small help to configure the axios middleware

I have attempted to hard code the token, but even that doesn't work :(

@sfmskywalker any help will be gratefully receieved

sfmskywalker commented 2 years ago

Hi @nickbeau , can you tell me which Elsa package versions you are using? The fact that it fails on the /features endpoint makes me believe that the issue may be solved when upgrading to the latest 2.6-preview release from MyGet. Perhaps you can give that a try and let me know? Alternatively, Elsa 2.6 is about to be released either today or tomorrow. But it would be good to know beforehand in case we can include a last-minute fix that makes it with 2.6 :)

nickbeau commented 2 years ago

Hi @sfmskywalker

We're using 2.5.0 across our solution, haven't tried 2.6 preview but can give it a crack tomorrow if that works :)

sfmskywalker commented 2 years ago

Yep, that works great :) Thanks!

nickbeau commented 2 years ago

Hi @sfmskywalker gave it a quick run, had a deadline but didn't take too much time, however superficially it seems to work! Thanks!

sfmskywalker commented 2 years ago

Great, thanks for letting me know! I’m releasing 2.6 coming Friday.

leddt commented 2 years ago

@sfmskywalker Quick question: how would you go about securing Elsa's API controllers with a different policy from the rest of the controllers?

Suppose I have an MVC app where I want Admin users to have access to the dashboard, but not regular users?

leddt commented 2 years ago

@sfmskywalker I managed to do it using this code:

public static IApplicationBuilder UseElsaApiAuthorization(this IApplicationBuilder app, string policyName)
{
    return app.UseWhen(IsElsaApiRequest, x => x.Use(ApplyPolicy));

    bool IsElsaApiRequest(HttpContext ctx)
    {
        var endpoint = ctx.Features.Get<IEndpointFeature>()?.Endpoint;
        var descriptor = endpoint?.Metadata.GetMetadata<ControllerActionDescriptor>();
        var controllerAssembly = descriptor?.ControllerTypeInfo.Assembly;

        return controllerAssembly == typeof(Elsa.Server.Api.ElsaApiOptions).Assembly;
    }

    async Task ApplyPolicy(HttpContext ctx, Func<Task> next)
    {
        var authorizationService = ctx.RequestServices.GetRequiredService<IAuthorizationService>();
        var authorizationResult = await authorizationService.AuthorizeAsync(ctx.User, policyName);

        if (authorizationResult.Succeeded)
        {
            await next();
        }
        else
        {
            ctx.Response.StatusCode = 403;
        }
    }
}

I'm not sure if there's a better way, but this works for me right now.

therese-william commented 2 years ago

@sfmskywalker I managed to do it using this code:

public static IApplicationBuilder UseElsaApiAuthorization(this IApplicationBuilder app, string policyName)
{
    return app.UseWhen(IsElsaApiRequest, x => x.Use(ApplyPolicy));

    bool IsElsaApiRequest(HttpContext ctx)
    {
        var endpoint = ctx.Features.Get<IEndpointFeature>()?.Endpoint;
        var descriptor = endpoint?.Metadata.GetMetadata<ControllerActionDescriptor>();
        var controllerAssembly = descriptor?.ControllerTypeInfo.Assembly;

        return controllerAssembly == typeof(Elsa.Server.Api.ElsaApiOptions).Assembly;
    }

    async Task ApplyPolicy(HttpContext ctx, Func<Task> next)
    {
        var authorizationService = ctx.RequestServices.GetRequiredService<IAuthorizationService>();
        var authorizationResult = await authorizationService.AuthorizeAsync(ctx.User, policyName);

        if (authorizationResult.Succeeded)
        {
            await next();
        }
        else
        {
            ctx.Response.StatusCode = 403;
        }
    }
}

I'm not sure if there's a better way, but this works for me right now.

Thanks @leddt for sharing, is this for api authentication only not dashboard?

leddt commented 2 years ago

@therese-william that's right. The dashboard can still be accessed, but it will not work as it depends on the API. I did not investigate for a way to secure the page itself. I wouldn't be surprised if it could be done in a similar way.

ArmyOfNinjas commented 2 years ago

@sfmskywalker in this block: const getAccessToken = async () => { const httpClient = axios.create({ baseURL: window.location.origin });

I'm getting ReferenceError: axios is not defined. How can I import axios to use it in this plugin? Thanks

ArmyOfNinjas commented 2 years ago

@leddt Hi, did you, by any chance, find how to apply authentication to the dashboard? I have an authentication service set up so that it should redirect to my IdP service if I access "http://localhost:5001/" or "http://localhost:5001/workflow-definitions", but it doesn't redirect from this URL. However, it redirects from any of elsa api endpoints, like "http://localhost:5001/v1/workflow-definitions". Do you know what might be the case?

leddt commented 2 years ago

@ArmyOfNinjas I did not, sorry. I think Elsa should expose some more official way of doing this, as my code for API authorization is already somewhat of a hack.

manicfarmer1 commented 1 year ago

Does anyone have any ideas of how I can log who did what in Elsa? I was able to secure the system without finding this without issues but as I was trying to figure out a way to secure the controllers with policies I stumbled upon this solution. I'll give leddt's solution a try for securing my controllers with specific policies. Any ideas on logging the user that made the updates to the database would be appreciated.

sjd2021 commented 1 year ago

One issue I'm noticing is that, using blazor, I can't find a way to quickly attach an event listener to elsa studio before the elsa js module does its thing and starts firing off HTTP calls before I've supplied my authentication middleware. Is there something obvious I'm missing?

I was thinking one thing I could add the JS in-line, but that doesn't seem to be supported with blazor (for good reason).

manicfarmer1 commented 1 year ago

@sjd2021 I had a similar issue. Blazor doesn't follow the typical DOM event loading model. You have to tap into an event after render. This is the code I use on one of my blazor pages that feeds a token to the axios middleware. The RegesterElsa function would probably better named RegisterToken or something like that. That is nothing more than the javascript in this artilce listed above. Let us know how it works out for you.

[Parameter]
public string workflowDefinitionId { get; set; } = string.Empty;

public string AccessToken { get; set; }

[Inject]
IAccessTokenProvider TokenProvider { get; set; }

protected async override Task OnAfterRenderAsync(bool firstRender)
{        
    await base.OnAfterRenderAsync(firstRender);

    var accessTokenResult = await TokenProvider.RequestAccessToken();
    AccessToken = string.Empty;

    if (accessTokenResult.TryGetToken(out var token))
    {
        AccessToken = token.Value;
    }

    await JS.InvokeVoidAsync("RegisterElsa",@AccessToken);

}
sjd2021 commented 1 year ago

@manicfarmer1 I've actually been trying to use OnAfterRenderAsync (only I'm not calling base.OnAfterRenderAsync since it appears to do nothing), but unless I put some sort of delay in there, it never waits until the elsa-studio-root element is there. Even with the delay, it's sometimes not there. But then my concern is that the module that starts converting elsa-studio-root into its fully fleshed out object might end up firing off an unauthenticated call to /v1/features or something too soon if I set too long of a wait.

edit: It looks like there's also a case where it can find the element and attach the event listener, but it's too late because the element is already initialized. We're contemplating using a mutation observer, but it's really not ideal.

manicfarmer1 commented 1 year ago

@sjd2021 I did wrap the javascript in a function for JS interop. I did have some issues like you described where it would call the service call before the token was obtained. I put an alert in there and it was getting called twice. The structure I have now has no issues though. I think I had to add that null reference check for the elsaStudioRoot but I can't remember for sure.

btw...access-control-headers is probably more specific to my implementation and probably not needed.

function RegisterElsa(token) {
    const elsaStudioRoot = document.querySelector('elsa-studio-root');
    if (elsaStudioRoot != null) {
        elsaStudioRoot.addEventListener('initializing', e => {
            const elsaStudio = e.detail;
            elsaStudio.pluginManager.registerPlugin(AuthorizationMiddlewarePlugin);
        });
    }
    function AuthorizationMiddlewarePlugin(elsaStudio) {
        const eventBus = elsaStudio.eventBus;
        eventBus.on('http-client-created', e => {
            // Register Axios middleware.
            e.service.register({
                onRequest(request) {
                    var bearerToken = "Bearer " + token;
                    request.headers = { 'Authorization': bearerToken, 'Access-Control-Allow-Headers': 'access-control-allow-headers,access-control-allow-methods,access-control-allow-origin,authorization', 'Content-Type': 'application/json' }
                    return request;
                }
            });
        });
    }

}

chandsalam1108 commented 1 year ago

Hi, How can we have sepearte authorization groups to control access to ELSA APIs and Dashboard? Like ReadAccess group to access APIs and AdminAccess group to access Dashboard...

manicfarmer1 commented 1 year ago

I am writing my own web api for Elsa in my project and not include their web api in my project. It is a lot of work but I know no other way to achieve this and I want the Apis that interface the workflow engine to be modeled the same as my application. I suggest taking a look at Elsa 3 also as that is the newest version they are working on. There is a discord server as well that you can ask questions and probably get better answers from community members.

sfmskywalker commented 1 year ago

Elsa 2 doesn't support fine-grained control over what permissions a user should have, unfortunately.

However, Elsa 3 does have this. It also includes a default Identity module that you can use to create users and roles, which in turn have permissions. This module is optional, however, and you can completely control how to create a ClaimsPrincipal and its permissions claim. For example, if you use Auth0 as your identity provider, you can install it just like you would in any other ASP.NET API application.

The interesting part will be creating a custom designer plugin (ideally using StencilJS) that redirects to Auth0 to let the user sign in and then redirect back to the designer app, from where your plugin receives the acces tokens so that your plugin can attach them to outgoing HTTP requests sent to the workflow API endpoints.

A customer is working on exactly that, so it's possible that they will open source it for others to use as well. If not, I will eventually provide an implementation myself, as time permits.

@manicfarmer1 If you use Elsa 3, you might try using the endpoints provided from Elsa.Workflows.Api, which use FastEndpoints instead of API controllers. They are configured with fine-grained permissions.

Alternatively, I am considering moving the implementation of each endpoint to mediator request/response handlers, so that you don't have to repeat the implementation details. Instead, all you should have to do from your controllers is send the appropriate request model and then return the response model. Your controllers then only need to make sure they expose the expected routes and verbs, and you are in full control of API security.

abdallahwishah commented 1 year ago

i have aissue i used this lisk https://aspnetzero.com/blog/integrating-elsa-with-aspnet-zero-angular to integrating ELSA with ASP.NET Zero (Angular) every thing was working fine but when i use this way image to send token all basic header removed befor: image after use :

image

and i replaced this: request.headers = { Authorization: Bearer ${token} }; to this request.headers = { ...request.headers, Authorization: 'Bearer secret-token', };

LinQiaoPorco commented 4 months ago

SSO is very important to connect the Elsa to existing system.

dehlers-cts commented 1 month ago

As soon as this feature is available, I'll start an ELSA project. until SSO is available, especially integration with MS Entra ID, I'll be on standby waiting for this feature. As an applications architect for my organization, I don't want account silos with different passwords for signing in regardless of how awesome and powerful ELSA is.

LinQiaoPorco commented 3 weeks ago

I assume the title is good to add 'SSO (Single Sign On)' words, etc. , then developer can understand the topic better.