I have a Spring (not Spring Boot) REST API that is secured using Spring Security. Many endpoints require authentication in the form of a JWT before they can be accessed. Often the principal is fetched to access information from the token:
CustomObject customObject = (CustomObject) SecurityContextHolder
.getContext()
.getAuthentication()
.getPrincipal();
I'm attempting to change the REST API to accept Keycloak tokens. Following the Keycloak documentation I've added the keycloak-spring-security-adapter dependency, setup a local Keycloak instance using Docker for development and added a keycloak.json to the API project. Everything seems to work, the API accepts an access token in the Authorization header of requests. However, when attempting to access information from the token an exception occurs because the KeycloakPrincipal class can't be cast to our CustomObject class.
I don't want to go through the whole project and change all casts when getting the principal from CustomObject to KeycloakPrincipal as that would be a significant amount of work. Besides, using the KeycloakPrincipal object makes our code implementation specific (Keycloak in this case), what if we want to move to a different token provider.
Is it possible to change the default KeycloakPrincipal set on the security context to a custom object so the above code for getting the principal still works? If so, what would be the best way to do that, through a Spring filter maybe?
Do not use Keycloak adapters, it is very deprecated.
I strongly recommend you take the time to read this set of 3 tutorials which end with exactly what you want: configure spring-security to populate security-context with a custom Authentication implementation containing data retrieved from a JWT access-token (issued by Keycloak or whatever).
Bad news is it based on spring-boot. Good news is everything is open-source and you can inspect any #Bean configured.
If you don't have spring-boot, you'll have a little more work to provide your application context with a SecurityFilterChain bean for an OAuth2 resource-server with a JWT decoder. I let you refer to the doc to have one defined in your conf.
Once you have it, defining your own Converter<Jwt, ? extends AbstractAuthenticationToken> on resource-server JWT decoder configurer should be enough:
interface ClaimsToAuthoritiesConverter extends Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> {}
#Bean
ClaimsToAuthoritiesConverter authoritiesConverter() {
return (Map<String, Object> claims) -> {
// Override this with the actual private-claim(s) your authorization-server puts roles into
// like resource_access.some-client.roles or whatever
final var realmAccessClaim = (Map<String, Object>) claims.getOrDefault("realm_access", Map.of());
final var rolesClaim = (Collection<String>) realmAccessClaim.getOrDefault("roles", List.of());
return rolesClaim.stream().map(SimpleGrantedAuthority::new).toList();
};
}
#Bean
SecurityFilterChain filterChain(HttpSecurity http, Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) throws Exception {
http.oauth2ResourceServer().jwt()
// omitted regular JWT decoder configuration like issuer, jwk-set-uri, etc.
.jwtAuthenticationConverter(jwt -> new YourOwnAuthentication(jwt.getTokenValue(), jwt.getClaims(), authoritiesConverter));
// Some more security conf like CORS etc.
return http.build();
}
This converter from Jwt to your own implementation of AbstractAuthenticationToken is called after the JWT access-token was sucessfully decoded and validated. In other words, it is pretty safe to interfere at this stage, it is just a matter of formating valid authentication data in the most convenient way for your business code and security rules.
Tips for designing YourOwnAuthentication:
public class YourOwnAuthentication extends AbstractAuthenticationToken {
private final CustomObject principal;
private final String bearerString;
public YourOwnAuthentication (String bearerString, Map<String, Object> claims, Converter<Map<String, Object>, Collection<? extends GrantedAuthority>> authoritiesConverter) {
super(authoritiesConverter.convert(claims));
super.setAuthenticated(true);
this.bearerString = bearerString;
this.principal = new CustomObject(claims); // I connot figure out how you'll actually build that
}
#Override
public String getCredentials() {
return bearerString;
}
#Override
public CustomObject getPrincipal() {
return principal;
}
public String getBearerString() {
return bearerString;
}
}
Side note: in addition to "manually" access your CustomObject instance from security-context as you do in your question you'll also be able to access it with spring "magic" authentication parameters for #Controller methods:
#PreAuthorise("isAuthenticated()") ResponseEntity<?> controllerMethod(YourOwnAuthentication auth) and then declare CustomObject customObject = auth.getPrincipal(); (note there is no cast here)
#PreAuthorise("isAuthenticated()") ResponseEntity<?> controllerMethod(#AuthenticationPrincipal CustomObject customObject)
Last, a production ready authorities converter here
Related
I would like to access the http request, specifically auth header in AuthenticationManager.authenticate() context.
Requirement is to authenticate a custom token. There is an external library which does that and so I don't have the luxury to read out principal from the token. Hence, in the custom filter, I am returning the full token in the getPreAuthenticatedPrincipal() method. This seems borderline incorrect and I would like to not pass the token pretending it to be principal.
Is there any way I can get it without violating any framework constraints?
Or is there a better way to handle the scenario which I'm trying to achieve?
Here's the config class:
#Configuration
#EnableWebSecurity(debug = true)
#EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
protected void configure(HttpSecurity httpSecurity) throws Exception{
CustomTokenFilter customTokenFilter = new CustomTokenFilter();
customTokenFilter.setAuthenticationManager(new CustomAuthenticationMgr());
httpSecurity
// csrf etc etc
.addFilter(customTokenFilter)
.authorizeRequests()
.mvcMatchers("/users/**")
.authenticated()
.and()
.authorizeRequests()
.mvcMatchers("/other-api/**")
.permitAll()
.and()
.httpBasic();
}
Here's the custom token filter class:
public class CustomTokenFilter extends AbstractPreAuthenticatedProcessingFilter {
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String authorization = request.getHeader("authorization");
if(authorization.indexOf("Custom") == 0){
return Map.of("Custom",authorization.split(" ")[1]);
}
return null;
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return "";
}
}
And finally, the custom authentication manager class:
public class CustomAuthenticationMgr implements AuthenticationManager {
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Map<String,String> map = (Map) authentication.getPrincipal();
String token = map.get("Custom");
// Custom validation - checking length here just to simplify
if(token.length() > 0)
authentication.setAuthenticated(true);
return authentication;
}
}
Version: Spring Boot 2.6.7 (transitive: spring-core 5.3.19)
Constraints: Cannot upgrade to other versions at the moment
Thanks in advance!
You're right, this isn't a good way to do it. (It's great you noticed -- too few people care whether their code is idiomatic.)
A better way would be to start by writing your own filter that actually just... does the authentication. You can extend OncePerRequestFilter rather than something more specific. That's what Spring Security itself does, both for basic authentication (BasicAuthenticationFilter) and for OAuth bearer tokens (BearerTokenAuthenticationFilter). You may want to take a careful look at the code for BearerTokenAuthenticationFilter since the problem it solves is very similar to yours. (I wouldn't extend it, though, since it's very clearly intended to do OAuth specifically. I wouldn't straight up copy the code either -- it's fairly simple as Spring Security filters go but probably still does more than you need. Try to understand the code instead; that will help a lot with your understanding of Spring Security in general.)
Okay, so you have a filter which looks a lot like BearerTokenAuthenticationFilter. That is, it contains an AuthenticationManager and its doFilter method consists of extracting the token from the request, passing that into the AuthenticationManager and then doing some SecurityContext-related stuff. Except, problem: AuthenticationManager.authenticate() expects an Authentication, not a String, and the token is a String.
The solution is to write a wrapper object for your token which implements Authentication. You can do this a couple of ways. Personally, what I'd do is use two classes: one which you pass into AuthenticationManager.authenticate(), and one which you get back. So we have, say, CustomTokenAuthenticationRequest implements Authentication and CustomTokenAuthentication implements Authentication. Both are immutable.
CustomTokenAuthenticationRequest basically just contains the token; its isAuthenticated() is return false, its getPrincipal() returns the token and its getCredentials() also returns the token. This is essentially what Spring Security itself does with BearerTokenAuthenticationToken.
CustomTokenAuthentication, on the other hand, probably contains a UserDetails of some sort; its isAuthenticated() is return true, its getName() is a username or user id or something, etc.
Now you need to teach the AuthenticationManager to authenticate CustomTokenAuthenticationRequests. The way to do this isn't to implement AuthenticationManager, it's to implement AuthenticationProvider. So you write a class that looks roughly like
public class CustomTokenAuthenticationProvider implements AuthenticationProvider {
#Override
public Authentication authenticate(Authentication a) {
String token = ((CustomTokenAuthenticationRequest) a).getToken();
if (/* the token is valid */) {
CustomTokenAuthentication returnValue = // whatever you want it to be
return returnValue;
}
throw new BadCredentialsException("Invalid token");
}
#Override
public boolean supports(Class<?> authClass) {
return authClass == CustomTokenAuthenticationRequest.class;
}
}
Finally, wire it all up. Add the authentication provider to your HttpSecurity using its authenticationProvider() method. (If you do this, and you don't change the default authentication manager configuration, authenticationProvider() results in your authentication provider getting added to an AuthenticationManager which Spring Security configures for you -- an instance of ProviderManager.) Add the filter using addFilterAt(BasicAuthenticationFilter.class). Also, don't call httpBasic() because this adds a BasicAuthenticationFilter which I am guessing you don't want. Or maybe you want basic authentication and also your custom token authentication? But you didn't say that. If you do want both, you'll want to add your filter with addFilterBefore or addFilterAfter, and you need to think about ordering. Generally filter ordering is important in Spring Security.
I glossed over a lot of stuff here, barely gave you any code, and still wrote something of blog post length. Spring Security is very complex, and the thing you're trying to do isn't easily done in an idiomatic manner if you don't have much experience. I highly recommend just reading the Spring Security reference documentation from start to finish before you try implementing any of my suggestions. You'll also need to read quite a lot of Javadoc and tutorials and/or framework code. If there's something specific you want to follow up on I might respond to a comment, but I don't promise it; I had to do some research for this answer and have already spent more time on it than I planned to.
you should look spring-security-lambda-dsl,add filter,add auth provider
I am trying to build a Spring Boot project with requires being signed into an OAuth2 SSO.
I have the following Maven dependencies:
org.springframework.boot:spring-boot-starter-web
org.springframework.security:spring-security-oauth2-client
org.springframework.security:spring-security-config
I use HttpSecurity to enforce OAuth2 authentication for the app, using the following:
#EnableWebSecurity
#Order(101)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatcher("/api/auth/oauth2/callback").permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login();
}
}
Now, what this does is: it sees that the user is not logged in, redirects them to the SSO, and after they have signed in it redirects the user back to the /api/auth/oauth2/callback?code=...&state=... endpoint. That all works fine. However, I am fairly new to Spring Boot and I don't understand how I persist the fact the user is now authenticated (I know I still need to validate the callback, that's not a problem).
Here is the authentication model that I would like to implement: I want to generate a hash within the callback endpoint, and store that hash in-memory within the app, and as a cookie on the user's browser. Then, in any subsequent requests, the app would read that cookie's value, find the row in the in-memory database with the hash in it and grab the corresponding user data from the database row.
I have looked extensively for a good example of this, however, all of the Spring Boot based OAuth2 examples use Github/Google OAuth and it seems to handle a lot of stuff under the hood (or perhaps I'm not understanding those properly).
Any help/guidance would be greatly appreciated!
In case it helps, here is my application.yml file:
spring:
security:
oauth2:
client:
registration:
custom_sso_name:
clientId: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
clientSecret: SUPER_SECRET
authorizationGrantType: authorization_code
redirectUri: https://dev.localhost/api/auth/oauth2/callback
provider:
custom_sso_name:
userInfoUri: https://sso.example.com/nidp/oauth/nam/userinfo
authorizationUri: https://sso.example.com/nidp/oauth/nam/authz
tokenUri: https://sso.example.com/nidp/oauth/nam/token
preferTokenInfo: false
You can check the Authentication Provider.
When you have set this, you can autowire the custom
AuthenticationProvider and login a user in your controller whith SecurityContext like this:
SecurityContextHolder.getContext().setAuthentication(authManager.authenticate(new UserInfo(yourHash, token, expirationDate)));
Here, UserInfo is an example class (which extends AbstractAuthenticationToken) that can hold the hash that you want to save, as any other data you may need.
In the example of the above link they use UsernamePasswordAuthenticationToken, which may me enough to you if you only want to store a hash. If you want to store extra info I would sugest to use a custom AuthenticationToken as it is UserInfo:
public class UserInfo extends AbstractAuthenticationToken {
private String hash;
private String oAuthToken;
private DateTime expirationTime;
public UserInfo (String hash, String oAuthToken, DateTime expirationTime){
super(Collections.emptyList());
this.hash= hash;
this.oAuthToken = oAuthToken;
this.expirationTime = expirationTime;
}
#Override
public String getCredentials() {
if(expirationTime.isAfter(DateTime.now())){
return oAuthToken;
} else {
return null;
}
}
#Override
public String getPrincipal() {
return hash; //Or anything you stored that may be useful for checking the authentication
}
Then, in any subsequent requests, the app would read that cookie's
value, find the row in the in-memory database with the hash in it and
grab the corresponding user data from the database row.
After this authentication is finished, you can customly check authentication in any request:
#GetMapping("/")
public String profileSettings(Principal principal) {
if (principal instanceof UserInfo){
String hash = ((UserInfo) principal).getPrincipal();
//Now you can use the hash for your custom logic, such like database reading
return "profileSettings";
} else {
return "login";
}
}
Let's say that I have a few #RestController classes in my app.
I am getting one specific parameter from the OAuth2Authentication context. A sample request is looking like this:
#RequestMapping("/contractor")
public Contractor contractor(OAuth2Authentication authentication) {
String email = String.valueOf(((LinkedHashMap<String, Object>) authentication.getUserAuthentication().getDetails()).get("email"));
return contractorRepository.findByEmail(email);
}
It is working as a charm, but there is one problem. There is a bunch of endpoints basing on the OAuth2Authentication object, precisely - on the email string from
String.valueOf(((LinkedHashMap<String, Object>) authentication.getUserAuthentication().getDetails()).get("email"));
I don't understand the Spring Security nor OAuth2 too well, but is there a way to somehow autowire this OAuth2Authentication and hold the String email as a field in the Rest controllers, or hold it in a separate class and inject it into the controllers?
I don't know if it works with OAuth2Authentication but you can give it a try.
You can use the SecurityContextHolder of spring-security to get the Authentication object like this:
#RequestMapping("/contractor")
public Contractor contractor() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
...
}
Spring Security 5.1.0.M2 (release notes) added support for automatic refreshing of tokens when using WebClient. However, I am using RestTemplate. Is there a similar mechanism for RestTemplate or do I need to implement that behavior myself?
The OAuth2RestTemplate class looks promising but it's from the separate Spring Security OAuth module and I would like to use plain Spring Security 5.1 on the client if possible.
OAuth2RestTemplate Will refresh tokens automatically. RestTemplate will not (refresh tokens is part of the OAut2 spec, hence the OAuth2RestTemplate.
You have 2 options:
Use Spring Security OAuth2 module and everything will work pretty much out of the box (configuration properties provided by Spring)
Create your own RestTemplate based on Spring's OAut2RestTemplate
Spring's OAuth2 module will be integrated into Spring Security in the future.
I would go for option 1.
OAuth2RestTemplate should be used instead of RestTemplate when JWT authentication is required. You can set AccessTokenProvider to it, which will tell how the JWT token will be retrieved: oAuth2RestTemplate.setAccessTokenProvider(new MyAccessTokenProvider());
In class implementing AccessTokenProvider you need to implement obtainAccessToken and refreshAccessToken methods. So in obtainAccessToken method it can be checked if token is expired, and if it is - token is retrieved through refreshAccessToken. Sample implementation (without the details of actual token retrieval and refreshing):
public class MyAccessTokenProvider implements AccessTokenProvider {
#Override
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails details, AccessTokenRequest parameters)
throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException {
if (parameters.getExistingToken() != null && parameters.getExistingToken().isExpired()) {
return refreshAccessToken(details, parameters.getExistingToken().getRefreshToken(), parameters);
}
OAuth2AccessToken retrievedAccessToken = null;
//TODO access token retrieval
return retrievedAccessToken;
}
#Override
public boolean supportsResource(OAuth2ProtectedResourceDetails resource) {
return false;
}
#Override
public OAuth2AccessToken refreshAccessToken(OAuth2ProtectedResourceDetails resource,
OAuth2RefreshToken refreshToken, AccessTokenRequest request)
throws UserRedirectRequiredException {
OAuth2AccessToken refreshedAccessToken = null;
//TODO refresh access token
return refreshedAccessToken;
}
#Override
public boolean supportsRefresh(OAuth2ProtectedResourceDetails resource) {
return true;
}
}
Did not find a way for Spring to call the refreshAccessToken automatically, if someone knows how to do that - please share.
Is it possible to use OAuth2 for certain endpoints in my rest application and use basic authentication too for some other endpoints.
It should all work on spring security version 2.0.1.RELEASE. I hope someone can help me further.
Yes, it's possible to use a basic authentication as well as an OAuth2 authentication intertwined, but I doubt you'll be able to set it up easily as HttpSecurity's authenticated() method doesn't allow you to pick which of your authentication method (oauth2Login/formLogin) will work.
However, there's a way to easily bypass that:
You could add a custom authority, let's call it ROLE_BASICAUTH, when an user connects using basic auth, and ROLE_OAUTH2 when an user connects using OAuth2. That way, you can use
.antMatchers("/endpoint-that-requires-basic-auth").hasRole("BASICAUTH")
.antMatchers("/endpoint-that-requires-oauth2").hasRole("OAUTH2")
.anyRequest().authenticated()
When they reach an endpoint that you want basic authentication (and not OAuth2), you check their current authorities, and if it's not BASICAUTH, then you invalidate their session you display a login form without OAuth2 (to force them to use the basic authentication).
The downside to doing that is that you'd need to implement both a custom UserDetailsService as well as a custom OAuth2UserService...
But that's actually not that hard:
#Service
public class UserService extends DefaultOAuth2UserService implements UserDetailsService {
// ...
#Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
OAuth2User user = super.loadUser(oAuth2UserRequest);
Map<String, Object> attributes = user.getAttributes();
Set<GrantedAuthority> authoritySet = new HashSet<>(user.getAuthorities());
String userNameAttributeName = oAuth2UserRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
authoritySet.add(new SimpleGrantedAuthority("ROLE_OAUTH2"));
return new DefaultOAuth2User(authoritySet, attributes, userNameAttributeName);
}
#Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = getUserFromDatabase(username); // you'll need to provide that method (where are the username/password stored?)
if (user == null) { // UserDetailsService doesn't allow loadUserByUsername to return null, so throw exception
throw new UsernameNotFoundException("Couldn't find user with username '"+username+"'");
}
// add ROLE_BASICAUTH (you might need a custom UserDetails implementation here, because by defaut, UserDetails.getAuthorities() is immutable (I think, I might be a liar)
return user;
}
}
Note that this is a rough implementation, so you'll have to work it out a bit on your end as well.
You can also use this repository I made https://github.com/TwinProduction/spring-security-oauth2-client-example/tree/master/custom-userservice-sample as a guideline for the custom OAuth2UserService
Good luck.