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

http wrapper to automatically refresh token when talking to google #522

Closed
wants to merge 1 commit into from

Conversation

scottburch
Copy link

Example use:

   Accounts.oauth2.http.get(Accounts.oauth2.http.GOOGLE, url, {});

@scottburch scottburch mentioned this pull request Dec 4, 2012
@avital avital mentioned this pull request Dec 4, 2012
@scottburch
Copy link
Author

The way this works is that it makes the request, if it receives a 401 back it then refreshes the token and resends the request. There was a suggestion that we use the seconds given when we receive the token, but I thought this was more reliable than assuming that the token was good or bad. Google sends the 401 if it is bad and that way it is caught no matter why it is bad (revoked auth...)

@joscha
Copy link

joscha commented Dec 5, 2012

@avital @scottburch When authenticating against Google, together with the accessToken, a number of seconds is given when the accessToken expires - is the resulting time (now + timeoutSeconds) saved somewhere? Because the refreshToken does only need to be used once the accesToken is not valid any more, hence when requests are made while it is still valid, numerous requests to get a new accessToken through the refreshToken can be saved.

@scottburch
Copy link
Author

I'm not convinced that this is true. I only get a new accessToken if I receive a 401. Otherwise, i return the response from google.

@scottburch
Copy link
Author

If I use the timeout, then what happens if authorization gets revoked for some other reason. The code would break.

@joscha
Copy link

joscha commented Dec 5, 2012

@scottburch are these answers targeted at my comment? If yes, I don't get them - my comment is talking about when the refreshToken needs to be used. The piece of code that determines whether the accessToken needs to be refreshed would also get a new accessToken if the token has been otherwise revoked. The only reason the time is needed is, to determine whether it even makes sense to try and use the accesToken at all.

@scottburch
Copy link
Author

Yes, my comment is regarding your comment. What am I missing? I guess I was confused by your comment. How does the accessToken timeout affect the requests that are made? Is this just a requirement for your code? How does it affect THIS pull request?

@joscha
Copy link

joscha commented Dec 5, 2012

@scottburch sorry for the confusion, @avital told me to carry this comment over from #464, which has been closed already.

@joscha
Copy link

joscha commented Dec 5, 2012

And to clarify - the current pull request is always making a request, right? In X% of the cases this might not be necessary though (exactly, when we are still within time X and X+deltaValiditySeconds) because the accessToken does not need to be updated, because it is still valid. Only if its not valid any more (after X+deltaValiditySeconds) it needs to be refreshed. So storing the delta somewhere would not only make sense for my use case, but also for this pull request here. It would be an optimization.

@scottburch
Copy link
Author

Read my comments. I don't think the code works that way. It is not 2X. It only updates the token when it is needed.

@joscha
Copy link

joscha commented Dec 5, 2012

I read your comments. I just don't think its a good idea to make that initial request. The lifetime is given for a reason. It is given so you don't need to make that first request. Requests are expensive, if there is a way save the overhead doing one, it should be done in my eyes.

@joscha
Copy link

joscha commented Dec 5, 2012

Sorry, I think I am the one that is causing confusion here here. The pull request is perfectly valid. It just makes the request whenever a 401 came back.

My initial comment was added because I want to use the accessToken to access a GMail account with it. It has nothing to do with an HTTP request. Therefore I need to know the time when the accessToken expires, hence I want the lifetime (or more specifically now()+liefetime) to be saved, so I can just use the accessToken and only refresh it if its expired before accessing GMail via IMAP. I don't think this pull request is the right place for this. Sorry again.

@scottburch
Copy link
Author

No problem. Glad it is clear.

I'm doing the same thing with IMAP. I don't think that the OAUTH token is necessary for IMAP. I have not had any problems with that.

@joscha
Copy link

joscha commented Dec 5, 2012

@scottburch Sorry, for this slight offtopic question, but how are you generating the XOAuth2 token to pass on to GMail xoauth2 if not with the accessToken or refreshToken?

@kenyee
Copy link

kenyee commented Apr 5, 2013

I think we need more than this.

Use case is: I want to use Google or Facebook APIs but let this package manage the logins.
So what I want is a blocking method named getAccessToken on the user service.
e.g., userobj.services['google'].getAccessToken()
so I can use it for calls into the API. Ideally, this would refresh the token for me (and block until it finishes) or return null if it fails.

I'm doing this now, but managing the refresh in my code that uses Google APIs...but OAuth is supposed to make the tokens expire automatically for any service...

@kynan
Copy link

kynan commented Sep 29, 2013

Is there anything blocking this being merged? I think this is a crucial missing feature and I always use @scottburch's code in my meteor apps to refresh tokens. Would be great if this was built in.

@ninajlu
Copy link

ninajlu commented Oct 15, 2013

I would also like this to be merged. @scottburch @kynan Since this isn't merged yet, do I have to download the source code for meteor and replace packages/accounts-oauth2-helper/oauth2_server.js with Scott's code in order to use it?

@kynan
Copy link

kynan commented Oct 16, 2013

@ninajlu You can also integrate the relevant bits of @scottburch's code into your app. That's what I've been doing.

@ryw
Copy link
Contributor

ryw commented Oct 20, 2013

@kynan are you integrating this with recent version of meteor? if so, have gist?

A while back, Avital referenced this PR on meteor-talk - looking for an updated PR https://groups.google.com/d/msg/meteor-talk/fyFy3hsZVA4/zABQUDo_aGQJ

I've got an app with problem with expired token accessing Google API, so I'm looking for solution on this to get into Meteor, or a package for now that adds the functionality.

@kynan
Copy link

kynan commented Oct 20, 2013

@ryw I've been using Meteor 0.6.5 but I'm not on the Meteor team and have no say on what gets integrated and what doesn't. I'm only lobbying for this to get merged since I think it's a really useful feature.

I'm using the last 4 functions from @scottburch's pull request and whenever a Google OAuth call fails I check for a 401. If I get one, I call getNewAccessToken to get a new token, update it in the user document and repeat the call. I can try to extract the relevant bits from my code and boil it down to a gist.

@ryw
Copy link
Contributor

ryw commented Oct 21, 2013

@kynan ah so you are handling the 401 within your app?

Here's the code I use to read future events from Google Calendar + create objects in my project — would love to see how your code could handle the 401 + refresh token.

Meteor.startup ->
  Meteor.methods

    importGoogleEvents: (userId) ->
      email = Meteor.user().services.google.email
      token = Meteor.user().services.google.accessToken
      date = (new Date()).toISOString()

      url = "https://www.googleapis.com/calendar/v3/calendars/"
      url += "#{email}/events?timeMin=#{date}&access_token=#{token}"

      Meteor.http.get url, (err, res) ->
        params = {}
        if err
          console.log err
        else
          console.log "imported events for " + userId
          Events.remove userId: userId
          _.each res.data.items, (element, index, events) ->
            params.googleId = element.id
            params.status = element.status
            params.location = element.location
            params.start = element.start.dateTime
            params.end = element.end.dateTime
            params.url = element.htmlLink
            params.title = element.summary
            params.attendees = element.attendees
            params.allDay = false
            params.userId = userId
            Events.insert params

@kynan
Copy link

kynan commented Oct 22, 2013

@ryw Like this:

Meteor.startup ->
  Meteor.methods

    refreshOAuthToken: (service) ->
      getNewAccessToken = (service) ->
        result = Meteor.http.post(service.url, {headers: {'Content-Type': 'application/x-www-form-urlencoded'}, content: oAuthRefreshBody(service)})
        return result.data?.access_token
      oAuthRefreshBody = (service) ->
        loginServiceConfig = Accounts.loginServiceConfiguration.findOne({service: service.name});
        return 'refresh_token=' + Meteor.user().services[service.name].refreshToken +
            '&client_id=' + loginServiceConfig.clientId +
            '&client_secret=' + loginServiceConfig.secret +
            '&grant_type=refresh_token'
      storeNewAccessToken = (service, newAccessToken) ->
        o = {}
        o['services.' + service.name + '.accessToken'] = newAccessToken
        Meteor.users.update Meteor.userId(), {$set: o}
      token = getNewAccessToken service
      console.log "Got new access token #{token} for", service
      storeNewAccessToken service, token
      return token

    importGoogleEvents: (userId, token) ->
      email = Meteor.user().services.google.email
      token = Meteor.user().services.google.accessToken
      date = (new Date()).toISOString()

      url = "https://www.googleapis.com/calendar/v3/calendars/"
      url += "#{email}/events?timeMin=#{date}&access_token=#{token}"

      Meteor.http.get url, (err, res) ->
        params = {}
        if !err
          console.log "imported events for " + userId
          Events.remove userId: userId
          _.each res.data.items, (element, index, events) ->
            params.googleId = element.id
            params.status = element.status
            params.location = element.location
            params.start = element.start.dateTime
            params.end = element.end.dateTime
            params.url = element.htmlLink
            params.title = element.summary
            params.attendees = element.attendees
            params.allDay = false
            params.userId = userId
            Events.insert params
        else if err.response.statusCode == 401
          # Refresh OAuth token if it has expired. Simplified version of
          # https://github.com/meteor/meteor/pull/522
          Meteor.call 'refreshOAuthToken',
            {name: 'google', url: 'https://accounts.google.com/o/oauth2/token'},
            (err, token) ->
              importGoogleEvents userId, token if !err
              console.log err if err
        else
          console.log err

@ryw
Copy link
Contributor

ryw commented Oct 26, 2013

@kynan thanks Florian - just implemented the code, need to wait for an expiration :)

@ryw
Copy link
Contributor

ryw commented Oct 28, 2013

Not working for me yet... see invalid_grant error before @kynan?

Error:

Exception in callback of Meteor.http.call Error: failed [400] {   "error" : "invalid_grant" }
    at Object.Future.wait (/mnt/data/2/node_modules/fibers/future.js:322:15)
    at Object.Meteor.http.call (app/packages/http/httpcall_server.js:125:16)
    at Object.Meteor.http.post (app/packages/http/httpcall_common.js:78:27)
    at getNewAccessToken (app/server/user/importGoogleEvents.coffee.js:12:32)
    at refreshOAuthToken (app/server/user/importGoogleEvents.coffee.js:37:17)
    at app/server/user/importGoogleEvents.coffee.js:71:20
    at Meteor.bindEnvironment.runWithEnvironment (app/packages/meteor/dynamics_nodejs.js:68:24)
    - - - - -
    at Object.Meteor.http._makeErrorByStatus (app/packages/http/httpcall_common.js:15:10)
    at Request.Meteor.http.call [as _callback] (app/packages/http/httpcall_server.js:116:29)
    at Request.init.self.callback (/mnt/data/2/node_modules/request/main.js:122:22)
    at Request.EventEmitter.emit (events.js:99:17)
    at Request.<anonymous> (/mnt/data/2/node_modules/request/main.js:661:16)
    at Request.EventEmitter.emit (events.js:126:20)
    at IncomingMessage.Request.start.self.req.self.httpModule.request.buffer (/mnt/data/2/node_modules/request/main.js:623:14)
    at IncomingMessage.EventEmitter.emit (events.js:126:20)
    at IncomingMessage._emitEnd (http.js:367:10)
    at HTTPParser.parserOnMessageComplete [as onMessageComplete] (http.js:149:23)

My code:

Meteor.startup ->
  Meteor.methods

    importGoogleEvents: (userId, token) ->
      refreshOAuthToken = (service) ->
        getNewAccessToken = (service) ->
          result = Meteor.http.post(service.url, {headers: {'Content-Type': 'application/x-www-form-urlencoded'}, content: oAuthRefreshBody(service)})
          return result.data?.access_token
        oAuthRefreshBody = (service) ->
          loginServiceConfig = Accounts.loginServiceConfiguration.findOne({service: service.name});
          return 'refresh_token=' + Meteor.user().services[service.name].refreshToken +
              '&client_id=' + loginServiceConfig.clientId +
              '&client_secret=' + loginServiceConfig.secret +
              '&grant_type=refresh_token'
        storeNewAccessToken = (service, newAccessToken) ->
          o = {}
          o['services.' + service.name + '.accessToken'] = newAccessToken
          Meteor.users.update Meteor.userId(), {$set: o}
        token = getNewAccessToken service
        console.log "Got new access token #{token} for", service
        storeNewAccessToken service, token
        return token

      email = Meteor.user().services.google.email
      token = Meteor.user().services.google.accessToken
      date = (new Date()).toISOString()

      url = "https://www.googleapis.com/calendar/v3/calendars/"
      url += "#{email}/events?timeMin=#{date}&access_token=#{token}"

      Meteor.http.get url, (err, res) ->
        params = {}
        if !err
          console.log "imported events for " + userId
          Events.remove userId: userId
          _.each res.data.items, (element, index, events) ->
            params.googleId = element.id
            params.status = element.status
            params.location = element.location
            params.start = element.start.dateTime
            params.end = element.end.dateTime
            params.url = element.htmlLink
            params.title = element.summary
            params.attendees = element.attendees
            params.allDay = false
            params.userId = userId
            Events.insert params
        else
          if err.response.statusCode is 401
            # Refresh OAuth token if it has expired. Simplified version of
            # https://github.com/meteor/meteor/pull/522
            refreshOAuthToken
              name: 'google'
              url: 'https://accounts.google.com/o/oauth2/token'
              (err, token) ->
                importGoogleEvents userId, token if !err
                console.log err if err
          else
            console.log err

/cc cfly15

@kynan
Copy link

kynan commented Oct 29, 2013

@ryw Haven't seen this one before, but maybe you haven't requested the offline permission for your app? That's required for being able to use refresh tokens.

@ryw
Copy link
Contributor

ryw commented Oct 30, 2013

Yay!

Got new access token ya29.AHES6ZTIXKx4eT4Qka7B1w6redactedANpa__B_vkXeklOXP for { name: 'google',
  url: 'https://accounts.google.com/o/oauth2/token' }

Trick was to set requestOfflineToken to true in Accounts.ui.config

Accounts.ui.config
  requestPermissions:
    google: ['openid email https://www.googleapis.com/auth/calendar']
  requestOfflineToken:
    google: true

@robertpitt
Copy link
Contributor

Is this still on the agenda to be merged?

@glasser
Copy link
Contributor

glasser commented Feb 20, 2015

Well, the PR appears to be out of date, for one (it doesn't merge cleanly).

@kynan
Copy link

kynan commented Feb 21, 2015

@glasser no surprise after more than 2 years!

@stubailo
Copy link
Contributor

I don't think this is likely to be merged, and seems like something that could easily be a package anyway.

@stubailo stubailo closed this Oct 13, 2015
@robertpitt
Copy link
Contributor

As an FYI on this, my solution was to create a package that managed the access tokens of various providers.

https://gist.github.com/robertpitt/5f0a9286a1a52d58bb18

StorytellerCZ pushed a commit that referenced this pull request Sep 18, 2021
…g-2.14.0

Update validate-commit-msg to the latest version 🚀
StorytellerCZ pushed a commit that referenced this pull request Oct 1, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

9 participants