lepture / authlib

The ultimate Python library in building OAuth, OpenID Connect clients and servers. JWS,JWE,JWK,JWA,JWT included.
https://authlib.org/
BSD 3-Clause "New" or "Revised" License
4.48k stars 447 forks source link

Refresh and Auto Update Token #245

Open samib6 opened 4 years ago

samib6 commented 4 years ago

I am trying to use signal for automatic token update . As mentioned in the docs we can use signals to do so. Here is my code for the client in Django.

@receiver(token_update)
def on_token_update(sender, token, refresh_token=None, access_token=None,**kwargs):
    print('inside token update client views')
    if refresh_token:
        item = OAuth2Token.find(name=name, refresh_token=refresh_token)
    elif access_token:
        item = OAuth2Token.find(name=name, access_token=access_token)
    else:
        return

    # update old token
    item.access_token = token['access_token']
    item.refresh_token = token.get('refresh_token')
    item.expires_at = token['expires_at']
    item.save()

oauth = OAuth()

oauth.register(
    name = '{{  client name }}',
    client_id = "{{  client id }}",
    client_secret = "{{ client secret }}",
    update_token = on_token_update,
    access_token_url = "https://127.0.0.1:8000/oauth/token/",
    refresh_token_url = "https://127.0.0.1:8000/oauth/token/",
    authorize_url = "https://127.0.0.1:8000/oauth/authorize",
    redirect_url = "https://127.0.0.2/authorize",
    grant_type = "authorization_code",
    id_token_signing_alg_values_supported = ['RS256'],
    authorize_params= {'access_type': 'offline'},

    client_kwargs = {
        'scope':'openid profile',
        'token_endpoint_auth_method': 'client_secret_basic',
    },
    jwks = {
        {{  key set values }}
    },
    prompt = "consent"
)

import time
def home(request):
    user = request.session.get('user')
    print('client user: session',user)
    if user:
        user = json.dumps(user)
    return render(request, 'home.html', context={'user': user})

def login(request):
    eyantra = oauth.create_client('eyantra')
    redirect_uri = "https://127.0.0.2:8001/authorize"
    return eyantra.authorize_redirect(request,redirect_uri)
    #return HttpResponse("login")

def auth(request):
    print('inside callback view in client')
    eyantra = oauth.create_client('eyantra')
    token = eyantra.authorize_access_token(request,verify=False,redirect_uri="https://127.0.0.2:8001/authorize",grant_type="authorization_code")
    print('token is :',token)
    user = eyantra.parse_id_token(request, token)

    request.session['user'] = user.get('name')
    return redirect('/')

def logout(request):
    request.session.pop('user',None)
    return redirect('/')

have added the update_token parameter inside oauth register function.Also the refresh_token_url. But token is not being updated after the respective expire_in time. Am I missing out something ?

dragonpaw commented 4 years ago

The sample in the docs simply doesn't work. (I stumbled across this as well.) But you should be able to modify the below to work for you:

from authlib.integrations.django_client.integration import token_update
from django.dispatch import receiver
import pendulum

import logging
logger = logging.getLogger(__name__)

class TokenManager(models.Manager):
    def update_token(self, name, token, refresh_token=None, access_token=None):
        logger.debug("Auto-update of token(%s) started", name)
        # This assumes the unique name as seen on the model below. 
        item = self.get(name=name)

        # update old token
        item.update(token)
        logger.debug("Update complete.")

class Token(models.Model):
    objects = TokenManager()

    name = models.CharField(max_length=20, unique=True)

    # Oauth tokens:
    token_type = models.CharField(max_length=40)
    access_token = models.CharField(max_length=5000)
    refresh_token = models.CharField(max_length=5000, null=True)
    expires_at = PendulumDateTimeField()
    refresh_url = models.URLField(null=True)

    def __str__(self):
        return f"{self.name}: a_t:{self.access_token[0:4]}..., exp:{self.expires_at}"

    def update(self, token):
        self.access_token = token["access_token"]
        self.refresh_token = token.get("refresh_token")
        self.expires_at = pendulum.from_timestamp(token["expires_at"])
        self.token_type = token["token_type"]
        self.save()

    @property
    def expired(self):
        return self.expires_at < pendulum.now()

    @property
    def expiration_remaining(self):
        return self.expires_at - pendulum.now()

    def to_token(self):
        return dict(
            access_token=self.access_token,
            token_type=self.token_type,
            refresh_token=self.refresh_token,
            expires_at=int(self.expires_at.timestamp()),
        )

@receiver(token_update)
def update_token(
    name, token, refresh_token=None, access_token=None, **kwargs
):  # pylint: disable=unused-argument
    Token.objects.update_token(name, token, refresh_token, access_token)

Admittedly I do the update in the manager as I prefer as much as possible for code to be on my model and not elsewhere, in case I want to update it later. So my receiver is just one line redirecting to the manager responsible. (And you probably want to use generic datetimefields, not the pendulum-backed ones I use.)

Also be aware, unless you're on authlib 0.14.3 or later, the django integration is broken for refresh: #193 (If you're using the metadata url)

lepture commented 3 years ago

@dragonpaw can you help me to update the documentation?

dragonpaw commented 3 years ago

@dragonpaw can you help me to update the documentation?

Sure, how can I help?

DeBelserArne commented 3 years ago
  1. Has this already been merged in the docs?
  2. @dragonpaw , by any chance, do you also have a Flask example?
dragonpaw commented 3 years ago
  1. @dragonpaw , by any chance, do you also have a Flask example?

Sadly, I do not. And with Flask, the implementation would be entirely dependent what database system you went with. The SqlAlchemy implementation would look nothing like what a mongo backend implementation would look like. (The last time I wrote a Flask app, it was Aerospike for the database...)

verdan commented 3 years ago

@aFluxx did you manage to make it work with the Flask App?

fredthomsen commented 2 years ago

@aFluxx I have the same question as @verdan. I am having issues getting this functioning in flask.

mklassen commented 2 years ago

In the flask client, Authlib uses current_app as the signal sender, contrary to the documentation which explicit states, "Never pass current_app as sender to a signal." (https://flask.palletsprojects.com/en/2.1.x/signals/?highlight=signals#sending-signals). As explained in the references documentation, Authlib should be using current_app._get_current_object() as the sender instead. The effective result is that Authlib currently does not reliably support signals in the flask client.