/
Relying Party実装

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

Related content

ユーザー認証の連携
ユーザー認証の連携
More like this
スクエアへのユーザー招待操作をAPIで行う
スクエアへのユーザー招待操作をAPIで行う
More like this
サービスの追加
サービスの追加
Read with this
認証の基本
More like this
アクティベーション
アクティベーション
Read with this
アクセストークンの取得とスコープについて
アクセストークンの取得とスコープについて
More like this