java 如何为 ADFS 配置 Spring Boot 安全 OAuth2?

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/43148552/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-11-03 07:08:11  来源:igfitidea点击:

How to configure spring boot security OAuth2 for ADFS?

javaspringspring-mvcspring-bootspring-security

提问by Erik Pearson

Has anyone successfully configured Spring Boot OAuth2 with ADFS as the identity provider? I followed this tutorial successfully for Facebook, https://spring.io/guides/tutorials/spring-boot-oauth2/, but ADFS doesn't appear to have a userInfoUri. I think ADFS returns the claims data in the token itself (JWT format?), but not sure how to make that work with Spring. Here is what I have so far in my properties file:

有没有人用 ADFS 作为身份提供者成功配置 Spring Boot 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?]

回答by Erik Pearson

tldr;ADFS embeds user information in the oauth token. You need to create and override the org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices object to extract this information and add it to the Principal object

tldr; ADFS 将用户信息嵌入到 oauth 令牌中。您需要创建并覆盖 org.springframework.boot.autoconfigure.security.oauth2.resource.UserInfoTokenServices 对象以提取此信息并将其添加到 Principal 对象

To get started, first follow the Spring OAuth2 tutorial: https://spring.io/guides/tutorials/spring-boot-oauth2/. Use these application properties (fill in your own domain):

首先,请先遵循 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

Note: We will be ignoring whatever is in the userInfoUri, but Spring OAuth2 seems to require something be there.

注意:我们将忽略 userInfoUri 中的任何内容,但 Spring OAuth2 似乎需要一些东西。

Create a new class, AdfsUserInfoTokenServices, which you can copy and tweak below (you will want to clean it up some). This is a copy of the Spring class; You could probably extend it if you want, but I made enough changes where that didn't seem like it gained me much:

创建一个新类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.Hymanson.core.type.TypeReference;
import com.fasterxml.Hymanson.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");
    }
}
}

The getMap method is where the token value is parsed and the JWT formatted user info is extracted and decoded (error checking can be improved here, this is a rough draft, but gives you the gist). See toward the bottom of this link for information on how ADFS embeds data in the token: https://blogs.technet.microsoft.com/askpfeplat/2014/11/02/adfs-deep-dive-comparing-ws-fed-saml-and-oauth/

getMap 方法是解析令牌值并提取和解码 JWT 格式的用户信息的地方(此处可以改进错误检查,这是一个粗略的草案,但为您提供了要点)。有关 ADFS 如何在令牌中嵌入数据的信息,请参阅此链接的底部:https: //blogs.technet.microsoft.com/askpfeplat/2014/11/02/adfs-deep-dive-comparing-ws-fed- saml-and-oauth/

Add this to your configuration:

将此添加到您的配置中:

@Autowired
private ResourceServerProperties sso;

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

Now follow the first part of these instructions to setup an ADFS client and a relying party trust: https://vcsjones.com/2015/05/04/authenticating-asp-net-5-to-ad-fs-oauth/

现在按照这些说明的第一部分设置 ADFS 客户端和信赖方信任https: //vcsjones.com/2015/05/04/authenticating-asp-net-5-to-ad-fs-oauth/

You need to add the id of your relying party trust to the properties file userAuthorizationUri as the value of the parameter 'resource'.

您需要将信赖方信任的 id 添加到属性文件 userAuthorizationUri 作为参数“resource”的值。

Claim Rules:

索赔规则:

If you don't want to have to create your own PrincipalExtractor or AuthoritiesExtractor (see the AdfsUserInfoTokenServices code), set whatever attribute you are using for the username (e.g. SAM-Account-Name) so that it has and Outgoing Claim Type 'username'. When creating claim rules for groups, make sure the Claim type is "authorities" (ADFS just let me type that in, there isn't an existing claim type by that name). Otherwise, you can write extractors to work with the ADFS claim types.

如果您不想创建自己的 PrincipalExtractor 或 AuthoritiesExtractor(请参阅 AdfsUserInfoTokenServices 代码),请设置您用于用户名的任何属性(例如 SAM-Account-Name),使其具有和 Outgoing Claim Type 'username' . 为组创建声明规则时,请确保声明类型为“权限”(ADFS 只是让我输入,没有该名称的现有声明类型)。否则,您可以编写提取器来处理 ADFS 声明类型。

Once that is all done, you should have a working example. There are a lot of details here, but once you get it down, it's not too bad (easier than getting SAML to work with ADFS). The key is understanding the way ADFS embeds data in the OAuth2 token and understanding how to use the UserInfoTokenServices object. Hope this helps someone else.

一旦这一切都完成了,你应该有一个工作示例。这里有很多细节,但是一旦你搞定了,它就不会太糟糕(比让 SAML 与 ADFS 一起工作更容易)。关键是了解 ADFS 在 OAuth2 令牌中嵌入数据的方式并了解如何使用 UserInfoTokenServices 对象。希望这对其他人有帮助。

回答by HGX

Additionally to the accepted answer:

除了已接受的答案:

@Ashika wants to know if you can use this with REST instead of form login. Just switch from @EnableOAuth2Sso to @EnableResourceServer annotation.

@Ashika 想知道您是否可以将它与 REST 一起使用而不是表单登录。只需从 @EnableOAuth2Sso 切换到 @EnableResourceServer 注释。

With the @EnableResourceServer annotation you keep the cabability to use SSO although you didn't use the @EnableOAuth2Sso annotation. Your running as a resource server.

使用 @EnableResourceServer 注释,您可以保留使用 SSO 的能力,尽管您没有使用 @EnableOAuth2Sso 注释。您作为资源服务器运行。

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

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

回答by selvinsource

Although this question is old, there is no other reference on the web on how to integrate Spring OAuth2 with ADFS.

虽然这个问题很老,但网上没有其他关于如何将 Spring OAuth2 与 ADFS 集成的参考。

I therefore added a sample project on how to integrate with Microsoft ADFS using the out of the box spring boot auto-configuration for Oauth2 Client:

因此,我添加了一个关于如何使用 Oauth2 客户端开箱即用的 Spring Boot 自动配置与 Microsoft ADFS 集成的示例项目:

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

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

回答by Amit Misra

@Erik, This is a very good explanation of how to get things going in terms of using ADFS as both identity and authorization provider. There was on thing I stumbled on was to get "upn" and "email" information in the JWT token. This is the decoded JWT information I received -

@Erik,这很好地解释了如何将 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"}

But post adding both email-id and upn in under the "Issuance Transform Rules" and adding "Send LDAP Attributes as Claims" claims rule to send User-Principal-Name as user_id (on of the PRINCIPAL_KETS that FixedPrincipalExtractor - Spring security) I was able to record the user_id being used to login on my UI application. Here is the decoded JWT post adding the claims rule -

但是在“发行转换规则”下添加电子邮件 id 和 upn 并添加“将 LDAP 属性作为声明发送”声明规则以发送用户主体名称作为 user_id(在固定主体提取器的 PRINCIPAL_KETS 上 - Spring 安全性)我是能够记录用于登录我的 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"}