1概要

このクイックチュートリアルでは、

Spring Security

で実装された

OAuth Authorization Server

によって付与されたトークンを取り消す方法を説明します。

ユーザーがログアウトしても、トークンはすぐにトークンストアから削除されるのではなく、自動的に期限切れになるまで有効のままです。

したがって、トークンを失効させると、そのトークンがトークンストアから削除されます。

また、この記事では、JWTトークンではなく、フレームワーク内の標準トークン実装についてのみ説明しています。


2

TokenStore


まず、トークンストアを設定しましょう。付随するデータソースとともに、

JdbcTokenStore

を使用します。

@Bean
public TokenStore tokenStore() {
    return new JdbcTokenStore(dataSource());
}

@Bean
public DataSource dataSource() {
    DriverManagerDataSource dataSource =  new DriverManagerDataSource();
    dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
    dataSource.setUrl(env.getProperty("jdbc.url"));
    dataSource.setUsername(env.getProperty("jdbc.user"));
    dataSource.setPassword(env.getProperty("jdbc.pass"));
    return dataSource;
}


3

DefaultTokenServices

Bean

すべてのトークンを処理するクラスは

DefaultTokenServices

– であり、この構成ではBeanとして定義する必要があります。

@Bean
@Primary
public DefaultTokenServices tokenServices() {
    DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
    defaultTokenServices.setTokenStore(tokenStore());
    defaultTokenServices.setSupportRefreshToken(true);
    return defaultTokenServices;
}


4トークンリストの表示

管理目的で、現在有効なトークンを表示する方法も設定しましょう。

コントローラ内の

TokenStore

にアクセスし、指定されたクライアントIDに対して現在格納されているトークンを取得します。

@Resource(name="tokenStore")
TokenStore tokenStore;

@RequestMapping(method = RequestMethod.GET, value = "/tokens")
@ResponseBody
public List<String> getTokens() {
    List<String> tokenValues = new ArrayList<String>();
    Collection<OAuth2AccessToken> tokens = tokenStore.findTokensByClientId("sampleClientId");
    if (tokens!=null){
        for (OAuth2AccessToken token:tokens){
            tokenValues.add(token.getValue());
        }
    }
    return tokenValues;
}


5アクセストークンの取り消し

トークンを無効にするために、

ConsumerTokenServices

インターフェースの

revokeToken()

APIを使用します。

@Resource(name="tokenServices")
ConsumerTokenServices tokenServices;

@RequestMapping(method = RequestMethod.POST, value = "/tokens/revoke/{tokenId:.** }")
@ResponseBody
public String revokeToken(@PathVariable String tokenId) {
    tokenServices.revokeToken(tokenId);
    return tokenId;
}

もちろんこれは非常に機密性の高い操作なので、内部でのみ使用するか、適切なセキュリティを確保して公開するように十分注意する必要があります。

** 6. フロントエンド

**

この例のフロントエンドでは、有効なトークンのリスト、失効要求を行っているログインユーザーが現在使用しているトークン、およびユーザーが取り消したいトークンを入力できるフィールドを表示します。

$scope.revokeToken =
  $resource("http://localhost:8082/spring-security-oauth-resource/tokens/revoke/:tokenId",
  {tokenId:'@tokenId'});
$scope.tokens = $resource("http://localhost:8082/spring-security-oauth-resource/tokens");

$scope.getTokens = function(){
    $scope.tokenList = $scope.tokens.query();
}

$scope.revokeAccessToken = function(){
    if ($scope.tokenToRevoke && $scope.tokenToRevoke.length !=0){
        $scope.revokeToken.save({tokenId:$scope.tokenToRevoke});
        $rootScope.message="Token:"+$scope.tokenToRevoke+" was revoked!";
        $scope.tokenToRevoke="";
    }
}

ユーザーが失効したトークンを再度使用しようとすると、ステータスコード401の「無効なトークン」エラーが発生します。


7. 更新トークンの取り消し

更新トークンを使用して新しいアクセストークンを取得できます。アクセストークンが取り消されるたびに、それと共に受信されたリフレッシュトークンは無効になります。

リフレッシュトークン自体も無効にしたい場合は、クラス

JdbcTokenStore

のメソッド

removeRefreshToken()

を使用できます。

@RequestMapping(method = RequestMethod.POST, value = "/tokens/revokeRefreshToken/{tokenId:.** }")
@ResponseBody
public String revokeRefreshToken(@PathVariable String tokenId) {
    if (tokenStore instanceof JdbcTokenStore){
        ((JdbcTokenStore) tokenStore).removeRefreshToken(tokenId);
    }
    return tokenId;
}

更新トークンが失効後に無効になったことをテストするには、次のテストを作成します。このテストでは、アクセストークンを取得し、更新してから更新トークンを削除して、もう一度更新します。

失効後、レスポンスエラーが発生します。

“無効なリフレッシュトークン”:

public class TokenRevocationLiveTest {
    private String refreshToken;

    private String obtainAccessToken(String clientId, String username, String password) {
        Map<String, String> params = new HashMap<String, String>();
        params.put("grant__type", "password");
        params.put("client__id", clientId);
        params.put("username", username);
        params.put("password", password);

        Response response = RestAssured.given().auth().
          preemptive().basic(clientId,"secret").and().with().params(params).
          when().post("http://localhost:8081/spring-security-oauth-server/oauth/token");
        refreshToken = response.jsonPath().getString("refresh__token");

        return response.jsonPath().getString("access__token");
    }

    private String obtainRefreshToken(String clientId) {
        Map<String, String> params = new HashMap<String, String>();
        params.put("grant__type", "refresh__token");
        params.put("client__id", clientId);
        params.put("refresh__token", refreshToken);

        Response response = RestAssured.given().auth()
          .preemptive().basic(clientId,"secret").and().with().params(params)
          .when().post("http://localhost:8081/spring-security-oauth-server/oauth/token");

        return response.jsonPath().getString("access__token");
    }

    private void authorizeClient(String clientId) {
        Map<String, String> params = new HashMap<String, String>();
        params.put("response__type", "code");
        params.put("client__id", clientId);
        params.put("scope", "read,write");

        Response response = RestAssured.given().auth().preemptive()
          .basic(clientId,"secret").and().with().params(params).
          when().post("http://localhost:8081/spring-security-oauth-server/oauth/authorize");
    }

    @Test
    public void givenUser__whenRevokeRefreshToken__thenRefreshTokenInvalidError() {
        String accessToken1 = obtainAccessToken("fooClientIdPassword", "john", "123");
        String accessToken2 = obtainAccessToken("fooClientIdPassword", "tom", "111");
        authorizeClient("fooClientIdPassword");

        String accessToken3 = obtainRefreshToken("fooClientIdPassword");
        authorizeClient("fooClientIdPassword");
        Response refreshTokenResponse = RestAssured.given().
          header("Authorization", "Bearer " + accessToken3)
          .get("http://localhost:8082/spring-security-oauth-resource/tokens");
        assertEquals(200, refreshTokenResponse.getStatusCode());

        Response revokeRefreshTokenResponse = RestAssured.given()
          .header("Authorization", "Bearer " + accessToken1)
          .post("http://localhost:8082/spring-security-oauth-resource/tokens/revokeRefreshToken/"+refreshToken);
        assertEquals(200, revokeRefreshTokenResponse.getStatusCode());

        String accessToken4 = obtainRefreshToken("fooClientIdPassword");
        authorizeClient("fooClientIdPassword");
        Response refreshTokenResponse2 = RestAssured.given()
          .header("Authorization", "Bearer " + accessToken4)
          .get("http://localhost:8082/spring-security-oauth-resource/tokens");
        assertEquals(401, refreshTokenResponse2.getStatusCode());
    }
}


8結論

このチュートリアルでは、OAuthアクセストークンとOauth更新トークンを無効にする方法を説明しました。

このチュートリアルの実装はhttps://github.com/Baeldung/spring-security-oauth[GitHubプロジェクト]にあります – これはMavenベースのプロジェクトなので、そのままインポートして実行するのが簡単なはずです。