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

  1. RP は必要なリクエストパラメータを含んだ Authentication Request を構築する
  2. RP は OP の Authorization Endpoint にリクエストを送信する
  3. OP は End-User を認証する
  4. OP は End-User の Consent/Authorization を取得する
  5. OP は End-User を code とともに RP に戻す
  6. RP は code を OP の Token Endpoint に送信し, Access Token と ID Token を受け取る
  7. 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で発行する認可コード。
stateAuthorization 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_tokenUserInfo 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を利用する場合

依存関係の追加

アプリケーションに以下の依存関係を追加します。

pom.xml
	<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>

認証制御用クラスの追加

認証制御用のクラスとして以下を追加します。

CloudGearAccessTokenProvider.java
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);
    }
}
OpenIdConnectFilter.java
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;
    }
}
OpenIdConnectUserDetails.java
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認証を有効化するために、以下のコンフィグレーションを追加します。

OpenIdConnectConfig.java
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;
    }
}
SecurityConfig.java
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に記述します。

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設定画面で取得したクライアントID1234abcd-efgh-12ab-1234-567890abcdef
cloudgear.oidc.secretサービスマネージャのOpenID Connect設定画面で設定したクライアントシークレットsecret
cloudgear.oidc.endpoint.authorizationCloudGear OPの認可エンドポイントURL(本番) https://accounts.cloudgear.services/authorize
(ベータ) https://accounts.beta.cloudgear.services/authorize
cloudgear.oidc.endpoint.tokenCloudGear OPのトークンエンドポイントURL(本番) https://accounts.cloudgear.services/token
(ベータ) https://accounts.beta.cloudgear.services/token
cloudgear.oidc.endpoint.logoutCloudGear OPのシングルログアウトエンドポイントURL

(本番) https://accounts.cloudgear.services/logout
(ベータ) https://accounts.beta.cloudgear.services/logout

cloudgear.oidc.endpoint.redirectUriOPで認証が完了した際のクライアントのリダイレクト先となるURLhttps://myapp.example.com/oidccallback

いくつかのプロパティは固定値ですが、利用するCloudGearの環境により異なります。(本番とベータ)