web-dev-qa-db-ja.com

ADFSのSpringBootセキュリティOAuth2を構成するにはどうすればよいですか?

IDプロバイダーとしてADFSを使用してSpringBoot OAuth2を正常に構成した人はいますか? Facebookでこのチュートリアルを正常に実行しました https://spring.io/guides/tutorials/spring-boot-oauth2/ ですが、ADFSにuserInfoUriがないようです。 ADFSはトークン自体(JWT形式?)でクレームデータを返すと思いますが、Springでそれを機能させる方法がわかりません。これが私のプロパティファイルにこれまでにあるものです:

security:
  oauth2:
    client:
      clientId: [client id setup with ADFS]
      userAuthorizationUri: https://[adfs domain]/adfs/oauth2/authorize?resource=[MyRelyingPartyTrust]
      accessTokenUri: https://[adfs domain]/adfs/oauth2/token
      tokenName: code
      authenticationScheme: query
      clientAuthenticationScheme: form
      grant-type: authorization_code
    resource:
      userInfoUri: [not sure what to put here?]
6
Erik Pearson

tldr;ADFSはユーザー情報をoauthトークンに埋め込みます。org.springframework.bootを作成してオーバーライドする必要があります.autoconfigure.security.oauth2.resource.UserInfoTokenServicesオブジェクトを使用して、この情報を抽出し、プリンシパルオブジェクトに追加します

開始するには、まずSpring OAuth2チュートリアルに従ってくださいhttps://spring.io/guides/tutorials/spring-boot-oauth2 / 。次のアプリケーションプロパティを使用します(独自のドメインに入力します)。

security:
  oauth2:
    client:
      clientId: [client id setup with ADFS]
      userAuthorizationUri: https://[adfs domain]/adfs/oauth2/authorize?resource=[MyRelyingPartyTrust]
      accessTokenUri: https://[adfs domain]/adfs/oauth2/token
      tokenName: code
      authenticationScheme: query
      clientAuthenticationScheme: form
      grant-type: authorization_code
    resource:
      userInfoUri: https://[adfs domain]/adfs/oauth2/token

注:userInfoUriにあるものはすべて無視しますが、SpringOAuth2には何かが必要なようです。

新しいクラス、AdfsUserInfoTokenServicesを作成します。これをコピーして、以下で微調整できます(いくつかクリーンアップする必要があります)。これはSpringクラスのコピーです。必要に応じて拡張することもできますが、十分な変更を加えたため、あまり効果がなかったようです。

package edu.bowdoin.oath2sample;

import Java.util.Base64;
import Java.util.Collections;
import Java.util.List;
import Java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor;
import org.springframework.boot.autoconfigure.security.oauth2.resource.FixedAuthoritiesExtractor;
import org.springframework.boot.autoconfigure.security.oauth2.resource.FixedPrincipalExtractor;
import org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.client.OAuth2RestOperations;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.util.Assert;

import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;

public class AdfsUserInfoTokenServices implements ResourceServerTokenServices {

protected final Logger logger = LoggerFactory.getLogger(getClass());

private final String userInfoEndpointUrl;

private final String clientId;

private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE;

private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();

private PrincipalExtractor principalExtractor = new FixedPrincipalExtractor();

public AdfsUserInfoTokenServices(String userInfoEndpointUrl, String clientId) {
    this.userInfoEndpointUrl = userInfoEndpointUrl;
    this.clientId = clientId;
}

public void setTokenType(String tokenType) {
    this.tokenType = tokenType;
}

public void setRestTemplate(OAuth2RestOperations restTemplate) {
    // not used
}

public void setAuthoritiesExtractor(AuthoritiesExtractor authoritiesExtractor) {
    Assert.notNull(authoritiesExtractor, "AuthoritiesExtractor must not be null");
    this.authoritiesExtractor = authoritiesExtractor;
}

public void setPrincipalExtractor(PrincipalExtractor principalExtractor) {
    Assert.notNull(principalExtractor, "PrincipalExtractor must not be null");
    this.principalExtractor = principalExtractor;
}

@Override
public OAuth2Authentication loadAuthentication(String accessToken)
        throws AuthenticationException, InvalidTokenException {
    Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
    if (map.containsKey("error")) {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("userinfo returned error: " + map.get("error"));
        }
        throw new InvalidTokenException(accessToken);
    }
    return extractAuthentication(map);
}

private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
    Object principal = getPrincipal(map);
    List<GrantedAuthority> authorities = this.authoritiesExtractor
            .extractAuthorities(map);
    OAuth2Request request = new OAuth2Request(null, this.clientId, null, true, null,
            null, null, null, null);
    UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
            principal, "N/A", authorities);
    token.setDetails(map);
    return new OAuth2Authentication(request, token);
}

/**
 * Return the principal that should be used for the token. The default implementation
 * delegates to the {@link PrincipalExtractor}.
 * @param map the source map
 * @return the principal or {@literal "unknown"}
 */
protected Object getPrincipal(Map<String, Object> map) {
    Object principal = this.principalExtractor.extractPrincipal(map);
    return (principal == null ? "unknown" : principal);
}

@Override
public OAuth2AccessToken readAccessToken(String accessToken) {
    throw new UnsupportedOperationException("Not supported: read access token");
}

private Map<String, Object> getMap(String path, String accessToken) {
    if (this.logger.isDebugEnabled()) {
        this.logger.debug("Getting user info from: " + path);
    }
    try {
        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(
                accessToken);
        token.setTokenType(this.tokenType);

        logger.debug("Token value: " + token.getValue());

        String jwtBase64 = token.getValue().split("\\.")[1];

        logger.debug("Token: Encoded JWT: " + jwtBase64);
        logger.debug("Decode: " + Base64.getDecoder().decode(jwtBase64.getBytes()));

        String jwtJson = new String(Base64.getDecoder().decode(jwtBase64.getBytes()));

        ObjectMapper mapper = new ObjectMapper();

        return mapper.readValue(jwtJson, new TypeReference<Map<String, Object>>(){});
    }
    catch (Exception ex) {
        this.logger.warn("Could not fetch user details: " + ex.getClass() + ", "
                + ex.getMessage());
        return Collections.<String, Object>singletonMap("error",
                "Could not fetch user details");
    }
}
}

GetMapメソッドは、トークン値が解析され、JWT形式のユーザー情報が抽出およびデコードされる場所です(エラーチェックはここで改善できます。これは大まかなドラフトですが、要点を示します)。 ADFSがトークンにデータを埋め込む方法については、このリンクの下部を参照してください: https://blogs.technet.Microsoft.com/askpfeplat/2014/11/02/adfs-deep-dive-comparing- ws-fed-saml-and-oauth /

これを構成に追加します:

@Autowired
private ResourceServerProperties sso;

@Bean
public ResourceServerTokenServices userInfoTokenServices() {
    return new AdfsUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
}

次に、これらの手順の最初の部分に従って、ADFSクライアントと証明書利用者の信頼をセットアップしますhttps://vcsjones.com/2015/05/04/authentication-asp-net-5-to-ad-fs-oauth /

証明書利用者の信頼のIDを、パラメーター 'resource'の値としてプロパティファイルuserAuthorizationUriに追加する必要があります。

クレームルール:

独自のPrincipalExtractorまたはAuthoritiesExtractorを作成する必要がない場合(AdfsUserInfoTokenServicesコードを参照)、ユーザー名に使用している属性(SAM-Account-Nameなど)を設定して、送信クレームタイプ「username」を設定します。 。グループのクレームルールを作成するときは、クレームの種類が「権限」であることを確認してください(ADFSで入力させてください。その名前の既存のクレームの種類はありません)。それ以外の場合は、ADFSクレームタイプを処理するエクストラクターを作成できます。

それがすべて完了すると、実用的な例が得られるはずです。ここには多くの詳細がありますが、一度理解すれば、それほど悪くはありません(SAMLをADFSで動作させるよりも簡単です)。重要なのは、ADFSがOAuth2トークンにデータを埋め込む方法を理解し、UserInfoTokenServicesオブジェクトを使用する方法を理解することです。これが他の誰かに役立つことを願っています。

11
Erik Pearson

受け入れられた答えに加えて:

@Ashikaは、フォームログインの代わりにRESTでこれを使用できるかどうかを知りたいと考えています。@ EnableOAuth2Ssoから@EnableResourceServerアノテーションに切り替えるだけです。

@EnableResourceServerアノテーションを使用すると、@ EnableOAuth2Ssoアノテーションを使用しなかった場合でも、SSOを使用できるようになります。リソースサーバーとして実行しています。

https://docs.spring.io/spring-security-oauth2-boot/docs/current/reference/htmlsingle/#boot-features-security-oauth2-resource-server

2
HGX

この質問は古いものですが、SpringOAuth2をADFSと統合する方法に関する他のリファレンスはWeb上にありません。

そのため、Oauth2クライアントのすぐに使用できるスプリングブート自動構成を使用してMicrosoftADFSと統合する方法に関するサンプルプロジェクトを追加しました。

https://github.com/selvinsource/spring-security/tree/oauth2login-adfs-sample/samples/boot/oauth2login#adfs-login

1
selvinsource

@Erik、これは、IDプロバイダーと承認プロバイダーの両方としてADFSを使用するという観点から物事を進める方法の非常に良い説明です。私が偶然見つけたのは、JWTトークンで「upn」と「email」の情報を取得することでした。これは私が受け取ったデコードされたJWT情報です-

2017-07-13 19:43:15.548  INFO 3848 --- [nio-8080-exec-7] c.e.demo.AdfsUserInfoTokenServices       : Decoded JWT: {"aud":"http://localhost:8080/web-app","iss":"http://adfs1.example.com/adfs/services/trust","iat":1500000192,"exp":1500003792,"apptype":"Confidential","appid":"1fd9b444-8ba4-4d82-942e-91aaf79f5fd0","authmethod":"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport","auth_time":"2017-07-14T02:43:12.570Z","ver":"1.0"}

しかし、「発行変換ルール」の下にemail-idとupnの両方を追加し、「LDAP属性をクレームとして送信」クレームルールを追加して、User-Principal-Nameをuser_idとして送信した後(FixedPrincipalExtractor-SpringセキュリティのPRINCIPAL_KETS上)私はUIアプリケーションへのログインに使用されているuser_idを記録できます。クレームルールを追加したデコードされたJWT投稿は次のとおりです-

2017-07-13 20:16:05.918  INFO 8048 --- [nio-8080-exec-3] c.e.demo.AdfsUserInfoTokenServices       : Decoded JWT: {"aud":"http://localhost:8080/web-app","iss":"http://adfs1.example.com/adfs/services/trust","iat":1500002164,"exp":1500005764,"upn":"[email protected]","apptype":"Confidential","appid":"1fd9b444-8ba4-4d82-942e-91aaf79f5fd0","authmethod":"urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport","auth_time":"2017-07-14T03:16:04.745Z","ver":"1.0"}
0
Amit Misra