SpringSecurityのOAuth2がinvalid_redirect_uri_parameterと言って怒る

# SpringSecurityのOAuth2がinvalid_redirect_uri_parameterと言って怒る
ローカルでは問題なく動いていたSpringSecurityでのOAuth2認証が評価環境に載せた途端に
``invalid_redirect_uri_parameter``と言って認証を通してくれなくなりました。
大分嵌ってしまったので備忘も兼ねて

## TL;DR
* nginxにproxy_set_header X-Forwardedをきちんとつける
* ForwardedHeaderFilterをFilterRegistrationBeanを使って高優先度でつける

## そもそもエラーを吐いている場所はどこなのか？
SpringSecurityのログレベルをDEBUGにすればスタックトレースを出してくれます。
SpringBootの場合は以下のような感じです。

``` application.yml
logging:
    file: logs/myapp.log
    level:
        org.springframework.security: DEBUG

```

これでStackTraceを出すと、どうやら
``OidcAuthorizationCodeAuthenticationProvider``の130行目が問題のようです。


``` java
		if (!authorizationResponse.getRedirectUri().equals(authorizationRequest.getRedirectUri())) {
			OAuth2Error oauth2Error = new OAuth2Error(INVALID_REDIRECT_URI_PARAMETER_ERROR_CODE);
			throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
		}

```
ざっとソースを読む限り、二つのルートでredirectUrlを取得して、その結果が一致しなかった場合エラーを吐くようです。
が、このエラー処理が非常に残念で、一致しなかった二つの値がそれぞれ何なのか出してくれません。
仕方ないのでさらに読み進めますと、Requestの方がapplication.ymlから読んできた値、
Responseの方が認可サーバーからリダイレクトしてきた時のURLを加工して作っているものでした。
前者については、application.ymlを確認すればいいだけなので、さらに後者を掘ります。

## Requestからのurlの生成

実際のRedirectUrlの生成部分は以下のような感じです。

``` java
		String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
				.replaceQuery(null)
				.build()
				.toUriString();

	public static String buildFullRequestUrl(HttpServletRequest r) {
		return buildFullRequestUrl(r.getScheme(), r.getServerName(), r.getServerPort(),
				r.getRequestURI(), r.getQueryString());
	}

```

ここまで読んで、「あー評価環境になってnginxを挟むようになったからここで何かForward系の設定が漏れたかな？」と思って以下のように一通り足しました。


```
                proxy_set_header Host               $host;
                proxy_set_header X-Forwarded-For    $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Host   $host;
                proxy_set_header X-Forwarded-Server $host;
                proxy_set_header X-Real-IP          $remote_addr;
                proxy_set_header X-Forwarded-Proto https;
                proxy_set_header X-Forwarded-Port 443;
                proxy_set_header X-Forwarded-Scheme https;
```

が、ここまで足しても変わらず。
そもそも推測が当たっているか自信がなくなってきたので、
無理やりAOPを使ってOidcAuthorizationCodeAuthenticationProvider.authenticateの引数を引っこ抜いてログに出してみました。
するとやはりRequestから生成している側が``https``ではなく``http``になっていました。

なんでnginx側の設定をしたのに変わらないんだろう？(host部分などはきちんと差し変わっています)
と思って調べるとまさかの``HttpServletRequest.getScheme()``はproxy_set_headerしてもそちら側を見てくれないようです。なんということだ。

## ForwardHeaderFilter
ここでAOPで無理やり書き換えてしまえというのも一瞬頭をよぎったのですが、
さすがにそんなことをしていたら誰も読めないコードになってしまうので、真っ当な方法を探します。

すると、``ForwardHeaderFilter`` なるものを見つけました。
これをBeanとして登録しておくと、Request時にSpringが自動で差し込んでよろしく値を整えてくれます。
早速Configに追加して適当なControllerで実験してみると、``HttpServletRequest.getSchema()``がきちんと``https``を返してくれました。
が、これだけではまだ解決に至りませんでした。

## Filterの優先順位
実験時のロジックはController上に書いたので、すべてのFilterの処理が終わってから実行されました。
が、SpringSecurityにおける認証認可処理は同じくFilterの処理なので必ずしもForwardHeaderFilterの後に実行されてくれるわけではありません。
きちんと優先度を設定することが必要です。
具体的には以下のような感じ。

``` java
	@Bean
	FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {

		FilterRegistrationBean<ForwardedHeaderFilter> bean = new FilterRegistrationBean<>();

		bean.setFilter(new ForwardedHeaderFilter());
		bean.setOrder(Ordered.HIGHEST_PRECEDENCE);

		return bean;
	}
```

ここまでやって、ようやく正常に動いてくれました。

## まとめ
* nginxの設定だけではHttpServletRequestに伝わらないものがある
* Filterを使う場合は優先度に気を付けて
* エラーログを出すなら原因となった引数もログに出そう