go-pkgz / auth

Authenticator via oauth2, direct, email and telegram
https://go-pkgz.umputun.dev/auth/
MIT License
1.06k stars 84 forks source link

Support OpenID and 3rd party IdTokens with cookie-less authentication #101

Open alek-sys opened 3 years ago

alek-sys commented 3 years ago

There are probably quite a few parts in this issue, I totally see those as separate small PRs / issues.

Context

Currently, auth package supports various OAuth2 providers and acts as a OAuth2 Authorization Server / proxy (without strongly committing to RFC) by issuing self-signed ID tokens based on info provided by /userinfo response, which are passed via a cookie, with extra XSRF protection.

Proposal

Support OpenID providers and ID tokens issued by an external identity provider as authentication tokens. In this scenario, the server won't have private keys and won't issue ID tokens itself. Potentially support OpenID Connect Discovery protocol. Unlike general OAuth2 spec, OpenID supports issuing signed ID tokens bound to a particular user, so these tokens can be used for authentication.

How I see the potential configuration:

    // code below will fetch OpenID Connect Discovery config 
    // from https://accounts.google.com/.well-known/openid-configuration
    // then it'll load keys from "jwks_uri" endpoint and store them under "google" provider
    service.AddOpenIDProvider("google", "https://accounts.google.com", "client-id", "client-secret") 

    m := service.Middleware()
    router := chi.NewRouter()

    // setup auth routes
    authRoutes, avaRoutes := service.Handlers()

    // for OpenID providers, /auth handler won't just fetch access token + userinfo, 
    // but will actually request a signed ID token from the provider. 
    // This token can be then passed to the client as is, as it is signed already 
    // and we have public keys for validate it.
    router.Mount("/auth", authRoutes)  // add auth handlers
    router.Mount("/avatar", avaRoutes) // add avatar handler

    // m.OpenIDAuth won't use own keys defined in opts.SecretReader, but instead will use public keys loaded before
    // the provider can be identified either by "iss" claim or manually configured, i.e. m.OpenIDAuth("google")

    // Also, OpenIDAuth shouldn't require a cookie, but Authorization: Bearer <jwt> header to work.
    router.With(m.OpenIDAuth).Get("/api-endpoint", protectedRouteHandler) // protected api

    router.With(m.OpenIDAuth(func(claims token.Claims) bool {
        // do any authorization checks on the token claims, e.g. Role 
    })).Get("/api-endpoint", protectedRouteHandler) // protected api

With OpenIDAuth authentication middleware, the server should act as a OAuth2 Resource Server, i.e. to expect Authorization: Bearer <jwt> header and use that for authentication.

Having this would allow:

Considerations:

umputun commented 3 years ago

I have not spent quality time thinking about this yet, but from a quick view, I have some questions and concerns about how this flow is supposed to work:

alek-sys commented 3 years ago

Thanks for feedback. See my comments below.

  • probably integrating it into existing middleware will be problematic, so all this opened provider path will likely need a separate middleware?
  • this separate middleware will support openid auth only, which can be somewhat confusing to use

That's a valid concern. My view is to have separate middleware for OpenID only. The main reason is using different JWT validation mechanisms - HS256 using secret supplied by SecretReader or RS256/512 using 3rd party provider's public keys.

  • the fundamental question (or maybe just my misunderstanding) - how do we even perform user check if we have no way to validate jwt? I mean, we get jwt header signed by a third party with a key we don't know. The only way to validate it will be another call to this third party, and doing it on each request doesn't seem to be such a good (or even physible) idea to me.

There is a way. OpenID providers usually use asymmetric encryption and publish public keys (using JWKS) to validate the JWT tokens they issue. These keys will be loaded when a provider is added via service.AddOpenIDProvider(...) call.

  • the part about cookies usage and duplicating the same info in the Authorization header is not clear to me. The reason for cookies usage in the current implementation is to keep all the auth transparent to the client, and the only thing the client cares about is XSRF check, which is provided by modern front-end frameworks.

That's a good reason. In my case, I'd like to make it not-so-transparent for the client, but consistent for API / client access by using Authorization header.

  • just a note - currently, auth supports cookie-less auth by sending JWT via header

But not via Authorization header though? Even if I set JWTHeaderName, Bearer prefix won't be stripped so token won't be validated correctly?

  • I think you plan to support what they call "Authentication (or Basic) Flow" and not "The Implicit Flow." Am I right?
  • From what I understand, this "Authentication (or Basic) Flow" is pretty much oauth2 with one-hour access token returned to the client. I'm failing to see how this one-hour token will be used by auth library. Are we supposed to maintain some store on the server-side associating this token to the user? If so, this seems to be sort of opposite to the current approach, where we have no storage and no sessions of any kind.

Not exactly, I don't have plans to support new authentication flows and keep using current authorization_code. OpenID supports that, with the only difference that in the end you can request id_token in addition to access_token. There will be no state on the server-side.

umputun commented 3 years ago

ah, I see. Asymmetric encryption answers the majority of my concerns, however, it adds some others ;)

We will be able to validate/verify the token, however, we won't be able to alter it. For the basic usage, it should be fine but for anything more advanced we will have to use some state on the server-side associating users to tokens. For example, any sort of rbac currently can be done with the modified JWT with ClaimsUpdater, however as you mentioned it won't work with opened tokens. Even not such advanced flow won't be supported directly, but the current avatar's support will need it.

You mentioned, "Probably OpenID should have its own configuration options?" and I have no clue what options (if any) it has. Likely we are looking for a way to send a custom payload/claim as a part of the auth flow.

alek-sys commented 3 years ago

I would like to avoid having server state by all means. Or at least make it optional, i.e. instead of relying on role claim mapped by ClaimsUpdater for RBAC, just let user perform authorization based on existing ID token claims:

router.With(m.OpenIDAuth(func(claims token.Claims) bool {
        // do any authorization checks on the token claims, e.g. Role 
    })).Get("/api-endpoint", protectedRouteHandler) // protected api

I guess the same true for the avatar support, maybe configuration param OpenIDOpts{ AvatarUrlClaim: 'avatar_url' } should be supported?

I'm quite happy with these limitations for all my use cases, the question is how useful do you think OpenID support would be with all of that in mind. For me the main drivers to integrate OpenID support are: 1. No need to manage / share secrets between services 2. Using Authorization header.

Regarding configuration options - I'm not quite sure which options should be there, but a lot of params are irrelevant for OpenID setup, e.g. token duration, since there are no tokens are issued by us anymore. I can only think of AvatarUrlClaim from the above. So this part is TBD.

umputun commented 3 years ago

I'm not sure what this part // do any authorization checks on the token claims, e.g. Role practically means. This check has to be invoked on each and every request (well, can be cached) as we don't know if the received token is allowed to do BLAH, because this info is not a part of the token. This gets very close to the typical session where every request should be authenticated against some external/internall place/service.

For avatar support, if we follow the same logic we won't have a place to put the avatar URL into the token and will need something similar to retrieve it. Probably it won't be much of a problem and we could have a separate request to do it and the client-side will have to deal with storing this info somewhere.

I'm quite happy with these limitations for all my use cases, the question is how useful do you think OpenID support would be with all of that in mind.

I'm not sure yet. So far it sounds like this path can lead to very different functionality and a very different user experience. I'm not saying it is worse or better, but different to the level it can be fundamentally incompatible with the current implementations.

However, it is hard to predict what we will get after some/all the ideas around openid are implemented. I do see a value in supporting this just not sure if it fits well. Probably making some POV PR may help to get a better feeling of what the final implementation going to look like.

umputun commented 3 years ago

Regarding configuration options - I'm not quite sure which options should be there, but a lot of params are irrelevant for OpenID setup, e.g. token duration, since there are no tokens are issued by us anymore. I can only think of AvatarUrlClaim from the above. So this part is TBD.

I meant things that are app-specific rather the low-level concerns. For instance one of the consumer's sets "blocked" flag allowing to deal with rejections on the consumer level, another consumer sets details like "email" even for providers what doesn't have this info but supplied by the user later on. All the RBAC deal is just another set of custom claims and so on.

We can consider all of those details as something requiring a lookup but again, this will limit the autonomy of the tokens and will make them just another kind of session.

zebox commented 2 years ago

@alek-sys, I maybe not completely understood root point of your discussion, but I can assume regarding one moment:

OpenID providers usually use asymmetric encryption and publish public keys (using JWKS) to validate the JWT tokens they issue.

The same mechanisms use in the Apple provider, which implemented in this package. Maybe it's info will helpful for implement OpenID support.