varabyte / kobweb

A modern framework for full stack web apps in Kotlin, built upon Compose HTML
https://kobweb.varabyte.com
Apache License 2.0
1.58k stars 71 forks source link

Expose a way that users can handle site auth #254

Open bitspittle opened 1 year ago

bitspittle commented 1 year ago

On traditional sites, the server can handle authentication. For context, see: https://ktor.io/docs/authentication.html

To get auth in ktor to work, you need to install authentication and then wrap various routes with "authentication" blocks.

However, Kobweb sets up the routes for you without giving the user access to them.

So the question is...

1) Is this still a relevant feature for something like Kobweb where you download the whole site at once and do mostly client-side navigation?

2) Would this be better handled using a Kobweb server plugin? Or by writing a kobwebx library that people can use to manage logged in states?

bitspittle commented 1 year ago

A possible approach for Kobweb server plugins would be to add a method that looks something like this:

fun wrapRoute(path: String, block: () -> Unit): (() -> Unit)?

By default, this would return null, but you might be able to add something like this:

return if (path.startsWith("/protected")) {
   return {
      basic_auth { content() }
   }
} else null
bitspittle commented 1 year ago

Meanwhile, a client-side implementation could look something like this:

@Page
@Auth
@Composable
fun AdminPage() { ... }

and would be interesting to tag a whole subdir as protected by auth as well.

Alternately, just a simple library function like:

@Page
@Composabe
fun AdminPage = requireAuth {

}

but that approach wouldn't make it trivial to tag a whole subdir as protected by auth.

bitspittle commented 1 year ago

It's possible that this feature would benefit from "Kobweb Spidering", the tentative name I have for separating server components apart (so that if you link inside your site to example.com/admin, it will forcefully download new content instead of navigating internally in the JS already pulled down)

bitspittle commented 1 year ago

Could be worth seeing how Android Compose apps handle auth

mrlem commented 2 days ago

Interesting topic indeed.

I'm quite new to Kobweb, so sorry if I don't know all its features, but if I may intervene, here's what usually happens on android apps (at least those I worked on):

Usually, all this is buried in lower layers (data layer in a clean architecture). But these usually provide a flow about authentication status based on what I described above, which can then be passed on to the upper layers to the view-state, which is sent to composables.

I see no reason why such a flow could not work with Kobweb. The way I see it:

Taking the example of session authentication, which relies on cookies, what you need is basically:

For a fullstack single page app, personally, I probably wouldn't have a call to check auth on each page change. Instead, at login time, I'd provide the user's profile / permissions to the client so the UI can decide for itself what to show or not. The APIs would protect sensitive data from being pulled. If a page happened to perform a forbidden API call anyway, it would get an error. 401 would translate to "the user is not authenticated anymore", 403 would translate to "the user tried accessing a resource that requires additional permissions".

In the current ApiContext, we can indeed get/set headers, so I guess we could send/retrieve cookies, but we lose access to all Ktor cool stuff about session management. For now, in my case, despite the really cool approach for Kobweb APIs, that is a good enough reason to stick to a raw Ktor server for backend. A server plugin might help, but 1/ I don't know yet enough about those^^ 2/ I have a feeling it's not gonna be trivial to exchange session information between the plugin and Kobweb Apis, so this would end up being just Ktor config at least for all authenticated routes, so I get access to Call.sessions). This reduces the interest of Kobweb as a backend (unless it offers other features I have not yet seen).

Maybe we could just expose Call.sessions / Call.principal somehow in the context? Or the raw call itself? This last option would respect Kobweb's philosophy to offer sugar-coated features but still give access to more complete features, in my opinion (and would be much simpler than designing a high-level auth / permissions framework).

bitspittle commented 2 days ago

Thank you for the detailed comment! It will be nice to have your information when we finally get a chance to revisit this more thoroughly.

I did not really understand the Call.sessions / Call.principal point -- maybe you could share some concrete examples of what you were thinking?

FYI over a year ago I had auth working on one of my project's using Firebase. You can see some example code here of what it looked like. This was not a fullstack site but rather a client-side only one.

Now, that project was only a single page app, which was a more trivial case to handle -- one main Index.kt page but with a state machine about what state that page was in. I'm not sure off the top of my head what I would do for a site with dozens of pages. Probably I would make an AuthenticatedLayout (that builds on top of PageLayout) and then have pages that require login use logic in there.

I think for a fullstack app I would probably use something like Firebase to provide the API key and, I don't know, maybe require every sensitive API method to take that API key in as part of the body or else return 401, and then put logic on the client somewhere that redirects every page to a login page if I do get a 401.

Ultimately, there's many different ways to handle auth, and I'm nervous about trying to add yet another solution onto the pile, at least until I have time to understand the problem better, or review a bunch of web apps that use auth effectively.

mrlem commented 1 day ago

Thanks for taking the time to answer!

I did not really understand the Call.sessions / Call.principal point -- maybe you could share some concrete examples of what you were thinking?

What I meant is that, when we are in an Api function, the only thing we have access to is the ApiContext. In it, we find:

This is good, when you want to handle everything yourself. Otherwise, there are missing bits.

For instance:

There's no way to access those in an Api function currently, this makes it harder to implement resource access restriction on the backend. So what I was suggesting, is that ApiContext, in addition to exposing its own res/req, could also expose the underlying call.

It isn't about building yet another custom security infrastructure :wink:, it's about being able to access the one already in place in Ktor from Api functions.

And what I meant when I mentioned this seems in line with Kobweb "opiniated framework" approach is: provide nice and smooth things for most usecases, but without blocking more advanced usages.

FYI over a year ago I had auth working on one of my project's using Firebase. You can see some example code here of what it looked like. This was not a fullstack site but rather a client-side only one.

It's a really nice code sample indeed, thanks! It's nice to see a working auth as a service sample (and cool firebase API, by the way! partial, but already very helpful).

My current use case, though, encourages me to use something based on a more classic session approach. Because in addition to need user authentication, I also need to allow some users to do things using an anonymous session (i.e. no authentication, but still remembering things between visits).