问题
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:
- 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.
- 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.
- 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.
- 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
const redirectUrl = localStorage.getItem('silent-redirect-url-key')
or
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:
In
silenwRenew
method, theredirectUrl
needs to be retrieved from thelocalStorage
, this is the URL what you are going to store in theredirect.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.
- you may need to use
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