Spring BootのOAuth2.0機能を利用して、Rest APIの認証・認可フローを実装する

## 認証方式の選択

Webの認証方式では、OpenID Connectを使う、というのが（[Googleの実装](https://developers.google.com/identity/protocols/OAuth2)などを見ても）ベスト・プラクティスのようですが、皆さん、実際にはどうしてらっしゃるでしょうか。

僕は、Springをサーバサイドで愛用しているのですが、SpringのRestサーバ機能では、[まだOpenID connectは実装されていない](http://qiita.com/TakahikoKawasaki/items/f2a0d25a4f05790b3baa)ようです。

セキュリティは安全な方式を選択すべき一方で、サポートされているものから選ぶしかないという現実もありますね。

最も簡単な認証として、未だに広くは、ユーザ登録時に振り出した固定のシークレットキーをそのままGET/POST時に送信する、というシステムも多いようですが、リプレイ攻撃に対しては変化するキーのほうがベター、ということで今回は、Spring Boot(Web service/security)のOAuth2.0機能を利用して、一時的なセッションキーを振り出す方式で実装することにしました。
＃本質的に変わらなかったらごめんなさい…

## OAuth2.0

[OAuth2.0](https://oauth.net/)には複数の認可フローがあります。今回使うのは、この中の『Resource Owner Password Credentials』です。
＃OAuth1.0/2.0について知りたい方は[こちら](https://murashun.jp/blog/20150920-01.html#chapter-3)をご参照ください

[IPAのセキュアプログラミングの解説](https://www.ipa.go.jp/security/awareness/vendor/programmingv2/contents/709.html)を見ると分かりやすく、かつ「難がある」という記載のある認可フローですが、ここに出てくる[「認可サーバ・リソースサーバ・アクセス主体」が自分のアプリケーションである場合には使うことが許容される](http://stackoverflow.com/questions/28285688/oauth-2-resource-owner-password-grant-type-for-javascript-client)という条件で、今回は利用します。
＃OAuth2.0の仕様にも、[「The credentials should only be used when there is a high degree of trust between the resource owner and the client」](https://tools.ietf.org/html/draft-ietf-oauth-v2-30#section-1.3.2)という記載があります。

私自身は、[「Web API The Good Parts」](https://www.oreilly.co.jp/books/9784873116860/)という書籍を読んで、この方式を知りましたが、実際に、この方式を紹介している記事も他に幾つかあるようです。[[1]](https://content.pivotal.io/blog/securing-restful-web-services-with-oauth2)  [[2]](http://websystique.com/spring-security/secure-spring-rest-api-using-oauth2/)

## 環境・ツール

サーバはSpring Bootを使っています。こちらのSpring公式の[『Spring Boot and OAuth2』というTutorial](https://spring.io/guides/tutorials/spring-boot-oauth2/)に従って、Mavenの依存関係を書きました。

```pom.xml
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.security.oauth</groupId>
	<artifactId>spring-security-oauth2</artifactId>
</dependency>
```
＃公式ページのOAuth2は、ProviderとしてFacebookやGithubを想定しているので、Resource Owner Password Credentialsとは異なる認可フローの作り方ですが、依存関係は同じもので大丈夫です。


また、クライアントは主に別実装のサーバが多数ありますが、Web画面からも利用していて、そこではAngular2を使用しています。
Web画面の動作確認にはPostmanを併用しました。


## 参考にしたソースコード

今回は、[この記事](http://websystique.com/spring-security/secure-spring-rest-api-using-oauth2/)のサンプルプログラムを参考にしました。


また、OAuth2に関しては、また別の公式の[全体的なガイド『OAuth 2 Developers Guide』](http://projects.spring.io/spring-security-oauth/docs/oauth2.html)があります。このページで紹介されている[サンプルプログラム](https://github.com/spring-projects/spring-security-oauth)をダウンロードしたソースを利用すると、さらに高度な制御ができると思います。


### 実際の改修

さきほどの記事のサンプルプログラムで、今回のOAuth2の認可フローに必要なのは、結局3つのクラスでした。

1. AuthorizationServerConfiguration
2. ResourceServerConfiguration
3. OAuth2SecurityConfiguration

OAuth2の登場人物（サーバ）では、まず認可してトークンを発行するAuthorizationServerと、それによって実際に、データを提供するResourceServerがありましたが、１番のConfigurationクラスと２番のConfigurationによって、Spring Securityが用意しているサーバ機能の設定を行って、自分の意図どおりに動作させる、という仕組みのようです。

３番のConfigurationクラスは、直接的にOAuth2に関連しているわけではなく、Spring Security自体の設定と動作（例えばAuthenticationManager）に関係していました。

これをそのまま自分のプログラムに適当なフォルダ（package）を作って入れれば、@configurationによってSpringに読み込まれて動かすことができます。（できました。）

一応、ダウンロードしたソース・プログラムも貼っておきます。

``` AuthorizationServerConfiguration.java

package com.websystique.springmvc.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.approval.UserApprovalHandler;
import org.springframework.security.oauth2.provider.token.TokenStore;

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {

	private static String REALM="MY_OAUTH_REALM";
	
	@Autowired
	private TokenStore tokenStore;

	@Autowired
	private UserApprovalHandler userApprovalHandler;

	@Autowired
	@Qualifier("authenticationManagerBean")
	private AuthenticationManager authenticationManager;

	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {

		clients.inMemory()
	        .withClient("my-trusted-client")
            .authorizedGrantTypes("password", "authorization_code", "refresh_token", "implicit")
            .authorities("ROLE_CLIENT", "ROLE_TRUSTED_CLIENT")
            .scopes("read", "write", "trust")
            .secret("secret")
            .accessTokenValiditySeconds(120).//Access token is only valid for 2 minutes.
            refreshTokenValiditySeconds(600);//Refresh token is only valid for 10 minutes.
	}

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints.tokenStore(tokenStore).userApprovalHandler(userApprovalHandler)
				.authenticationManager(authenticationManager);
	}

	@Override
	public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
		oauthServer.realm(REALM+"/client");
	}

}

```

``` ResourceServerConfiguration.java

package com.websystique.springmvc.security;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler;

@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

	private static final String RESOURCE_ID = "my_rest_api";
	
	@Override
	public void configure(ResourceServerSecurityConfigurer resources) {
		resources.resourceId(RESOURCE_ID).stateless(false);
	}

	@Override
	public void configure(HttpSecurity http) throws Exception {
		http.
		anonymous().disable()
		.requestMatchers().antMatchers("/user/**")
		.and().authorizeRequests()
		.antMatchers("/user/**").access("hasRole('ADMIN')")
		.and().exceptionHandling().accessDeniedHandler(new OAuth2AccessDeniedHandler());
	}

}

```

``` OAuth2SecurityConfiguration.java

package com.websystique.springmvc.security;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.approval.ApprovalStore;
import org.springframework.security.oauth2.provider.approval.TokenApprovalStore;
import org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler;
import org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;

@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfiguration extends WebSecurityConfigurerAdapter {

	@Autowired
	private ClientDetailsService clientDetailsService;
	
	@Autowired
    public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
        .withUser("bill").password("abc123").roles("ADMIN").and()
        .withUser("bob").password("abc123").roles("USER");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
		http
		.csrf().disable()
		.anonymous().disable()
	  	.authorizeRequests()
	  	.antMatchers("/oauth/token").permitAll();
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


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

	@Bean
	@Autowired
	public TokenStoreUserApprovalHandler userApprovalHandler(TokenStore tokenStore){
		TokenStoreUserApprovalHandler handler = new TokenStoreUserApprovalHandler();
		handler.setTokenStore(tokenStore);
		handler.setRequestFactory(new DefaultOAuth2RequestFactory(clientDetailsService));
		handler.setClientDetailsService(clientDetailsService);
		return handler;
	}
	
	@Bean
	@Autowired
	public ApprovalStore approvalStore(TokenStore tokenStore) throws Exception {
		TokenApprovalStore store = new TokenApprovalStore();
		store.setTokenStore(tokenStore);
		return store;
	}
	
}


```


これで、OAuth2のサーバ機能としては動かせるのですが、クライアント機能と合わせると、まだまだ、はまりどころがあったので、少しご紹介しておきます。
（長いので、別記事にしました）

## この後のはまりどころ（１）HTTP OPTIONSの送信

最初のPOSTを送信する前に、Angular2側から動かしたところ、HTTP POSTの代わりにHTTP OPTIONSが送信され、エラーに…

ナニコレ、と共感頂ける方は、[こちらに](http://qiita.com/tamaex/items/e19921a728db2012dd90)。


## この後のはまりどころ（２）sync/await機能の導入

①access_token取得後にトークンを保存し、即時に、②そのトークンを使ってサーバにリクエストを投げる処理を書きたいところですが、Angular2を使うと、http通信は非同期処理で、戻り値はObservableまたは変換したPromiseオブジェクトだと思います。

そうすると、①でトークンを保存する前に、②でトークンを参照してしまい、エラーになる、という状態に（そのままだと）なってしまいます。

こういうときは待ち合わせ機能使いたい…Typescriptではsync/await機能が用意されていますので、これを使いたい！よし！…と思ったら、Angular2のデフォルトの環境では使えないのです、ズコーっ！

こちらも、共感頂ける方は、[こちらに](http://qiita.com/tamaex/items/39df6a38f2040295eb3a)。


長文お付き合いありがとうございました！
（終わり）