JAX-RSをやってみる (19) - OAuth1Providerを実装する -

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

以前やった「JAX-RSをやってみる (18) – OAuth1Provider -」のだと、jersey-oauthに含まれているDefaultOAuth1Providerを使ってるので、アクセストークンなどはインスタンス上で管理されるはずなのでサーバーダウンしたりするとアクセストークンなどが失効するのではないかと思われる。まぁ要はDefaultOAuth1Providerのようなのをつくればいいだけなので、OAuth1Providerを実装することでできるのでやってみた

※前回の「gradle jettyでJDBCRealm」の設定が別途で必要になるのでそれも前提となる

データベースを作る

jetty JDBCRealmでusersとrolesとuser_rolesに関わる部分は前回で作ってるのをそのまま使うのでいいとして、コンシュマーキーなどを格納するconsumerテーブル及び発行済みアクセストークンを格納するtokenテーブルを作っておく

下準備が出来たのであとはOAuth1Providerを実装するだけ

Consumer.java

package sample;

import java.security.Principal;

import org.glassfish.jersey.server.oauth1.OAuth1Consumer;

public class Consumer implements OAuth1Consumer {

    private String consumerKey;
    private String consumerSecret;

    public Consumer(String consumerKey, String consumerSecret) {
        this.consumerKey    = consumerKey;
        this.consumerSecret = consumerSecret;
    }

    @Override
    public String getKey() {
        return consumerKey;
    }

    @Override
    public String getSecret() {
        return consumerSecret;
    }

    @Override
    public Principal getPrincipal() {
        // OAuth1Consumer.getPrincipalを呼ぶ必要があるのであればコンシュマーキー等を使ってDBから引くなりで
        return null;
    }

    @Override
    public boolean isInRole(String role) {
        return false;
    }
}

まぁ単純にOAuth1Consumerを実装したクラスを作る

Token.java

package sample;

import java.security.Principal;
import java.util.HashSet;
import java.util.Set;

import javax.ws.rs.core.MultivaluedHashMap;
import javax.ws.rs.core.MultivaluedMap;

import org.glassfish.jersey.server.oauth1.OAuth1Consumer;
import org.glassfish.jersey.server.oauth1.OAuth1Token;

import com.sun.security.auth.UserPrincipal;

public class Token implements OAuth1Token {

    private OAuth1Consumer consumer;
    private String token;
    private String tokenSecret;
    private Set<String> roles = new HashSet<String>();
    private Principal user;

    public Token(OAuth1Consumer consumer, String token, String tokenSecret) {
        this(consumer, token, tokenSecret, null, null);
    }

    public Token(OAuth1Consumer consumer, String token, String tokenSecret, Set<String> roles, String username) {
        this.consumer = consumer;
        this.token = token;
        this.tokenSecret = tokenSecret;

        if (roles != null) {
            this.roles.addAll(roles);
        }

        if (username != null) {
            user = new UserPrincipal(username);
        }
    }

    @Override
    public String getToken() {
        return token;
    }

    @Override
    public String getSecret() {
        return tokenSecret;
    }

    @Override
    public OAuth1Consumer getConsumer() {
        return consumer;
    }

    @Override
    public MultivaluedMap<String, String> getAttributes() {
        return new MultivaluedHashMap<String, String>();
    }

    @Override
    public Principal getPrincipal() {
        return user;
    }

    @Override
    public boolean isInRole(String role) {
        return roles.contains(role);
    }
}

リクエストトークンだったりアクセストークンだったりで使うクラス、これも同様にOAuth1Tokenを実装するだけ。ただisInRoleメソッドは内部から(RolesAllowed等を利用することで)呼ばれるので、アクセストークンを生成する際のインスタンスにそのトークンを持つユーザーのロール権限を持っておく必要がある

OAuthService.java

package sample;

import java.security.Principal;
import java.sql.ResultSet;
import java.sql.SQLException;

import org.glassfish.jersey.server.oauth1.OAuth1Consumer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Component;

@Component
public class OAuthService {

    @Autowired
    private JdbcTemplate jdbc;

    public Consumer getConsumer(String consumerKey) {
        // 引数に指定されたキーからDBのconsumerテーブルから探してConsumerインスタンスを返す
    }

    public Token newAccessToken(final OAuth1Consumer consumer, final Principal principal) {
        // 引数からtokenテーブルを走査してすでに発行済みトークンがあるならTokenインスタンスを生成して返して、なければ新しく生成したのちに同様にインスタンスを返す
    }

    public Token getAccessToken(String token) {
        // 既存するトークンだけを探してTokenインスタンスを生成する
    }
}

まぁSpring JDBCを使ってデータベースからデータを引くだけのクラス。残りはOAuth1Providerを実装したクラスだけ

SampleOAuth1Provider.java

package sample;

import java.security.Principal;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.glassfish.jersey.server.oauth1.OAuth1Consumer;
import org.glassfish.jersey.server.oauth1.OAuth1Provider;
import org.glassfish.jersey.server.oauth1.OAuth1Token;
import org.springframework.beans.factory.annotation.Autowired;

import static sample.OAuthUtil.newUUIDString;

public class SampleOAuth1Provider implements OAuth1Provider {

    // 発行したリクエストトークンを一時期インスタンス上で保管しておく
    private Map<String, OAuth1Token> requestToken = new ConcurrentHashMap<String, OAuth1Token>();

    // 発行した認証キーをインスタンス上で保管しておく
    private Map<String, Verifier> authorizeVerifier = new ConcurrentHashMap<String, Verifier>();

    @Autowired
    private OAuthService oauth;

    @Override
    public OAuth1Consumer getConsumer(String consumerKey) {
        return oauth.getConsumer(consumerKey);
    }

    @Override
    public OAuth1Token newRequestToken(String consumerKey, String callbackUrl, Map<String, List<String>> attributes) {
        // リクエストトークンを発行
        Token token = new Token(getConsumer(consumerKey), newUUIDString(), newUUIDString());

        // 一時的に保管しておく
        requestToken.put(token.getToken(), token);

        return token;
    }

    @Override
    public OAuth1Token getRequestToken(String token) {
        // クエリーから発行されているリクエストトークンを取得する
        OAuth1Token t = requestToken.get(token);

        if (t == null) {
            throw new RuntimeException();
        }

        return t;
    }

    @Override
    public OAuth1Token newAccessToken(OAuth1Token requestToken, String verifier) {
        // アクセストークンを発行する際にリクエストークンは削除する
        this.requestToken.remove(requestToken.getToken());

        Verifier v = authorizeVerifier.remove(requestToken.getToken());

        if (!v.checkVerifier(verifier)) {
            throw new IllegalStateException();
        }

        // 認証キーとの一致が確認出来たらコンシュマーとユーザーでアクセストークンを発行
        return oauth.newAccessToken(requestToken.getConsumer(), v.getUser());
    }

    @Override
    public OAuth1Token getAccessToken(String token) {
        return oauth.getAccessToken(token);
    }

    public String authorize(OAuth1Token token, Principal user) {
        OAuth1Token authorizeToken = requestToken.get(token.getToken());

        if (authorizeToken == null) {
            throw new NullPointerException();
        }

        if (authorizeVerifier.containsKey(authorizeToken.getToken())) {
            throw new IllegalStateException();
        }

        // 認証キーを発行して保管しておく
        String verifier = newUUIDString();
        authorizeVerifier.put(authorizeToken.getToken(), new Verifier(verifier, user));

        return verifier;
    }

    private static final class Verifier {

        private String verifier;
        private Principal user;

        public Verifier(String verifier, Principal user) {
            this.verifier = verifier;
            this.user = user;
        }

        public Principal getUser() {
            return user;
        }

        public boolean checkVerifier(String verifier) {
            boolean verified = false;

            if (this.verifier.equals(verifier)) {
                verified = true;
            }

            return verified;
        }
    }
}

っていう感じで。適当だけど

sample.SampleOAuth1Provider - getConsumer
sample.SampleOAuth1Provider - newRequestToken
sample.SampleOAuth1Provider - getRequestToken
sample.SampleOAuth1Provider - authorize
sample.SampleOAuth1Provider - getRequestToken
sample.SampleOAuth1Provider - newAccessToken
sample.SampleOAuth1Provider - getConsumer
sample.SampleOAuth1Provider - getAccessToken

てな感じな処理フローになる模様(若干の誤差はある、ちょっと前のログなのでクラス内からメソッド呼び出しでログ取ってるものもあるはずなので)

終わり。てな感じでやれば以降は前回と変わらない(OAuth.java以外は)

SampleApplication.java

package sample;

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

public class SampleApplication extends ResourceConfig {
    public SampleApplication() {
        register(
            new OAuth1ServerFeature(
                new SampleOAuth1Provider(),
                "oauth/request_token",
                "oauth/access_token"
            )
        );
        register(RolesAllowedDynamicFeature.class);
        packages("sample");
    }
}

OAuth.java

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 org.glassfish.jersey.server.oauth1.OAuth1Provider;
import org.glassfish.jersey.server.oauth1.OAuth1Token;

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

    @Context
    private OAuth1Provider provider;

    @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();
        }

        SampleOAuth1Provider defProvider  = (SampleOAuth1Provider)provider;
        OAuth1Token requestToken          = defProvider.getRequestToken(token);

        return defProvider.authorize(requestToken, user);
    }
}

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();
    }
}

client.rb

require 'oauth'

consumer = OAuth::Consumer.new(
  'key1',
  'secret1',
  :site => 'http://localhost:8080/resources' # contextPathを空にしているので前回とちょっと異なる
)

request_token = consumer.get_request_token
puts "open #{request_token.authorize_url}"
print 'verify code: '
verify = gets.to_s.chomp

puts "verify: #{verify}"

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}"

tsharkでgeoip gradle jettyでJDBCRealm