Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation for setting up authentication for server & dashboard #2681

Open
sfmskywalker opened this issue Jan 20, 2022 · 22 comments
Open
Labels
documentation Documentation is needed

Comments

@sfmskywalker
Copy link
Member

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

  • As an example, setup Azure B2C that acts as the identity provider. Or any other identity provider is fine also, ideally one that has a free tier or at least a free trial.

ASP.NET Core

  • Configure Authentication Middleware (Open ID Connect as an example).
  • Protect Elsa API controllers.

Dashboard

  • Create a dashboard plugin that adds Axios middleware to attach an access token.

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.

@sfmskywalker sfmskywalker added the documentation Documentation is needed label Jan 20, 2022
@sfmskywalker sfmskywalker added this to To do in Nexxbiz via automation Jan 20, 2022
@sfmskywalker sfmskywalker changed the title Document setting up authentication Documentation for setting up authentication for server & dashboard Jan 20, 2022
@sfmskywalker sfmskywalker moved this from To do to In progress in Nexxbiz Jan 25, 2022
@nickbeau
Copy link

nickbeau commented Mar 10, 2022

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
Copy link
Member Author

sfmskywalker commented Mar 14, 2022

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
Copy link

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
Copy link
Member Author

Yep, that works great :) Thanks!

@nickbeau
Copy link

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
Copy link
Member Author

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

@leddt
Copy link

leddt commented Jul 28, 2022

@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
Copy link

leddt commented Aug 1, 2022

@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
Copy link

@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
Copy link

leddt commented Sep 15, 2022

@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
Copy link

ArmyOfNinjas commented Nov 2, 2022

@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
Copy link

ArmyOfNinjas commented Nov 4, 2022

@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
Copy link

leddt commented Nov 4, 2022

@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
Copy link

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
Copy link

sjd2021 commented Dec 2, 2022

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
Copy link

manicfarmer1 commented Dec 2, 2022

@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
Copy link

sjd2021 commented Dec 2, 2022

@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
Copy link

manicfarmer1 commented Dec 2, 2022

@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
Copy link

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
Copy link

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 sfmskywalker removed this from In progress in Nexxbiz May 10, 2023
@sfmskywalker
Copy link
Member Author

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
Copy link

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',
};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Documentation is needed
Projects
Status: Todo
Development

No branches or pull requests

9 participants