ReactJS - Silently renew token with iframe

生来就可爱ヽ(ⅴ<●) 提交于 2020-03-21 20:26:30

问题


I'm trying to wrap my had around authentication processes, and implement, every 60 minutes, a silent token renew in my React app, like so:

  1. Create some watcher function, which checks the expiration time of the access token. If the token is about to expire, it is time to renew it.
  2. Render an iframe tag, the src should be the same URL which you are using for redirecting to the Auth server, with one difference: change the return URL to a static file, let's call it redirect.html. The server should know about the user calling this URL, from the stored cookie, so it should just simply redirect you to the redirect.html file, now with a fresh access token.
  3. In this redirect.html write a short script, which takes out the token from the URL and override it with the one you already have in local storage.
  4. Destroy the iframe.

Spotify page:

At Spotify dev page, I have my usual redirection URL saved, for when the token is first fetched from authorization URL server:

http://localhost

and then I add a new redirection URL for my iframe with Spotify as well:

http://localhost/redirect_html


App

App.jsx

This is the component I have for silent renewal, so far, which I'm testing at localhost/test-silent-renew at my parent component, like so:

 <Route exact path='/test-silent-renew' render={() => (
     <SilentTokenRenew
     />
 )} />

Component

And this is the actual refresh component:

SilentTokenRenew.jsx

import React, { Component } from 'react'


class SilentTokenRenew extends Component {
  constructor(props) {
    super(props)
    this.state = { 
      renewing: false,
      isAuthenticated: false
     }
    this.currentAttempt = 0
    this.maxNumberOfAttempts = 20
    this.state.renderIframe = this.renderIframe.bind(this);
    this.state.handleOnLoad = this.handleOnLoad.bind(this);
  };

  shouldComponentUpdate(nextProps, nextState) {
    return this.state.renewing !== nextState.renewing
  }

  componentDidMount() {
    this.timeInterval = setInterval(this.handleCheckToken, 20000)
  }

  componentWillUnmount() {
    clearInterval(this.timeInterval)
  }

  willTokenExpire = () => {
    const accessToken = localStorage.getItem('spotifyAuthToken');
    console.log('access_token', accessToken)
    const expirationTime = 3600
    const token = { accessToken, expirationTime } // { accessToken, expirationTime }
    const threshold = 300 // 300s = 5 minute threshold for token expiration

    const hasToken = token && token.accessToken
    const now = (Date.now() / 1000) + threshold
    console.log('NOW', now)

    return !hasToken || (now > token.expirationTime)
  }

  handleCheckToken = () => {
    if (this.willTokenExpire()) {
      this.setState({ renewing: true })
      clearInterval(this.timeInterval)
    }
  }

  silentRenew = () => {
    return new Promise((resolve, reject) => {
      const checkRedirect = () => {
        // This can be e
        const redirectUrl = localStorage.getItem('silent-redirect-url-key')
        console.log('REDIRECT URL', redirectUrl)

        if (!redirectUrl) {
          this.currentAttempt += 1
          if (this.currentAttempt > this.maxNumberOfAttempts) {
            reject({
              message: 'Silent renew failed after maximum number of attempts.',
              short: 'max_number_of_attempts_reached',
            })
            return
          }
          setTimeout(() => checkRedirect(), 500)
          return
        }
        // Clean up your localStorage for the next silent renewal
        localStorage.removeItem('silent-redirect-url-key') // /redirect.html#access_token=......
        // // Put some more error handlers here
        // // Silent renew worked as expected, lets update the access token
        const session = this.extractTokenFromUrl(redirectUrl) // write some function to get out the access token from the URL
        // // Following your code you provided, here is the time to set
        // // the extracted access token back to your localStorage under a key Credentials.stateKey
        localStorage.setItem(Credentials.stateKey, JSON.stringify(session))
        resolve(session)
      }
      checkRedirect()
    })
  }

  handleOnLoad = () => {
    this.silentRenew()
      .then(() => {
        this.setState({ renewing: false })
        this.currentAttempt = 0
        this.timeInterval = setInterval(this.handleCheckToken, 60000)
        // Access token renewed silently.
      })
      .catch(error => {
        this.setState({ renewing: false })
        // handle the errors
      })
  }

 generateRandomString(length) {
    let text = '';
    const possible =
      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
    for (let i = 0; i < length; i++) {
      text += possible.charAt(Math.floor(Math.random() * possible.length));
    }
    return text;
  } 

       renderIframe = () => {
    const state = this.generateRandomString(16);
    const url = new URL('https://accounts.spotify.com/authorize?response_type=token&client_id=my_id&scope=user-read-currently-playing%20user-read-private%20user-library-read%20user-read-email%20user-read-playback-state%20user-follow-read%20playlist-read-private%20playlist-modify-public%20playlist-modify-private&redirect_uri=http%3A%2F%2Flocalhost&state=rBZaR9s1gHchWEME')
    console.log('URL HREF', url.href)
    console.log(url.searchParams.get('redirect_uri'))

    url.searchParams.set(Credentials.stateKey, state)
    url.searchParams.set('redirect_uri', 'http://localhost/redirect.html') // the redirect.html file location
    url.searchParams.set('prompt', 'none')
    //window.location = url; 

    return (
      <iframe
        style={{ width: 0, height: 0, position: 'absolute', left: 0, top: 0, display: 'none', visibility: 'hidden' }}
        width={0}
        height={0}
        title="silent-token-renew"
        src={url.href}
        onLoad={this.handleOnLoad}
      />
    )
  }

  render() {
    const { renewing } = this.state
    return renewing ? this.renderIframe() : null
  }
}

export default SilentTokenRenew;

HTML

And this is the code for my iframe:

 <!DOCTYPE html>
<html>

<head>
    <title>OAuth - Redirect</title>
</head>

<body>
<p>Renewing...</p>
<script>
  // Get name of window which was set by the parent to be the unique request key
  // or if no parameter was specified, we have a silent renew from iframe
  const requestKey = 'silent-redirect-url-key'
  // Update corresponding entry with the redirected url which should contain either access token or failure reason in the query parameter / hash
  window.localStorage.setItem(requestKey, window.location.href);
  window.close();
</script>
</body>

</html>

I can see that if I do the following:

url.searchParams.set('prompt', 'none')
window.location = url; /// <-------

The new token is there, at browser url redirect.

But I don't seem to be able to make my <script> work at localhost/redirect.html file location, which is under the same root as the component.

There must be some problem with my redirect.html script, or file, setting my requestKey, because I'm console logging redirectUrl as undefined or null, if I use either

  1. const redirectUrl = localStorage.getItem('silent-redirect-url-key')

    or

  2. const redirectUrl = localStorage['silent-redirect-url-key']


EDIT

Chrome goes silent, but Firefox tells me that:

Load denied by X-Frame-Options: “deny” from “https://accounts.spotify.com/login?continue=https%3A%2F%2Fac…prompt%3Dnone%26client_id%my_id”, site does not permit any framing. Attempted to load into “http://localhost/test”

I have a nginx proxy, where client is configured like so:

server {

  listen 80;

  location / {
    proxy_pass        http://client:3000;
    proxy_redirect    default;
    proxy_set_header  Upgrade $http_upgrade;
    proxy_set_header  Connection "upgrade";
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Host $server_name;
    proxy_read_timeout 86400s;
    proxy_send_timeout 86400s;
  }

  location /redirect.html {
    proxy_pass        http://client:3000;
    proxy_redirect    default;
    proxy_set_header  Upgrade $http_upgrade;
    proxy_set_header  Connection "upgrade";
    proxy_set_header  Host $host;
    proxy_set_header  X-Real-IP $remote_addr;
    proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header  X-Forwarded-Host $server_name;
    proxy_read_timeout 86400s;
    proxy_send_timeout 86400s;
  }
}

Is there a workaround for that limitation changing my nginx config above?


Or, if not, and only without framing I seem to periodically get this new token, like so:

const url = new URL('https://accounts.spotify.com/authorize?response_type=token&client_id=my_id&scope=user-read-currently-playing%20user-read-private%20user-library-read%20user-read-email%20user-read-playback-state%20user-follow-read%20playlist-read-private%20playlist-modify-public%20playlist-modify-private&redirect_uri=http%3A%2F%2Flocalhost&state=rBZaR9s1gHchWEME')

window.location = url // <----

Can I extract this token with no prejudice to app usability, using scraping etc?


回答1:


I think your issue might be in your return condition in SilentTokenRenew.willTokenExpire(), specifically this: (now > token.accessToken.expirationTime). now > token.accessToken.expirationTime yields false, so your function will likely always return false unless a token doesn't exist.

The token object looks like:

{
  accessToken: TOKEN,
  expirationTime: TIME,
}

That portion of your conditional should instead be: (now > token.expirationTime).

Hopefully that helps you out.




回答2:


  1. In silenwRenew method, the redirectUrl needs to be retrieved from the localStorage, this is the URL what you are going to store in the redirect.html file, under the same key. So, create a key which you will use for both parts. For example:

    const redirectUrl = localStorage['silent-derirect-url-key']

    • you may need to use localStorage.getItem('silent-derirect-url-key'), but it should work with both ways.
  2. In redirect.html file, use the same key, so for setting the URL to the store, use:

    const requestKey = 'silent-derirect-url-key'

Lastly, the method extractTokenFromUrl should be simple, something like this:

extractTokenFromUrl(redirectUrl = '') {
    let accessToken = null
    let decodedAccessToken = null
    const accessTokenMatch = redirectUrl.match(/(?:[?&#/]|^)access_token=([^&]+)/)
    if (accessTokenMatch) {
      accessToken = accessTokenMatch[1]
      decodedAccessToken = JSON.parse(atob(accessToken.split('.')[1]))
    }

    let expireDurationSeconds = 3600
    const expireDurationSecondsMatch = redirectUrl.match(/expires_in=([^&]+)/)
    if (expireDurationSecondsMatch) {
      expireDurationSeconds = parseInt(expireDurationSecondsMatch[1], 10)
    }

    return {
      accessToken,
      decodedAccessToken,
      expireDurationSeconds,
    }
  }

Ofc you can make the code nicer, but you get the idea.



来源:https://stackoverflow.com/questions/60348097/reactjs-silently-renew-token-with-iframe

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!