At my Spring Boot application I have configured Spring OAuth2 server with JWT tokens.
Also I have added Spring Social configuration in order to be able authenticate users via various Social Networks like Twitter, Facebook and so on.
This is my SpringSocial config:
#Configuration
#EnableSocial
public class SocialConfig extends SocialConfigurerAdapter {
#Bean
public ProviderSignInController providerSignInController(ConnectionFactoryLocator connectionFactoryLocator, UsersConnectionRepository usersConnectionRepository) {
return new ProviderSignInController(connectionFactoryLocator, usersConnectionRepository, new SimpleSignInAdapter(authTokenServices, "client_id", userService));
}
...
}
Also, based on the folksinging answer Integrate Spring Security OAuth2 and Spring Social I have implemented SimpleSignInAdapter in order to handle successful authentication with 3rdparty Social Networks:
public class SimpleSignInAdapter implements SignInAdapter {
final static Logger logger = LoggerFactory.getLogger(SimpleSignInAdapter.class);
public static final String REDIRECT_PATH_BASE = "/#/login";
public static final String FIELD_TOKEN = "access_token";
public static final String FIELD_EXPIRATION_SECS = "expires_in";
private final AuthorizationServerTokenServices authTokenServices;
private final String localClientId;
private final UserService userService;
public SimpleSignInAdapter(AuthorizationServerTokenServices authTokenServices, String localClientId, UserService userService){
this.authTokenServices = authTokenServices;
this.localClientId = localClientId;
this.userService = userService;
}
#Override
public String signIn(String userId, Connection<?> connection, NativeWebRequest request) {
UserDetails userDetails = loadUserById(Long.parseLong(userId));
OAuth2AccessToken oauth2Token = authTokenServices.createAccessToken(convertAuthentication(userDetails));
String redirectUrl = new StringBuilder(REDIRECT_PATH_BASE)
.append("?").append(FIELD_TOKEN).append("=")
.append(encode(oauth2Token.getValue()))
.append("&").append(FIELD_EXPIRATION_SECS).append("=")
.append(oauth2Token.getExpiresIn())
.toString();
return redirectUrl;
}
private OAuth2Authentication convertAuthentication(UserDetails userDetails) {
OAuth2Request request = new OAuth2Request(null, localClientId, null, true, null, null, null, null, null);
return new OAuth2Authentication(request, new UsernamePasswordAuthenticationToken(userDetails, "N/A", userDetails.getAuthorities()));
}
private String encode(String in) {
String res = in;
try {
res = UriUtils.encode(in, "UTF-8");
} catch(UnsupportedEncodingException e){
logger.error("ERROR: unsupported encoding: " + "UTF-8", e);
}
return res;
}
public UserDetails loadUserById(Long id) throws UsernameNotFoundException {
User user = userService.findUserById(id);
if (user == null) {
throw new UsernameNotFoundException("User " + id + " not found.");
}
Set<Permission> permissions = userService.getUserPermissions(user);
return new DBUserDetails(user, permissions);
}
}
Everything works fine except one thing - the following line of code produces plain OAuth2 access token:
OAuth2AccessToken oauth2Token = authTokenServices.createAccessToken(convertAuthentication(userDetails));
but I need to create JWT token instead.
How to create or convert this token to JWT based ? I suppose I can use JwtAccessTokenConverter class for this purpose but don't know how at this moment.
This worked for me, after i wanted my own Custom JWT token.
DefaultTokenServices service = new DefaultTokenServices();
service.setTokenStore(jwtAccessTokenConverter);
service.setTokenEnhancer(jwtAccessTokenConverter);
OAuth2AccessToken token = service.createAccessToken(authentication);
Autowire the jwtAccessTokenConverter
#Autowired
private JwtAccessTokenConverter jwtAccessTokenConverter;
After debugging I have found a solution:
private final TokenEnhancer tokenEnhancer;
...
OAuth2Authentication authentication = convertAuthentication(userDetails);
OAuth2AccessToken accessToken = authTokenServices.createAccessToken(authentication);
accessToken = tokenEnhancer.enhance(accessToken, authentication);
Related
this is my AuthenticationProvider in authorization server
#Service
public class UmUserAuthenticationProvider implements AuthenticationProvider {
#Autowired
#Qualifier("UmUserDetailsService")
private UserDetailsService userDetailService;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// TODO Auto-generated method stub
String username = authentication.getName();
String password = (String) authentication.getCredentials();
long userId = (new SecurityUtil()).checkUser(umUserMapper, username, password);
if (userId <= 0) {
throw new BadCredentialsException("login failed");
}
UserDetails user = userDetailService.loadUserByUsername(username);
//I've try different ways to put user detail in here
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null,
user.getAuthorities());
auth.setDetails(user);
return auth;
}
#Override
public boolean supports(Class<?> authentication) {
// TODO Auto-generated method stub
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
and this is my resource server, I cant get what I set in Authentication,
getPrincipal is String
getDetails is type of org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationDetails
#RequestMapping("/test")
public class UserController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
#RequestMapping(value = "/test", method = RequestMethod.GET)
public String getRouters() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
logger.info(auth.getPrincipal().toString());
logger.info(auth.getDetails().toString());
JSONObject jo = new JSONObject();
return jo.toString();
}
the logger print
so How cat I get the custom detail in Authentication?
In the end, I solve this problem in a trick way
whatever I set in UsernamePasswordAuthenticationToken, I got the type of String, So I fill it with json string;
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
new JSONObject(user).toString(), password, userDetailService.getAuthorities(username));
so I can parse json string at the controller.
But I still want to know what cause it
I think the problem is, that you are setting the UserDetails on authentication, but it is overriden by the default.
A custom authentication converter might be the solution.
public class CustomAuthenticationConverter implements Converter<Jwt, AbstractAuthenticationToken> {
#Override
public AbstractAuthenticationToken convert(#NotNull final Jwt jwt) {
String username = jwt.getClaimAsString("username");
UserDetails user = userDetailService.loadUserByUsername(username);
return new UsernamePasswordAuthenticationToken (jwt, user, userDetailService.getAuthorities(username)));
}
}
I have a Spring OAuth 2 server based on Spring Boot 1.5 (Spring Security v4) which generates customized tokens and a few resource servers who communicate with this authorization server, making use of /oauth/check_token endpoint by configuration of RemoteTokenServices.
All the logic related to storing/retrieving tokens on Authorization server side is done with JdbcTokenStore.
I am building a new Spring Boot 2 application which is build with Spring webflux module and trying to implement client_credentials flow with existing Authorization Server using Spring Security 5.1.1.
I found that support for resource servers was added in 5.1.0.RC1 (https://spring.io/blog/2018/08/21/spring-security-5-1-0-rc1-released#oauth2-resource-servers) and updated in 5.1.0.RC2 (https://spring.io/blog/2018/09/10/spring-security-5-1-0-rc2-released#oauth2-resource-server) but looks like it's only possible to configure it with JWT support.
I might be messing up with concepts here but looking for more info and a way to configure all these components together.
I'm in same situation as you.I solve that problem in next way, maybe it can help you:
spring-boot-starter-parent.version: 2.1.1
spring-cloud-dependencies.version: Greenwich.R1
Security configuration:
#EnableWebFluxSecurity
public class SecurityConfig {
#Autowired
private ReactiveAuthenticationManager manager; //custom implementation
#Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange()
.pathMatchers("/role").hasRole("ADMIN")
.pathMatchers("/test").access(new HasScope("server")) //custom implementation
.anyExchange().authenticated()
.and()
.httpBasic().disable()
.oauth2ResourceServer()
.jwt()
.authenticationManager(manager)
.and().and()
.build();
}
}
ReactiveAuthorizationManager (HasScope) implementation:
Helper which allow search for scopes in authentication object
public class HasScope implements ReactiveAuthorizationManager<AuthorizationContext> {
public HasScope(String...scopes) {
this.scopes = Arrays.asList(scopes);
}
private final Collection<String> scopes;
#Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext object) {
return authentication
.flatMap(it -> {
OAuth2Authentication auth = (OAuth2Authentication) it;
Set<String> requestScopes = auth.getOAuth2Request().getScope();
boolean allow = requestScopes.containsAll(scopes);
return Mono.just(new AuthorizationDecision(allow));
});
}
}
ReactiveAuthenticationManager implementation:
That is the main component in configuration which create OAuth2Authentication. There is a problem with response for wrong access_token, it returns only status code without body response.
#Component
public class ReactiveAuthenticationManagerImpl implements ReactiveAuthenticationManager {
private final ResourceServerProperties sso;
private final WebClient.Builder webClient;
private final ObjectMapper objectMapper;
private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();
public ReactiveAuthenticationManagerImpl(ResourceServerProperties sso,
#Qualifier("loadBalancedWebClient") WebClient.Builder webClient, ObjectMapper objectMapper) {
this.sso = sso;
this.webClient = webClient;
this.objectMapper = objectMapper;
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.cast(BearerTokenAuthenticationToken.class)
.flatMap(it -> getMap(it.getToken()))
.flatMap(result -> Mono.just(extractAuthentication(result)));
}
private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
Object principal = getPrincipal(map);
OAuth2Request request = getRequest(map);
List<GrantedAuthority> authorities = authoritiesExtractor.extractAuthorities(map);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
token.setDetails(map);
return new OAuth2Authentication(request, token);
}
private Object getPrincipal(Map<String, Object> map) {
if (map.containsKey("principal")) {
try {
//that is the case for user authentication
return objectMapper.convertValue(map.get("principal"), UserPrincipal.class);
} catch (IllegalArgumentException ex) {
//that is the case for client authentication
return objectMapper.convertValue(map.get("principal"), String.class);
}
}
return null;
}
#SuppressWarnings({"unchecked"})
private OAuth2Request getRequest(Map<String, Object> map) {
Map<String, Object> request = (Map<String, Object>) map.get("oauth2Request");
String clientId = (String) request.get("clientId");
Set<String> scope = new LinkedHashSet<>(request.containsKey("scope") ?
(Collection<String>) request.get("scope") : Collections.emptySet());
return new OAuth2Request(null, clientId, null, true, new HashSet<>(scope),
null, null, null, null);
}
private Mono<Map<String, Object>> getMap(String accessToken) {
String uri = sso.getUserInfoUri();
return webClient.build().get()
.uri(uri)
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + accessToken)
.exchange()
.flatMap(it -> it.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {}))
.onErrorMap(InvalidTokenException.class, mapper -> new InvalidTokenException("Invalid token: " + accessToken));
}
I am trying to sign in to my web application (developed using Spring Boot) using social logins. The logins for Google & facebook are okay. But the for some reason there is a token issue in the twitter login. I have created the project in the twitter developer site obtained all the credentials. Please refer to my code below.
My Property file values are mentioned below.
twitter.client.client-id=XXXXXXX
twitter.client.client-secret=XXXXXXXX
twitter.client.access-token-uri=https://api.twitter.com/oauth/access_token
twitter.client.user-authorization-uri=https://api.twitter.com/oauth/authorize
twitter.client.token-name=oauth_token
twitter.client.authentication-scheme=form
twitter.resource.user-info-uri=https://api.twitter.com/1.1/account/verify_credentials.json
The filter method
private Filter ssoTwitterFilter(String processingUrl, PrincipalExtractor principalExtractor) {
OAuth2ClientAuthenticationProcessingFilter twitterFilter = new OAuth2ClientAuthenticationProcessingFilter(
processingUrl);
LOGGER.debug("processingUrl :{} ", processingUrl);
twitterFilter.setAuthenticationSuccessHandler(authenticationSuccessHandlerAndRegistrationFilter());
OAuth2RestTemplate twitterTemplate = new OAuth2RestTemplate(twitter(), oauth2ClientContext);
twitterFilter.setRestTemplate(twitterTemplate);
UserInfoTokenServices tokenServices = new UserInfoTokenServices(twitterResource().getUserInfoUri(),
twitter().getClientId());
tokenServices.setRestTemplate(twitterTemplate);
tokenServices.setPrincipalExtractor(principalExtractor);
return twitterFilter;
}
These are the bean configurations.
#Bean
#ConfigurationProperties("twitter.client")
public AuthorizationCodeResourceDetails twitter() {
return new AuthorizationCodeResourceDetails();
}
#Bean
#ConfigurationProperties("twitter.resource")
public ResourceServerProperties twitterResource() {
return new ResourceServerProperties();
}
This is the error that I get
enter image description here
Please can anyone shed some light on this. Because all the samples I found were related getting profile information from twitter where as i need a sample for sign in using spring Boot. Thanks in advance
You can configure Twitter login like this:
#Configuration
#EnableSocial
public class SocialConfig implements SocialConfigurer {
#Autowired
private UserAuthorizationService userAuthorizationService;
#Override
public void addConnectionFactories(ConnectionFactoryConfigurer cfConfig, Environment env) {
cfConfig.addConnectionFactory(new TwitterConnectionFactory(
env.getProperty("twitter.consumer-key"),
env.getProperty("twitter.consumer-secret")
));
}
#Override
public UserIdSource getUserIdSource() {
return new UserIdSource() {
#Override
public String getUserId() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new IllegalStateException("Unable to get a ConnectionRepository: no user signed in");
}
return authentication.getName();
}
};
}
#Override
public UsersConnectionRepository getUsersConnectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
InMemoryUsersConnectionRepository usersConnectionRepository = new InMemoryUsersConnectionRepository(
connectionFactoryLocator
);
return usersConnectionRepository;
}
#Autowired
private TwitterConnectionSignup twitterConnectionSignup;
#Autowired
private ConnectionFactoryLocator connectionFactoryLocator;
#Autowired
private UsersConnectionRepository usersConnectionRepository;
#Bean
public ProviderSignInController providerSignInController() {
((InMemoryUsersConnectionRepository) usersConnectionRepository)
.setConnectionSignUp(twitterConnectionSignup);
return new ProviderSignInController(
connectionFactoryLocator,
usersConnectionRepository,
new TwitterSignInAdapter(userAuthorizationService));
}
}
Configure TwitterConnectionSignup:
#Service
public class TwitterConnectionSignup implements ConnectionSignUp {
#Autowired
private UserRepo userRepo;
#Override
public String execute(Connection<?> connection) {
//add your logic to save user to your db
return connection.getDisplayName();
}
}
Now configure TwitterSignInAdapter:
public class TwitterSignInAdapter implements SignInAdapter {
private UserAuthorizationService userAuthorizationService;
public TwitterSignInAdapter(UserAuthorizationService userAuthorizationService) {
this.userAuthorizationService = userAuthorizationService;
}
#Override
public String signIn(String localUserId, Connection<?> connection, NativeWebRequest webRequest) {
log.debug(" Email {}", localUserId);
UserAuthDto userAuthDto = (UserAuthDto) userAuthorizationService.loadUserByUsername(localUserId);
UsernamePasswordAuthenticationToken updatedAuth = new UsernamePasswordAuthenticationToken(userAuthDto, userAuthDto.getSocialId(),
userAuthDto.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(updatedAuth);
HttpServletRequest servletRequest = webRequest.getNativeRequest(HttpServletRequest.class);
// add authentication to the session
servletRequest.getSession().setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext());
return "/";
}
}
I have a resource server configured with #EnableResourceServer annotation and it refers to authorization server via user-info-uri parameter as follows:
security:
oauth2:
resource:
user-info-uri: http://localhost:9001/user
Authorization server /user endpoint returns an extension of org.springframework.security.core.userdetails.User which has e.g. an email:
{
"password":null,
"username":"myuser",
...
"email":"me#company.com"
}
Whenever some resource server endpoint is accessed Spring verifies the access token behind the scenes by calling the authorization server's /user endpoint and it actually gets back the enriched user info (which contains e.g. email info, I've verified that with Wireshark).
So the question is how do I get this custom user info without an explicit second call to the authorization server's /user endpoint. Does Spring store it somewhere locally on the resource server after authorization or what is the best way to implement this kind of user info storing if there's nothing available out of the box?
The solution is the implementation of a custom UserInfoTokenServices
https://github.com/spring-projects/spring-boot/blob/master/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/security/oauth2/resource/UserInfoTokenServices.java
Just Provide your custom implementation as a Bean and it will be used instead of the default one.
Inside this UserInfoTokenServices you can build the principal like you want to.
This UserInfoTokenServices is used to extract the UserDetails out of the response of the /usersendpoint of your authorization server. As you can see in
private Object getPrincipal(Map<String, Object> map) {
for (String key : PRINCIPAL_KEYS) {
if (map.containsKey(key)) {
return map.get(key);
}
}
return "unknown";
}
Only the properties specified in PRINCIPAL_KEYS are extracted by default. And thats exactly your problem. You have to extract more than just the username or whatever your property is named. So look for more keys.
private Object getPrincipal(Map<String, Object> map) {
MyUserDetails myUserDetails = new myUserDetails();
for (String key : PRINCIPAL_KEYS) {
if (map.containsKey(key)) {
myUserDetails.setUserName(map.get(key));
}
}
if( map.containsKey("email") {
myUserDetails.setEmail(map.get("email"));
}
//and so on..
return myUserDetails;
}
Wiring:
#Autowired
private ResourceServerProperties sso;
#Bean
public ResourceServerTokenServices myUserInfoTokenServices() {
return new MyUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
}
!!UPDATE with Spring Boot 1.4 things are getting easier!!
With Spring Boot 1.4.0 a PrincipalExtractor was introduced. This class should be implemented to extract a custom principal (see Spring Boot 1.4 Release Notes).
All the data is already in the Principal object, no second request is necessary. Return only what you need. I use the method below for Facebook login:
#RequestMapping("/sso/user")
#SuppressWarnings("unchecked")
public Map<String, String> user(Principal principal) {
if (principal != null) {
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) principal;
Authentication authentication = oAuth2Authentication.getUserAuthentication();
Map<String, String> details = new LinkedHashMap<>();
details = (Map<String, String>) authentication.getDetails();
logger.info("details = " + details); // id, email, name, link etc.
Map<String, String> map = new LinkedHashMap<>();
map.put("email", details.get("email"));
return map;
}
return null;
}
In the Resource server you can create a CustomPrincipal Class Like this:
public class CustomPrincipal {
public CustomPrincipal(){};
private String email;
//Getters and Setters
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
}
Implement a CustomUserInfoTokenServices like this:
public class CustomUserInfoTokenServices implements ResourceServerTokenServices {
protected final Log logger = LogFactory.getLog(getClass());
private final String userInfoEndpointUrl;
private final String clientId;
private OAuth2RestOperations restTemplate;
private String tokenType = DefaultOAuth2AccessToken.BEARER_TYPE;
private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();
private PrincipalExtractor principalExtractor = new CustomPrincipalExtractor();
public CustomUserInfoTokenServices(String userInfoEndpointUrl, String clientId) {
this.userInfoEndpointUrl = userInfoEndpointUrl;
this.clientId = clientId;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public void setRestTemplate(OAuth2RestOperations restTemplate) {
this.restTemplate = restTemplate;
}
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) {
CustomPrincipal customPrincipal = new CustomPrincipal();
if( map.containsKey("principal") ) {
Map<String, Object> principalMap = (Map<String, Object>) map.get("principal");
customPrincipal.setEmail((String) principalMap.get("email"));
}
//and so on..
return customPrincipal;
/*
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");
}
#SuppressWarnings({ "unchecked" })
private Map<String, Object> getMap(String path, String accessToken) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Getting user info from: " + path);
}
try {
OAuth2RestOperations restTemplate = this.restTemplate;
if (restTemplate == null) {
BaseOAuth2ProtectedResourceDetails resource = new BaseOAuth2ProtectedResourceDetails();
resource.setClientId(this.clientId);
restTemplate = new OAuth2RestTemplate(resource);
}
OAuth2AccessToken existingToken = restTemplate.getOAuth2ClientContext()
.getAccessToken();
if (existingToken == null || !accessToken.equals(existingToken.getValue())) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(
accessToken);
token.setTokenType(this.tokenType);
restTemplate.getOAuth2ClientContext().setAccessToken(token);
}
return restTemplate.getForEntity(path, Map.class).getBody();
}
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");
}
}
}
A Custom PrincipalExtractor:
public class CustomPrincipalExtractor implements PrincipalExtractor {
private static final String[] PRINCIPAL_KEYS = new String[] {
"user", "username", "principal",
"userid", "user_id",
"login", "id",
"name", "uuid",
"email"};
#Override
public Object extractPrincipal(Map<String, Object> map) {
for (String key : PRINCIPAL_KEYS) {
if (map.containsKey(key)) {
return map.get(key);
}
}
return null;
}
#Bean
public DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setForcePrincipalAsString(false);
return daoAuthenticationProvider;
}
}
In your #Configuration file define a bean like this one
#Bean
public ResourceServerTokenServices myUserInfoTokenServices() {
return new CustomUserInfoTokenServices(sso.getUserInfoUri(), sso.getClientId());
}
And in the Resource Server Configuration:
#Configuration
public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
#Override
public void configure(ResourceServerSecurityConfigurer config) {
config.tokenServices(myUserInfoTokenServices());
}
//etc....
If everything is set correctly you can do something like this in your controller:
String userEmail = ((CustomPrincipal) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getEmail();
Hope this helps.
A Map representation of the JSON object returned by the userdetails endpoint is available from the Authentication object that represents the Principal:
Map<String, Object> details = (Map<String,Object>)oauth2.getUserAuthentication().getDetails();
If you want to capture it for logging, storage or cacheing I'd recommend capturing it by implementing an ApplicationListener. For example:
#Component
public class AuthenticationSuccessListener implements ApplicationListener<AuthenticationSuccessEvent> {
private Logger log = LoggerFactory.getLogger(this.getClass());
#Override
public void onApplicationEvent(AuthenticationSuccessEvent event) {
Authentication auth = event.getAuthentication();
log.debug("Authentication class: "+auth.getClass().toString());
if(auth instanceof OAuth2Authentication){
OAuth2Authentication oauth2 = (OAuth2Authentication)auth;
#SuppressWarnings("unchecked")
Map<String, Object> details = (Map<String, Object>)oauth2.getUserAuthentication().getDetails();
log.info("User {} logged in: {}", oauth2.getName(), details);
log.info("User {} has authorities {} ", oauth2.getName(), oauth2.getAuthorities());
} else {
log.warn("User authenticated by a non OAuth2 mechanism. Class is "+auth.getClass());
}
}
}
If you specifically want to customize the extraction of the principal from the JSON or the authorities then you could implement org.springframework.boot.autoconfigure.security.oauth2.resource.PrincipalExtractor and/ org.springframework.boot.autoconfigure.security.oauth2.resource.AuthoritiesExtractor respectively.
Then, in a #Configuration class you would expose your implementations as beans:
#Bean
public PrincipalExtractor merckPrincipalExtractor() {
return new MyPrincipalExtractor();
}
#Bean
public AuthoritiesExtractor merckAuthoritiesExtractor() {
return new MyAuthoritiesExtractor();
}
You can use JWT tokens. You won't need datastore where all user information is stored instead you can encode additional information into the token itself. When token is decoded you app will be able to access all this information using Principal object
We retrieve it from the SecurityContextHolder's getContext method, which is static, and hence can be retrieved from anywhere.
// this is userAuthentication's principal
Map<?, ?> getUserAuthenticationFromSecurityContextHolder() {
Map<?, ?> userAuthentication = new HashMap<>();
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (!(authentication instanceof OAuth2Authentication)) {
return userAuthentication;
}
OAuth2Authentication oauth2Authentication = (OAuth2Authentication) authentication;
Authentication userauthentication = oauth2Authentication.getUserAuthentication();
if (userauthentication == null) {
return userAuthentication;
}
Map<?, ?> details = (HashMap<?, ?>) userauthentication.getDetails(); //this effect in the new RW OAUTH2 userAuthentication
Object principal = details.containsKey("principal") ? details.get("principal") : userAuthentication; //this should be effect in the common OAUTH2 userAuthentication
if (!(principal instanceof Map)) {
return userAuthentication;
}
userAuthentication = (Map<?, ?>) principal;
} catch (Exception e) {
logger.error("Got exception while trying to obtain user info from security context.", e);
}
return userAuthentication;
}
I'm trying to understand how to use a OAuth2RestTemplate object to consume my OAuth2 secured REST service (which is running under a different project and let's assume also on a different server etc...)
An example of my REST service is:
http://localhost:8082/app/helloworld
-> Accessing this URL generates an error as I am not authenticated
To request a token I would go to:
http://localhost:8082/app/oauth/token?grant_type=password&client_id=restapp&client_secret=restapp&username=**USERNAME**&password=**PASSWORD**
After I receive the token I can then connect to the REST API by using the following URL (example token inserted)
http://localhost:8082/app/helloworld/?access_token=**4855f557-c6ee-43b7-8617-c24591965206**
Now my question is how do I implement a second application which can consume this OAuth2 secured REST API? I really haven't found any working examples where you provide the user name and password (e.g. coming from a login form) and then a token is generated which can be re-used to get data from the REST API.
I currently tried something with the following objects:
BaseOAuth2ProtectedResourceDetails baseOAuth2ProtectedResourceDetails = new BaseOAuth2ProtectedResourceDetails();
baseOAuth2ProtectedResourceDetails.setClientId("restapp");
baseOAuth2ProtectedResourceDetails.setClientSecret("restapp");
baseOAuth2ProtectedResourceDetails.setGrantType("password");
// how to set user name and password ???
DefaultAccessTokenRequest accessTokenRequest = new DefaultAccessTokenRequest();
OAuth2ClientContext oAuth2ClientContext = new DefaultOAuth2ClientContext(accessTokenRequest());
OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(baseOAuth2ProtectedResourceDetails,oAuth2ClientContext);
But this just isn't working :(
Any ideas, links to working examples or tutorials are greatly appreciated.
You can find examples for writing OAuth clients here:
https://github.com/spring-projects/spring-security-oauth
In your case you can't just use default or base classes for everything, you have a multiple classes Implementing OAuth2ProtectedResourceDetails. The configuration depends of how you configured your OAuth service but assuming from your curl connections I would recommend:
#EnableOAuth2Client
#Configuration
class MyConfig{
#Value("${oauth.resource:http://localhost:8082}")
private String baseUrl;
#Value("${oauth.authorize:http://localhost:8082/oauth/authorize}")
private String authorizeUrl;
#Value("${oauth.token:http://localhost:8082/oauth/token}")
private String tokenUrl;
#Bean
protected OAuth2ProtectedResourceDetails resource() {
ResourceOwnerPasswordResourceDetails resource;
resource = new ResourceOwnerPasswordResourceDetails();
List scopes = new ArrayList<String>(2);
scopes.add("write");
scopes.add("read");
resource.setAccessTokenUri(tokenUrl);
resource.setClientId("restapp");
resource.setClientSecret("restapp");
resource.setGrantType("password");
resource.setScope(scopes);
resource.setUsername("**USERNAME**");
resource.setPassword("**PASSWORD**");
return resource;
}
#Bean
public OAuth2RestOperations restTemplate() {
AccessTokenRequest atr = new DefaultAccessTokenRequest();
return new OAuth2RestTemplate(resource(), new DefaultOAuth2ClientContext(atr));
}
}
#Service
#SuppressWarnings("unchecked")
class MyService {
#Autowired
private OAuth2RestOperations restTemplate;
public MyService() {
restTemplate.getAccessToken();
}
}
Do not forget about #EnableOAuth2Client on your config class, also I would suggest to try that the urls you are using are working with curl first, also try to trace it with the debugger because lot of exceptions are just consumed and never printed out due security reasons, so it gets little hard to find where the issue is. You should use logger with debug enabled set.
Good luck
I uploaded sample springboot app on github https://github.com/mariubog/oauth-client-sample
to depict your situation because I could not find any samples for your scenario .
In the answer from #mariubog (https://stackoverflow.com/a/27882337/1279002) I was using password grant types too as in the example but needed to set the client authentication scheme to form. Scopes were not supported by the endpoint for password and there was no need to set the grant type as the ResourceOwnerPasswordResourceDetails object sets this itself in the constructor.
...
public ResourceOwnerPasswordResourceDetails() {
setGrantType("password");
}
...
The key thing for me was the client_id and client_secret were not being added to the form object to post in the body if resource.setClientAuthenticationScheme(AuthenticationScheme.form); was not set.
See the switch in:
org.springframework.security.oauth2.client.token.auth.DefaultClientAuthenticationHandler.authenticateTokenRequest()
Finally, when connecting to Salesforce endpoint the password token needed to be appended to the password.
#EnableOAuth2Client
#Configuration
class MyConfig {
#Value("${security.oauth2.client.access-token-uri}")
private String tokenUrl;
#Value("${security.oauth2.client.client-id}")
private String clientId;
#Value("${security.oauth2.client.client-secret}")
private String clientSecret;
#Value("${security.oauth2.client.password-token}")
private String passwordToken;
#Value("${security.user.name}")
private String username;
#Value("${security.user.password}")
private String password;
#Bean
protected OAuth2ProtectedResourceDetails resource() {
ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
resource.setAccessTokenUri(tokenUrl);
resource.setClientId(clientId);
resource.setClientSecret(clientSecret);
resource.setClientAuthenticationScheme(AuthenticationScheme.form);
resource.setUsername(username);
resource.setPassword(password + passwordToken);
return resource;
}
#Bean
public OAuth2RestOperations restTemplate() {
return new OAuth2RestTemplate(resource(), new DefaultOAuth2ClientContext(new DefaultAccessTokenRequest()));
}
}
#Service
#SuppressWarnings("unchecked")
class MyService {
#Autowired
private OAuth2RestOperations restTemplate;
public MyService() {
restTemplate.getAccessToken();
}
}
I have different approach if you want access token and make call to other resource system with access token in header
Spring Security comes with automatic security: oauth2 properties access from application.yml file for every request and every request has SESSIONID which it reads and pull user info via Principal, so you need to make sure inject Principal in OAuthUser and get accessToken and make call to resource server
This is your application.yml, change according to your auth server:
security:
oauth2:
client:
clientId: 233668646673605
clientSecret: 33b17e044ee6a4fa383f46ec6e28ea1d
accessTokenUri: https://graph.facebook.com/oauth/access_token
userAuthorizationUri: https://www.facebook.com/dialog/oauth
tokenName: oauth_token
authenticationScheme: query
clientAuthenticationScheme: form
resource:
userInfoUri: https://graph.facebook.com/me
#Component
public class OAuthUser implements Serializable {
private static final long serialVersionUID = 1L;
private String authority;
#JsonIgnore
private String clientId;
#JsonIgnore
private String grantType;
private boolean isAuthenticated;
private Map<String, Object> userDetail = new LinkedHashMap<String, Object>();
#JsonIgnore
private String sessionId;
#JsonIgnore
private String tokenType;
#JsonIgnore
private String accessToken;
#JsonIgnore
private Principal principal;
public void setOAuthUser(Principal principal) {
this.principal = principal;
init();
}
public Principal getPrincipal() {
return principal;
}
private void init() {
if (principal != null) {
OAuth2Authentication oAuth2Authentication = (OAuth2Authentication) principal;
if (oAuth2Authentication != null) {
for (GrantedAuthority ga : oAuth2Authentication.getAuthorities()) {
setAuthority(ga.getAuthority());
}
setClientId(oAuth2Authentication.getOAuth2Request().getClientId());
setGrantType(oAuth2Authentication.getOAuth2Request().getGrantType());
setAuthenticated(oAuth2Authentication.getUserAuthentication().isAuthenticated());
OAuth2AuthenticationDetails oAuth2AuthenticationDetails = (OAuth2AuthenticationDetails) oAuth2Authentication
.getDetails();
if (oAuth2AuthenticationDetails != null) {
setSessionId(oAuth2AuthenticationDetails.getSessionId());
setTokenType(oAuth2AuthenticationDetails.getTokenType());
// This is what you will be looking for
setAccessToken(oAuth2AuthenticationDetails.getTokenValue());
}
// This detail is more related to Logged-in User
UsernamePasswordAuthenticationToken userAuthenticationToken = (UsernamePasswordAuthenticationToken) oAuth2Authentication.getUserAuthentication();
if (userAuthenticationToken != null) {
LinkedHashMap<String, Object> detailMap = (LinkedHashMap<String, Object>) userAuthenticationToken.getDetails();
if (detailMap != null) {
for (Map.Entry<String, Object> mapEntry : detailMap.entrySet()) {
//System.out.println("#### detail Key = " + mapEntry.getKey());
//System.out.println("#### detail Value = " + mapEntry.getValue());
getUserDetail().put(mapEntry.getKey(), mapEntry.getValue());
}
}
}
}
}
}
public String getAuthority() {
return authority;
}
public void setAuthority(String authority) {
this.authority = authority;
}
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getGrantType() {
return grantType;
}
public void setGrantType(String grantType) {
this.grantType = grantType;
}
public boolean isAuthenticated() {
return isAuthenticated;
}
public void setAuthenticated(boolean isAuthenticated) {
this.isAuthenticated = isAuthenticated;
}
public Map<String, Object> getUserDetail() {
return userDetail;
}
public void setUserDetail(Map<String, Object> userDetail) {
this.userDetail = userDetail;
}
public String getSessionId() {
return sessionId;
}
public void setSessionId(String sessionId) {
this.sessionId = sessionId;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
#Override
public String toString() {
return "OAuthUser [clientId=" + clientId + ", grantType=" + grantType + ", isAuthenticated=" + isAuthenticated
+ ", userDetail=" + userDetail + ", sessionId=" + sessionId + ", tokenType="
+ tokenType + ", accessToken= " + accessToken + " ]";
}
#RestController
public class YourController {
#Autowired
OAuthUser oAuthUser;
// In case if you want to see Profile of user then you this
#RequestMapping(value = "/profile", produces = MediaType.APPLICATION_JSON_VALUE)
public OAuthUser user(Principal principal) {
oAuthUser.setOAuthUser(principal);
// System.out.println("#### Inside user() - oAuthUser.toString() = " + oAuthUser.toString());
return oAuthUser;
}
#RequestMapping(value = "/createOrder",
method = RequestMethod.POST,
headers = {"Content-type=application/json"},
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public FinalOrderDetail createOrder(#RequestBody CreateOrder createOrder) {
return postCreateOrder_restTemplate(createOrder, oAuthUser).getBody();
}
private ResponseEntity<String> postCreateOrder_restTemplate(CreateOrder createOrder, OAuthUser oAuthUser) {
String url_POST = "your post url goes here";
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
headers.add("Authorization", String.format("%s %s", oAuthUser.getTokenType(), oAuthUser.getAccessToken()));
headers.add("Content-Type", "application/json");
RestTemplate restTemplate = new RestTemplate();
//restTemplate.getMessageConverters().add(new MappingJackson2HttpMessageConverter());
HttpEntity<String> request = new HttpEntity<String>(createOrder, headers);
ResponseEntity<String> result = restTemplate.exchange(url_POST, HttpMethod.POST, request, String.class);
System.out.println("#### post response = " + result);
return result;
}
}
My simple solution. IMHO it's the cleanest.
First create a application.yml
spring.main.allow-bean-definition-overriding: true
security:
oauth2:
client:
clientId: XXX
clientSecret: XXX
accessTokenUri: XXX
tokenName: access_token
grant-type: client_credentials
Create the main class: Main
#SpringBootApplication
#EnableOAuth2Client
public class Main extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/").permitAll();
}
public static void main(String[] args) {
SpringApplication.run(Main.class, args);
}
#Bean
public OAuth2RestTemplate oauth2RestTemplate(ClientCredentialsResourceDetails details) {
return new OAuth2RestTemplate(details);
}
}
Then Create the controller class: Controller
#RestController
class OfferController {
#Autowired
private OAuth2RestOperations restOperations;
#RequestMapping(value = "/<your url>"
, method = RequestMethod.GET
, produces = "application/json")
public String foo() {
ResponseEntity<String> responseEntity = restOperations.getForEntity(<the url you want to call on the server>, String.class);
return responseEntity.getBody();
}
}
Maven dependencies
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.5.RELEASE</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>2.1.5.RELEASE</version>
</dependency>
</dependencies>