Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Implicit Flow with silent refresh in React

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:

enter image description here

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?

like image 764
8-Bit Borges Avatar asked Dec 31 '19 18:12

8-Bit Borges


People also ask

How do you refresh token with implicit flow?

Refreshing tokens To refresh either type of token, you can perform the same hidden iframe request from above using the prompt=none parameter to control the identity platform's behavior. If you want to receive a new id_token , be sure to use id_token in the response_type and scope=openid , as well as a nonce parameter.

How does silent renew work?

When silent renew is enabled, the lib will attempt to perform a renew before returning the authorization state. This allows the application to authorize a user, that is already authenticated, without performing redirects. Silent renew requires CSP configuration on the server to allow iframes and also CORS.

How do you implement auto refresh in React?

If set to true, the browser will do a complete page refresh from the server and not from the cached version of the page. import React from 'react'; function App() { function refreshPage() { window. location. reload(false); } return ( <div> <button onClick={refreshPage}>Click to reload!

What is silent refresh?

Silent refresh is a mechanism to generate new access token from refresh token automatically in the event of browser refresh or when access token is expired but refresh token is available and valid.


1 Answers

So basically you would need to do one of the followings:-

Assuming your access token expires within 1 hour.

Option 1) Set a timeout that gets triggered to get a new access token after let's say 45 minutes of user activity.

Option 2) Avoid setting timeouts, and you would introduce a technique to detect user activity and silently get a token, for example, if you are protecting your routes by a getToken method that would check the token expiration time, you would here add another method that will trigger a silent refresh.

method(){
let iframeElement = this.getDocument().getElementById("anyId");
if (iframeElement == null) {
  iframeElement = document.createElement("iframe");
  iframeElement.setAttribute("id", "anyId");
  document.getElementsByTagName("body")[0].appendChild(iframeElement);
}
  iframeElement.setAttribute("src", tokenUrl); //token url is the authorization server token endpoint
},

Now your iframe will get a new access token within the hash, note that your tokenUrl needs to have prompt=none within the parameters.

The way you handle the new token storage depends on how you store the token in your applicaiton, maybe you would need to call parent.storing_method to store it.

like image 120
Ziko Avatar answered Oct 15 '22 03:10

Ziko