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の環境により異なります。(本番とベータ)