JAX-RSをやってみる (18) - OAuth1Provider -

2014-10-08T00:00:00+00:00 Java JAX-RS Ruby

公式ドキュメント: https://jersey.java.net/documentation/latest/security.html#d0e11010

参考: https://github.com/jersey/jersey/blob/master/tests/e2e/src/test/java/org/glassfish/jersey/tests/e2e/oauth/OAuthClientServerTest.java

※暫定なので大幅修正する可能性あり

org.glassfish.jersey.securityのoaut1-serverパッケージを使えばOAuth1Providerが使えるようになる模様。っていう事でやってみた

※事前にorg.glassfish.jersey.security:oauth1-serverな依存性参照をしておく

※基本的に上記のサンプルコードを参考にしているので実用的では無いと思われる

SampleApplication.java

package sample;

import javax.ws.rs.core.MultivaluedHashMap;

import jersey.repackaged.com.google.common.collect.Sets;

import org.glassfish.jersey.server.ResourceConfig;
import org.glassfish.jersey.server.filter.RolesAllowedDynamicFeature;
import org.glassfish.jersey.server.oauth1.DefaultOAuth1Provider;
import org.glassfish.jersey.server.oauth1.OAuth1ServerFeature;

import com.sun.security.auth.UserPrincipal;

public class SampleApplication extends ResourceConfig {

    private static final String CONSUMER_OWNER = "owner1";
    private static final String CONSUMER_KEY = "key1";
    private static final String CONSUMER_SECRET = "secret1";

    private static final String CONSUMER_TOKEN_KEY = "token_key1";
    private static final String CONSUMER_TOKEN_SECRET = "token_secret1";

    public SampleApplication() {
        final DefaultOAuth1Provider provider = new DefaultOAuth1Provider();

        provider.registerConsumer(
            CONSUMER_OWNER,
            CONSUMER_KEY,
            CONSUMER_SECRET,
            new MultivaluedHashMap<String, String>()
        );

        provider.addAccessToken(
            CONSUMER_TOKEN_KEY,
            CONSUMER_TOKEN_SECRET,
            CONSUMER_KEY,
            null, // callback url
            null, // user principal
            null, // roles
            new MultivaluedHashMap<String, String>()
        );

        register(
            new OAuth1ServerFeature(
                provider,
                "oauth/request_token",
                "oauth/access_token"
            )
        );
        register(RolesAllowedDynamicFeature.class);
        packages("sample");
    }
}

本来addAccessTokenはこっちでやるべき事ではない気がしますが(ユーザー登録などの処理をする際にユーザーに対してaddAccessTokenなどをするべきなのでは)

で一般的にOAuth1な処理フロー的には

  • /request_tokenにてリクエストトークンを取得
  • /authorizeして検証コードなどを取得
  • /access_tokenにてAPIにアクセスするのに必要なアクセストークンを取得

というようなフローが必要なのだけど、上記のOAuth1ServerFeatureのコンストラクタで指定している引数で、そのURLのパスを指定出来る。なのでリクエストトークンを取得する場合には/[コンテキストルート]/resources/oauth/reuqest_tokenっていうような形になる。authorizeに関しては自身で実装する必要があるので(これは後記するので)

OAuth.java

/authorizeでリクエストトークンから認可する際に当たる検証コードを出力するだけのリソースアクションを定義する

package sample;

import java.security.Principal;

import javax.annotation.security.RolesAllowed;
import javax.ws.rs.ForbiddenException;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.SecurityContext;

import jersey.repackaged.com.google.common.collect.Sets;

import org.glassfish.jersey.server.oauth1.DefaultOAuth1Provider;
import org.glassfish.jersey.server.oauth1.OAuth1Provider;
import org.glassfish.jersey.server.oauth1.TokenResource;

@Path("/oauth")
public class OAuth {

    @Context
    private OAuth1Provider provider;

    @TokenResource
    @RolesAllowed("user")
    @Path("/authorize")
    @GET
    public String authorize(@QueryParam("oauth_token") String token, @Context SecurityContext sc) {
        Principal user = sc.getUserPrincipal();

        if (user == null) {
            throw new ForbiddenException();
        }

        DefaultOAuth1Provider       defProvider  = (DefaultOAuth1Provider)provider;
        DefaultOAuth1Provider.Token requestToken = defProvider.getRequestToken(token);

        return defProvider.authorizeToken(
            requestToken,
            // リソースメソッド等でSecurityContextを介して取得出来る
            user,
            // 認可するロールを間違えると利用するリソースでのRolesAllowedで許可されないのでエラーになる
            Sets.newHashSet("user")
        );
    }
}

authorizeする際にユーザーを特定するには、そこをsecurity-constraint等によって保護してアクセスする人を特定した後にauthorizeする必要がある。あとはコメントで書いてる通り

API.java

package sample;

import javax.annotation.security.RolesAllowed;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.SecurityContext;

@Path("/api")
public class API {

    @RolesAllowed("user")
    @Path("/greet")
    @GET
    public String greet(@Context SecurityContext sc) {
        return "Hello World " + sc.getUserPrincipal().getName();
    }
}

っていう感じでやれば、OAuthリクエストを認可されアクセストークンを使ってRolesAllowedされたリソースにもアクセスする事が可能になる。

上記のOAuth.javaにはちょろっと書いてるけど、@TokenResourceっていうアノテーションを使うとOAuth1ServerFilterによってOAuthアクセストークンを利用した保護領域へのアクセスに対してユーザー処理等が行われない。その為、このリソースクラスのメソッドに@TokenResourceを使ってしまうとOAuthによるユーザー処理されずに認証できない状態になるのでHTTP/403エラーとなる

まぁざっくりとした使い方はこんな感じなんだけど、DefaultOAuth1Providerの実装自体がアクセストークンをインスタンスによる管理が行われる為にサーバーを再起動したりすると既に発行されているアクセストークンを利用してもアクセス出来なくなると思われる。なのでがっつりやるのであればOAuth1Providerインターフェースを実装したクラスでもって処理する必要があるのではないかと

んまぁテストケースに関しては一番上の参考のリンク先がテストケースになっているのでテスト書いたりする際にはそれを参考にすれば良いのではないかと

余談: Rubyでクライアント書いてアクセスしてみる

require 'oauth'

consumer = OAuth::Consumer.new(
  'key1',
  'secret1',
  :site => 'http://localhost:8080/jersey/resources'
)

request_token = consumer.get_request_token

puts "open #{request_token.authorize_url}"

print 'verify code: '
verify = gets.to_s.chomp

access_token = request_token.get_access_token(oauth_verifier: verify)

puts "access_token: #{access_token.token}"
puts "access_token_secret: #{access_token.secret}"

response = access_token.get('/api/greet')
puts "response: #{response.body}"

っていう感じで書いて実行するとauthorizeするURLが表示されるので、そこにブラウザ等でアクセスして表示されるベリファイコードをコピーするとAPIにアクセス出来るような感じ。その際にアクセストークンが表示されるので

require 'oauth'

consumer = OAuth::Consumer.new(
  'key1',
  'secret1',
  :site => 'http://localhost:8080/jersey/resources'
)

access_token = OAuth::AccessToken.new(
  consumer,
  'access token',
  'access token secret'
)

response = access_token.get('/api/greet')
puts "response: #{response.body}"

っていう感じでやればアクセストークンを使ってAPIにアクセスする事も出来る。もちろん上記で書いてるようにDefaultOAuth1Providerをそのまま使ってる場合にはサーバーを落とすとそのアクセストークンは使えなくなる

gradle jettyでJDBCRealm JAX-RSをやってみる (17) - SecurityContext -