-
Notifications
You must be signed in to change notification settings - Fork 59
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
Draft for adding OAuth support to shiny #518
base: main
Are you sure you want to change the base?
Conversation
Thanks for working this! Obviously it's a big PR so it'll take me + @jcheng5 a little while to get our heads around this, but we really appreciate you working on it! |
…s not authenticated + minor doc fixes
Thanks, totally understand! I figured it made sense to start with a fully working example to get the discussion going on how the API could work. There's still plenty of room for improvement, but I felt it was at a point where some feedback would be really helpful. |
I don't know if it's appropiate to say this, but could we make this someway works for plumber or httpuv usage too? Futhermore, I don't know if it would be better to have a specific package for that. |
Hi, I'm one of the people who worked on viewer-based OAuth credentials in Posit Connect, which does what I think you're aiming for here. I can't speak to all of the technical choices you've made here, but I thought I could give a sense of some of the problems we saw that led us to solve this in the platform rather than in individual Shiny apps, as you've done here:
Some of it also reflected patterns we see in organizations, which might not be applicable to pure open-source power users:
Finally, although it might be irrelevant here: we wanted something that worked for Python and Quarto, too. (Plus non-Shiny interactive R content, although it's rare to see that in practice.) |
Thanks for the input, @atheriel.
Are you referring to setting the state value in the OAuth flow? It's pretty standard to set this as a cookie (httponly, secure, samesite=lax), see e.g. authlib, next-auth and mod_auth_openidc. I would be interested to hear more about why local browser storage would be a better choice.
Agree. I think this caveat is pretty well described in the draft.
I don't. The PR specifically highlights that refresh tokens would need to be figured out. You can opt-in to store the access token in an encrypted cookie by setting access_token_validity > 0.
Confidential clients are defined as
I guess your mileage may vary depending on the provider. Lots of access tokens are valid for at least 60 minutes and could be useful without refresh tokens, even if suboptimal (e.g. Azure, Github, Google, Spotify).
My experience is actually the other way around. Most providers allow multiple redirects, while others requires a single redirect. E.g. Github only allows a single redirect, but Azure, Google and Spotify allows multiple. It can be a pain to manage, but it can also be relatively easily automated using IaC. This PR does not use json files, but environment variables (e.g.
I agree. And just to be clear, I'm not advocating this as an "enterprise-ready" PR. If anyone wants that, I would suggest they stick to Posit Connect or a certified OSS solution like mod_auth_openidc. This is a draft for suggesting and discussing a simple shiny OAuth solution which extends on the basis of previous work by @jcheng5. I had a need for it during a prototype, and thought it would benefit the community to share it when I saw #47. My personal opinion is that Shiny is more useful with some form of OAuth support than without it. I'm more than happy to see it built in an entirely different way than this draft PR or dropped if that's what the maintainers prefer. I do feel it is one of the features that could help increase the adoption of shiny even further in the long term, though. |
@thohan88 just a quick comment but I'm about to leave for vacation. I agree with your points that this doesn't need to be perfect, but it's useful to include something that's purely open source. I don't think we can do much better than this with our support on the deployment side, and it's fine for that better support to live in connect. I asked @atheriel to comment just so we can get a good sense of the pros and cons of both approaches. |
Info: This is a draft for discussion purposes. It's not a polished PR and currently includes minimal error handling and documentation. It may be big enough to warrant a separate package, but it may also be so tighly coupled with httr2 that it makes sense for it to be integrated.
Summary
This PR addresses #47 and attempts to bring support for OAuth 2.0 apps to shiny. It builds upon the cookie and routing approach developed in r-lib/gargle#157 and extends this to:
httr2
functionality (e.g. token objects)This PR primarily addresses two key scenarios:
A more detailed guide for getting started is included in
vignettes/articles/shiny.Rmd
.Demo
You can run this locally or view an example application on shinyapps.io or a deployed version on Google Cloud Run. The demo application runs
oauth_shiny_app_example()
and stores no user information. Here is a preview of what to expect:OAuth 2.0 Authentication for Apps
To enforce login within a Shiny application, use
oauth_shiny_app()
with a configuration of OAuth clients:Standard clients (e.g.
oauth_client_github()
) resolve client IDs and secrets using environment variables (OAUTH_GITHUB_ID
andOAUTH_GITHUB_SECRET
). This setup will present a sign-in UI with login buttons (based on Google Material) for clients marked asauth_provider = TRUE
. Buttons link to client login endpoints (e.g.login/github
) which triggers the OAuth flow.Upon loading, the application checks for a signed cookie containing standard claims (at minimum
identifier
andprovider
) , which is set after successful authentication for a client withauth_provider = TRUE
. These claims can be retrieved from the server side in Shiny to customize the user interface:To disable authentication, pass
require_auth = FALSE
:Fetching access tokens
By default, access tokens retrieved after completing the OAuth flow are not stored (
access_token_validity = 0
). Client access tokens can be stored as encrypted cookies with amax-age
equal toaccess_token_validity
. These tokens can be retrieved asoauth_token
objects from the server side using:Here is an example which requests user information from Github in a public app :
Logging out
Redirecting users to
logout
clears both app cookies and all client access token cookies. Redirecting tologout/{client}
will only clear a single client's access token cookies.Example application
An example application (same as demo) is included to facilitate debugging client configurations and token retrieval:
OAuth Shiny Client
This PR introduces
oauth_shiny_client()
, similar tooauth_client
, but with additional information necessary to complete the authorization code flow. Standardized clients for GitHub, Google, Microsoft, and Spotify are included, but custom clients can be added easily, e.g. for Strava:For OAuth 2.0 applications compliant with the OpenID specification, it is enough to pass the
open_issuer_url
andscope
and optionally the claims to retrieve:This will automatically resolve the
auth_url
andtoken_url
endpoints and verify the signature of retrieved access tokens using public JSON Web Keys (JWK).Limitations
State Loss During Redirection
Currently, Shiny OAuth apps lose state during the OAuth redirection process. This could potentially be addressed by setting a server-side bookmarked state as a secure cookie, but this is not something I have given much thought.
Local Development
http://127.0.0.1
instead ofhttp://localhost
as the redirect URL. Cookies set at localhost won’t persist when using 127.0.0.1, causing the OAuth flow to fail.options(shiny.launch.browser = TRUE)
to avoid issues with the built-in browser, which does not support external redirects and OAuth cookies.Shinyapps.io
Shinyapps.io works well with non-OpenID providers (e.g., GitHub, Spotify). However, OpenID providers like Google and Microsoft may cause issues due to how Shinyapps.io handles redirection, potentially triggering a loading screen that replays the OAuth flow with the same authorization code causing it to fail.
Cloud Deployment
Shiny OAuth apps can be deployed as Docker containers, even on serverless platforms like Azure Container Apps and Google Cloud Run. Ensure you set the
HTTR2_OAUTH_APP_URL
environment variable to guarantee the correct server URL is inferred.Shiny Server
Shiny Server is not compatible with Shiny OAuth apps because it strips cookies.
Further Work
HTTR2_OAUTH_APP_URL
.req_oauth_auth_code_shiny
: Finalize API before adding this.oauth_shiny_config()
instead of passing separate arguments tooauth_shiny_app
.sodium
?Closing Remarks
I hope this serves as a sufficient draft for discussing how OAuth support could be integrated in shiny. I think this functionality could make it much easier for others to integrate OAuth apps. It feels like httr2 could be the right place for it, but happy to discuss this. This is my first PR for a public R package, apologies in advance if I have made errors or overlooked standards.👍