How to configure spring boot security OAuth2 for ADFS?
Solution 1
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.
Solution 2
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.
Solution 3
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:
Related videos on Youtube

Comments
-
Erik Pearson 12 months
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:
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?]
-
Ashika Umanga Umagiliya about 6 yearscan this be used to integrate OAuth2+ ADFS for Spring Rest API?
-
Erik Pearson about 6 years@Ashika, I am not sure. I think it is currently limited to using the form login, which would be prohibitive for using for a rest api by itself. Microsoft is adding new functionality in MS Server 2016 which might provide this capability: docs.microsoft.com/en-us/windows-server/identity/ad-fs/…
-
renanleandrof over 4 yearsSomeone should sue microsoft for this mess.
-
renanleandrof over 4 years@ErikPearson It worked perfectly! Can you help me with another thing. After Authentication, i need to get my User from database and put it on the session... How can i do that/