I am struggling to do this simple thing in Spring: Redirecting to the Login page when the access token expires.
I have:
a Edge Server (Zuul) for routing.
a OAuth2 authorization / authentication server.
a Resource Server that serves static files.
For specific security reasons I do not want any refresh token, when my TOKEN expires I want the user to login again.
In my understanding the Resource Server should be the one handling this mechanism (correct me if I am wrong).
I have been trying different unsuccessful approaches:
Adding a filter After OAuth2AuthenticationProcessingFilter and check for Token validity and sendRedirect
Adding a custom OAuth2AuthenticationEntryPoint to my application (extending ResourceServerConfigurerAdapter)
Adding .exceptionHandling() with a custom handler / entry point.
Below my security configurations.
Auth Server Security Config:
#Configuration
#EnableAuthorizationServer
public class AuthorizationServerOAuthConfig extends AuthorizationServerConfigurerAdapter {
....
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(this.authenticationManager)
.accessTokenConverter(accessTokenConverter())
.approvalStore(approvalStore())
.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore());
}
/**
* Configure the /check_token endpoint.
* This end point will be accessible for the resource servers to verify the token validity
* #param securityConfigurer
* #throws Exception
*/
#Override
public void configure(AuthorizationServerSecurityConfigurer securityConfigurer) throws Exception {
securityConfigurer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
}
}
#Configuration
#Order(-20)
public class AuthorizationServerSecurityConfig extends WebSecurityConfigurerAdapter {
private static final Logger log = LoggerFactory.getLogger(AuthorizationServerSecurityConfig.class);
#Autowired
private DataSource oauthDataSource;
#Autowired
private AuthenticationFailureHandler eventAuthenticationFailureHandler;
#Autowired
private UserDetailsService userDetailsService;
#Autowired
public void globalUserDetails(AuthenticationManagerBuilder auth) throws UserDetailsException {
try {
log.debug("Updating AuthenticationManagerBuilder to use userDetailService with a BCryptPasswordEncoder");
auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder());
} catch (Exception e) {
throw new UserDetailsException(e);
}
}
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
public UserDetailsService userDetailsService() {
return this.userDetailsService;
}
#Override
protected void configure(final HttpSecurity http) throws Exception {
log.debug("Updating HttpSecurity configuration");
// #formatter:off
http
.requestMatchers()
.antMatchers("/login*", "/login?error=true", "/oauth/authorize", "/oauth/confirm_access")
.and()
.authorizeRequests()
.antMatchers("/login*", "/oauth/authorize", "/oauth/confirm_access").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.failureUrl("/login?error=true")
.failureHandler(eventAuthenticationFailureHandler);
// #formatter:on
}
Its application.yml
server:
port: 9999
contextPath: /uaa
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
security:
user:
password: password
spring:
datasource_oauth:
driverClassName: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/oauth2_server
username: abc
password: 123
jpa:
ddl-create: true
timeleaf:
cache: false
prefix: classpath:/templates
logging:
level:
org.springframework.security: DEBUG
authentication:
attempts:
maximum: 3
Resource Server Security Config - UPDATED:
#EnableResourceServer
#EnableEurekaClient
#SpringBootApplication
public class Application extends ResourceServerConfigurerAdapter {
private CustomAuthenticator customFilter = new CustomAuthenticator();
/**
* Launching Spring Boot
* #param args
*/
public static void main(String[] args) {
SpringApplication.run(Application.class, args); //NOSONAR
}
/**
* Configuring Token converter
* #return
*/
#Bean
public AccessTokenConverter accessTokenConverter() {
return new DefaultAccessTokenConverter();
}
#Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.authenticationManager(customFilter);
}
/**
* Configuring HTTP Security
* #param http
* #throws Exception
*/
#Override
public void configure(HttpSecurity http) throws Exception {
// #formatter:off
http
.authorizeRequests().antMatchers("/**").authenticated()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.addFilterBefore(customFilter, AbstractPreAuthenticatedProcessingFilter.class);
// #formatter:on
}
protected static class CustomAuthenticator extends OAuth2AuthenticationManager implements Filter {
private static Logger logger = LoggerFactory.getLogger(CustomAuthenticator.class);
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
private AuthenticationManager authenticationManager;
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();
private boolean inError = false;
#Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
try {
return super.authenticate(authentication);
}
catch (Exception e) {
inError = true;
return new CustomAuthentication(authentication.getPrincipal(), authentication.getCredentials());
}
}
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
logger.debug("Token Error redirecting to Login page");
if(this.inError) {
logger.debug("In error");
RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
final HttpServletRequest req = (HttpServletRequest) request;
final HttpServletResponse res = (HttpServletResponse) response;
redirectStrategy.sendRedirect(req, res, "http://localhost:8765/login");
this.inError = false;
return;
} else {
filterChain.doFilter(request, response);
}
}
#Override
public void destroy() {
}
#Override
public void init(FilterConfig arg0) throws ServletException {
}
#SuppressWarnings("serial")
protected static class CustomAuthentication extends PreAuthenticatedAuthenticationToken {
public CustomAuthentication(Object principal, Object credentials) {
super(principal, credentials);
}
}
}
}
Its application.yml:
server:
port: 0
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
security:
oauth2:
resource:
userInfoUri: http://localhost:9999/uaa/user
logging:
level:
org.springframework.security: DEBUG
Edge Server Configuration:
#SpringBootApplication
#EnableEurekaClient
#EnableZuulProxy
#EnableOAuth2Sso
public class EdgeServerApplication extends WebSecurityConfigurerAdapter {
/**
* Configuring Spring security
* #return
*/
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.logout()
.and()
.authorizeRequests().antMatchers("/**").authenticated()
.and()
.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// #formatter:on
}
/**
* Launching Spring Boot
* #param args
*/
public static void main(String[] args) {
SpringApplication.run(EdgeServerApplication.class, args); //NOSONAR
}
}
Its application.yml config:
server:
port: 8765
eureka:
client:
serviceUrl:
defaultZone: http://localhost:8761/eureka/
security:
user:
password: none
oauth2:
client:
accessTokenUri: http://localhost:9999/uaa/oauth/token
userAuthorizationUri: http://localhost:9999/uaa/oauth/authorize
clientId: spa
clientSecret: spasecret
resource:
userInfoUri: http://localhost:9999/uaa/user
zuul:
debug:
request: true
routes:
authorization-server:
path: /uaa/**
stripPrefix: false
fast-funds-service:
path: /**
logging:
level:
org.springframework.security: DEBUG
Thanks for the help
Try setting the validity of the refresh token to 0 or 1 seconds.
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("javadeveloperzone")
.secret("secret")
.accessTokenValiditySeconds(2000) // expire time for access token
.refreshTokenValiditySeconds(-1) // expire time for refresh token
.scopes("read", "write") // scope related to resource server
.authorizedGrantTypes("password", "refresh_token"); // grant type
https://javadeveloperzone.com/spring-security/spring-security-oauth2-success-or-failed-event-listener/#22_SecurityOAuth2Configuration
Based on your comment you can add this:
http.exceptionHandling()
.authenticationEntryPoint(unauthorizedEntryPoint())
#Bean
public AuthenticationEntryPoint unauthorizedEntryPoint() {
return (request, response, authException) -> response.response.sendRedirect("/login"));
}
Related
I want to point my Organization service to the Authentication server.
When I am trying to call the following request:
GET http://localhost:8082/v1/organizations/{{organizationId}}
I am receiving the following WARNING
o.s.b.a.s.o.r.UserInfoTokenServices : Could not fetch user details: class org.springframework.web.client.ResourceAccessException, I/O error on GET request for "http://localhost:8901/auth/user": Connection refused (Connection refused); nested exception is java.net.ConnectException: Connection refused (Connection refused)
And I am also receiving the following response in POSTMAN
{
"error": "invalid_token",
"error_description": "6afd2822-b23d-4421-9902-423f0934d385"
}
However, when I am accesing GET http://localhost:8901/auth/user directly through Postman, without accesing it via my Organization service, the request works fine.
I am using Spring Cloud Hoxton SR11
My Authentication server has the following configuration:
#SpringBootApplication
#RestController
#EnableResourceServer
#EnableAuthorizationServer
public class AuthenticationServiceApplication {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthenticationServiceApplication.class);
#RequestMapping(value = { "/user" }, produces = "application/json")
public Map<String, Object> user(OAuth2Authentication user) {
LOGGER.debug("Request to get user info");
Map<String, Object> userInfo = new HashMap<>();
userInfo.put("user", user.getUserAuthentication().getPrincipal());
userInfo.put("authorities", AuthorityUtils.authorityListToSet(user.getUserAuthentication().getAuthorities()));
return userInfo;
}
public static void main(String[] args) {
SpringApplication.run(AuthenticationServiceApplication.class, args);
}
}
application.yml
eureka:
instance:
preferIpAddress: true
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
server:
servlet:
context-path: /auth
#Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
#Autowired
private AuthenticationManager authenticationManager;
#Autowired
private UserDetailsService userDetailsService;
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("eagleeye")
.secret("{noop}thisissecret")
.authorizedGrantTypes("refresh_token", "password", "client_credentials")
.scopes("webclient", "mobileclient");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
}
}
#Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
#Override
#Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
#Bean
#Primary
public UserDetailsService userDetailsServiceBean() throws Exception {
return super.userDetailsServiceBean();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("john.carnell").password("{noop}password1").roles("USER")
.and()
.withUser("william.woodward").password("{noop}password2").roles("USER", "ADMIN");
}
}
My Organization service has the following configuration
#SpringBootApplication
#EnableEurekaClient
#EnableCircuitBreaker
#RefreshScope
#EnableResourceServer
public class OrganizationServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrganizationServiceApplication.class, args);
}
}
#Configuration
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception{
http
.authorizeRequests()
.antMatchers(HttpMethod.DELETE, "/v1/organizations/**")
.hasRole("ADMIN")
.anyRequest()
.authenticated();
}
}
application.yml
eureka:
instance:
preferIpAddress: true
client:
registerWithEureka: true
fetchRegistry: true
serviceUrl:
defaultZone: http://localhost:8761/eureka/
security:
oauth2:
resource:
userInfoUri: http://localhost:8901/auth/user
Thank you!
I am trying to implement Single Sign On with Spring Security OAuth2 and JWT.
I use two separate applications:
An Authorization Server – which is the central authentication mechanism
Client Application: the applications using SSO
When a user tries to access a secured page in the client app, they’ll be redirected to authenticate first, via the Authentication Server.
And I am using the Authorization Code grant type out of OAuth2 to drive the delegation of authentication.
Authorization server:
#Configuration
#EnableAuthorizationServer
public class OAuth2AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
public static final Logger LOGGER = LoggerFactory.getLogger(AuthorizationServerConfigurerAdapter.class);
#Autowired
private AuthenticationManager authenticationManager;
#Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("abcd");
return converter;
}
#Bean
#Primary
public DefaultTokenServices tokenServices() {
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenStore(tokenStore());
defaultTokenServices.setSupportRefreshToken(true);
defaultTokenServices.setTokenEnhancer(accessTokenConverter());
return defaultTokenServices;
}
#Override
public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
clientDetailsServiceConfigurer
.inMemory()
.withClient("webapp")
.secret("Pass")
.authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code")
.scopes("user_info")
.autoApprove(true);
}
#Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("permitAll()")
.checkTokenAccess("isAuthenticated()");
}
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager);
}
}
Security Configuration on Authorization Server
#Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
#Value("${ldap.url}")
private String ldapUrl;
#Value("${ldap.userDnPatterns}")
private String ldapUserDnPatterns;
#Autowired
private PersonService personService;
#Autowired
private RoleService roleService;
#Override
protected void configure(HttpSecurity http) throws Exception { // #formatter:off
http.requestMatchers()
.antMatchers("/login", "/oauth/authorize")
.and()
.authorizeRequests()
.anyRequest()
.authenticated()
.and()
.formLogin()
.permitAll();
} // #formatter:on
#Bean(name = "authenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(this.ldapAndDatabaseAuthenticationProvider());
}
#Bean(name="ldapAuthenticationProvider")
public AuthenticationProvider ldapAndDatabaseAuthenticationProvider(){
LdapUserDetailsMapper userDetailsMapper = new LdapUserDetailsMapper();
userDetailsMapper.setRoleAttributes(new String[]{"groupMembership"});
LdapAndDatabaseAuthenticationProvider provider =
new LdapAndDatabaseAuthenticationProvider(
this.ldapAuthenticator(),
this.ldapAuthoritiesPopulator(),
this.personService);
provider.setUserDetailsContextMapper(userDetailsMapper);
return provider;
}
#Bean( name = "ldapAuthoritiesPopulator" )
public LdapAndDatabaseAuthoritiesPopulator ldapAuthoritiesPopulator(){
return new LdapAndDatabaseAuthoritiesPopulator(this.contextSource(), "", roleService);
}
#Bean( name = "ldapAuthenticator" )
public LdapAuthenticator ldapAuthenticator() {
BindAuthenticator authenticator = new BindAuthenticator( this.contextSource() );
authenticator.setUserDnPatterns(new String[]{"cn={0},ou=prod,o=TEMP"});
return authenticator;
}
#Bean( name = "contextSource" )
public DefaultSpringSecurityContextSource contextSource() {
DefaultSpringSecurityContextSource contextSource =
new DefaultSpringSecurityContextSource( ldapUrl );
return contextSource;
}
}
application.properties:
server.port=8888
server.context-path=/auth
security.basic.enabled=false
When I login the client application, It correctly forwards to Authorization Server for Single Sign On.
I enter the user credentials. User successfully get authenticated, but then I see the below error on browser:
OAuth Error
error="invalid_grant", error_description="A redirect_uri can only be
used by implicit or authorization_code grant types."
URL Shows:
http://localhost:8888/auth/oauth/authorize?client_id=webapp&redirect_uri=http://localhost:8080/jwt/webapp&response_type=code&state=LGvAzj
I also see the below at the log:
02:14:43.610 [http-nio-8888-exec-6] DEBUG o.s.s.o.p.e.FrameworkEndpointHandlerMapping/getHandlerInternal Looking up handler method for path /oauth/authorize
02:14:43.614 [http-nio-8888-exec-6] DEBUG o.s.s.o.p.e.FrameworkEndpointHandlerMapping/getHandlerInternal Returning handler method [public org.springframework.web.servlet.ModelAndView org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.authorize(java.util.Map<java.lang.String, java.lang.Object>,java.util.Map<java.lang.String, java.lang.Str ing>,org.springframework.web.bind.support.SessionStatus,java.security.Principal)]
02:14:43.849 [http-nio-8888-exec-6] INFO o.s.s.o.p.e.AuthorizationEndpoint/handleOAuth2Exception Handling OAuth2 error: error="invalid_grant", error_description="A redirect_uri can only be used by implicit or authorization_code grant types."
Can you please help me to find the problem?
UPDATE
Actually, Dur is right. This configuration is correct and works fine. I had another configuration file which configures JdbcClientDetails and it was overwriting the clientDetailsService created with inmemory in this configuration.
I’ve used Jhipster to generate an app with the security option OAuth 2.0 / OIDC Authentication. I reconfigured said app to use Okta instead of keycloak following the instructions at http://www.jhipster.tech/security/#okta. All works as expected and the login flow performs as expected.
I now want to use OAuth 2.0 access_tokens to access my api resources from additional clients (Postman, Wordpress). I’ve retrieved a valid token from Okta added it to my Postman get request for localhost:8080/api/events and get a 401 in response.
The logs (https://pastebin.com/raw/R3D0GHHX) show that the spring security oauth2 doesn’t seem to be triggered by the presence of the Authorization bearer token.
Does Jhipster with OAuth 2.0 / OIDC Authentication support
access_token in the Authorization bearer header or url param out of
the box?
If not can you suggest what additional configurations I should make?
OAuth2Configuration.java
#Configuration
#Profile("dev")
public class OAuth2Configuration {
public static final String SAVED_LOGIN_ORIGIN_URI = OAuth2Configuration.class.getName() + "_SAVED_ORIGIN";
private final Logger log = LoggerFactory.getLogger(OAuth2Configuration.class);
#Bean
public FilterRegistrationBean saveLoginOriginFilter() {
Filter filter = new OncePerRequestFilter() {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
if (request.getRemoteUser() == null && request.getRequestURI().endsWith("/login")) {
String referrer = request.getHeader("referer");
if (!StringUtils.isBlank(referrer) &&
request.getSession().getAttribute(SAVED_LOGIN_ORIGIN_URI) == null) {
log.debug("Saving login origin URI: {}", referrer);
request.getSession().setAttribute(SAVED_LOGIN_ORIGIN_URI, referrer);
}
}
filterChain.doFilter(request, response);
}
};
FilterRegistrationBean bean = new FilterRegistrationBean(filter);
bean.setOrder(Ordered.HIGHEST_PRECEDENCE);
return bean;
}
#Bean
public static DefaultRolesPrefixPostProcessor defaultRolesPrefixPostProcessor() {
return new DefaultRolesPrefixPostProcessor();
}
public static class DefaultRolesPrefixPostProcessor implements BeanPostProcessor, PriorityOrdered {
#Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
if (bean instanceof FilterChainProxy) {
FilterChainProxy chains = (FilterChainProxy) bean;
for (SecurityFilterChain chain : chains.getFilterChains()) {
for (Filter filter : chain.getFilters()) {
if (filter instanceof OAuth2ClientAuthenticationProcessingFilter) {
OAuth2ClientAuthenticationProcessingFilter oAuth2ClientAuthenticationProcessingFilter =
(OAuth2ClientAuthenticationProcessingFilter) filter;
oAuth2ClientAuthenticationProcessingFilter
.setAuthenticationSuccessHandler(new OAuth2AuthenticationSuccessHandler());
}
}
}
}
return bean;
}
#Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
#Override
public int getOrder() {
return PriorityOrdered.HIGHEST_PRECEDENCE;
}
}
}
SecurityConfiguration.java
#Configuration
#Import(SecurityProblemSupport.class)
#EnableOAuth2Sso
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
private final CorsFilter corsFilter;
private final SecurityProblemSupport problemSupport;
public SecurityConfiguration(CorsFilter corsFilter, SecurityProblemSupport problemSupport) {
this.corsFilter = corsFilter;
this.problemSupport = problemSupport;
}
#Bean
public AjaxLogoutSuccessHandler ajaxLogoutSuccessHandler() {
return new AjaxLogoutSuccessHandler();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring()
.antMatchers(HttpMethod.OPTIONS, "/**")
.antMatchers("/app/**/*.{js,html}")
.antMatchers("/i18n/**")
.antMatchers("/content/**")
.antMatchers("/swagger-ui/index.html")
.antMatchers("/test/**")
.antMatchers("/h2-console/**");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.addFilterBefore(corsFilter, CsrfFilter.class)
.exceptionHandling()
.authenticationEntryPoint(problemSupport)
.accessDeniedHandler(problemSupport)
.and()
.logout()
.logoutUrl("/api/logout")
.logoutSuccessHandler(ajaxLogoutSuccessHandler())
.permitAll()
.and()
.headers()
.frameOptions()
.disable()
.and()
.authorizeRequests()
.antMatchers("/api/profile-info").permitAll()
.antMatchers("/api/**").authenticated()
.antMatchers("/websocket/tracker").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/websocket/**").permitAll()
.antMatchers("/management/health").permitAll()
.antMatchers("/management/**").hasAuthority(AuthoritiesConstants.ADMIN)
.antMatchers("/v2/api-docs/**").permitAll()
.antMatchers("/swagger-resources/configuration/ui").permitAll()
.antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesConstants.ADMIN);
}
#Bean
public SecurityEvaluationContextExtension securityEvaluationContextExtension() {
return new SecurityEvaluationContextExtension();
}
}
application.yml
security:
basic:
enabled: false
oauth2:
client:
access-token-uri: https://dev-800787.oktapreview.com/oauth2/ausb3ecnmsz8Ucjqw0h7/v1/token
user-authorization-uri: https://dev-800787.oktapreview.com/oauth2/ausb3ecnmsz8Ucjqw0h7/v1/authorize
client-id: <okta-client-id>
client-secret: <okta-client-secret>
client-authentication-scheme: form
scope: openid profile email
resource:
filter-order: 3
user-info-uri: https://dev-800787.oktapreview.com/oauth2/ausb3ecnmsz8Ucjqw0h7/v1/userinfo
token-info-uri: https://dev-800787.oktapreview.com/oauth2/ausb3ecnmsz8Ucjqw0h7/v1/introspect
prefer-token-info: false
server:
session:
cookie:
http-only: true
Matt's answer point me to the right direction, thanks!
and here is my current working configuration:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.web.util.matcher.RequestHeaderRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
#Configuration
#EnableResourceServer
#EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class OAuth2AuthenticationConfiguration extends ResourceServerConfigurerAdapter {
#Bean
public RequestMatcher resources() {
return new RequestHeaderRequestMatcher("Authorization");
}
#Override
public void configure(HttpSecurity http) throws Exception {
http
.requestMatcher(resources())
.authorizeRequests()
.anyRequest().authenticated();
}
}
This answer was helpful too, thanks.
You need to use Spring Security OAuth's #EnableResourceServer for this functionality. If you're using Okta, you can also try using its Spring Boot Starter.
I have an HTTP API, protected with Spring Security and JWT.
I get a 401 when I'm trying to access a protected resource.
I get the resource if I'm authenticated (JWT is valid) and I have the correct role. The resource is protected with #PreAuthorize("hasRole('USER')").
The issue I have is that when I don't have the correct role I'd like to return a 403 (in the following code it is a 401 for the sake of testing).
But right know I get a 500 because of the AccessDeniedException which is thrown when the role is incorrect.
The weird thing is that it goes to my JwtAccessDeniedHandler custom code but the response is already committed (isCommitted() == true) so whenever I try to set the status etc it does nothing.
Do you have any ideas about what could be misconfigured or missing?
Config:
#Slf4j
#EnableWebSecurity
#EnableGlobalMethodSecurity(
prePostEnabled = true,
securedEnabled = true,
jsr250Enabled = true
)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private ObjectMapper objectMapper;
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.addFilterBefore(
jwtAuthenticationFilter(joseHelper(jsonWebKey())),
UsernamePasswordAuthenticationFilter.class)
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(new JwtAuthenticationEntryPoint())
.accessDeniedHandler(new JwtAccessDeniedHandler());
}
#Bean
public JwtAuthenticationFilter jwtAuthenticationFilter(JoseHelper joseHelper) {
return new JwtAuthenticationFilter(joseHelper);
}
#Bean
public JoseHelper joseHelper(PublicJsonWebKey key) {
return new JoseHelper(key);
}
#Bean
public PublicJsonWebKey jsonWebKey() throws IOException, JoseException {
return RsaJwkGenerator.generateJwk(2048);
}
private void sendUnauthorized(HttpServletResponse httpServletResponse) throws IOException {
httpServletResponse.setContentType("application/json");
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ApiError apiError = ApiError.builder()
.code(HttpStatus.UNAUTHORIZED.name())
.message(HttpStatus.UNAUTHORIZED.getReasonPhrase())
.httpStatus(HttpStatus.UNAUTHORIZED)
.build();
httpServletResponse.getWriter().print(objectMapper.writeValueAsString(apiError));
}
private class JwtAccessDeniedHandler implements AccessDeniedHandler {
#Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
log.info("accessDeniedHandler", e);
sendUnauthorized(httpServletResponse);
}
}
private class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
#Override
public void commence(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
AuthenticationException e) throws IOException, ServletException {
sendUnauthorized(httpServletResponse);
}
}
}
Filter:
#Slf4j
#Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private static final String BEARER = "Bearer ";
private JoseHelper joseHelper;
#Autowired
public JwtAuthenticationFilter(JoseHelper joseHelper) {
this.joseHelper = joseHelper;
}
#Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String header = httpServletRequest.getHeader("Authorization");
if (header == null || !header.startsWith(BEARER)) {
log.error("JWT token is not valid");
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
final String encryptedToken = header.substring(BEARER.length());
try {
final String decryptedJwt = joseHelper.decryptJwt(encryptedToken);
final String verifiedJwt = joseHelper.verifyJwt(decryptedJwt);
final JwtClaims jwtClaims = joseHelper.parse(verifiedJwt);
List<SimpleGrantedAuthority> authorities = jwtClaims.getStringListClaimValue("userRoles")
.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
JwtAuthenticationToken jwtAuthenticationToken = new JwtAuthenticationToken(jwtClaims, null, authorities);
SecurityContextHolder.getContext().setAuthentication(jwtAuthenticationToken);
filterChain.doFilter(httpServletRequest, httpServletResponse);
} catch (JoseException | InvalidJwtException | MalformedClaimException e) {
log.error("JWT token is not valid", e);
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
}
}
The issue was because I use Jersey apparently. I didn't really had time to investigate the why right now.
Once I registered an exception mapper in my JerseyConfig I was able to capture and handle the AccessDeniedException correctly.
And from that point the access denied handler is not called anymore and becomes useless.
A bit weird, but there is probably a good reason.
I'm in the process of building separate resource server and authhorization server. For now i'm using the user-info-uri in resource server to extract the Principal from the authorization server matching the access-token, with config:
spring:
oauth2:
resource:
userInfoUri: http://localhost:9999/uaa/user
In the resource server I have protected endpoints, based on role, as follows:
http
.authorizeRequests()
.antMatchers("/invoices/**").hasRole("END_USER")
.anyRequest().authenticated();
When I manually access the user-info-uri, I can see that the the authorities contain:
"authority": "ROLE_END_USER"
But when I try to access the /invoices resource I get an Access-Denied exception, and in the log I see:
OAuth2Authentication#bc5074a8: Principal: my-login; Credentials: [PROTECTED]; Authenticated: true; Details: remoteAddress=0:0:0:0:0:0:0:1, tokenType=BearertokenValue=<TOKEN>; Granted Authorities: ROLE_USER
Authoriteis = "ROLE_USER". Where does that come from, should'nt it be "ROLE_END_USER" at this point also?
I've seen implementations using a shared database for the storage of tokens, is that really necessary for what I want to achive?
Resource Server:
#SpringBootApplication
#EnableOAuth2Resource
public class EndUserResourceServiceApplication extends ResourceServerConfigurerAdapter {
#Override
public void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/invoices/**").hasRole("END_USER")
.anyRequest().authenticated();
}
public static void main(String[] args) {
SpringApplication.run(EndUserResourceServiceApplication.class, args);
}
}
Auth Server:
#SpringBootApplication
#RestController
#EnableResourceServer
public class ApiAuthServerApplication extends ResourceServerConfigurerAdapter {
#Configuration
#EnableWebSecurity
#Order(-10)
protected static class LoginConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService userDetailsService;
#Bean(name = "authenticationManagerBean")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
#Override
#Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin().permitAll()
.and()
.requestMatchers().antMatchers("/login", "/oauth/authorize", "/oauth/confirm_access")
.and()
.authorizeRequests().anyRequest().authenticated();
}
#Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
#Configuration
#EnableAuthorizationServer
protected static class OAuth2Config extends AuthorizationServerConfigurerAdapter {
#Autowired
private TokenStore tokenStore;
#Autowired
#Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
#Autowired
private CustomUserDetailsService userDetailsService;
#Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(this.authenticationManager)
.userDetailsService(this.userDetailsService)
.tokenStore(this.tokenStore);
}
#Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("my-client")
.secret("our_s3cret")
.authorities("ROLE_CLIENT")
.authorizedGrantTypes("implicit", "password", "refresh_token")
.redirectUris("http://anywhere")
.scopes("read")
.autoApprove(true);
}
#Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
public static void main(String[] args) {
SpringApplication.run(ApiAuthServerApplication.class, args);
}
#RequestMapping("/user")
public Principal user(Principal user) {
return user;
}
}
Summary:
Can I use user-info-uri for validating access-tokens and to use "hasRole"
Is it necessary to use shared database for token storage when using separate resource server and authorization server?
I ended up using a shared token store, which works well at the moment