I'm currently using Spring Security's OAuth2 to implement authorization across a number of micro services. Our AuthService performs all of the authentication with OAuth2 tokens etc and can create users.
Consider two clients: Client A and Client B.
Client A has authorities: CREATE_USER, CREATE_POST
Client B has authorities: READ_USER
(Yes we could use a scope instead, but this is just an example!)
Aim:
Only Client A, which has the authority CREATE_USER, should be allowed to create a user. Users are creating by posting to /users.
Problem:
The problem is that when I send a POST request to the /users endpoint with the basic authentication header for Client A, the CREATE_USER authority is not found because the request hit the AnonymousAuthenticationFilter and the only authority found is ROLE_ANONYMOUS and I receive the following:
10:38:34.852 [http-nio-9999-exec-1] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Secure object: FilterInvocation: URL: /users; Attributes: [#oauth2.throwOnError(#oauth2.hasAuthority('CREATE_USER))]
10:38:34.852 [http-nio-9999-exec-1] DEBUG o.s.s.w.a.i.FilterSecurityInterceptor - Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken#9055c2bc: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
10:38:34.854 [http-nio-9999-exec-1] DEBUG o.s.s.access.vote.AffirmativeBased - Voter: org.springframework.security.web.access.expression.WebExpressionVoter#a63e3e8, returned: -1
10:38:34.856 [http-nio-9999-exec-1] DEBUG o.s.s.w.a.ExceptionTranslationFilter - Access is denied (user is anonymous); redirecting to authentication entry point
org.springframework.security.access.AccessDeniedException: Access is denied
One incredibly hacky solution would be to register a custom security filter that reads the basic auth header and verifies that a client's name is equal to Client A, but this will not work for a third client, Client C which also has the CREATE_VIEWER authority, as the name and not the authorities are verified here.
// UsersController.kt
#PostMapping("/users")
#ResponseStatus(HttpStatus.OK)
#ResponseBody
fun createUser(): String {
return "Created user!"
}
Client Configuration
override fun configure(clients: ClientDetailsServiceConfigurer?) {
clients!!.inMemory()
.withClient("ClientA")
.scopes("all")
.authorities("CREATE_USER", "CREATE_POST")
.authorizedGrantTypes("refresh_token", "password")
.and()
.withClient("ClientB")
.scopes("all")
.authorities("READ_USER")
.authorizedGrantTypes("refresh_token", "password")
}
WebSecurityConfigurerAdaptor impl
override fun configure(http: HttpSecurity) {
http.requestMatchers().antMatchers("/oauth/authorize", "/oauth/confirm_access")
.and()
.authorizeRequests()
.antMatchers("/users").access("hasAuthority('CREATE_USER')")
.anyRequest().authenticated()
.and()
.csrf().disable()
}
override fun configure(auth: AuthenticationManagerBuilder) {
auth.authenticationProvider(authenticationProvider())
}
#Bean
open fun authenticationProvider(): DaoAuthenticationProvider {
val authProvider = DaoAuthenticationProvider()
authProvider.setUserDetailsService(userCredentialService)
authProvider.setPasswordEncoder(passwordEncoderService)
return authProvider
}
Related
This question already has an answer here:
can't get hasIpAddress working on Spring Security
(1 answer)
Closed 2 months ago.
I have two different means of security.
is locked down with an IP check, and only an IP check, I do not care about further authentication.
is a role check where I want to authenticate with a resource server and validate the token.
With the code I have, the role check is working as expected. I get a 200, 401, and 403 correctly. This issue is when I try to hit the endpoint that does the IP check, I get an error of
Full authentication is required to access this resourceunauthorized
I only want to perform an IP address check.
LOGS
Checking match of request : '/locked'; against '\/locked'
Secure object: FilterInvocation: URL: /locked; Attributes: [#oauth2.throwOnError(hasIpAddress('192.168.216.0/23') or hasIpAddress('10.4.7.59'))]
Previously Authenticated: org.springframework.security.authentication.AnonymousAuthenticationToken#c36755eb: Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
Voter: org.springframework.security.web.access.expression.WebExpressionVoter#40adb6a3, returned: -1
Access is denied (user is anonymous); redirecting to authentication entry point
Code
(Spring Boot 2.2.2)
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
#Configuration
#EnableResourceServer
#ConditionalOnProperty(prefix = "whitelist", value = "enabled", havingValue = "true", matchIfMissing = false)
public class EnabledSecurityConfig extends ResourceServerConfigurerAdapter {
private static final String ESIGN_PURGE_BATCH = "ESIGN_PURGE_BATCH";
#Override
public void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable();
http
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http
.csrf().disable();
http
.authorizeRequests()
.regexMatchers(HttpMethod.GET, "\\/locked").access("hasIpAddress('192.168.216.0/23') or hasIpAddress('10.4.7.59')")
.antMatchers(HttpMethod.POST, "/purgePackets").hasRole(ESIGN_PURGE_BATCH)
.anyRequest().permitAll();
}
}
implementation("org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.2.2.RELEASE")
then in my properties I set (But this works for the role check as mentioned)
security.oauth2.resource.jwk.key-set-uri=https://ourOauthserver.com/uaa/oauth/token_keys
The answer in my case was the wrong IP address for testing. You do not want to use the IP that the provider assigns, instead the local IP that I should be using, which was
0:0:0:0:0:0:0:1
Spring does NOT provide a log that says IPs failed to match, and so when it said "Full Authentication is needed" I just ASSUMED it was doing more than I wanted and getting passed the IP.
I've a Spring Boot side-project that uses JWTs to authorize users for hitting the end points: /users/** based on Authority in my Web Security Config as .mvcMatchers("/users/**").hasAuthority("USER")
I'm using Postman to test and following these steps:
(1) login using a REST Controller which responses with an access token (works fine as user is authenticated)
(2) with the access token, I chose Bearer Token in the Authorization in Postman and paste it there
(3) But I get 403 Forbidden in Postman
My question:
Why am I getting Access is Denied when the user has the authority USER in my database.
Edit
When I change .mvcMatchers("/users/**").hasAuthority("USER") to
.mvcMatchers("/users/**").authenticated() it works fine but I still want to use hasAuthority('USER')
I tried the following so far:
changed to 'ROLE_USER' in my db for the user
changed hasRole('User') in my Web Security Config
Stack trace
2022-10-25 08:24:37.190 TRACE 17964 --- [nio-8080-exec-5] o.s.s.w.a.i.FilterSecurityInterceptor : Authorizing filter invocation [GET /users/profile/] with attributes [hasAuthority('USER')]
2022-10-25 08:24:37.193 TRACE 17964 --- [nio-8080-exec-5] o.s.s.w.a.expression.WebExpressionVoter : Voted to deny authorization
2022-10-25 08:24:37.193 TRACE 17964 --- [nio-8080-exec-5] o.s.s.w.a.i.FilterSecurityInterceptor : Failed to authorize filter invocation [GET /users/profile/] with attributes [hasAuthority('USER')] using AffirmativeBased [DecisionVoters=[org.springframework.security.web.access.expression.WebExpressionVoter#6ce8bf64], AllowIfAllAbstainDecisions=false]
2022-10-25 08:24:37.197 TRACE 17964 --- [nio-8080-exec-5] o.s.s.w.a.ExceptionTranslationFilter : Sending JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt#4afa460a, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=null], Granted Authorities=[SCOPE_USER]] to access denied handler since access is denied
org.springframework.security.access.AccessDeniedException: Access is denied
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:73) ~[spring-security-core-5.7.3.jar:5.7.3]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.attemptAuthorization(AbstractSecurityInterceptor.java:239) ~[spring-security-core-5.7.3.jar:5.7.3]
My Controller
#GetMapping("/users/profile")
public ResponseEntity<?> getUserData(Principal principal) {
User user = userService.findUserByUsername(principal.getName());
List<UserData> userData = userDataService.getAllUserDataForUser(user.getId());
return ResponseEntity.ok(userData);
}
My Web Security Config
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Autowired
private JpaUserDetailsService jpaUserDetailsService;
private final RsaKeyProperties rsaKeys;
public SecurityConfig(RsaKeyProperties rsaKeys) {
this.rsaKeys = rsaKeys;
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.mvcMatchers("/css/**", "/js/**", "/", "/about", "/register", "/test", "/login", "/loginrest").permitAll()
.mvcMatchers("/users/**").hasAuthority("USER")
.anyRequest().authenticated()
.and()
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/users/profile",true)
.and()
.httpBasic(Customizer.withDefaults());
return http.build();
}
#Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withPublicKey(rsaKeys.publicKey()).build();
}
#Bean
JwtEncoder jwtEncoder() {
JWK jwk = new RSAKey.Builder(rsaKeys.publicKey()).privateKey(rsaKeys.privateKey()).build();
JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
return new NimbusJwtEncoder(jwks);
}
#Bean
DaoAuthenticationProvider daoAuthenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setPasswordEncoder(passwordEncoder());
provider.setUserDetailsService(jpaUserDetailsService);
return provider;
}
It doesn't matter if a user has an authority in the database when you're using JWT after login, because authorities (roles) are fetched from the token itself - that's the whole point of using self-contained tokens.
So you should "guide" Spring and your JwtDecoder on how to search authorities in your JWT and convert them to GrantedAuthority to be used in SecurityContextHolder.
For that create and configure a bean of JwtAuthenticationConverter type:
#Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
final JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
// here choose a claim name where you stored authorities on login (defaults to "scope" and "scp" if not used)
grantedAuthoritiesConverter.setAuthoritiesClaimName("roles");
// here choose a scope prefix (defaults to "SCOPE_" if not used)
grantedAuthoritiesConverter.setAuthorityPrefix("");
final JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
return jwtAuthenticationConverter;
}
This configuration will allow JwtDecoder to convert any authority in the "roles" claim of JWT to GrantedAuthority with no prefix.
I have this rest endpoint:
#RestController
#RequestMapping("/data")
public class DataPagesController {
#GetMapping("/page")
public ResponseEntity<?> page() {
List<String> list = new ArrayList<>();
list.add("test 1");
list.add("test 2");
list.add("test 3");
list.add("test 4");
return new ResponseEntity<>(list, HttpStatus.OK);
}
}
I have configured context path prefix into the project using:
server:
port: 8080
servlet:
context-path: /engine
I want to restrict the access using:
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.httpBasic()
// Allow GET for dashboard page
.authorizeRequests().antMatchers(HttpMethod.GET, "/data/page").authenticated()
// Allow all requests by logged in users
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
But I get error:
GET http://localhost:8080/engine/data/page 401
Do you know what is the proper way to configure endpoint permissions for GET request?
One possible idea:
Principal: anonymousUser; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails#b364: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: null; Granted Authorities: ROLE_ANONYMOUS
19:12:37.834 [http-nio-8080-exec-8] DEBUG AffirmativeBased[decide:66] - Voter: org.springframework.security.web.access.expression.WebExpressionVoter#659a59ae, returned: -1
19:12:37.834 [http-nio-8080-exec-8] DEBUG ExceptionTranslationFilter[handleSpringSecurityException:180] - Access is denied (user is anonymous); redirecting to authentication entry point
I need to remove maybe .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
401 means that you are not authorized, so it means you are not passing the token you have configured, or your are not passing as spring it is expecting, or the token you are passing is wrong / expired
Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. The response must include a WWW-Authenticate header field containing a challenge applicable to the requested resource. See Basic access authentication and Digest access authentication.[32] 401 semantically means "unauthorised",[33] the user does not have valid authentication credentials for the target resource.
401 error code from wikipedia
I have an application using a custom Jwt token implementation.
The authentication portion works just fine, with the token being created/validated just fine. My Security configuration looks like this:
#Configuration
#EnableGlobalMethodSecurity(prePostEnabled = true)
#EnableWebSecurity
public class DJWTSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
JwtTokenProvider jwtTokenProvider;
#Override
protected void configure(HttpSecurity http) throws Exception {
http.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/*").authenticated()
.antMatchers("/api/auth/signin").permitAll()
.and()
.apply(new JwtConfigurer(jwtTokenProvider));
}
}
For some reason, security is not being enforced to api requests. For requests sent without bearer header, it seems that Spring considers "anonymous" users as being authenticated. Inspecting the security context:
org.springframework.security.authentication.AnonymousAuthenticationToken#f27f7551:
Principal: anonymousUser;
Credentials: [PROTECTED];
Authenticated: true;
Details: org.springframework.security.web.authentication.WebAuthenticationDetails#b364: RemoteIpAddress: 0:0:0:0:0:0:0:1;
SessionId: null;
Granted Authorities: ROLE_ANONYMOUS
Is this expected behavior that should be fixed by adding a role constraint to the route?
Wildcard was wrong - it should be /api/***
not relevant for the given error, but the order should be the other way around
I have implemented a gateway to be oauth2 client in front of my resources services and ui . Every thing is working good except when a token expire i receive
<oauth>
<error_description>bfc5a9f6-0537-4ab9-91c1-e756501b429d</error_description>
<error>invalid_token</error>
</oauth>
Checking the log i found out Gateway is considering the user as authenticated as the session already exist
2017-06-21 09:17:34.311 DEBUG 32482 --- [nio-8080-exec-6] o.s.s.w.a.i.FilterSecurityInterceptor : Previously Authenticated: org.springframework.security.oauth2.provider.OAuth2Authentication#a80f4caf: Principal: user; Credentials: [PROTECTED]; Authenticated: true; Details: remoteAddress=0:0:0:0:0:0:0:1, sessionId=<SESSION>, tokenType=bearertokenValue=<TOKEN>; Granted Authorities: ROLE_ACTUATOR, ROLE_USER
2017-06-21 09:17:34.311 DEBUG 32482 --- [nio-8080-exec-6] o.s.s.access.vote.AffirmativeBased : Voter: org.springframework.security.web.access.expression.WebExpressionVoter#1aaae9c5, returned: 1
while my Resource service or UI don't
2017-06-21 09:17:34.532 WARN 32484 --- [nio-9001-exec-1] o.s.b.a.s.o.r.UserInfoTokenServices : Could not fetch user details: class org.springframework.security.oauth2.client.resource.UserRedirectRequiredException, A redirect is required to get the users approval
Gateway Configuration
#SpringBootApplication
#EnableDiscoveryClient
#EnableZuulProxy
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
#Configuration
#EnableOAuth2Sso
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.authorizeRequests()
.anyRequest().authenticated();
}
}
security:
oauth2:
client:
accessTokenUri: http://localhost:9191/uaa/oauth/token
userAuthorizationUri: http://localhost:9191/uaa/oauth/authorize
clientId: acme
clientSecret: acmesecret
resource:
user-info-uri: http://localhost:9191/uaa/user
prefer-token-info: false
zuul:
ignored-services: '*'
routes:
authserver: /uaa/**
resource-service: /resource/**
ui:
path: /ui/**
strip-prefix: false
UI Configuration or Any Resource Server
#SpringBootApplication
#EnableDiscoveryClient
#EnableResourceServer
public class UiApplication {
public static void main(String[] args) {
SpringApplication.run(UiApplication.class, args);
}
}
security:
oauth2:
resource:
user-info-uri: http://localhost:9191/uaa/user
server:
port: 9001
context-path: /${spring.application.name}
What i do expect and trying to do is that gateway check if the token is valid and if it is not redirect the user to login page or use the refresh token to update the token ?
After talking to #dave-syer on gitter he told me that we need to declare OAuth2RestOperations inside the gateway as it is not created by default in spring-boot and it is needed to request the refresh token in the OAuth2TokenRelayFilter
So just adding the below fixed every thing
#Bean
public OAuth2RestOperations oAuth2RestOperations(OAuth2ClientContext oauth2ClientContext, OAuth2ProtectedResourceDetails details) {
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(details, oauth2ClientContext);
return oAuth2RestTemplate;
}