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

OAuth (2.0) #1368

Open
tentacleuno opened this issue Jan 25, 2021 · 25 comments
Open

OAuth (2.0) #1368

tentacleuno opened this issue Jan 25, 2021 · 25 comments
Labels

Comments

@tentacleuno
Copy link

tentacleuno commented Jan 25, 2021

Is your proposal related to a problem?

Implement OAuth so, instead of the application deriving a JWT from email / password, the server does. This has obvious (substantial) privacy and security benefits. Tokens can be revoked, while an email / password combination requires a more involved approach: manually changing the password. With OAuth, the application will never know the password.

Describe the solution you'd like

An OAuth endpoint, along with clear scoped permissions. This would be a browser endpoint, with a clear button for consent. If the user agrees, they are either:

  • Redirected to the redirect_uri, if provided, or...
  • Returned a code in plaintext to enter into the application.

Please note: I have searched for this in here and I'd just like to reiterate: this is NOT about authenticating with external services.

Keywords: oauth, auth, sso, token, jwt


Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.

@tentacleuno tentacleuno added the enhancement New feature or request label Jan 25, 2021
@tentacleuno tentacleuno changed the title OAuth OAuth (2.0) Jan 25, 2021
@dessalines
Copy link
Member

application deriving a JWT from email / password, the server does

What's the application you're referring to?

@tentacleuno
Copy link
Author

Applications that use the Lemmy API.

@tentacleuno
Copy link
Author

tentacleuno commented Jan 25, 2021

Remember: handing out your email / password combination to random clients is a substantial security risk. OAuth can prevent that. Most projects like Mastodon and Reddit have implemented this! 😄 I'm building a client for Lemmy, but sadly I'm worried people won't trust it due to no OAuth. I actually have OAuth coded into the application. It feels wrong to have to revert code just to add insecure functionality which can easily be abused.

Since you need a legitimate use-case, let me give you one: let's say I do build this client. Someone else hosts it, minifies the build, and injects some cute listeners for the password / email fields. Who, aside from developers, would know the difference?

Another point: remember email phishing? Those random sites that ask you for your Google email & password, and call it 'Google Login'? Who in their right mind would trust some random person with their email and password, when they have been taught to do the exact opposite?

So, instead of telling people 'Yes, you need to enter an email & password specifically for Lemmy clients, nobody else does this, and your client may or may not acquire them', it would be better to get people used to using normal OAuth, and raise suspicion when a website asks for an email & password.

Anyway, I think I've made my point. This is obviously very bad for security, especially when people are building clients. 😄 Additionally, it would be possible to add support for other authentication schemes (like a Smart Card, self-hosted SSO, etc.) without forcing everyone to keep adding support for these authentication methods.

Mastodon does this really well. Firstly, you create an Application. This gives you client_secret and client_id combination. Then, you open a new tab in the user's browser (something like /oauth/acquire?client_secret=[...]&client_id=[...]&redirect_uri=[...]). When the user clicks accept, they are redirected back to the redirect_uri set in the URL.

TL;DR: not adding OAuth could allow bad actors to do some serious harm.

@asonix
Copy link
Collaborator

asonix commented Jan 25, 2021

fwiw i've played with implementing an OAuth2 server in Actix Web before and can offer assistance for this (just not during business hours). I suggest taking a look at the oxide-auth crate, which does the bulk of the heavy-lifting

@Mart-Bogdan
Copy link
Contributor

Hello, @tentacleuno. What kind of client is it? It's web-UI or a mobile one?

Mastodon does this really well. Firstly, you create an Application. This gives you client_secret and client_id combination. Then, you open a new tab in the user's browser (something like /oauth/acquire?client_secret=[...]&client_id=[...]&redirect_uri=[...]). When the user clicks accept, they are redirected back to the redirect_uri set in the URL.

That's strange. CLient secret shouldn't leave server of an app.

And for mobile apps/spas there are flow without secret, PKCE. But I've read about it quite a while ago, so don't remember what benefit over implicit flow it has.

@Mart-Bogdan
Copy link
Contributor

I am wondering what server would handle OAuth and accept cookie/login+pass. Current lemmy-ui? Or a real static html page on the backend?

@dessalines
Copy link
Member

I'd prefer a simple static html page on lemmy. I'll also probably need a good amount of help with this as I'm not too familiar with oauth.

@Mart-Bogdan
Copy link
Contributor

This is really secure to have a static page without JS as this negates any malware inside npm libs. But this isn't a popular option, and not so user-friendly, without AJAX, lol.

@NicolasBFR
Copy link

I didn't manage to make SSO on my lemmy instance from my KeyCloak instance.
Are you considering implementing it properly (not like my experiments) soon ?

@dessalines
Copy link
Member

I have other priorities rn but someone else should feel free to start on this.

@canpolat
Copy link

I have other priorities rn but someone else should feel free to start on this.

@dessalines I started using Lemmy recently so I still consider myself an outsider to Lemmy development. There may be things I misunderstood. Hopefully what I will say now makes sense.

I haven't read the spec but I have some experience with OAuth. As far as I can see, implementing OAuth will be a breaking change for the API. To bring auth up to date, the following needs to be done:

  • JWT should be self-contained: This way the clients can validate the token themselves before using it. It should also contain claims like exp (expiration time) and nbf (not before time). This last point is also tracked by [Security Issue]: Auth JWT does not have expiration time and is sent in the URL to all requests. #3499
  • Clients should pass the token in Authorization header: Currently the token is passed in either the URL or as part of the model (auth property).
  • Necessary flows need to be implemented. These are typically "Authorization Code Flow with PKCE" and "Client Credentials". But I suspect that you may want to implement the no-longer-recommended "Resource Owner Password Credentials Grant" to make the transition easier on the existing clients. This can be done by use of a dedicated component (typically Keycloak) on the backend. There are also client libraries. So, this isn't as daunting as it sounds.

These will have the following consequences:

  • Use of an identity and access management component (typically Keycloak) in the backend.
  • Major API upgrade to remove auth from models and require token in Authorization header.
  • Update of all clients to implement one of the supported auth flows and the new API.

All in all, this is a significant change that will impact almost everything in Lemmy. For that reason, I think it needs to be something the development team agrees on and adds to the roadmap before anybody starts working on it. This way, all parties are sufficiently informed of the consequences of this breaking change.

I think the minimum viable implementation would look like this:

  • Backend: Remove the auth from the models (or at least deprecate it so that the backend does no longer depend on that). Require Authorization header.
  • Frontend: Use the "Resource Owner Password Credentials Grant" to obtain a token and use it in the Authorization header in requests.

A good second step in the client would be to move to "Authorization Code Flow with PKCE" and implement refreshing the token. There are good Typescript libraries that does this, so lemmy-ui would get a lot for free.

I'm not a Rust developer and I don't feel confident hacking a security related component as my first toy project. But I can help with testing. OAuth can be a bit difficult to get into, so I can also help with getting familiar with the concepts if that's necessary.

@erlend-sh
Copy link

typically Keycloak

kanidm by @Firstyear and @yaleman describes itself as a Keycloak replacement.

@canpolat
Copy link

kanidm by @Firstyear and @yaleman describes itself as a Keycloak replacement.

I didn't know about kanidm. I'm not familiar with the Rust landscape. Keycloak is quite popular, but any production ready component will do, of course :)

@Firstyear
Copy link

kanidm by @Firstyear and @yaleman describes itself as a Keycloak replacement.

I didn't know about kanidm. I'm not familiar with the Rust landscape. Keycloak is quite popular, but any production ready component will do, of course :)

It's "not quite" a keycloak replacement, but yes we support OIDC/Oauth2.

Keycloak is a project that lets you add OIDC/Oauth2 in front of existing auth stacks like LDAP.

Kanidm is a self-contained identity management system which contains user storage, ldap and OIDC/Oauth2.

Saying this I would advise that you look into https://docs.rs/openidconnect/latest/openidconnect/index.html for this as it has all the required components for lemmy here as the client/rs to operate correctly with both keycloak or kanidm.

@gkasdorf
Copy link

This would have the added benefit of allowing for scopes (i.e. read only scope for server-side applications that might be accessing something like the inbox). Currently something like push notifications requires full access to the account which is obviously not the best solution.

@canpolat
Copy link

Kanidm is a self-contained identity management system which contains user storage, ldap and OIDC/Oauth2.

@Firstyear does this mean, the user storage will have to be moved to (or proxied by) Kanidm?

Saying this I would advise that you look into https://docs.rs/openidconnect/latest/openidconnect/index.html for this as it has all the required components for lemmy here as the client/rs to operate correctly with both keycloak or kanidm.

OpenID Connet is tracked by #2930. Perhaps these two can be considered together if they will be in the roadmap.

@Firstyear
Copy link

Kanidm is a self-contained identity management system which contains user storage, ldap and OIDC/Oauth2.

@Firstyear does this mean, the user storage will have to be moved to (or proxied by) Kanidm?

This is up to lemmy. They can either use an external OIDC portal like kanidm for authorising their users, or they can become their own AS and store their own users so that other applications can do OIDC via lemmy instances.

Kanidm isn't a generic OIDC AS you can just plugin anywhere, it's intended to be a full auth/idm stack similar to freeipa or ad.

@erlend-sh
Copy link

erlend-sh commented Jul 14, 2023

For prior art of a Rust ActivityPub project supporting OIDC logins, see Kitsune by @aumetra

https://docs.joinkitsune.org/en/configuring/oidc/

It’s important to make a distinction between the two different milestones at play here:

  1. ‘Social login’ support, like the Discourse forum’s DiscourseConnect or Ory Kratos.

  2. OAuth provider/server, like Mastodon enables via Doorkeeper, or Ory Hydra.

(1) would enable people to log into Lemmy instances using their Mastodon account. The other way around will require (2).

It’s also worth noting that OAuth also paves the way for IndieAuth support, since it’s an OAuth 2.0 extension.

@aumetra
Copy link

aumetra commented Jul 14, 2023

To elaborate a bit as to how Kitsune does it:

We are a full OAuth provider backed by oxide-auth. To enable OIDC support, we redirect the user to the OIDC authorization page when we an authorization request.

The OIDC server then handles all the authentication and we then receive back the information about the user. We correlate the information with the authentication request via the CSRF token we generated.

Then we go on and issue an authorization token for the user in question, and redirect back to the OAuth client.

Since what we do between the redirect from the client and the redirect back to the client is completely opaque, this works with all OAuth clients.

Of course Lemmy can choose to do something completely different. This is how we implemented it because we have the constraints of having to support Mastodon clients which are built around the assumption that each server is always a full OAuth provider.

@Freakazoid182
Copy link
Contributor

Frontend: Use the "Resource Owner Password Credentials Grant" to obtain a token and use it in the Authorization header in requests.

I would not recommend implementing the Resource Owner Password Credentials grant. It's mentioned that it MUST NOT be used by the OAuth working group. It's omitted from OAuth2.1.

https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics-19#section-2.4

@canpolat
Copy link

@Freakazoid182 I agree with that. I proposed it as a stepping stone so that various clients can continue working with minimal changes.

@Freakazoid182
Copy link
Contributor

@canpolat Understood.

I'm not sure, but wouldn't it be possible to have a temporary situation allowing for the current login mechanism and JWT validation, but also allowing login trough OIDC (Authorization Code with PKCE). Both eventually require JWT bearer auth, although the validation of an OIDC token has a bit more steps compared to the current signature validation.

That would give clients time to migrate to the new login before the old solution is deprecated. I would think you don't really need have Resource Owner Password grant as an temporary replacement for the current login. I'm not familiar with Keycloak or kanidm, so it might be I'm missing a critical part in your proposal.

I also noticed there's some discussion about refresh tokens on #3636. Just to share my experience and view here. Sorry if this is all obvious, it wasn't all for me when first getting to work with OIDC.

  • I don't see another option to use these if you want JWT access tokens that have a relative short expiration, and not have users needing to keep logging into the app each time their token expires.

  • The most important choice is going to be the lifetimes of your access and refresh token. It's a tradeoff between security (shorter lifetimes are generally more secure), load on your server (longer lifetime will result in less refreshing of access token) and user experience (a refresh token with a long lifetime will keep user logged in longer while not using the client). Having this to be configurable is recommended.

  • Single use refresh tokens. More secure than tokens than can be used indefinitely, but can technically also cause unexpected logouts of the user. For instance, the client requests a new refresh token, the old token is invalidated, but the client doesn't receive a new token due to connectivity issues. The only option that remains is to log the user out. This shouldn't happen regularly, but it's definitely an odd experience for the user. I would still definitely recommend single use refresh tokens.

  • I would also recommend some kind of mechanism to cleanup expired refresh tokens from the database on a schedule.

Likely these things are covered in something like keycloak or kanidm, but still good to be aware of.

@canpolat
Copy link

@Freakazoid182

I'm not sure, but wouldn't it be possible to have a temporary situation allowing for the current login mechanism and JWT validation, but also allowing login trough OIDC (Authorization Code with PKCE). Both eventually require JWT bearer auth, although the validation of an OIDC token has a bit more steps compared to the current signature validation.

I haven't looked into how the current mechanism works. Current solution doesn't use an Authorization header. Instead, it passes the token in the Body. My initial intuition was that it would be difficult to maintain both solutions, but I may very well be wrong. And maybe it would make more sense to go from bad to good in one go instead of bad-notSoGood-good. Considering the whole API would change to support this, the breaking change may as well implement the correct solution from the get go.

I agree with your points about refresh tokens. We should follow industry best practices when implementing an auth solution.

@K4LCIFER
Copy link

K4LCIFER commented Aug 5, 2023

Want to back this issue? Post a bounty on it! We accept bounties via Bountysource.

Please remove this statement. Bountysource has turned rogue, and appears to be stealing peoples money. As seen from their issue tracker:

image

@grahhnt
Copy link

grahhnt commented Aug 8, 2023

I would also suggest adding /.well-known/openid-configuration for openid/oauth auto-discovery

/.well-known/openid-configuration spec: https://openid.net/specs/openid-connect-discovery-1_0.html#ProviderMetadata

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests