Relying Party実装
概要
アプリケーションをCloudGearが提供するOpenID Connect Provider(以下OPとします)のRelying Party(以下RPとします)とするためには、OpenID Connectの認証方式に対応した認証ロジックをアプリケーションに導入する必要があります。
Relying Partyの実装を行う為には、認証の基本ページで紹介した、Authorization Code Flowに従う必要があります。
以下にAuthorization Code FlowのOPとRPの処理について記述します。
Authorization Code Flow
- RP は必要なリクエストパラメータを含んだ Authentication Request を構築する
- RP は OP の Authorization Endpoint にリクエストを送信する
- OP は End-User を認証する
- OP は End-User の Consent/Authorization を取得する
- OP は End-User を code とともに RP に戻す
- RP は code を OP の Token Endpoint に送信し, Access Token と ID Token を受け取る
- RP はそれらのトークンを検証し, End-User の Subject Identifier を取得する
ここでは、RP で必要になる処理について説明いたします。
Authentication Request の準備
RP が End-User を認証したい、もしくは既に認証済みかを知りたい場合、RP は OP のAuthorization EndpointにAuthentication Requestを送ります。
以下に Authentication Request URLの例を示します。
https://accounts.cloudgear.services/authorize? response_type=code &client_id=abcdefgh-0123-4567-8901-abcdefghijkl &redirect_uri=https%3A%2F%2Fclient.example.org%2Fcb &scope=openid%20profile &state=af0ifjsldkj
Authnetication Request に使用するパラメータの詳細については、以下をご確認ください。
http://openid-foundation-japan.github.io/openid-connect-basic-1_0.ja.html#RequestParameters
Authentication Requestの送信
RP は Authorization Endpoint に Authentication Request を送信します。
Authorization Endpointへの通信は TLS を利用する必要があります。
Authorization Endpoint へのリクエストは、HTTP GET または HTTP POST を利用することが出来ます。
End-Userの認証
End-User がログインに成功した場合、OP は code を発行し、以下のクエリパラメータを Redirection URI のクエリー部に application/x-www-form-urlencoded フォーマットで付与して RP に渡します。
パラメータ | 説明 |
---|---|
code | エンドユーザのログイン成功時にOPで発行する認可コード。 |
state | Authorization Request に state が含まれていた場合に付与されます。RP は state が Authorization Request に含めた値と同一であることを検証する必要がある。 |
アクセストークンの取得
RP は、返却された認可コードを付与し、Access Token Request を送信します。
Token Endpoint との通信には TLS を利用する必要があります。
以下は、Token Request の例です。
POST /token HTTP/1.1 Host: accounts.cloudgear.services Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb &client_id=abcdefgh-0123-4567-8901-abcdefghijkl&client_secret=secret
RP は以下のパラメータをレスポンスとして受け取ります。
パラメータ | 説明 |
---|---|
access_token | UserInfo Endpoint にアクセスするためのアクセストークン。 |
token_type | この値は RP が OAuth 2.0 Bearer Token Usage となる。 |
id_token | OP による End-User 認証に関する Claim を含んだセキュリティトークン。 |
expires_in | アクセストークンが生成されてから期限切れになるまでの秒数。 |
refresh_token | アクセストークンの有効期限が切れた時、新しいアクセストークンを取得する為のトークン。 |
ユーザー情報の取得
RP は、返却されたアクセストークンを付与し、UserInfo Request を送信します。
UserInfo Request では、アクセストークンを Authrization ヘッダーに付与して認証を行う必要があります。
以下は、UserInfo Request の例です。
GET /userinfo HTTP/1.1 Host: accounts.cloudgear.services Authorization: Bearer SlAV32hkKG
UserInfo Request で返却されるパラメータの詳細は、以下をご確認ください。
http://openid-foundation-japan.github.io/openid-connect-basic-1_0.ja.html#StandardClaims
実装例
以下では、Javaでの実装例をご紹介します。
Spring Bootを利用する場合
依存関係の追加
アプリケーションに以下の依存関係を追加します。
<dependencies> ... <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> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> </dependency> ... </dependencies>
認証制御用クラスの追加
認証制御用のクラスとして以下を追加します。
package services.cloudgear.security; import org.springframework.http.HttpHeaders; import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException; import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; import org.springframework.security.oauth2.client.token.AccessTokenRequest; import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.stereotype.Component; import org.springframework.util.MultiValueMap; @Component public class CloudGearAccessTokenProvider extends AuthorizationCodeAccessTokenProvider { @Override protected OAuth2AccessToken retrieveToken( AccessTokenRequest request, OAuth2ProtectedResourceDetails resource, MultiValueMap<String, String> form, HttpHeaders headers) throws OAuth2AccessDeniedException { form.set("client_id", resource.getClientId()); form.set("client_secret", resource.getClientSecret()); return super.retrieveToken(request, resource, form, headers); } }
package services.cloudgear.security; import com.fasterxml.jackson.databind.ObjectMapper; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.jwt.Jwt; import org.springframework.security.jwt.JwtHelper; import org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter; import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException; import org.springframework.security.oauth2.common.OAuth2AccessToken; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.security.oauth2.common.exceptions.OAuth2Exception; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collection; import java.util.Map; public class OpenIdConnectFilter extends OAuth2ClientAuthenticationProcessingFilter { Logger log = LoggerFactory.getLogger(OpenIdConnectFilter.class); public OpenIdConnectFilter(String defaultFilterProcessesUrl) { super(defaultFilterProcessesUrl); setAuthenticationManager(new AuthenticationManager() { @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { return authentication; } }); } @Override public Authentication attemptAuthentication( HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { OAuth2AccessToken accessToken; try { accessToken = restTemplate.getAccessToken(); } catch (OAuth2Exception e) { throw new BadCredentialsException("Could not obtain access token", e); } catch (UserRedirectRequiredException e) { Map<String, String> params = e.getRequestParams(); HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder .getRequestAttributes()).getRequest(); // append query params to redirect to referer page after user sign up. String url = req.getRequestURL().toString(); params.put("ref", url); throw e; } try { String idToken = accessToken.getAdditionalInformation().get("id_token").toString(); Jwt tokenDecoded = JwtHelper.decode(idToken); Map<String, String> authInfo = new ObjectMapper().readValue(tokenDecoded.getClaims(), Map.class); OpenIdConnectUserDetails user = new OpenIdConnectUserDetails(authInfo, accessToken); Collection<? extends GrantedAuthority> authorities = checkAuthorities(user); log.info("User logged in. [{}]", user.getUsername()); return new UsernamePasswordAuthenticationToken(user, null, authorities); } catch (InvalidTokenException e) { throw new BadCredentialsException("Could not obtain user details from token", e); } } private Collection<? extends GrantedAuthority> checkAuthorities(OpenIdConnectUserDetails user) { Collection<? extends GrantedAuthority> authorities = user.getAuthorities(); if (authorities.size() == 0) { throw new AccessDeniedException("This user does not have authority to access this service."); } return authorities; } }
package com.example.faceplusapi.config; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.oauth2.common.OAuth2AccessToken; import java.util.Collection; import java.util.HashSet; import java.util.Map; import java.util.Set; public class OpenIdConnectUserDetails implements UserDetails { private String userId; private OAuth2AccessToken token; public OpenIdConnectUserDetails(Map<String, String> userInfo, OAuth2AccessToken token) { this.userId = userInfo.get("sub"); this.token = token; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>(); authorities.add(new SimpleGrantedAuthority("ROLE_USER")); return authorities; } @Override public String getPassword() { return token.getValue(); } @Override public String getUsername() { return userId; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
コンフィグレーションの追加
OpenID Conenct認証を有効化するために、以下のコンフィグレーションを追加します。
package services.cloudgear.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.security.oauth2.client.OAuth2ClientContext; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails; import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider; import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; import java.util.Arrays; @Configuration @EnableOAuth2Client public class OpenIdConnectConfig { @Value("${cloudgear.oidc.clientid}") private String clientId; @Value("${cloudgear.oidc.secret}") private String clientSecret; @Value("${cloudgear.oidc.endpoint.token}") private String accessTokenUri; @Value("${cloudgear.oidc.endpoint.authorization}") private String userAuthorizationUri; @Value("${cloudgear.oidc.endpoint.redirectUri}") private String redirectUri; public OAuth2ProtectedResourceDetails cgOpenId() { AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails(); details.setClientId(clientId); details.setClientSecret(clientSecret); details.setAccessTokenUri(accessTokenUri); details.setUserAuthorizationUri(userAuthorizationUri); details.setScope(Arrays.asList("openid", "profile")); details.setPreEstablishedRedirectUri(redirectUri); details.setUseCurrentUri(false); return details; } @Autowired @Bean public OAuth2RestTemplate openIdTemplate( OAuth2ClientContext clientContext, AuthorizationCodeAccessTokenProvider cloudGearAccessTokenProvider) { OAuth2RestTemplate rt = new OAuth2RestTemplate(cgOpenId(), clientContext); rt.setAccessTokenProvider(cloudGearAccessTokenProvider); return rt; } }
package services.cloudgear.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import org.springframework.http.HttpStatus; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; 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.core.Authentication; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.filter.OAuth2ClientContextFilter; import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; import org.springframework.security.web.authentication.logout.HttpStatusReturningLogoutSuccessHandler; import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import services.cloudgear.security.OpenIdConnectFilter; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private OAuth2RestTemplate restTemplate; @Bean public OpenIdConnectFilter openIdConnectFilter() { OpenIdConnectFilter filter = new OpenIdConnectFilter("/oidccallback"); filter.setRestTemplate(restTemplate); return filter; } @Override protected void configure(HttpSecurity http) throws Exception { LoginUrlAuthenticationEntryPoint ep = new LoginUrlAuthenticationEntryPoint("/oidccallback"); ep.setUseForward(true); http .addFilterAfter(new OAuth2ClientContextFilter(), AbstractPreAuthenticatedProcessingFilter.class) .addFilterAfter(openIdConnectFilter(), OAuth2ClientContextFilter.class) .httpBasic() .authenticationEntryPoint(ep) .and() .authorizeRequests() .antMatchers("/resources/**").permitAll() .anyRequest().authenticated(); http.headers().frameOptions().disable(); http.csrf().disable(); http.logout() .logoutRequestMatcher(new AntPathRequestMatcher("/logout")) .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler(HttpStatus.OK) { @Override public void onLogoutSuccess( HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("text/html"); super.onLogoutSuccess(request, response, authentication); } }) .invalidateHttpSession(true).permitAll(); } }
環境依存値の設定
最後に、環境依存値をapplication.propertiesに記述します。
# OpenID Connect Parameters cloudgear.oidc.clientid=1234abcd-efgh-12ab-1234-567890abcdef cloudgear.oidc.secret=secret cloudgear.oidc.endpoint.authorization=https://accounts.cloudgear.services/authorize cloudgear.oidc.endpoint.token=https://accounts.cloudgear.services/token cloudgear.oidc.endpoint.logout=http://accounts.cloudgear.services/logout cloudgear.oidc.endpoint.redirectUri=http://myapp.example.com/oidccallback
プロパティ | 説明 | 設定例 |
---|---|---|
cloudgear.oidc.clientId | サービスマネージャのOpenID Connect設定画面で取得したクライアントID | 1234abcd-efgh-12ab-1234-567890abcdef |
cloudgear.oidc.secret | サービスマネージャのOpenID Connect設定画面で設定したクライアントシークレット | secret |
cloudgear.oidc.endpoint.authorization | CloudGear OPの認可エンドポイントURL | (本番) https://accounts.cloudgear.services/authorize (ベータ) https://accounts.beta.cloudgear.services/authorize |
cloudgear.oidc.endpoint.token | CloudGear OPのトークンエンドポイントURL | (本番) https://accounts.cloudgear.services/token (ベータ) https://accounts.beta.cloudgear.services/token |
cloudgear.oidc.endpoint.logout | CloudGear OPのシングルログアウトエンドポイントURL | (本番) https://accounts.cloudgear.services/logout |
cloudgear.oidc.endpoint.redirectUri | OPで認証が完了した際のクライアントのリダイレクト先となるURL | https://myapp.example.com/oidccallback |
いくつかのプロパティは固定値ですが、利用するCloudGearの環境により異なります。(本番とベータ)