mcollovati / quarkus-hilla

A Quarkus extension to run Hilla applications on Quarkus.
Apache License 2.0
13 stars 0 forks source link

Add stateless authentication (Quarkus OIDC) #30

Open Dudeplayz opened 1 year ago

Dudeplayz commented 1 year ago

In the first, check if it working OOB.

UbiquitousBear commented 8 months ago

I've spent a few hours trying to get OIDC working with Quakus + Hilla.

For notes, I've set up my config as such:

quarkus.oidc.auth-server-url=https://
quarkus.oidc.client-id=code
quarkus.oidc.credentials.secret=woof
quarkus.oidc.application-type=web_app
quarkus.http.auth.permission.authenticated.paths=/*
quarkus.http.auth.permission.authenticated.policy=authenticated
quarkus.oidc.authentication.scopes=openid,profile,email
quarkus.oidc.authentication.user-info-required=true
quarkus.oidc.roles.role-claim-path=groups

This has enabled the app to automatically redirect to the OAuth RS to authorize, and the application receives and stores the auth.

I've had to add an Endpoint, one I'm calling AuthenticationApi with the following code:

@BrowserCallable
@AnonymousAllowed
@AllArgsConstructor
public class AuthenticationApi {
    private final SingleSignOnContext context;
    private final UserInfo userInfo;
    private final SecurityIdentity securityIdentity;

    public @Nonnull SingleSignOnData fetchAll() {
        return context.getSingleSignOnData();
    }

    public String token() {
        return Optional.ofNullable(securityIdentity.getCredential(AccessTokenCredential.class)).map(token -> token.getToken()).orElse(null);
    }

    public @Nonnull List<@Nonnull String> getRegisteredProviders() {
        return context.getRegisteredProviders();
    }

    public OidcUser getAuthenticatedUser() {
        return context.getSingleSignOnData().isAuthenticated() ? OidcUser.fromUserInfo(userInfo) : null;
    }
}

Of interest is the token method, as I need to get the token to bootstrap the HTTPXmlRequests made to the API from the react front-end, using a middleware:

import { Middleware, MiddlewareContext, MiddlewareNext } from '@hilla/frontend';
import { AuthenticationApi } from 'Frontend/generated/endpoints';

export const AuthenticatedRequestMiddleware: Middleware = async function(
  context: MiddlewareContext,
  next: MiddlewareNext
) {

    if (!context.request.url.includes("AuthenticationApi")) {
        const token = await AuthenticationApi.token()
        context.request.headers.append("Authorization", "Bearer " + token)
    }

    return await next(context);    
};

With the above, sadly, it's making a new call to get the token, so I should store it in a cache (or swr).

The conundrum however appears at the controller level: per Quarkus' documentation, the @PermitAll annotation: Specifies that all security roles are allowed to invoke the specified methods. @PermitAll lets everybody in, even without authentication., which is in contrast to what Hilla does: @PermitAll Allows any authenticated user to call a method via the request.

I suppose the only way around this right now is to use RBAC: @RolesAllowed("dcc-editor"). which does work. I may need to create a custom Annotation to hide the above.

mcollovati commented 8 months ago

Hi @UbiquitousBear, thank you for giving a try to quarkus-hilla and for the feedback.

To better understand the problem, is the issue related to calls to QuarkusEndpointController.serveEndpoint() or to the specific methods in the @BrowserCallable annotated class?

mcollovati commented 8 months ago

If the problem is the @BrowserCallable methods blocked by Quarkus security and redirected to the Identity Provider, you should permit access to the QuarkusEndpointController path

quarkus.http.auth.permission.hilla.paths=/connect/*
quarkus.http.auth.permission.hilla.policy=permit

This should probably be done automatically by quarkus-hilla in some way.

UbiquitousBear commented 7 months ago

@mcollovati To clarify, I believe there's two different definitions for @PermitAll: which makes things confusing - that said, what I've done so far is to force OIDC on all endpoints, which seems to fit the job for me, then use @RolesAllowed. Using @PermitAll doesn't seem to permit unauthenticated calls, but I need to play around a bit more.

On a different note, is it possible to define a custom annotation as such:

@BrowserCallable
@RolesAllowed("dcc-editor")
@Retention(RetentionPolicy.RUNTIME)
public @interface AuthenticatedApiEndpoint {}

Any use this on Endpoints/BrowserCallables? It'd mean I don't need to replicate @BrowserCallable and @RolesAllowed("dcc-editor") across endpoints. I've tried using this, but getting a 404 from Quarkus - presumably because the endpoint isn't beaned (dev-ui doesn't show it as an endpoint).

mcollovati commented 7 months ago

I believe there's two different definitions for @PermitAll

That's indeed true. What we can do is to introduce a build step that converts the @PermitAll annotation to @RolesAllowed("**") on classes annotated with @BrowserCallable or @EndpointExposed.

About the meta annotations, I don't know if this works out-of-the-box in a plain Hilla project. You can try to add the @Stereotype annotation to your definition and see if in this way the bean is discovered

UbiquitousBear commented 7 months ago

@mcollovati what are we defining as Stateless here; that requests are made without the sessions cookie?

Dudeplayz commented 7 months ago

@UbiquitousBear we define it as not be bound to a server side session/context (e.g. this definition) in terms of authentication/authorization. I named the ticket this way, because I wasn't sure what other auth mechanisms are available, so it is a bit missleading. Cookie/Header based auth per request is basically always stateless, as long it is not bound to a specific server side session or context. Hilla is designed to be stateless and as we are replacing Spring Boot internals with equivalent Quarkus code, it should work. We just haven't tested it ourself nor added tests for it. So we can't guarantee for it.