JAX-RSをやってみる (18) - OAuth1Provider -
公式ドキュメント: https://jersey.java.net/documentation/latest/security.html#d0e11010
※暫定なので大幅修正する可能性あり
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をそのまま使ってる場合にはサーバーを落とすとそのアクセストークンは使えなくなる