仕様書が日本語訳されているので気になる人は読んでください
依存にspring-cloud-starter-oauth2
を追加するだけ
@EnableResourceServer
@EnableOAuth2Sso
, @EnableOAuth2Client
@EnableAuthorizationServer
こんなアノテーションでできる
イメージ
curl https://start.spring.io/starter.tgz \ -d dependencies=cloud-oauth2 \ -d baseDir=authz \ -d name=authz \ -d type=gradle-project \ -d bootVersion=1.5.8.RELEASE | tar -xzvf -
Spring Initializrで楽ちん。
curl https://start.spring.io/starter.tgz \ -d dependencies=cloud-oauth2 \ -d baseDir=authz \ -d name=authz \ -d type=gradle-project \ -d bootVersion=1.5.8.RELEASE | tar -xzvf -
spring-cloud-starter-oauth2を依存に追加している。
@SpringBootApplication@EnableAuthorizationServer@EnableResourceServer@RestControllerpublic class AuthzApplication { public static void main(String[] args) { SpringApplication.run(AuthzApplication.class, args); } @GetMapping("/userinfo") public Object userinfo(Authentication a) { return a; }}
(ここまで5分)
@SpringBootApplication@EnableAuthorizationServer@EnableResourceServer@RestControllerpublic class AuthzApplication { public static void main(String[] args) { SpringApplication.run(AuthzApplication.class, args); } @GetMapping("/userinfo") public Object userinfo(Authentication a) { return a; }}
認可サーバーの機能を有効にする。
認可エンドポイントやトークンエンドポイント、それらへのセキュリティ設定が有効化される。
@SpringBootApplication@EnableAuthorizationServer@EnableResourceServer@RestControllerpublic class AuthzApplication { public static void main(String[] args) { SpringApplication.run(AuthzApplication.class, args); } @GetMapping("/userinfo") public Object userinfo(Authentication a) { return a; }}
リソースサーバーの機能を有効にする。
リソースへのセキュリティ設定が有効化される。
@SpringBootApplication@EnableAuthorizationServer@EnableResourceServer@RestControllerpublic class AuthzApplication { public static void main(String[] args) { SpringApplication.run(AuthzApplication.class, args); } @GetMapping("/userinfo") public Object userinfo(Authentication a) { return a; }}
リソースサーバーが提供するリソース。
server.context-path=/authzserver.port=9090security.user.name=dukesecurity.user.password=javajavasecurity.user.role=DOWNLOADsecurity.oauth2.client.client-id=hellosecurity.oauth2.client.client-secret=secretsecurity.oauth2.client.scope=user
application.propertiesの内容。
server.context-path=/authzserver.port=9090security.user.name=dukesecurity.user.password=javajavasecurity.user.role=DOWNLOADsecurity.oauth2.client.client-id=hellosecurity.oauth2.client.client-secret=secretsecurity.oauth2.client.scope=user
リソースオーナーの情報。 ユーザー名、パスワード、ロール。
Spring Securityで遊んでいるとよく見る設定。
server.context-path=/authzserver.port=9090security.user.name=dukesecurity.user.password=javajavasecurity.user.role=DOWNLOADsecurity.oauth2.client.client-id=hellosecurity.oauth2.client.client-secret=secretsecurity.oauth2.client.scope=user
クライアントの情報。 クライアントID、クライアントシークレット、スコープ。
これで認可サーバー兼リソースサーバーは完成。
curl https://start.spring.io/starter.tgz \ -d dependencies=cloud-oauth2 \ -d baseDir=hello \ -d name=hello \ -d type=gradle-project \ -d bootVersion=1.5.8.RELEASE | tar -xzvf -
次はクライアント側。
@SpringBootApplication@EnableOAuth2Sso@RestControllerpublic class HelloApplication { public static void main(String[] args) { SpringApplication.run(HelloApplication.class, args); } @GetMapping public String sayHello(Authentication a) { return String.format("Hello, %s!", a.getName()); }}
@SpringBootApplication@EnableOAuth2Sso@RestControllerpublic class HelloApplication { public static void main(String[] args) { SpringApplication.run(HelloApplication.class, args); } @GetMapping public String sayHello(Authentication a) { return String.format("Hello, %s!", a.getName()); }}
OAuth 2.0による認可処理を利用して認証する設定。
@SpringBootApplication@EnableOAuth2Sso@RestControllerpublic class HelloApplication { public static void main(String[] args) { SpringApplication.run(HelloApplication.class, args); } @GetMapping public String sayHello(Authentication a) { return String.format("Hello, %s!", a.getName()); }}
このAuthenticationにリソースサーバーから返されるユーザー情報が入る。
security.oauth2.client.client-id=hellosecurity.oauth2.client.client-secret=secretsecurity.oauth2.client.user-authorization-uri=\http://localhost:9090/authz/oauth/authorizesecurity.oauth2.client.access-token-uri=\http://localhost:9090/authz/oauth/tokensecurity.oauth2.resource.user-info-uri=\http://localhost:9090/authz/userinfo
security.oauth2.client.client-id=hellosecurity.oauth2.client.client-secret=secretsecurity.oauth2.client.user-authorization-uri=\http://localhost:9090/authz/oauth/authorizesecurity.oauth2.client.access-token-uri=\http://localhost:9090/authz/oauth/tokensecurity.oauth2.resource.user-info-uri=\http://localhost:9090/authz/userinfo
(ここまで10分)
クライアントの情報。
security.oauth2.client.client-id=hellosecurity.oauth2.client.client-secret=secretsecurity.oauth2.client.user-authorization-uri=\http://localhost:9090/authz/oauth/authorizesecurity.oauth2.client.access-token-uri=\http://localhost:9090/authz/oauth/tokensecurity.oauth2.resource.user-info-uri=\http://localhost:9090/authz/userinfo
認可サーバーが提供する認可エンドポイントのURI。
認可エンドポイントは「helloっていうクライアントにユーザー情報へのアクセスを許可するかい?」「うん、いいよー」をするためのもの。
security.oauth2.client.client-id=hellosecurity.oauth2.client.client-secret=secretsecurity.oauth2.client.user-authorization-uri=\http://localhost:9090/authz/oauth/authorizesecurity.oauth2.client.access-token-uri=\http://localhost:9090/authz/oauth/tokensecurity.oauth2.resource.user-info-uri=\http://localhost:9090/authz/userinfo
認可サーバーが提供するトークンエンドポイントのURI。
トークンエンドポイントは認可コードとアクセストークンを引き換えるもの。
security.oauth2.client.client-id=hellosecurity.oauth2.client.client-secret=secretsecurity.oauth2.client.user-authorization-uri=\http://localhost:9090/authz/oauth/authorizesecurity.oauth2.client.access-token-uri=\http://localhost:9090/authz/oauth/tokensecurity.oauth2.resource.user-info-uri=\http://localhost:9090/authz/userinfo
リソースサーバーが提供するユーザー情報エンドポイントのURI。
このエンドポイントがAuthenticationの元になる。
authz
とhello
を両方起動して http://localhost:8080 にアクセスする
※会場でデモをする
認可サーバーとリソースサーバーが1つのSpring Bootアプリケーションに同居
認可サーバー equal リソースサーバーなので、クライアントからリソースサーバーへ送られたアクセストークンを検証できるのは自明。
AuthorizationEndpoint.authorize
)にリダイレクト(OAuth2ClientAuthenticationProcessingFilter
が投げたUserRedirectRequiredException
をOAuth2ClientContextFilter
のcatch
で処理)AuthorizationEndpoint.approveOrDeny
)TokenEndpoint
)で認可コードとアクセストークンを引き換える/userinfo
へリクエストを投げてユーザー情報を取得する(OAuth2AuthenticationProcessingFilter
でAuthentication
をSecurityContext
へセット)ログインしたいだけなら認可サーバーだけにすることも可能
アクセストークンからAuthenticationを得る必要があるが、この場合はユーザー情報エンドポイントの代わりにチェックトークンエンドポイントがAuthenticationを返す。
チェックトークンエンドポイントはspring-security-oauth2が提供する認可サーバーの機能。
少しコードを見てみる。
@SpringBootApplication@EnableAuthorizationServer//@EnableResourceServer//@RestControllerpublic class AuthzApplication { public static void main(String[] args) { SpringApplication.run(AuthzApplication.class, args); }// @GetMapping("/userinfo")// public Object userinfo(Authentication a) {// return a;// }}
リソースサーバー関連のコードを削除する。
先述の設定に次の設定を追加する
security.oauth2.authorization.check-token-access=\isAuthenticated()
チェックトークンエンドポイントはデフォルトではアクセス拒否されるので、アクセス制御の設定をしないといけない。
先述の設定を次のように変更する
#security.oauth2.resource.user-info-uri=\#http://localhost:9090/authz/userinfosecurity.oauth2.resource.token-info-uri=\http://localhost:9090/authz/oauth/check_token
ユーザー情報エンドポイントのURI設定を削除して、チェックトークンエンドポイントのURI設定を追加する。
認可サーバーとリソースサーバーを分けることももちろん可能
(ここまで15分)
リソースサーバーから認可サーバーへ伸びている矢印がポイント。
認可サーバーとリソースサーバー間のやりとりはOAuth 2.0の仕様の範囲外。 spring-security-oauth2の場合は認可サーバーへアクセストークンを投げてAuthenticationを取得することで認可済みだと判断する。
この場合リソースサーバーはクライアントからアクセストークンを受け取って、それを認可サーバーへリレーしてAuthenticationを取得している。
@SpringBootApplication@EnableResourceServer@RestControllerpublic class ResourceApplication { public static void main(String[] args) { SpringApplication.run(ResourceApplication.class, args); } @GetMapping("/hello") public String sayHello(Authentication a) { return String.format("Hello, %s!", a.getName()); }}
これまでのコードの応用になるが、一応コードを紹介する。
クライアントで行っていたこんにちは処理をリソースサーバーに持ってきた。
server.context-path=/resourceserver.port=7070security.oauth2.resource.user-info-uri=\http://localhost:9090/authz/userinfo
アクセストークンをリレーしてAuthenticationを取得するだけなのでユーザー情報エンドポイントのURI設定だけで良い。
private final OAuth2RestTemplate restTemplate;@GetMappingpublic String sayHello() { return restTemplate.getForObject( "http://localhost:7070/resource/hello", String.class);}
OAuth2RestTemplateを使ってリソースサーバーからこんにちはを取得する。
なお、設定ファイルは特に変更しない。
spring-cloud-starter-oauth2
を使えば認可サーバー、リソースサーバー、クライアントを構築できるapplication.properties
でできるクライアント1、2、3それぞれに異なるクライアントIDを振りたい
OAuth2AutoConfiguration
がインポートしたOAuth2AuthorizationServerConfiguration
にstaticにネストしたBaseClientDetailsConfiguration
がBaseClientDetails
をbean登録OAuth2AuthorizationServerConfiguration
のconfigure
でクライアント情報を管理するClientDetailsService
を構築👉 クライアント情報は1つしか持てない
application.propertiesに書かれたクライアント情報を元にこんな感じで構築。
AuthorizationServerConfigurer
を実装したコンポーネントを用意する(AuthorizationServerConfigurerAdapter
を継承してもよい)configure
メソッドでクライアント情報を構築する@Componentclass AuthzConfig extends AuthorizationServerConfigurerAdapter { @Override public void configure( ClientDetailsServiceConfigurer clients) throws Exception { //ここでクライアント情報を構築する }}
横に長くなりがちで、ありえない改行をしていてごめん。
clients.inMemory() .withClient("hello").secret("secret").scopes("user") .authorizedGrantTypes("authorization_code") .and() .withClient("client2").secret("secret2").scopes("user") .authorizedGrantTypes("authorization_code") .and() .withClient("client3").secret("secret3").scopes("user") .authorizedGrantTypes("authorization_code");
configureメソッドの引数clientsがビルダーになっており、こんな感じでクライアント情報を構築できる。
これまでの例ではauthorizedGrantTypesは指定していなかったけど、BaseClientDetailsConfigurationが設定してくれていたから。
これで複数クライアントに対応できた。
デフォルトだとクライアント情報はインメモリで持っている
DBで管理したい場合もやはりAuthorizationServerConfigurer
実装クラスでカスタマイズする。
private final DataSource dataSource;@Overridepublic void configure( ClientDetailsServiceConfigurer clients) throws Exception { //clients.inMemory()... デフォルトはインメモリ clients.jdbc(dataSource);}
(ここまで20分)
ClientDetailsServiceConfigurerのjdbcメソッドにDataSourceを渡せばOK。
spring-security-oauth
プロジェクトのspring-security-oauth2/src/test/resources/schema.sql
にDDLがあるJdbcClientDetailsService
のsetFindClientDetailsSql
メソッドなどで発行されるクエリをカスタマイズできるのでオレオレ定義なテーブルにも対応できるJdbcUserDetailsManager
みたいな雰囲気src/main/resourcesにあればspring-security-oauth2のJARに同梱されるからFlywayあたりでマイグレーションできるのになぁ、と思ったりする。
//clients.inMemory()...//clients.jdbc(dataSource)...ClientDetailsService clientDetailsService = new OreOreClientDetailsServiceImpl();clients.withClientDetails(clientDetailsService);
用意されているClientDetailsService実装はインメモリとJDBCの2つだけ。
もっと別の方法でクライアント情報を管理したい場合はClientDetailsServiceを実装して、それをwithClientDetailsに渡せばよい。
デフォルトだとクライアントシークレットは平文で管理される
ハッシュ化するためには、
ClientDetailsService
に登録するクライアントシークレットをハッシュ化するAuthorizationServerConfigurer
でAuthorizationServerSecurityConfigurer
にPasswordEncoder
を設定する@Componentclass AuthzConfig extends AuthorizationServerConfigurerAdapter { final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); ...}
まずAuthorizationServerConfigurer実装クラスにPasswordEncoderのフィールドを準備。
もちろんSpringのコンポーネントにしてもよい。
@Overridepublic void configure( ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory().withClient("hello") .secret(passwordEncoder.encode("secret")) .scopes("user") .authorizedGrantTypes("authorization_code");}
次に、クライアント情報を登録する時にパスワードをハッシュ化しておく。
@Overridepublic void configure( AuthorizationServerSecurityConfigurer security) throws Exception { security.passwordEncoder(passwordEncoder);}
最後に、AuthorizationServerSecurityConfigurerを引数に取る方のconfigureメソッドでPasswordEncoderを設定する。 これはクライアントから送信されたクライアントシークレットを認証する時に使う。
TokenStore
に保存されるInMemoryTokenStore
AuthorizationServerConfigurer
のAuthorizationServerEndpointsConfigurer
を引数に取るconfigure
メソッドで実装を切り替える
@Overridepublic void configure( AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(new JdbcTokenStore(dataSource));}
DDLは先ほどと同じくspring-security-oauth2/src/test/resources/schema.sql
にある
AuthorizationServerTokenServices
TokenEnhancer
AccessTokenConverter
ApprovalStore
TokenGranter
AuthorizationServerTokenServicesはアクセストークンを生成するもの。 アクセストークンを好きな体系にできる。
TokenEnhancerはアクセストークンを生成する過程でアクセストークンを変更できるもの。 AuthorizationServerTokenServicesのデフォルト実装、DefaultTokenServicesの内部で呼ばれる。 単純にアクセストークンに情報を付与したい場合はAuthorizationServerTokenServicesに手を加えるよりも楽。 アクセストークンをJWTにできるJwtAccessTokenConverterなんかが用意されていたりする。
ApprovalStoreはリソースオーナーがクライアントに出した許可を記憶しておくもの。 デフォルトはやはりインメモリなのでDBに保存しておきたい場合などにカスタマイズするポイント。
TokenGranterは認可グラントのインターフェース。ここまで何も言わずにきたけど、このセッションではずっとグラントタイプが認可コードの場合を話してきた。 他にもインプリシット、リソースオーナーパスワードクレデンシャル、クライアントクレデンシャルが使える。 独自の認可グラントを作成する場合にカスタマイズするポイント。
(ここまで25分)
認可サーバーのカスタマイズは後ほどOpenID Connectの話をする時にも出てくる。
これまで紹介してきたのはspring-security-oauth2
の機能で、Spring Bootではspring-security-oauth2
はspring-cloud-starter-oauth2
経由で依存関係に追加される
Spring Security 5からはSpring Security自体にOAuth 2.0対応が入る
Spring Security 5.0.0.RC1ではOAuth 2.0によるログイン機能が入っている
まずは準備
curl https://start.spring.io/starter.tgz \ -d dependencies=web,security \ -d baseDir=hello2 \ -d name=hello2 \ -d type=gradle-project \ -d bootVersion=2.0.0.M6 | tar -xzvf -
先ほどまではdependenciesにcloud-oauth2を指定していたが、ここではwebとsecurityを指定
build.gradle
に依存を足す
compile('org.springframework.security:' + 'spring-security-oauth2-client')compile('org.springframework.security:' + 'spring-security-oauth2-jose')
@SpringBootApplication@RestControllerpublic class Hello2Application { public static void main(final String[] args) { SpringApplication.run(Hello2Application.class, args); } @GetMapping public String sayHello(final Authentication a) { return String.format("Hello, %s!!!", a.getName()); }}
一番最初に示したこんにちは!ウェブサイトのコードとほぼ同じ。 @EnableOAuth2Ssoがなくなった。
@Configurationclass Hello2Config extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated() .and().oauth2Login().clientRegistrationRepository( clientRegistrationRepository()); } ...}
見慣れたWebSecurityConfigurerのコードにoauth2Loginメソッド呼び出しと、clientRegistrationRepositoryメソッド呼び出しが加わる。
@Beanpublic ClientRegistrationRepository clientRegistrationRepository() { ClientRegistration registration = ClientRegistration .withRegistrationId("hello") .clientName("HELLO") .clientId("hello") .clientSecret("secret") .scope("user") .authorizationGrantType( AuthorizationGrantType.AUTHORIZATION_CODE)
ClientRegistrationRepositoryの構築は少し縦に長いので2つに分けて説明する。
@Beanpublic ClientRegistrationRepository clientRegistrationRepository() { ClientRegistration registration = ClientRegistration .withRegistrationId("hello") .clientName("HELLO") .clientId("hello") .clientSecret("secret") .scope("user") .authorizationGrantType( AuthorizationGrantType.AUTHORIZATION_CODE)
Spring Security 5のOAuth 2.0ログインでは複数のプロバイダを指定できる。
registrationIdはプロバイダの識別のために使う。
clientNameはプロバイダを示す表示用の名前。
@Beanpublic ClientRegistrationRepository clientRegistrationRepository() { ClientRegistration registration = ClientRegistration .withRegistrationId("hello") .clientName("HELLO") .clientId("hello") .clientSecret("secret") .scope("user") .authorizationGrantType( AuthorizationGrantType.AUTHORIZATION_CODE)
clientId、clientSecret、scope、authorizationGrantTypeは先ほどまでの例と一緒の意味合い。
.authorizationUri( "http://localhost:9090/authz/oauth/authorize") .tokenUri("http://localhost:9090/authz/oauth/token") .redirectUri( "http://localhost:8080/login/oauth2/code/hello") .userInfoUri("http://localhost:9090/authz/userinfo") .userNameAttributeName("name") .build(); return new InMemoryClientRegistrationRepository( registration);}
.authorizationUri( "http://localhost:9090/authz/oauth/authorize") .tokenUri("http://localhost:9090/authz/oauth/token") .redirectUri( "http://localhost:8080/login/oauth2/code/hello") .userInfoUri("http://localhost:9090/authz/userinfo") .userNameAttributeName("name") .build(); return new InMemoryClientRegistrationRepository( registration);}
authorizationUri、tokenUri、redirectUri、userInfoUriは先ほどまでと同じ意味合い。 と言いつつredirectUriは初めて出てきたかな? 認可エンドポイントから認可コードを付与してリダイレクトされるURI。
.authorizationUri( "http://localhost:9090/authz/oauth/authorize") .tokenUri("http://localhost:9090/authz/oauth/token") .redirectUri( "http://localhost:8080/login/oauth2/code/hello") .userInfoUri("http://localhost:9090/authz/userinfo") .userNameAttributeName("name") .build(); return new InMemoryClientRegistrationRepository( registration);}
(ここまで30分)
userNameAttributeNameはユーザー情報エンドポイントのレスポンスの中でどの項目がuserNameに当たるのかを設定する。
このuserNameAttributeNameが私を悩ませている。
spring.security.oauth2.client.provider.authz.authorization-uri=http://localhost:9090/authz/oauth/authorizespring.security.oauth2.client.provider.authz.token-uri=http://localhost:9090/authz/oauth/tokenspring.security.oauth2.client.provider.authz.user-info-uri=http://localhost:9090/authz/userinfospring.security.oauth2.client.provider.authz.user-name-attribute=namespring.security.oauth2.client.registration.hello.authorization-grant-type=authorization_codespring.security.oauth2.client.registration.hello.client-id=hellospring.security.oauth2.client.registration.hello.client-name=HELLOspring.security.oauth2.client.registration.hello.client-secret=secretspring.security.oauth2.client.registration.hello.provider=authzspring.security.oauth2.client.registration.hello.redirect-uri=http://localhost:8080/login/oauth2/code/hellospring.security.oauth2.client.registration.hello.scope=user
コードで設定していたけれど実はapplication.propertiesでも設定できる。
spring.security.oauth2.client.provider.authz.authorization-uri=http://localhost:9090/authz/oauth/authorizespring.security.oauth2.client.provider.authz.token-uri=http://localhost:9090/authz/oauth/tokenspring.security.oauth2.client.provider.authz.user-info-uri=http://localhost:9090/authz/userinfospring.security.oauth2.client.provider.authz.user-name-attribute=namespring.security.oauth2.client.registration.hello.authorization-grant-type=authorization_codespring.security.oauth2.client.registration.hello.client-id=hellospring.security.oauth2.client.registration.hello.client-name=HELLOspring.security.oauth2.client.registration.hello.client-secret=secretspring.security.oauth2.client.registration.hello.provider=authzspring.security.oauth2.client.registration.hello.redirect-uri=http://localhost:8080/login/oauth2/code/hellospring.security.oauth2.client.registration.hello.scope=user
application.propertiesで設定する時は、プロバイダーの設定と……
spring.security.oauth2.client.provider.authz.authorization-uri=http://localhost:9090/authz/oauth/authorizespring.security.oauth2.client.provider.authz.token-uri=http://localhost:9090/authz/oauth/tokenspring.security.oauth2.client.provider.authz.user-info-uri=http://localhost:9090/authz/userinfospring.security.oauth2.client.provider.authz.user-name-attribute=namespring.security.oauth2.client.registration.hello.authorization-grant-type=authorization_codespring.security.oauth2.client.registration.hello.client-id=hellospring.security.oauth2.client.registration.hello.client-name=HELLOspring.security.oauth2.client.registration.hello.client-secret=secretspring.security.oauth2.client.registration.hello.provider=authzspring.security.oauth2.client.registration.hello.redirect-uri=http://localhost:8080/login/oauth2/code/hellospring.security.oauth2.client.registration.hello.scope=user
クライアントの設定を分けて行う。
spring.security.oauth2.client.provider.authz.authorization-uri=http://localhost:9090/authz/oauth/authorizespring.security.oauth2.client.provider.authz.token-uri=http://localhost:9090/authz/oauth/tokenspring.security.oauth2.client.provider.authz.user-info-uri=http://localhost:9090/authz/userinfospring.security.oauth2.client.provider.authz.user-name-attribute=namespring.security.oauth2.client.registration.hello.authorization-grant-type=authorization_codespring.security.oauth2.client.registration.hello.client-id=hellospring.security.oauth2.client.registration.hello.client-name=HELLOspring.security.oauth2.client.registration.hello.client-secret=secretspring.security.oauth2.client.registration.hello.provider=authzspring.security.oauth2.client.registration.hello.redirect-uri=http://localhost:8080/login/oauth2/code/hellospring.security.oauth2.client.registration.hello.scope=user
クライアントの設定で使用するプロバイダを指定する。
このように、すべてapplication.propertiesで設定できる、、、
spring.security.oauth2.client.provider.authz.authorization-uri=http://localhost:9090/authz/oauth/authorizespring.security.oauth2.client.provider.authz.token-uri=http://localhost:9090/authz/oauth/tokenspring.security.oauth2.client.provider.authz.user-info-uri=http://localhost:9090/authz/userinfospring.security.oauth2.client.provider.authz.user-name-attribute=namespring.security.oauth2.client.registration.hello.authorization-grant-type=authorization_codespring.security.oauth2.client.registration.hello.client-id=hellospring.security.oauth2.client.registration.hello.client-name=HELLOspring.security.oauth2.client.registration.hello.client-secret=secretspring.security.oauth2.client.registration.hello.provider=authzspring.security.oauth2.client.registration.hello.redirect-uri=http://localhost:8080/login/oauth2/code/hellospring.security.oauth2.client.registration.hello.scope=user
spring.security.oauth2.client.provider.authz.authorization-uri=http://localhost:9090/authz/oauth/authorizespring.security.oauth2.client.provider.authz.token-uri=http://localhost:9090/authz/oauth/tokenspring.security.oauth2.client.provider.authz.user-info-uri=http://localhost:9090/authz/userinfospring.security.oauth2.client.provider.authz.user-name-attribute=namespring.security.oauth2.client.registration.hello.authorization-grant-type=authorization_codespring.security.oauth2.client.registration.hello.client-id=hellospring.security.oauth2.client.registration.hello.client-name=HELLOspring.security.oauth2.client.registration.hello.client-secret=secretspring.security.oauth2.client.registration.hello.provider=authzspring.security.oauth2.client.registration.hello.redirect-uri=http://localhost:8080/login/oauth2/code/hellospring.security.oauth2.client.registration.hello.scope=user
ここで設定しているuser-name-attributeがSpring Boot 2.0.0.M6では使われていない。
なので……
こんなエラーになる。
application.propertiesのuser-name-attributeが機能してくれたら……
@Configurationclass Hello2Config extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated() .and().oauth2Login() }}
WebSecurityConfigurerのコードはかなりすっきりするはず。 clientRegistrationRepositoryの定義が要らなくなる。
バグっぽいから直ってくれるといいなー、と思ってる。 誰かPR投げませんか?
それはともかく、動作を見てみたい。
※会場でデモをする(GitHubログインのデモもしたい)
CommonOAuth2Provider
というenum
に幾つかのプロバイダの設定がある仕様書が日本語訳されているので気になる人は読んでください
ここで話しているのは認可グラントが認可コードの場合のみ。 認可グラントがインプリシットなら認可エンドポイントがIDトークン返したりする。
JWTの説明、これでいいのかな? まあ、そんな感じです。
ClientRegistration registration = ClientRegistration.withRegistrationId("hello") .clientName("HELLO") .clientId("hello") .clientSecret("secret") .scope("openid") //(中略) .build();
scope
にopenid
を含めるだけ
(ここまで35分)
OpenID Connectはscopeにopenidを含まなければいけない。
openidというscopeの有無でOAuth2LoginAuthenticationProviderとOidcAuthorizationCodeAuthenticationProviderが使い分けられる。
spring-security-oauth2
にはOpenID Connect対応は無い最低限、これらをすればIDプロバイダになれそう
Nimbus OAuth 2.0/OpenID Connect SDKを使って実装してみた
なんて読むのかな? ニンバス? Spring Security 5も使っている。
JWKの集合はクライアントがIDトークンを検証する時に使う。 IDプロバイダーはIDトークンを秘密鍵で署名して、クライアントはJWK集合エンドポイントから取得した公開鍵で検証、という感じ。
準備はビルドスクリプトの編集のみ。
JWKの集合はIDトークンの署名とエンドポイントの両方で使用する。
最初に示した例の、認可サーバー兼リソースサーバーauthz
をベースにしている
compile('com.nimbusds:oauth2-oidc-sdk:5.38')compile('com.nimbusds:nimbus-jose-jwt:5.1')
JOSE = Javascript Object Signing and Encryption
@Beanpublic JWKSet jwkSet() { //try-catch省略 KeyPairGenerator g = KeyPairGenerator.getInstance("RSA"); g.initialize(2048); KeyPair kp = g.generateKeyPair(); RSAKey rsaKey = new RSAKey.Builder( (RSAPublicKey) kp.getPublic()) .privateKey((RSAPrivateKey) kp.getPrivate()) .keyID(UUID.randomUUID().toString()) .build(); return new JWKSet(rsaKey);}
@Beanpublic JWKSet jwkSet() { //try-catch省略 KeyPairGenerator g = KeyPairGenerator.getInstance("RSA"); g.initialize(2048); KeyPair kp = g.generateKeyPair(); RSAKey rsaKey = new RSAKey.Builder( (RSAPublicKey) kp.getPublic()) .privateKey((RSAPrivateKey) kp.getPrivate()) .keyID(UUID.randomUUID().toString()) .build(); return new JWKSet(rsaKey);}
まずRSA鍵ペアを作る。 これはJava SEのAPI。
もちろん、keytoolなどで事前に作っておいても良い。 というかたぶん普通は事前に作っておく。
@Beanpublic JWKSet jwkSet() { //try-catch省略 KeyPairGenerator g = KeyPairGenerator.getInstance("RSA"); g.initialize(2048); KeyPair kp = g.generateKeyPair(); RSAKey rsaKey = new RSAKey.Builder( (RSAPublicKey) kp.getPublic()) .privateKey((RSAPrivateKey) kp.getPrivate()) .keyID(UUID.randomUUID().toString()) .build(); return new JWKSet(rsaKey);}
次に公開鍵と秘密鍵をビルダーに渡して……
@Beanpublic JWKSet jwkSet() { //try-catch省略 KeyPairGenerator g = KeyPairGenerator.getInstance("RSA"); g.initialize(2048); KeyPair kp = g.generateKeyPair(); RSAKey rsaKey = new RSAKey.Builder( (RSAPublicKey) kp.getPublic()) .privateKey((RSAPrivateKey) kp.getPrivate()) .keyID(UUID.randomUUID().toString()) .build(); return new JWKSet(rsaKey);}
このJSON Web KeyのIDを設定して……
このIDはJWK集合に含まれる鍵を識別するためのもの。
@Beanpublic JWKSet jwkSet() { //try-catch省略 KeyPairGenerator g = KeyPairGenerator.getInstance("RSA"); g.initialize(2048); KeyPair kp = g.generateKeyPair(); RSAKey rsaKey = new RSAKey.Builder( (RSAPublicKey) kp.getPublic()) .privateKey((RSAPrivateKey) kp.getPrivate()) .keyID(UUID.randomUUID().toString()) .build(); return new JWKSet(rsaKey);}
buildするとRSAKeyを作成できる。
@Beanpublic JWKSet jwkSet() { //try-catch省略 KeyPairGenerator g = KeyPairGenerator.getInstance("RSA"); g.initialize(2048); KeyPair kp = g.generateKeyPair(); RSAKey rsaKey = new RSAKey.Builder( (RSAPublicKey) kp.getPublic()) .privateKey((RSAPrivateKey) kp.getPrivate()) .keyID(UUID.randomUUID().toString()) .build(); return new JWKSet(rsaKey);}
作成したRSAKeyを引数にしてJWKSetをインスタンス化する。 ちなみにRSAKeyクラスはJWKクラスのサブクラス。
この例では1つのRSA鍵しか含んでいないが、集合なので複数の鍵を含めることができる。
このビルダーやRSAKeyクラス、JWKSetクラすはNimbus OAuth 2.0/OpenID Connect SDKのAPI。
TokenEnhancer
を利用するTokenEnhancer
は生成されたアクセストークンに追加の情報を加えられるTokenEnhancer
実装を作ってコンポーネント登録する@Componentclass OidcTokenEnhancer implements TokenEnhancer { private final JWKSet jwkSet; public OidcTokenEnhancer(JWKSet jwkSet) { this.jwkSet = Objects.requireNonNull(jwkSet); } @Override public OAuth2AccessToken enhance( OAuth2AccessToken accessToken, OAuth2Authentication authentication) { //中身は後述 }}
(ここまで40分)(あと10枚)
IDトークンを作る際、秘密鍵で署名するのでJWKSetを持つようにしている。
enhanceメソッドの中身はクライムの構築、IDトークンの生成、アクセストークンの拡張に分けて見ていく。
クライムの構築
JWTClaimsSet claims = new JWTClaimsSet.Builder() .issuer("https://localhost:9090/authz/") .subject(authentication.getName()) .audience("hello") .jwtID(UUID.randomUUID().toString()) .expirationTime(Timestamp.valueOf( LocalDateTime.now().plusHours(1))) .issueTime(Timestamp.valueOf(LocalDateTime.now())) .build();
(あと9枚)
IDトークンの生成
JWK jwk = jwkSet.getKeys().get(0);JWSHeader header = new JWSHeader.Builder( JWSAlgorithm.RS256) .keyID(jwk.getKeyID()) .build();SignedJWT jwt = new SignedJWT(header, claims);JWSSigner signer = new RSASSASigner((RSAKey) jwk);jwt.sign(signer);String idToken = jwt.serialize();
(あと8枚)
アクセストークンの拡張
DefaultOAuth2AccessToken enhanced = new DefaultOAuth2AccessToken(accessToken);enhanced.getAdditionalInformation() .put("id_token", idToken);return enhanced;
(あと7枚)
@Configurationclass AuthzConfig extends AuthorizationServerConfigurerAdapter { private final TokenEnhancer tokenEnhancer; //コンストラクタ省略 @Override public void configure( AuthorizationServerEndpointsConfigurer endpoints) { endpoints.tokenEnhancer(tokenEnhancer); }}
(あと6枚)
作成したTokenEnhancerをAuthorizationServerConfigurer実装クラスのconfigureメソッドでAuthorizationServerEndpointsConfigurerにセットする。
@GetMapping(path = "/jwkset", produces = "application/json")public Object jwkset() throws Exception { return jwkSet.toJSONObject().toJSONString();}
(あと5枚)
JWKSetにJSONへシリアライズするメソッドがあるのでそれを利用する。 便利。
ResourceServerConfigurer
実装クラスでアクセス制御の設定をしておく。
@Configurationclass AuthzConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/jwkset").permitAll() .anyRequest().authenticated(); }}
(あと4枚)
(あと3枚)
インプリシットの場合とか端折ったものもある。
とはいえ、これでSpring Security 5のOpenID Connect対応でログインするためのIDプロバイダーは作れた。
リソースサーバーのサポートは入りそう
認可サーバーやIDプロバイダーのサポートはどうなのだろう?
知っていたら教えてください
(あと2枚)
spring-security-oauth2
もカスタマイズしたらIDプロバイダーになれそうなお、この資料とサンプルコードは後日公開予定です
(おわり)
Keyboard shortcuts
↑, ←, Pg Up, k | Go to previous slide |
↓, →, Pg Dn, Space, j | Go to next slide |
Home | Go to first slide |
End | Go to last slide |
Number + Return | Go to specific slide |
b / m / f | Toggle blackout / mirrored / fullscreen mode |
c | Clone slideshow |
p | Toggle presenter mode |
t | Restart the presentation timer |
?, h | Toggle this help |
Esc | Back to slideshow |