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

Azure Oauth2.0 With AD Access ("Refresh Token") #4223

Open
VedoBlaze opened this issue Dec 19, 2022 · 10 comments · Fixed by #4227
Open

Azure Oauth2.0 With AD Access ("Refresh Token") #4223

VedoBlaze opened this issue Dec 19, 2022 · 10 comments · Fixed by #4227

Comments

@VedoBlaze
Copy link

Hi,

We are using this custom plugin but are getting a issue when calling the Azure platform 2.0 OAuth.
The issue we are getting is
"ERROR: panel.auth - AzureAd OAuth provider returned a HTTP 400: Bad Request error. The full response was: {'error': 'invalid_grant', 'error_description': 'AADSTS54005: OAuth2 Authorization code was already redeemed, please retry with a new valid code or use an existing refresh token.\r\nTrace ID: ff5daa04-a393-4bfc-9cfd-1ae511fb3e00\r\nCorrelation ID: e78f94a1-90d9-41d5-a7a1-221238ffa5b2\r\nTimestamp: 2022-11-29 14:30:03Z', 'error_codes': [54005], 'timestamp': '2022-11-29 14:30:03Z', 'trace_id': 'ff5daa04-a393-4bfc-9cfd-1ae511fb3e00', 'correlation_id': 'e78f94a1-90d9-41d5-a7a1-221238ffa5b2'}"

Through some error searching we have navigated that the solution is in enabling a way in getting refresh tokens. LInk below:
https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-auth-code-flow#refresh-the-access-token

Is hit something you can implement so that we can further use panel?

Thanks in advance for you contribution.

@shkarlsson
Copy link

I suppose we're looking for a way to save the refresh token from the first auth response and then re-use it in subsequent requests that session. @VedoBlaze, please specify if necessary.

@philippjfr, I see you are very active on this repo and was hoping you could help us resolve this. Is this something that could be supported in Panel? If it's not reasonable to build it into Panel, would it be possible to get some guidance on how to implement this in our setup?

Thanks!

@philippjfr
Copy link
Member

Yes, I can investigate this. To be very clear under what conditions does this error occur exactly? On initial page load or are you trying to make requests to Azure APIs after the application is loaded? If you could provide as much detail as possible for me to reproduce this that would be great.

@shkarlsson
Copy link

I'll let VedoBlaze answer that, but here is some other information: the command we run panel with (on a Docker in AWS):panel serve application.py --port 80 --allow-websocket-origin="*" --oauth-provider=azure --oauth-key=<our-auth-key> --oauth-secret=<our-secret> --cookie-secret=anything --oauth-encryption=<our-encryption>

@philippjfr
Copy link
Member

philippjfr commented Dec 21, 2022

@shkarlsson I've now made the refresh_token available in OAuth. If you could I'd appreciate it if you could try the PR. To do so check out the repo, then run pip install -e . (make sure you have nodejs available) and then run panel build panel. That should be sufficient for testing.

You should then have access to pn.state.refresh_token in your app.

@philippjfr
Copy link
Member

I'll probably go ahead and merge it and cut a dev release you can test.

@xnsde
Copy link

xnsde commented Jan 11, 2023

I realize this is closed and maybe this needs a new issue, but:
Refresh tokens are single-use. When you refresh the access token you get a new refresh token and the old one is invalidated (some auth platforms even invalidates all child refresh/access tokens if a previous refresh token is used more than once).
For Azure: https://learn.microsoft.com/en-us/azure/active-directory/develop/refresh-tokens#refresh-token-lifetime

So in practice, #4227 doesn't really solve the problem unless there's a way to update the cookie through the websocket. (Which I believe there isn't?)

@philippjfr philippjfr reopened this Jan 11, 2023
@philippjfr
Copy link
Member

Thanks @xnsde. Would love to chat to figure out what exactly is needed here. Based on those docs my understanding was that in general a refresh token is valid for 24 hours, and didn't see any reference to them being single-use.

@MarcSkovMadsen
Copy link
Collaborator

MarcSkovMadsen commented Jan 12, 2023

Hi @xnsde

Since you can use Panel to execute javascript scripts, I believe you can update secure cookies through the web socket. There are many ways to invoke javascript scripts.

For awesome-panel.org/sharing I needed optional (github) oauth for public apps. I could not see how to use Panels built in OAuth to support this use case. So I built my own Github OAuth component inspired by Panels OAuth and using ReactiveHTML. You can find the full OAuth component here. For inspiration I carved out the component to get and delete the cookie below.

import panel as pn
import param

class JSActions(pn.reactive.ReactiveHTML):  # pylint: disable=too-many-ancestors
    """Helper class for triggering js functions"""

    _set_cookie = param.Dict()
    _delete_secure_cookie = param.String()

    _template = """<div id="jsaction" style="height:0px;width:0px"></div>"""
    _scripts = {
        "_set_cookie": """
function createCookie(name,value,days) {
    if (days) {
        var date = new Date();
        date.setTime(date.getTime()+(days*24*60*60*1000));
        var expires = "; expires="+date.toGMTString();
    }
    else var expires = "";
    document.cookie = name+"="+value+expires+"; path=/;secure";
}      
const {name, value, days}=data._set_cookie
createCookie(name, value, days)""",
        
        "_delete_secure_cookie": """
value=data._delete_secure_cookie+'=; Max-Age=-99999999; path=/;secure'
document.cookie = value
""",
    }

    def __init__(self):
        super().__init__(height=0, width=0, sizing_mode="fixed", margin=0)

    def set_secure_cookie(self, name, value, days=1.0):
        """Sets a cookie as a secure cookie
        Args:
            name: The name of the cookie
            value: The value of the cookie. Please note you will have to encrypt this your self
            days: Days to expiration. Defaults to 1.0.
        """
        self._set_cookie = {"name": name, "value": value, "days": days}

    def delete_secure_cookie(self, name):
        """Deletes the cookie
        Args:
            name: The name of the cookie to delete
        """
        self._delete_secure_cookie = name

@xnsde
Copy link

xnsde commented Jan 12, 2023

Thanks @xnsde. Would love to chat to figure out what exactly is needed here. Based on those docs my understanding was that in general a refresh token is valid for 24 hours, and didn't see any reference to them being single-use.

Oh, seems you're actually right as far as Azure is concerned:
The Microsoft identity platform doesn't revoke old refresh tokens when used to fetch new access tokens.
You do get a new one though, but apparently the old one stays valid for 90 days (in the case of a confidentialclient which panel somewhat is)

@xnsde
Copy link

xnsde commented Jan 12, 2023

Hi @xnsde

Since you can use Panel to execute javascript scripts, I believe you can update secure cookies through the web socket. There are many ways to invoke javascript scripts.

For awesome-panel.org/sharing I needed optional (github) oauth for public apps. I could not see how to use Panels built in OAuth to support this use case. So I built my own Github OAuth component inspired by Panels OAuth and using ReactiveHTML. You can find the full OAuth component here. For inspiration I carved out the component to get and delete the cookie below.

Hmm..
That might actually solve the problem for me, yes :) (although, pn.state.access_token/refresh_token etc will be expired, that's workaroundable)

Just to give some context:

  • Panel app with Oauth2
  • API that requires passing said Oauth2 to aloing (Well, technically a replaced on-behalf-of token with slightly different permissions)
  • High-availability so multiple instances of the same app. No guarantee that you hit the same server for auth and websocket (which complicates local filesystem caching of tokens)
  • Must support multiple tabs connected to the same app (which admittedly isn't a big problem if the refresh tokens aren't actually invalidated)
  • Azure tokens are only valid for roughly 1 hour so they need constant replacement.
  • (Another tangential issue is that Azure v2 tokens are insanely big and adding more, e.g. refresh token to those is potentially problematic. See for example AzureV2 authentication does not successfully save access_token as a secure cookie when encryption is enabled #4161. I've already had to add some hacks to the ingress definition when running behind an nginx load balancer in kubernetes)

The reactivehtml solution does seem to mostly solve it.
If I update the cookie and perhaps store the access token in a user-specific part of the pn.state.cache then most use cases would probably work. I'll investigate.

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

Successfully merging a pull request may close this issue.

5 participants