I am trying to front or "wrap" a RESTful API service application in front of a backend HTTP service. The backend service maintains authentication state via proprietary sessions. Authentication with the backend server is achieved through a convoluted conversation between the authenticating client and the server. In general, interaction with this server is non-standard and not good enough to expose to our customers as an API. However there is too much legacy to overhaul and re-engineed the entire HTTP interface.
My objective is to abstract the interface to this backend server by fronting this server with a RESTful server that implements OAuth2 authentication via password grant so that 3rd party APIs can interface with the backend server in a standard and well understood way. I have built a simple REST SpringBoot application that can authenticate with the backend. The part I am battling with is figuring out how best to map authenticated tokens/requests to the backend's session IDs.
In essence I am trying to associate a bit of state on each authenticated authorization. As a solution I've thought about implementing my own TokenStore which will maintain the mapping. Alternatively I have thought about implementing a "Custom Token Granter". However I have a strong intuition that there is a better and easier way to achieve my objective.
Additional information:
@EnableAuthorizationServer
and @EnableResourceServer
are in the same application.
Version information:
org.springframework.security.oauth:spring-security-oauth2
2.0.13.RELEASE
org.springframework.security:spring-security-config
4.2.2.RELEASE
org.springframework.boot:spring-boot-starter-security
1.5.3.RELEASE
This is my Resource Server Config:
@Configuration
@Profile(Common.CONST_EXTAPI_PROFILE)
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED).and()
.headers().frameOptions().disable().and()
.authorizeRequests()
.antMatchers(HttpMethod.GET, "/", "/home","/register","/login", "/auth").permitAll()
.antMatchers(HttpMethod.OPTIONS,"/oauth/token").permitAll()
.antMatchers("/extapi/agent", "/extapi/agent/**", "/extapi/account/**", "/extapi/sale/**").authenticated();
}
}
This is my Authentication Server Config:
@Configuration
@Profile(Common.CONST_EXTAPI_PROFILE)
@EnableAuthorizationServer
public class OAuth2AuthorizationConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Order(-1)
public class MyWebSecurity extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers(HttpMethod.OPTIONS, "/oauth/token").permitAll();
}
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "foobar".toCharArray())
.getKeyPair("test");
converter.setKeyPair(keyPair);
return converter;
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception
{
clients.inMemory()
.withClient("acme")
.autoApprove(true)
.secret("acmesecret")
.authorizedGrantTypes("password", "authorization_code", "refresh_token").scopes("openid");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.authenticationManager(authenticationManager)
.accessTokenConverter(jwtAccessTokenConverter());
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer)
throws Exception {
oauthServer.tokenKeyAccess("permitAll()").checkTokenAccess(
"isAuthenticated()");
}
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
And the Custom Authentication Provider
@Component
@Profile(Common.CONST_EXTAPI_PROFILE)
public class CustomAuthenticationProvider implements AuthenticationProvider
{
@Autowired
private LoginSessionData sessionData;
@Autowired
private GuiAuthenticationService authService;
@Autowired
private ContextService contextService;
@Autowired
private RSAEncryptionService rsaService;
@Autowired
private ObjectMapper mapper;
@Override
public Authentication authenticate(Authentication auth) throws AuthenticationException {
final String username = auth.getName();
final String password = auth.getCredentials().toString();
Authentication result = null;
DataWrapper oauthResponse = new DataWrapper();
try
{
BaseResponse resp = authService.authenticate(contextService.getCompanyID(), false, null);
oauthResponse.setAuth(rsaService.getPublicKey());
oauthResponse.setData(mapper.writeValueAsString(resp));
if(resp.getState().equals("REQUIRE_UTF8_USERNAME")){
BaseRequest request = new BaseRequest();
request.setData(username);
request.setCid(Integer.toString(contextService.getCompanyID()));
sessionData.captureData(request);
resp = authService.process(request);
}
if(resp.getState().equals("REQUIRE_RSA_PASSWORD"))
{
BaseRequest request = new BaseRequest();
request.setData(password);
request.setCid(Integer.toString(contextService.getCompanyID()));
resp = authService.process(request);
}
if(resp.getState().equals("AUTHENTICATED"))
{
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(username, password, Collections.emptyList());
result = token;
}
}
catch (Exception e)
{
sessionData.setCurrentState(AuthenticationState.INVALID);
oauthResponse.setError(e.getLocalizedMessage());
oauthResponse.setData(Common.CONST_FATAL_ERROR);
e.printStackTrace();
}
if(result == null)
throw new BadCredentialsException("External system authentication failed");
return result;
}
@Override
public boolean supports(Class<?> auth) {
return auth.equals(UsernamePasswordAuthenticationToken.class);
}
}