Background
I'm testing Implicit Flow auth in my React app and trying to implement so-called Silent Refresh capabilities, where I periodically ask for a new access token while the user is logged in, without the need to ask him for a new authorization.
The following is the Flow schema, where the Auth0 Tenant
, in my case, is Spotify:
While SPAs(single page applications) using the Implicit Grant cannot use Refresh Tokens, there are other ways to provide similar functionality:
Use
prompt=none
when invoking the/authorize
endpoint. The user will not see the login or consent dialogs.Call
/authorize
from a hidden iframe and extract the new Access Token from the parent frame. The user will not see the redirects happening.
Another approach is the implementation of something like the package axios-auth-refresh, a library that
helps you implement automatic refresh of authorization via
axios
interceptors. You can easily intercept the original request when it fails, refresh the authorization and continue with the original request, without any user interaction.
Usage:
import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
// Function that will be called to refresh authorization
const refreshAuthLogic = failedRequest => axios.post('https://www.example.com/auth/token/refresh').then(tokenRefreshResponse => {
localStorage.setItem('token', tokenRefreshResponse.data.token);
failedRequest.response.config.headers['Authorization'] = 'Bearer ' + tokenRefreshResponse.data.token;
return Promise.resolve();
});
// Instantiate the interceptor (you can chain it as it returns the axios instance)
createAuthRefreshInterceptor(axios, refreshAuthLogic);
// Make a call. If it returns a 401 error, the refreshAuthLogic will be run,
// and the request retried with the new token
axios.get('https://www.example.com/restricted/area')
.then(/* ... */)
.catch(/* ... */);
Set Up
This is my Parent
component (please note that isAuthenticated
state refers to my app authentication, not related to the Spotify token I need for Silent Refresh):
import SpotifyAuth from './components/spotify/Spotify';
class App extends Component {
constructor() {
super();
this.state = {
isAuthenticated: false,
isAuthenticatedWithSpotify: false,
spotifyToken: '',
tokenRenewed:''
};
this.logoutUser = this.logoutUser.bind(this);
this.loginUser = this.loginUser.bind(this);
this.onConnectWithSpotify = this.onConnectWithSpotify.bind(this);
};
UNSAFE_componentWillMount() {
if (window.localStorage.getItem('authToken')) {
this.setState({ isAuthenticated: true });
};
};
logoutUser() {
window.localStorage.clear();
this.setState({ isAuthenticated: false });
};
loginUser(token) {
window.localStorage.setItem('authToken', token);
this.setState({ isAuthenticated: true });
};
onConnectWithSpotify(token){
this.setState({ spotifyToken: token,
isAuthenticatedWithSpotify: true
}, () => {
console.log('Spotify Token', this.state.spotifyToken)
});
}
render() {
return (
<div>
<NavBar
title={this.state.title}
isAuthenticated={this.state.isAuthenticated}
/>
<section className="section">
<div className="container">
<div className="columns">
<div className="column is-half">
<br/>
<Switch>
<Route exact path='/' render={() => (
<SpotifyAuth
onConnectWithSpotify={this.onConnectWithSpotify}
spotifyToken={this.state.spotifyToken}
/>
)} />
<Route exact path='/login' render={() => (
<Form
formType={'Login'}
isAuthenticated={this.state.isAuthenticated}
loginUser={this.loginUser}
userId={this.state.id}
/>
)} />
<Route exact path='/logout' render={() => (
<Logout
logoutUser={this.logoutUser}
isAuthenticated={this.state.isAuthenticated}
spotifyToken={this.state.spotifyToken}
/>
)} />
</Switch>
</div>
</div>
</div>
</section>
</div>
)
}
};
export default App;
and the following is my SpotifyAuth
component, whereby the user clicks on a button in order to authorize and authenticate his Spotify account with the app when he logs in.
import Credentials from './spotify-auth.js'
import './Spotify.css'
class SpotifyAuth extends Component {
constructor (props) {
super(props);
this.state = {
isAuthenticatedWithSpotify: this.props.isAuthenticatedWithSpotify
};
this.state.handleRedirect = this.handleRedirect.bind(this);
};
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;
}
getHashParams() {
const hashParams = {};
const r = /([^&;=]+)=?([^&;]*)/g;
const q = window.location.hash.substring(1);
let e = r.exec(q);
while (e) {
hashParams[e[1]] = decodeURIComponent(e[2]);
e = r.exec(q);
}
return hashParams;
}
componentDidMount() {
//if (this.props.isAuthenticated) {
const params = this.getHashParams();
const access_token = params.access_token;
const state = params.state;
const storedState = localStorage.getItem(Credentials.stateKey);
localStorage.setItem('spotifyAuthToken', access_token);
localStorage.getItem('spotifyAuthToken');
if (window.localStorage.getItem('authToken')) {
this.setState({ isAuthenticatedWithSpotify: true });
};
if (access_token && (state == null || state !== storedState)) {
alert('Click "ok" to finish authentication with Spotify');
} else {
localStorage.removeItem(Credentials.stateKey);
}
this.props.onConnectWithSpotify(access_token);
};
handleRedirect(event) {
event.preventDefault()
const params = this.getHashParams();
const access_token = params.access_token;
console.log(access_token);
const state = this.generateRandomString(16);
localStorage.setItem(Credentials.stateKey, state);
let url = 'https://accounts.spotify.com/authorize';
url += '?response_type=token';
url += '&client_id=' + encodeURIComponent(Credentials.client_id);
url += '&scope=' + encodeURIComponent(Credentials.scope);
url += '&redirect_uri=' + encodeURIComponent(Credentials.redirect_uri);
url += '&state=' + encodeURIComponent(state);
window.location = url;
};
render() {
return (
<div className="button_container">
<h1 className="title is-4"><font color="#C86428">Welcome</font></h1>
<div className="Line" /><br/>
<button className="sp_button" onClick={(event) => this.handleRedirect(event)}>
<strong>LINK YOUR SPOTIFY ACCOUNT</strong>
</button>
</div>
)
}
}
export default SpotifyAuth;
Silent Refresh, however, would not need the button above, nor render anything.
For the sake of completeness, this is the endpoint I use for my app authentication process, which uses jwt
-json web tokens to encrypt tokens and pass them via cookies from server to client (but this encryption tool is not being used for Spotify token being passed to my client, so far):
@auth_blueprint.route('/auth/login', methods=['POST'])
def login_user():
# get post data
post_data = request.get_json()
response_object = {
'status': 'fail',
'message': 'Invalid payload.'
}
if not post_data:
return jsonify(response_object), 400
email = post_data.get('email')
password = post_data.get('password')
try:
user = User.query.filter_by(email=email).first()
if user and bcrypt.check_password_hash(user.password, password):
auth_token = user.encode_auth_token(user.id)
if auth_token:
response_object['status'] = 'success'
response_object['message'] = 'Successfully logged in.'
response_object['auth_token'] = auth_token.decode()
return jsonify(response_object), 200
else:
response_object['message'] = 'User does not exist.'
return jsonify(response_object), 404
except Exception:
response_object['message'] = 'Try again.'
return jsonify(response_object), 500
QUESTION
Considering the options and the code above, how do I use my set up in order to add Silent refresh and handle the redirect to Spotify and get a new token every hour on the background?
Something that sits in between this solution and my code?