How to configure spring boot security OAuth2 for ADFS?

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.

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.

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


Although this question is old, there is no other reference on the web on how to integrate Spring OAuth2 with 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:

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


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

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):

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.

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:

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");
    }
}
}

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/

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/

You need to add the id of your relying party trust to the properties file userAuthorizationUri as the value of the parameter '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.

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.