web-dev-qa-db-ja.com

Reactでのサイレントリフレッシュによる暗黙的なフロー

背景

私はテストしています Implicit Flow auth in my ReactアプリといわゆるSilent Refresh機能。ユーザーがログインしている間、新しい認証を要求する必要なしに、定期的に新しいアクセストークンを要求します。

以下はフロースキーマで、Auth0 Tenant(私の場合)はSpotifyです。

enter image description here

Implicit Grantを使用するSPA(シングルページアプリケーション)は更新トークンを使用できませんが、同様の機能を提供する他の方法があります。

  • Prompt=noneエンドポイントを呼び出すときに/authorizeを使用します。ユーザーには、ログインダイアログや同意ダイアログは表示されません。

  • 非表示のiframeから/authorizeを呼び出し、親フレームから新しいアクセストークンを抽出します。ユーザーにはリダイレクトの発生は表示されません。


別のアプローチは、パッケージ axios-auth-refresh などの実装です。

axiosインターセプターを介して承認の自動更新を実装するのに役立ちます。失敗したときに元のリクエストを簡単に傍受し、認証を更新して、ユーザーの操作なしで元のリクエストを続行できます。

使用法

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(/* ... */);

セットアップ

これは私のParentコンポーネントです(isAuthenticated状態は私のアプリ認証を指し、私が必要とするSpotifyトークンとは関係ないことに注意してください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;

次に、私のSpotifyAuthコンポーネントを示します。これにより、ユーザーはログイン時にアプリでSpotifyアカウントを認証および認証するためにボタンをクリックします。

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;

ただし、サイレントリフレッシュでは、上のボタンは必要なく、何もレンダリングしません。


完全を期すために、これは私がアプリの認証プロセスに使用するエンドポイントであり、jwt -json Webトークンを使用してトークンを暗号化し、Cookieを介してサーバーからクライアントに渡します(ただし、この暗号化ツールはSpotifyトークンがクライアントに渡されるために使用されます):

@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

[〜#〜]質問[〜#〜]

上記のオプションとコードを考慮して、サイレントリフレッシュを追加し、Spotifyへのリダイレクトを処理してバックグラウンドで1時間ごとに新しいトークンを取得するには、セットアップをどのように使用すればよいですか?

この解決策 と私のコードの間にある何か?

6
8-Bit Borges

Spotify APIを使用してサイレント認証リクエストを実行できるかどうかは、すぐにはわかりません。彼らの 認可ガイド によると、暗黙のフローは一時的なものであり、トークンの更新は含まれていません。

Auth0のコンテキストでは、iFrameを使用してユーザーのCookieを含むサイレントリクエストを送信します。セッションはリクエストを検証し、新しいアクセストークンを発行します。これは、上記のようにPrompt = noneオプションを使用して行われます。

0
Dan Woda