The standard JSON format that an authorization server usually gives you, has a property named "expires_in", but now I'm working with an autorization server that gives me a property named "access_token_expires_in". Because of this, my OAuth2AccessToken always returns isExpired to false even when then access_token is expired, and that makes sens because it's trying to read the "expires_in" property that dose not exist. The getAdditionalInformation from OAuth2AccessToken returns my "access_token_expires_in" property value with 18000.
I was wondering if I can tell spring to use the "access_token_expires_in" property as an expiration value for my access_token?
My code:
#Configuration
class OAuth2RestConfiguration {
#Bean
protected OAuth2ProtectedResourceDetails resource() {
final ClientCredentialsResourceDetails resourceDetails = new ClientCredentialsResourceDetails();
resourceDetails.setAccessTokenUri("<tokenUri>");
resourceDetails.setClientId("<clientId>");
resourceDetails.setClientSecret("<clientSecret>");
return resourceDetails;
}
#Bean
public OAuth2RestTemplate restTemplate() throws Exception {
final AccessTokenRequest atr = new DefaultAccessTokenRequest();
final OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(resource(),
new DefaultOAuth2ClientContext(atr));
return oAuth2RestTemplate;
}
}
Authorization server response sample:
{
"refresh_token_expires_in": 0,
"access_token": "<access_token>",
"access_token_expires_in": 18000,
"token_type": "bearer"
}
EDIT 1:
As a workaround, I've extended the OAuth2RestTemplate class and override the getAccessToken method:
public class CustomOAuth2RestTemplate extends OAuth2RestTemplate {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomOAuth2RestTemplate.class);
private OAuth2ClientContext context;
private Long LAST_RESET = getCurrentTimeSeconds();
private Long FORCE_EXPIRATION;
public CustomOAuth2RestTemplate(OAuth2ProtectedResourceDetails resource) {
super(resource);
this.context = super.getOAuth2ClientContext();
this.FORCE_EXPIRATION = 10800L;
}
public CustomOAuth2RestTemplate(OAuth2ProtectedResourceDetails resource,
DefaultOAuth2ClientContext defaultOAuth2ClientContext, Long forceExpiration) {
super(resource, defaultOAuth2ClientContext);
this.context = defaultOAuth2ClientContext;
this.FORCE_EXPIRATION = Objects.requireNonNull(forceExpiration, "Please set expiration!");
}
#Override
public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException {
OAuth2AccessToken accessToken = context.getAccessToken();
final Long diff = getCurrentTimeSeconds() - LAST_RESET;
/*
Either use a hardcoded variable or use the value stored in
context.getAccessToken().getAdditionalInformation().
*/
if (diff > FORCE_EXPIRATION) {
LOGGER.info("Access token has expired! Generating new one...");
this.LAST_RESET = getCurrentTimeSeconds();
context.setAccessToken(null);
accessToken = acquireAccessToken(context);
} else {
accessToken = super.getAccessToken();
}
LOGGER.info("Access token: " + context.getAccessToken().getValue());
return accessToken;
}
private Long getCurrentTimeSeconds() {
return System.currentTimeMillis() / 1000L;
}
}
And now the bean:
#Bean
public OAuth2RestTemplate restTemplate() throws Exception {
final AccessTokenRequest atr = new DefaultAccessTokenRequest();
final OAuth2RestTemplate oAuth2RestTemplate = new CustomOAuth2RestTemplate(resource(),
new DefaultOAuth2ClientContext(atr), 10800L); //example: 3h
oAuth2RestTemplate.setRequestFactory(customRequestFactory());
return oAuth2RestTemplate;
}
EDIT 2:
After I analyzed the OAuth2RestTemplate class more thoroughly, code refactoring was required:
public class CustomOAuth2RestTemplate extends OAuth2RestTemplate {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomOAuth2RestTemplate.class);
private Long LAST_RESET = getCurrentTimeSeconds();
private Long FORCE_EXPIRATION;
public CustomOAuth2RestTemplate(OAuth2ProtectedResourceDetails resource) {
super(resource);
this.FORCE_EXPIRATION = 10800L; //3h
}
public CustomOAuth2RestTemplate(OAuth2ProtectedResourceDetails resource,
DefaultOAuth2ClientContext defaultOAuth2ClientContext, Long forceExpiration) {
super(resource, defaultOAuth2ClientContext);
this.FORCE_EXPIRATION = Objects.requireNonNull(forceExpiration, "Please set expiration!");
}
#Override
public OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException {
final Long diff = getCurrentTimeSeconds() - LAST_RESET;
/*
Either use a hardcoded variable or use the value stored in
context.getAccessToken().getAdditionalInformation().
*/
if (diff > FORCE_EXPIRATION) {
LOGGER.info("Access token has expired! Generating new one...");
this.LAST_RESET = getCurrentTimeSeconds();
final OAuth2ClientContext oAuth2ClientContext = getOAuth2ClientContext();
oAuth2ClientContext.setAccessToken(null);
return acquireAccessToken(oAuth2ClientContext);
}
return super.getAccessToken();
}
private Long getCurrentTimeSeconds() {
return System.currentTimeMillis() / 1000L;
}
}
You can add custom parameter by implementing TokenEnhancer interface and overriding its method as follows:
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
public class CustomTokenEnhancer implements TokenEnhancer {
#Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
final Map<String, Object> additionalInfo = new HashMap<>();
// additionalInfo.put("CUSTOM_PARAM1", "CUSTOM_VALUE1");
additionalInfo.put("username", authentication.getPrincipal());//adding username param
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
#Configuration
#EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends
AuthorizationServerConfigurerAdapter {
#Override
public void configure(final AuthorizationServerEndpointsConfigurer
endpoints) throws Exception {
final TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer()));
endpoints.tokenStore(tokenStore)
.tokenEnhancer(tokenEnhancerChain).authenticationManager(authenticationManager);
}
#Bean
public TokenEnhancer tokenEnhancer() {
return new CustomTokenEnhancer();
}
Hope it helps!
I'll post this as an answer since it works just fine. The only thing that was added was synchronized for concurrency, so that multiple access tokens should never be requested.
Final code:
public class CustomOAuth2RestTemplate extends OAuth2RestTemplate {
private static final Logger LOGGER = LoggerFactory.getLogger(CustomOAuth2RestTemplate.class);
private Long LAST_RESET = getCurrentTimeSeconds();
private Long FORCE_EXPIRATION;
public CustomOAuth2RestTemplate(OAuth2ProtectedResourceDetails resource) {
super(resource);
this.FORCE_EXPIRATION = 10800L; // 3h
}
public CustomOAuth2RestTemplate(OAuth2ProtectedResourceDetails resource,
DefaultOAuth2ClientContext defaultOAuth2ClientContext, Long forceExpiration) {
super(resource, defaultOAuth2ClientContext);
this.FORCE_EXPIRATION = Objects.requireNonNull(forceExpiration, "Please set expiration!");
}
#Override
public synchronized OAuth2AccessToken getAccessToken() throws UserRedirectRequiredException {
final Long diff = getCurrentTimeSeconds() - LAST_RESET;
/*
* Either use a hardcoded variable or use the value stored in
* context.getAccessToken().getAdditionalInformation().
*/
if (diff > FORCE_EXPIRATION) {
LOGGER.info("Access token has expired! Generating new one...");
this.LAST_RESET = getCurrentTimeSeconds();
final OAuth2ClientContext oAuth2ClientContext = getOAuth2ClientContext();
oAuth2ClientContext.setAccessToken(null);
return acquireAccessToken(oAuth2ClientContext);
}
return super.getAccessToken();
}
private Long getCurrentTimeSeconds() {
return System.currentTimeMillis() / 1000L;
}
}
Related
I have a spring service:
#Service
public class AuthorizationServiceImpl implements AuthorizationService {
#Value("${local.address}")
private String localIP;
private final String LOCALHOST_IPV4 = "127.0.0.1";
private final String LOCALHOST_IPV6 = "0:0:0:0:0:0:0:1";
#CrossOrigin(origins = "*")
#Override
public boolean isAuthorized(String token, HttpServletRequest request) {
try {
TokenUserModel user = new TokenUserModel();
user.setSessionID("");
user = getTokenUserModel(token);
IServiceAAA client = new ServiceAAA().getBasicHttpBindingIServiceAAA();
int systemId = client.getUserSystemId("InvoiceAdmin");
String xRealIP = request.getRemoteAddr();
String ipAddress = xRealIP.equals(LOCALHOST_IPV4) || xRealIP.equals(LOCALHOST_IPV6) ? localIP : xRealIP;
String userAgent = request.getHeader("User-Agent");
int response = client.checkPermissionAndUserData(user.getSessionID(), "Admin",
userAgent,
ipAddress, systemId, systemId, "");
return response == 0;
} catch (Exception ex) {
throw new AuthorizationException(AuthorizationError.ERR_WHILE_AUTHORISATION);
}
}
}
#Value("${local.address}")
private String localIP; - this line of code didn't working, not populating local.address field from application.properties:
local.address=10.10.16.13
Update:
I have 3 application properties file for spring profiles, named:
application.properties,application-dev.properties,application-prod.properties,
my current active profile is application-dev.properties
('spring.profiles.active=dev' - it's inside of my
application.properties file).
I'm sure that 'local.address'
property is defined in application-dev.properties
local.address=10.10.16.13
AuthorizationService is a just interface that AutheroziationServiceImpl impliments.
public interface AuthorizationService {
boolean isAuthorized(String token, HttpServletRequest request) throws UnsupportedEncodingException;
void logout(String token) throws UnsupportedEncodingException;
TokenUserModel getTokenUserModel(String token) throws UnsupportedEncodingException;
}
I'm using it in constructor of one of controllers:
#Autowired
public AuthorizationController(AuthorizationService authorizationService) {
this.authorizationService = authorizationService;
}
private final AuthorizationService authorizationService;
Yes, it's spring managed bean annotated as #Service
I have problem with #DeleteMapping.
Situation is like below.
If I request to /v1/cache/{cacheEntry} with method DELETE,
It respond with 404, but body was empty. no message, no spring default json 404 response message.
If i request to /v1/cache/{cacheEntry} with method POST,
It respond with 405 and body was below. (This action is correct, not a bug.)
If I change #DeleteMapping to #PostMapping, and request /v1/cache/{cacheEntry} with method POST, It respond success with code 200.
{
"timestamp": 1643348039913,
"status": 405,
"error": "Method Not Allowed",
"message": "",
"path": "/v1/cache/{cacheEntry}"
}
// Controller
#Slf4j
#RestController
#RequestMapping("/v1/cache")
#RequiredArgsConstructor
public class CacheController {
private final CacheService cacheService;
#PostMapping("/{cacheEntry}")
public CacheClearResponse clearCacheEntry(#PathVariable("cacheEntry") CacheChannels cacheEntry) {
try {
log.info("Cache entry :: " + cacheEntry);
cacheService.evictCacheEntry(cacheEntry);
return CacheClearResponse.builder()
.result(
RequestResult.builder()
.code(9200)
.message("SUCCESS")
.build()
)
.common(
Common.builder().build()
)
.date(LocalDateTime.now())
.build();
} catch (Exception e) {
e.printStackTrace();
StringWriter sw = new StringWriter();
e.printStackTrace(new PrintWriter(sw));
return CacheClearResponse.builder()
.result(
RequestResult.builder()
.code(9999)
.message(sw.toString())
.build()
)
.common(
Common.builder().build()
)
.date(LocalDateTime.now())
.build();
}
}
}
}
// CacheService
#Service
#RequiredArgsConstructor
public class CacheService {
private final CacheManager cacheManager;
public void evictCacheEntry(CacheChannels cacheEntry) {
Cache cache = cacheManager.getCache(cacheEntry.getCacheName());
if (cache != null) {
cache.clear();
}
}
public void evictCache(CacheChannels cacheEntry, String cacheKey) {
Cache cache = cacheManager.getCache(cacheEntry.getCacheName());
if (cache != null) {
cache.evict(cacheKey);
}
}
}
// Enum
#Getter
#AllArgsConstructor
public enum CacheChannels {
CACHE_TEN_MIN(Names.CACHE_TEN_MIN, Duration.ofMinutes(10)),
CACHE_HALF_HR(Names.CACHE_HALF_HR, Duration.ofMinutes(30)),
CACHE_ONE_HR(Names.CACHE_ONE_HR, Duration.ofHours(1)),
CACHE_THREE_HR(Names.CACHE_THREE_HR, Duration.ofHours(3)),
CACHE_SIX_HR(Names.CACHE_SIX_HR, Duration.ofHours(6)),
CACHE_ONE_DAY(Names.CACHE_ONE_DAY, Duration.ofDays(1));
private final String cacheName;
private final Duration cacheTTL;
public static CacheChannels from(String value) {
return Arrays.stream(values())
.filter(cacheChannel -> cacheChannel.cacheName.equalsIgnoreCase(value))
.findAny()
.orElse(null);
}
public static class Names {
public static final String CACHE_TEN_MIN = "cache10Minutes";
public static final String CACHE_HALF_HR = "cache30Minutes";
public static final String CACHE_ONE_HR = "cache1Hour";
public static final String CACHE_THREE_HR = "cache3Hours";
public static final String CACHE_SIX_HR = "cache6Hours";
public static final String CACHE_ONE_DAY = "cache1Day";
}
}
// Converter
#Slf4j
public class StringToCacheChannelConverter implements Converter<String, CacheChannels> {
#Override
public CacheChannels convert(String source) {
log.info("Convert Target: " + source);
return CacheChannels.from(source);
}
}
// Security Config
#Configuration
#EnableWebSecurity
#Order(1)
public class APISecurityConfig extends WebSecurityConfigurerAdapter {
#Value("${spring.security.auth-token-header-name:Authorization}")
private String apiKeyHeader;
#Value("${spring.security.secret}")
private String privateApiKey;
#Override
protected void configure(HttpSecurity http) throws Exception {
APIKeyAuthFilter filter = new APIKeyAuthFilter(apiKeyHeader);
filter.setAuthenticationManager(new AuthenticationManager() {
#Override
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
String requestedApiKey = (String) authentication.getPrincipal();
if (!privateApiKey.equals(requestedApiKey)) {
throw new BadCredentialsException("The API Key was not found or not the expected value");
}
authentication.setAuthenticated(true);
return authentication;
}
});
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilter(filter)
.authorizeRequests()
.antMatchers("/v1/cache/**")
.authenticated();
}
}
// Filter
#Slf4j
public class APIKeyAuthFilter extends AbstractPreAuthenticatedProcessingFilter {
private String apiKeyHeader;
public APIKeyAuthFilter(String apiKeyHeader) {
this.apiKeyHeader = apiKeyHeader;
}
#Override
protected Object getPreAuthenticatedPrincipal(HttpServletRequest httpServletRequest) {
log.info("Check authenticated.");
return httpServletRequest.getHeader(apiKeyHeader);
}
#Override
protected Object getPreAuthenticatedCredentials(HttpServletRequest httpServletRequest) {
return "N/A";
}
}
// Web Config
#Configuration
public class WebConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.addConverter(new StringToCacheChannelConverter());
}
#Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new HiddenHttpMethodFilter();
}
}
This can be expected the controller was loaded, endpoint was mapped.
I tried change #DeleteMapping to #PostMapping and it was successfully respond against to POST request.
What am I missing?
I found reason why received 404 without any messages.
My tomcat is on remote server. It configured with security-constraint and disable DELETE method for all enpoints.
I just comment out it and It work properly with delete method.
I'm updating an old application to use WebFlux but I've gotten a bit lost when it comes to handling JWT validation with Spring Security.
The existing code (which works with standard Spring Web) looks like:
(Validating a Firebase Token)
public class FirebaseAuthenticationTokenFilter extends AbstractAuthenticationProcessingFilter {
private static final String TOKEN_HEADER = "X-Firebase-Auth";
public FirebaseAuthenticationTokenFilter() {
super("/v1/**");
}
#Override
public Authentication attemptAuthentication(
final HttpServletRequest request, final HttpServletResponse response) {
for (final Enumeration<?> e = request.getHeaderNames(); e.hasMoreElements(); ) {
final String nextHeaderName = (String) e.nextElement();
final String headerValue = request.getHeader(nextHeaderName);
}
final String authToken = request.getHeader(TOKEN_HEADER);
if (Strings.isNullOrEmpty(authToken)) {
throw new RuntimeException("Invaild auth token");
}
return getAuthenticationManager().authenticate(new FirebaseAuthenticationToken(authToken));
}
However when switching to WebFlux we lose HttpServletRequest and HttpServletResponse. There is a GitHub issue which suggests there is an alternative method/fix https://github.com/spring-projects/spring-security/issues/5328 however following it through I'm not able to identify what was actually changed to make this work.
The Spring Security docs while great, don't really explain how to handle the use-case.
Any tips on how to proceed?
Got there in the end:
First need to update the filter chain with a custom filter just like before
#Configuration
public class SecurityConfig {
private final FirebaseAuth firebaseAuth;
public SecurityConfig(final FirebaseAuth firebaseAuth) {
this.firebaseAuth = firebaseAuth;
}
#Bean
public SecurityWebFilterChain springSecurityFilterChain(final ServerHttpSecurity http) {
http.authorizeExchange()
.and()
.authorizeExchange()
.pathMatchers("/v1/**")
.authenticated()
.and()
.addFilterAt(firebaseAuthenticationFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
.csrf()
.disable();
return http.build();
}
private AuthenticationWebFilter firebaseAuthenticationFilter() {
final AuthenticationWebFilter webFilter =
new AuthenticationWebFilter(new BearerTokenReactiveAuthenticationManager());
webFilter.setServerAuthenticationConverter(new FirebaseAuthenticationConverter(firebaseAuth));
webFilter.setRequiresAuthenticationMatcher(ServerWebExchangeMatchers.pathMatchers("/v1/**"));
return webFilter;
}
}
The main workhorse of the process is FirebaseAuthenticationConverter where I validate the incoming JWT against Firebase, and perform some standard logic against it.
#Slf4j
#Component
#RequiredArgsConstructor
public class FirebaseAuthenticationConverter implements ServerAuthenticationConverter {
private static final String BEARER = "Bearer ";
private static final Predicate<String> matchBearerLength =
authValue -> authValue.length() > BEARER.length();
private static final Function<String, Mono<String>> isolateBearerValue =
authValue -> Mono.justOrEmpty(authValue.substring(BEARER.length()));
private final FirebaseAuth firebaseAuth;
private Mono<FirebaseToken> verifyToken(final String unverifiedToken) {
try {
final ApiFuture<FirebaseToken> task = firebaseAuth.verifyIdTokenAsync(unverifiedToken);
return Mono.justOrEmpty(task.get());
} catch (final Exception e) {
throw new SessionAuthenticationException(e.getMessage());
}
}
private Mono<FirebaseUserDetails> buildUserDetails(final FirebaseToken firebaseToken) {
return Mono.just(
FirebaseUserDetails.builder()
.email(firebaseToken.getEmail())
.picture(firebaseToken.getPicture())
.userId(firebaseToken.getUid())
.username(firebaseToken.getName())
.build());
}
private Mono<Authentication> create(final FirebaseUserDetails userDetails) {
return Mono.justOrEmpty(
new UsernamePasswordAuthenticationToken(
userDetails.getEmail(), null, userDetails.getAuthorities()));
}
#Override
public Mono<Authentication> convert(final ServerWebExchange exchange) {
return Mono.justOrEmpty(exchange)
.flatMap(AuthorizationHeaderPayload::extract)
.filter(matchBearerLength)
.flatMap(isolateBearerValue)
.flatMap(this::verifyToken)
.flatMap(this::buildUserDetails)
.flatMap(this::create);
}
}
To the previous answer there could be added that this method also works fine:
private Mono<FirebaseToken> verifyToken(final String unverifiedToken) {
try {
return Mono.just(FirebaseAuth.getInstance().verifyIdToken(unverifiedToken));
} catch (final Exception e) {
throw new SessionAuthenticationException(e.getMessage());
}
}
And this one does not provid warnings regarding unnecessary use of blocking methods (like get())
I want spring-vault configuration marked with VaultPropertySource to be able to retry the requests to the vault if they fail.
What should i mark as retryable ? I'm using Spring-Retry and i was looking over http://www.baeldung.com/spring-retry .
There is no visible method to mark as retryable. Should I change the implementation of the vaultTemplate and mark the vaultOperations as retryable ?
ProvisioningSecrets.java
#Configuration
#VaultPropertySource(
value="secret/provisioning",
propertyNamePrefix = "provisioning.",
renewal = Renewal.RENEW
)
#EnableRetry
#Lazy
#Profile("!test")
public class ProvisioningSecrets {
private static final Logger logger = LoggerFactory.getLogger(ProvisioningSecrets.class);
#Autowired
public void setPassword(#Value("${provisioning.password}") final String password) throws Exception {
logger.info("We successfully set the provisioning db password.");
EnvVars.changeSetting(Setting.PROVISIONING_PASS, password);
}
#Autowired
public void setHost(#Value("${provisioning.host}") final String host) throws Exception {
logger.info("We successfully set the provisioning db host.");
EnvVars.changeSetting(Setting.PROVISIONING_HOST, host);
}
#Autowired
public void setPort(#Value("${provisioning.port}") final int port) throws Exception {
logger.info("We successfully set the provisioning db port.");
EnvVars.changeSetting(Setting.PROVISIONING_PORT, Integer.toString(port));
}
#Autowired
public void setUsername(#Value("${provisioning.username}") final String username) throws Exception {
logger.info("We successfully set the provisioning db username.");
EnvVars.changeSetting(Setting.PROVISIONING_USER, username);
}
#Autowired
public void setDbName(#Value("${provisioning.name}") final String name) throws Exception {
logger.info("We successfully set the provisioning db name.");
EnvVars.changeSetting(Setting.PROVISIONING_DB_NAME, name);
}
}
VaultConfiguration.java
#Configuration
#Profile("!test")
public class VaultConfiguration extends AbstractVaultConfiguration {
private static final Logger logger = LoggerFactory.getLogger(VaultConfiguration.class);
private URI vaultHost;
private String vaultToken;
/**
* Configure the Client Authentication.
*
* #return A configured ClientAuthentication Object.
* #see ClientAuthentication
*/
#Override
public ClientAuthentication clientAuthentication() {
// testing out environment variable value injection
logger.debug("Vault Token configuration done.");
return new TokenAuthentication(vaultToken);
}
#Override
#Bean
#DependsOn("vaultToken")
public SessionManager sessionManager() {
return super.sessionManager();
}
#Override
public SslConfiguration sslConfiguration() {
logger.info("Configuring Vault SSL with NONE.");
return SslConfiguration.NONE;
}
/**
* Specify an endpoint for connecting to Vault.
*
* #return A configured VaultEndpoint.
* #see VaultEndpoint
*/
#Override
public VaultEndpoint vaultEndpoint() {
logger.debug("Vault Host:" + vaultHost.toString());
if (vaultHost.toString().isEmpty()) {
logger.info("Creating default Vault Endpoint.");
return new VaultEndpoint();
}
logger.info("Creating Vault Endpoint based on address: " + vaultHost.toString());
final VaultEndpoint endpoint = VaultEndpoint.from(vaultHost);
logger.info("Created Vault Endpoint: " + endpoint.toString());
return endpoint;
}
#Bean("vaultHost")
public URI vaultHost(#Value("${spring.vault.host}") final URI vaultHost) {
this.vaultHost = vaultHost;
return vaultHost;
}
#Override
#Bean
#DependsOn("vaultHost")
public VaultTemplate vaultTemplate() {
return super.vaultTemplate();
}
#Bean("vaultToken")
public String vaultToken(#Value("${spring.vault.token}") final String vaultToken) {
this.vaultToken = vaultToken;
return vaultToken;
}
}
How about creating a custom VaultTemplate bean class using RetryTemplate?
public class RetryableVaultTemplate extends VaultTemplate {
private final RetryTemplate retryTemplate;
public RetryableVaultTemplate(VaultEndpointProvider endpointProvider,
ClientHttpRequestFactory clientHttpRequestFactory,
SessionManager sessionManager, RetryTemplate retryTemplate) {
super(endpointProvider, clientHttpRequestFactory, sessionManager);
this.retryTemplate = retryTemplate;
}
#Override
public VaultResponse read(final String path) {
return retryTemplate
.execute(new RetryCallback<VaultResponse, RuntimeException>() {
#Override
public VaultResponse doWithRetry(RetryContext context) {
System.out.println("doWithRetry");
return RetryableVaultTemplate.super.read(path);
}
});
}
#Override
public <T> VaultResponseSupport<T> read(final String path, final Class<T> responseType) {
return retryTemplate
.execute(new RetryCallback<VaultResponseSupport<T>, RuntimeException>() {
#Override
public VaultResponseSupport<T> doWithRetry(RetryContext context) {
return RetryableVaultTemplate.super.read(path, responseType);
}
});
}
}
Make sure to register this bean class as vaultTemplate bean instead of VaultTemplate.
I have a server that is just an API endpoint, no client front-end, no jsp, no html. It uses Spring Boot and I'm trying to secure it with Shiro. The relevent parts of my SpringBootServletInitializer look like this. I'm trying to get Shiro to return a 403 response if it fails the roles lookup as defined in BasicRealm. Yet it seems to default to redirecting to a non-existent login.jsp and no matter what solution I seem to use. I can't override that. Any help would be greatly appreciated.
#SpringBootApplication
public class RestApplication extends SpringBootServletInitializer {
...
#Bean(name = "shiroFilter")
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
Map<String, String> filterChain = new HashMap<>();
filterChain.put("/admin/**", "roles[admin]");
shiroFilter.setFilterChainDefinitionMap(filterChain);
shiroFilter.setSecurityManager(securityManager());
return shiroFilter;
}
#Bean
public org.apache.shiro.mgt.SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
CookieRememberMeManager rmm = new CookieRememberMeManager();
rmm.setCipherKey(Base64.decode("XXXXXXXXXXXXXXXXXXXXXX"));
securityManager.setRememberMeManager(rmm);
return securityManager;
}
#Bean(name = "userRealm")
#DependsOn("lifecycleBeanPostProcessor")
public BasicRealm userRealm() {
return new BasicRealm();
}
#Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
}
public class BasicRealm extends AuthorizingRealm {
private static Logger logger = UserService.logger;
private static final String REALM_NAME = "BASIC";
public BasicRealm() {
super();
}
#Override
protected AuthenticationInfo doGetAuthenticationInfo(final AuthenticationToken token)
throws AuthenticationException {
UsernamePasswordToken upToken = (UsernamePasswordToken) token;
String userid = upToken.getUsername();
User user = Global.INST.getUserService().getUserById(userid);
if (user == null) {
throw new UnknownAccountException("No account found for user [" + userid + "]");
}
return new SimpleAuthenticationInfo(userid, user.getHashedPass().toCharArray(), REALM_NAME);
}
#Override
protected AuthorizationInfo doGetAuthorizationInfo(final PrincipalCollection principals) {
String userid = (String) principals.getPrimaryPrincipal();
if (userid == null) {
return new SimpleAuthorizationInfo();
}
return new SimpleAuthorizationInfo(Global.INST.getUserService().getRoles(userid));
}
}
OK, here is how I solved it. I created a class ...
public class AuthFilter extends RolesAuthorizationFilter {
private static final String MESSAGE = "Access denied.";
#Override
protected boolean onAccessDenied(final ServletRequest request, final ServletResponse response) throws IOException {
HttpServletResponse httpResponse ;
try {
httpResponse = WebUtils.toHttp(response);
}
catch (ClassCastException ex) {
// Not a HTTP Servlet operation
return super.onAccessDenied(request, response) ;
}
if (MESSAGE == null) {
httpResponse.sendError(403);
} else {
httpResponse.sendError(403, MESSAGE);
}
return false; // No further processing.
}
}
... and then in my shiroFilter() method above I added this code ...
Map<String, Filter> filters = new HashMap<>();
filters.put("roles", new AuthFilter());
shiroFilter.setFilters(filters);
... hope this helps someone else.
In Shiro 1.4+ you can set the login url in your application.properties:
https://github.com/apache/shiro/blob/master/samples/spring-boot-web/src/main/resources/application.properties#L20
Earlier versions you should be able to set ShiroFilterFactoryBean.setLoginUrl("/login")
https://shiro.apache.org/static/current/apidocs/org/apache/shiro/spring/web/ShiroFilterFactoryBean.html