How to configure bearer-only = true when Using spring-boot-starter-oauth2-client - spring-boot

I am trying to setup o auth2 authentication in spring cloud gateway for my rest apis using keycloak. keyclcoak redirects my request to login page while passing access token as bearer token. In many places I found solution for this is to set bearer-only = true in keycloak adapter. where to set this while using spring-boot-starter-oauth2-client. I cannot use keycloak-spring-boot-starter to set this in application.yml
Thanks

I had the same question as you. Not having found an answer, I developed a filter with a light keycloak Client that calls the Endpoint instrospect of keycloak to validate the token
the client:
public class KeycloakClient {
private final KeycloakConfigurationProperties kcProperties;
private final WebClient client;
public KeycloakClient(final KeycloakConfigurationProperties keycloakConfigurationProperties) {
this.kcProperties = keycloakConfigurationProperties;
this.client = WebClient.builder()
.baseUrl(keycloakConfigurationProperties.getAuth_server_url())
.filter(logRequest())
.build();
}
public Mono<Boolean> validateBearerToken(String bearerToken) {
//todo: improve error management
return prepareAndRunRequest(bearerToken).retrieve()
.bodyToMono(KeycloakValidationResponse.class)
.flatMap(response -> Mono.just(response.getActive()))
.onErrorResume(WebClientResponseException.class,
ex -> Mono.just(false));
}
private WebClient.RequestHeadersSpec<?> prepareAndRunRequest(String bearerToken) {
return client.post()
.uri(uriBuilder -> uriBuilder.path("/auth/realms/")
.path(kcProperties.getRealm())
.path("/protocol/openid-connect/token/introspect")
.build())
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromFormData("client_id", kcProperties.getResource())
.with("client_secret", kcProperties.getCredentials_secret())
the filter:
public class ValidationTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<ValidationTokenGatewayFilterFactory.Config> {
private final KeycloakClient client;
public ValidationTokenGatewayFilterFactory(KeycloakClient client) {
super(Config.class);
this.client = client;
}
#Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String token = exchange.getRequest()
.getHeaders()
.get(AUTHORIZATION)
.get(0);
token = token.substring(7);
log.trace("-- ValidationToken(): token={}", token);
return client.validateBearerToken(token)
.flatMap(validated -> {
if (validated) {
log.debug("-- ValidationToken(): Token valid");
return chain.filter(exchange);
} else {
log.debug("-- ValidationToken(): Token invalid");
exchange.getResponse()
.setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse()
.setComplete();
}
});
};
}
public static class Config {
}
}
You can find a full sample here: https://gitlab.com/-/snippets/2105967

Related

Spring reactive security

I am trying for reactive security and the unauthenticated calls are not going to auth manager.
#Configuration
#EnableWebFluxSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig{
#Autowired
private WebAuthenticationManager authenticationManager;
#Autowired
private ServerSecurityContextRepository securityContextRepository;
private static final String[] AUTH_WHITELIST = {
"/login/**",
"/logout/**",
"/authorize/**",
"/favicon.ico",
};
#Bean
public SecurityWebFilterChain securitygWebFilterChain(ServerHttpSecurity http) {
return http.exceptionHandling().authenticationEntryPoint((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
});
}).accessDeniedHandler((swe, e) -> {
return Mono.fromRunnable(() -> {
swe.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
});
}).and().csrf().disable()
.formLogin().disable()
.httpBasic().disable()
.authenticationManager(authenticationManager)
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance())
.authorizeExchange().pathMatchers(HttpMethod.OPTIONS).permitAll()
.pathMatchers(AUTH_WHITELIST).permitAll()
.anyExchange().authenticated().and().build();
}
#Bean
public PBKDF2Encoder passwordEncoder() {
return new PBKDF2Encoder();
}
}
WebAuthentication Manager,
#Component
public class WebAuthenticationManager implements ReactiveAuthenticationManager {
#Autowired
private JWTUtil jwtUtil;
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
String authToken = authentication.getCredentials().toString();
String username;
try {
username = jwtUtil.getUsernameFromToken(authToken);
} catch (Exception e) {
username = null;
}
if (username != null && jwtUtil.validateToken(authToken)) {
Claims claims = jwtUtil.getAllClaimsFromToken(authToken);
List<String> rolesMap = claims.get("role", List.class);
List<Role> roles = new ArrayList<>();
for (String rolemap : rolesMap) {
roles.add(Role.valueOf(rolemap));
}
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(
username,
null,
roles.stream().map(authority -> new SimpleGrantedAuthority(authority.name())).collect(Collectors.toList())
);
return Mono.just(auth);
} else {
return Mono.empty();
}
}
}
Here, I have registered my WebAuthentication manager in Securityconfig. But, still the unauthenticated calls are not going to the WebAuthenticationManager.
It is expected to go to AuthenticationManager when the protected URL's are hit. For ex,
http://localhost:8080/api/v1/user.
Not sure, why the calls are not going to AuthManager.
In non reactive, we have OncePerRequestFilter and the auth is being taken care over there. Not sure, how to implement the same for reactive.
You disabled all authentication mechanisms hence there is nothing calling your authentication manager. As you mentioned, you can implement authentication flow through filters.
Sample implementation of authentication filter:
#Bean
public AuthenticationWebFilter webFilter() {
AuthenticationWebFilter authenticationWebFilter = new AuthenticationWebFilter(authenticationManager);
authenticationWebFilter.setServerAuthenticationConverter(tokenAuthenticationConverter());
authenticationWebFilter.setRequiresAuthenticationMatcher(serverWebExchangeMatcher());
authenticationWebFilter.setSecurityContextRepository(NoOpServerSecurityContextRepository.getInstance());
return authenticationWebFilter;
}
Then add this filter to ServerHttpSecurity: http.addFilterBefore(webFilter(),SecurityWebFiltersOrder.HTTP_BASIC)
Then finally your authentication manager will be called.
You must provide few additional things to make it working.
Matcher to check if Authorization header is added to request:
#Bean
public ServerWebExchangeMatcher serverWebExchangeMatcher() {
return exchange -> {
Mono<ServerHttpRequest> request = Mono.just(exchange).map(ServerWebExchange::getRequest);
return request.map(ServerHttpRequest::getHeaders)
.filter(h -> h.containsKey(HttpHeaders.AUTHORIZATION))
.flatMap($ -> ServerWebExchangeMatcher.MatchResult.match())
.switchIfEmpty(ServerWebExchangeMatcher.MatchResult.notMatch());
};
}
Token converter responsible for getting token from request and preparing basic AbstractAuthenticationToken
#Bean
public ServerAuthenticationConverter tokenAuthenticationConverter() {
return exchange -> Mono.justOrEmpty(exchange)
.map(e -> getTokenFromRequest(e))
.filter(token -> !StringUtils.isEmpty(token))
.map(token -> getAuthentication(token));
}
I intentionally omitted implementation of getTokenFromRequest and getAuthentication because there is a lot of examples available.

Spring Boot 2 OIDC (OAuth2) client / resource server not propagating the access token in the WebClient

Sample project available on Github
I have successfully configured two Spring Boot 2 application2 as client/resource servers against Keycloak and SSO between them is fine.
Besides, I am testing authenticated REST calls to one another, propagating the access token as an Authorization: Bearer ACCESS_TOKEN header.
After starting Keycloak and the applications I access either http://localhost:8181/resource-server1 or http://localhost:8282/resource-server-2 and authenticate in the Keycloak login page. The HomeController uses a WebClient to invoke the HelloRestController /rest/hello endpoint of the other resource server.
#Controller
class HomeController(private val webClient: WebClient) {
#GetMapping
fun home(httpSession: HttpSession,
#RegisteredOAuth2AuthorizedClient authorizedClient: OAuth2AuthorizedClient,
#AuthenticationPrincipal oauth2User: OAuth2User): String {
val authentication = SecurityContextHolder.getContext().authentication
println(authentication)
val pair = webClient.get().uri("http://localhost:8282/resource-server-2/rest/hello").retrieve()
.bodyToMono(Pair::class.java)
.block()
return "home"
}
}
This call returns a 302 since the request is not authenticated (it's not propagating the access token):
2019-12-25 14:09:03.737 DEBUG 8322 --- [nio-8181-exec-5] 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
at org.springframework.security.access.vote.AffirmativeBased.decide(AffirmativeBased.java:84) ~[spring-security-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:233) ~[spring-security-core-5.2.1.RELEASE.jar:5.2.1.RELEASE]
OAuth2Configuration:
#Configuration
class OAuth2Config : WebSecurityConfigurerAdapter() {
#Bean
fun webClient(): WebClient {
return WebClient.builder()
.filter(ServletBearerExchangeFilterFunction())
.build()
}
#Bean
fun clientRegistrationRepository(): ClientRegistrationRepository {
return InMemoryClientRegistrationRepository(keycloakClientRegistration())
}
private fun keycloakClientRegistration(): ClientRegistration {
val clientRegistration = ClientRegistration
.withRegistrationId("resource-server-1")
.clientId("resource-server-1")
.clientSecret("c00670cc-8546-4d5f-946e-2a0e998b9d7f")
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.redirectUriTemplate("{baseUrl}/login/oauth2/code/{registrationId}")
.scope("openid", "profile", "email", "address", "phone")
.authorizationUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/auth")
.tokenUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/token")
.userInfoUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/userinfo")
.userNameAttributeName(IdTokenClaimNames.SUB)
.jwkSetUri("http://localhost:8080/auth/realms/insight/protocol/openid-connect/certs")
.clientName("Keycloak")
.providerConfigurationMetadata(mapOf("end_session_endpoint" to "http://localhost:8080/auth/realms/insight/protocol/openid-connect/logout"))
.build()
return clientRegistration
}
override fun configure(http: HttpSecurity) {
http.authorizeRequests { authorizeRequests ->
authorizeRequests
.anyRequest().authenticated()
}.oauth2Login(withDefaults())
.logout { logout ->
logout.logoutSuccessHandler(oidcLogoutSuccessHandler())
}
}
private fun oidcLogoutSuccessHandler(): LogoutSuccessHandler? {
val oidcLogoutSuccessHandler = OidcClientInitiatedLogoutSuccessHandler(clientRegistrationRepository())
oidcLogoutSuccessHandler.setPostLogoutRedirectUri(URI.create("http://localhost:8181/resource-server-1"))
return oidcLogoutSuccessHandler
}
}
As you can see I'm setting a ServletBearerExchangeFilterFunction in the WebClient. This is what I've seen debugging:
The SubscriberContext isn't setting anything because authentication.getCredentials() instanceof AbstractOAuth2Token is false. Actually it is just a String:
public class OAuth2AuthenticationToken extends AbstractAuthenticationToken {
...
#Override
public Object getCredentials() {
// Credentials are never exposed (by the Provider) for an OAuth2 User
return "";
}
What's the problem here? How can I automate the propagation of the token?
There doesn't seem to be an out of the box solution for pure OAuth2/OIDC login applications, I've created a Github issue for this.
In the meantime, I've created a specific ServletBearerExchangeFilterFunction that retrieves the access token from the OAuth2AuthorizedClientRepository.
This is my custom solution:
#Autowired
lateinit var oAuth2AuthorizedClientRepository: OAuth2AuthorizedClientRepository
#Bean
fun webClient(): WebClient {
val servletBearerExchangeFilterFunction = ServletBearerExchangeFilterFunction("resource-server-1", oAuth2AuthorizedClientRepository)
return WebClient.builder()
.filter(servletBearerExchangeFilterFunction)
.build()
}
...
private fun keycloakClientRegistration(): ClientRegistration {
return ClientRegistration
.withRegistrationId("resource-server-1")
...
const val SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY = "org.springframework.security.SECURITY_CONTEXT_ATTRIBUTES"
class ServletBearerExchangeFilterFunction(private val clientRegistrationId: String,
private val oAuth2AuthorizedClientRepository: OAuth2AuthorizedClientRepository?) : ExchangeFilterFunction {
/**
* {#inheritDoc}
*/
override fun filter(request: ClientRequest, next: ExchangeFunction): Mono<ClientResponse> {
return oauth2Token()
.map { token: AbstractOAuth2Token -> bearer(request, token) }
.defaultIfEmpty(request)
.flatMap { request: ClientRequest -> next.exchange(request) }
}
private fun oauth2Token(): Mono<AbstractOAuth2Token> {
return Mono.subscriberContext()
.flatMap { ctx: Context -> currentAuthentication(ctx) }
.map { authentication ->
val authorizedClient = oAuth2AuthorizedClientRepository?.loadAuthorizedClient<OAuth2AuthorizedClient>(clientRegistrationId, authentication, null)
if (authorizedClient != null) {
authorizedClient.accessToken
} else {
Unit
}
}
.filter { it != null }
.cast(AbstractOAuth2Token::class.java)
}
private fun currentAuthentication(ctx: Context): Mono<Authentication> {
return Mono.justOrEmpty(getAttribute(ctx, Authentication::class.java))
}
private fun <T> getAttribute(ctx: Context, clazz: Class<T>): T? { // NOTE: SecurityReactorContextConfiguration.SecurityReactorContextSubscriber adds this key
if (!ctx.hasKey(SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY)) {
return null
}
val attributes: Map<Class<T>, T> = ctx[SECURITY_REACTOR_CONTEXT_ATTRIBUTES_KEY]
return attributes[clazz]
}
private fun bearer(request: ClientRequest, token: AbstractOAuth2Token): ClientRequest {
return ClientRequest.from(request)
.headers { headers: HttpHeaders -> headers.setBearerAuth(token.tokenValue) }
.build()
}
}

Spring WebFlux : WebClient + Fallback on error

I want to set up a fallback when my program works if the first service is unavailable.
On the second service, I use WebClient which accesses the first service and receives data from it.
I made two options but they do not work for me.
If both services are alive, then everything works well.
If the first service is unavailable, then when I try to send a request via WebClient, nothing happens, I see a blank screen in the browser.
1) The first option:
#Service
public class WebClientService {
private static final String API_MIME_TYPE = "application/json";
private static final String API_BASE_URL = "http://localhost:8081";
private static final String USER_AGENT = "User Service";
private static final Logger logger = LoggerFactory.getLogger(WebClientService.class);
private WebClient webClient;
public WebClientService() {
this.webClient = WebClient.builder()
.baseUrl(API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, API_MIME_TYPE)
.defaultHeader(HttpHeaders.USER_AGENT, USER_AGENT)
.filter(WebClientService.errorHandlingFilter())
.build();
}
public Flux<Bucket> getDataByWebClient() {
return webClient
.get()
.uri("/getAll")
.exchange()
.flatMapMany(clientResponse -> clientResponse.bodyToFlux(Bucket.class));
}
public static ExchangeFilterFunction errorHandlingFilter() {
return ExchangeFilterFunction.ofResponseProcessor(clientResponse -> {
if(clientResponse.statusCode()!=null && (clientResponse.statusCode().is5xxServerError() || clientResponse.statusCode().is4xxClientError()) ) {
return clientResponse.bodyToMono(String.class)
.flatMap(errorBody -> {
return Mono.error(new MyCustomServerException());
});
}else {
return Mono.just(clientResponse);
}
});
}
}
Class MyCustomServerException
public class MyCustomServerException extends Throwable {
public String getAllEmployeesList() {
return "Server error";
}
public MyCustomServerException() {
getAllEmployeesList();
}
}
2) The second option:
#Service
public class WebClientService {
private static final String API_MIME_TYPE = "application/json";
private static final String API_BASE_URL = "http://localhost:8081";
private static final String USER_AGENT = "User Service";
private static final Logger logger = LoggerFactory.getLogger(WebClientService.class);
private WebClient webClient;
public WebClientService() {
this.webClient = WebClient.builder()
.baseUrl(API_BASE_URL)
.defaultHeader(HttpHeaders.CONTENT_TYPE, API_MIME_TYPE)
.defaultHeader(HttpHeaders.USER_AGENT, USER_AGENT)
.build();
}
public Flux<Bucket> getDataByWebClient() {
return webClient
.get()
.uri("/stream/buckets/delay")
.exchange()
.flatMapMany(clientResponse -> clientResponse.bodyToFlux(Bucket.class));
}
public Flux<Bucket> getDataByWebClient() {
return webClient
.get()
.uri("/getAll")
.retrieve()
.onStatus(HttpStatus::is4xxClientError, response -> {
System.out.println("4xx eror");
return Mono.error(new RuntimeException("4xx"));
})
.onStatus(HttpStatus::is5xxServerError, response -> {
System.out.println("5xx eror");
return Mono.error(new RuntimeException("5xx"));
})
.onStatus(HttpStatus::isError, clientResponse -> {
System.out.println("eror");
return Mono.error(new MyCustomServerException());
})
.bodyToFlux(Bucket.class);
}
}
Why is this not working? Can anyone tell me?
I want the browser to display the message "Server error" from my class with an error.
Thanks!

Spring Webflux OAuth 2 resource server

I have a Spring OAuth 2 server based on Spring Boot 1.5 (Spring Security v4) which generates customized tokens and a few resource servers who communicate with this authorization server, making use of /oauth/check_token endpoint by configuration of RemoteTokenServices.
All the logic related to storing/retrieving tokens on Authorization server side is done with JdbcTokenStore.
I am building a new Spring Boot 2 application which is build with Spring webflux module and trying to implement client_credentials flow with existing Authorization Server using Spring Security 5.1.1.
I found that support for resource servers was added in 5.1.0.RC1 (https://spring.io/blog/2018/08/21/spring-security-5-1-0-rc1-released#oauth2-resource-servers) and updated in 5.1.0.RC2 (https://spring.io/blog/2018/09/10/spring-security-5-1-0-rc2-released#oauth2-resource-server) but looks like it's only possible to configure it with JWT support.
I might be messing up with concepts here but looking for more info and a way to configure all these components together.
I'm in same situation as you.I solve that problem in next way, maybe it can help you:
spring-boot-starter-parent.version: 2.1.1
spring-cloud-dependencies.version: Greenwich.R1
Security configuration:
#EnableWebFluxSecurity
public class SecurityConfig {
#Autowired
private ReactiveAuthenticationManager manager; //custom implementation
#Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
return http
.authorizeExchange()
.pathMatchers("/role").hasRole("ADMIN")
.pathMatchers("/test").access(new HasScope("server")) //custom implementation
.anyExchange().authenticated()
.and()
.httpBasic().disable()
.oauth2ResourceServer()
.jwt()
.authenticationManager(manager)
.and().and()
.build();
}
}
ReactiveAuthorizationManager (HasScope) implementation:
Helper which allow search for scopes in authentication object
public class HasScope implements ReactiveAuthorizationManager<AuthorizationContext> {
public HasScope(String...scopes) {
this.scopes = Arrays.asList(scopes);
}
private final Collection<String> scopes;
#Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authentication, AuthorizationContext object) {
return authentication
.flatMap(it -> {
OAuth2Authentication auth = (OAuth2Authentication) it;
Set<String> requestScopes = auth.getOAuth2Request().getScope();
boolean allow = requestScopes.containsAll(scopes);
return Mono.just(new AuthorizationDecision(allow));
});
}
}
ReactiveAuthenticationManager implementation:
That is the main component in configuration which create OAuth2Authentication. There is a problem with response for wrong access_token, it returns only status code without body response.
#Component
public class ReactiveAuthenticationManagerImpl implements ReactiveAuthenticationManager {
private final ResourceServerProperties sso;
private final WebClient.Builder webClient;
private final ObjectMapper objectMapper;
private AuthoritiesExtractor authoritiesExtractor = new FixedAuthoritiesExtractor();
public ReactiveAuthenticationManagerImpl(ResourceServerProperties sso,
#Qualifier("loadBalancedWebClient") WebClient.Builder webClient, ObjectMapper objectMapper) {
this.sso = sso;
this.webClient = webClient;
this.objectMapper = objectMapper;
}
#Override
public Mono<Authentication> authenticate(Authentication authentication) {
return Mono.just(authentication)
.cast(BearerTokenAuthenticationToken.class)
.flatMap(it -> getMap(it.getToken()))
.flatMap(result -> Mono.just(extractAuthentication(result)));
}
private OAuth2Authentication extractAuthentication(Map<String, Object> map) {
Object principal = getPrincipal(map);
OAuth2Request request = getRequest(map);
List<GrantedAuthority> authorities = authoritiesExtractor.extractAuthorities(map);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
token.setDetails(map);
return new OAuth2Authentication(request, token);
}
private Object getPrincipal(Map<String, Object> map) {
if (map.containsKey("principal")) {
try {
//that is the case for user authentication
return objectMapper.convertValue(map.get("principal"), UserPrincipal.class);
} catch (IllegalArgumentException ex) {
//that is the case for client authentication
return objectMapper.convertValue(map.get("principal"), String.class);
}
}
return null;
}
#SuppressWarnings({"unchecked"})
private OAuth2Request getRequest(Map<String, Object> map) {
Map<String, Object> request = (Map<String, Object>) map.get("oauth2Request");
String clientId = (String) request.get("clientId");
Set<String> scope = new LinkedHashSet<>(request.containsKey("scope") ?
(Collection<String>) request.get("scope") : Collections.emptySet());
return new OAuth2Request(null, clientId, null, true, new HashSet<>(scope),
null, null, null, null);
}
private Mono<Map<String, Object>> getMap(String accessToken) {
String uri = sso.getUserInfoUri();
return webClient.build().get()
.uri(uri)
.accept(MediaType.APPLICATION_JSON)
.header("Authorization", "Bearer " + accessToken)
.exchange()
.flatMap(it -> it.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {}))
.onErrorMap(InvalidTokenException.class, mapper -> new InvalidTokenException("Invalid token: " + accessToken));
}

Convert Spring Boot and OAuth2 client from using Client Credentials flow to Authorization Code Grant flow

I have been struggling while trying to create a sample client that uses the OAuth 2.0 Authorization Code Grant flow.
I am able to successfully use the Client Credentials flow but when I try to use the Authorization Code flow I do not get redirected to the correct uri.
When calling the OAuth2RestTmplate.exchange method, I get a redirect exception in the RestTemplate.doExecute(...) method. It is thrown from the finally clause.
The response is null, but the if is not stopping it.
finally {
if (response != null) {
response.close();
}
I still get prompted for login and authorization, but am not directed to the response containing the data. I am just redirected back to the client home page. The same call from Postman using the authorization code flow with the same client credentials is successful so I know the client registration is correct.
I could use another pair of eyes to see what I'm missing. Thanks in Advance! Below are my code excerpts.
Working client using oauth2 client credentials flow:
Client App:
#SpringBootApplication(exclude = {SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class})
public class ClientExampleClientCredentials extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(ClientExampleClientCredentials.class, args);
}
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(ClientExampleClientCredentials.class);
}
}
Controller:
#RestController
public class HomeController {
#Value("${security.oauth2.client.clientId}")
private String clientId;
#Value("${security.oauth2.client.clientSecret}")
private String clientSecret;
#Value("${security.oauth2.client.apiUrl}")
private String apiUrl;
#Value("${security.oauth2.client.scope}")
private List<String> scopes;
#Value("${security.oauth2.client.accessTokenUri}")
private String accessTokenUri;
/**
* Example of using the OAuth2RestTemplate to access external resources
*
* The OAuth2RestTemplate takes care of exchanging credentials with the auth server, as well as adding the
* bearer token to each request to the FHIR services.
*
*/
#RequestMapping("/ex-1")
public String retrievePatientByIdUsingRestTemplate(#RequestParam String id) {
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(getClientCredentialsResourceDetails(), new DefaultOAuth2ClientContext());
ResponseEntity<String> response = oAuth2RestTemplate.exchange(apiUrl + "/Patient/" + id, HttpMethod.GET, null, String.class);
String responseBody = response.getBody();
return responseBody;
}
private ClientCredentialsResourceDetails getClientCredentialsResourceDetails() {
ClientCredentialsResourceDetails clientCredentialsResourceDetails = new ClientCredentialsResourceDetails();
clientCredentialsResourceDetails.setAccessTokenUri(accessTokenUri);
clientCredentialsResourceDetails.setAuthenticationScheme(AuthenticationScheme.header);
clientCredentialsResourceDetails.setClientId(clientId);
clientCredentialsResourceDetails.setClientSecret(clientSecret);
clientCredentialsResourceDetails.setScope(scopes);
return clientCredentialsResourceDetails;
}
}
application.yml
security:
oauth2:
client:
clientId: client_id
clientSecret: secret
apiUrl: http://localhost:8080/testData/data
accessTokenUri: http://localhost:8080/test-auth/token
scope: system/*.read
This works great authenticating me and then redirecting to my service url. However, The Authorization Code flow is not working.
Broken client using oauth2 authorization code flow:
Client App:
#SpringBootApplication (exclude = {SecurityAutoConfiguration.class, ManagementWebSecurityAutoConfiguration.class})
public class ClientExampleAccessToken extends SpringBootServletInitializer {
public static void main(String[] args) {
SpringApplication.run(ClientExampleAccessToken.class, args);
}
#Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(ClientExampleAccessToken.class);
}
}
Controller:
package org.ihc.clinical.controller;
#Configuration
#EnableOAuth2Client
#RestController
public class HomeController {
#Value("${security.oauth2.client.clientId}")
private String clientId;
#Value("${security.oauth2.client.clientSecret}")
private String clientSecret;
#Value("${security.oauth2.client.accessTokenUri}")
private String accessTokenUri;
#Value(("${security.oauth2.client.userAuthorizationUri}"))
private String userAuthorizationUri;
#Value("${security.oauth2.client.apiUrl}")
private String apiUrl;
#Value("${security.oauth2.client.redirectUri}")
private String redirectUri;
#RequestMapping("/ex-1")
public String retrievePatientByIdUsingRestTemplate(#RequestParam String empi) {
OAuth2ProtectedResourceDetails resource = resource();
String path = apiUrl + "/Patient/" + empi;
OAuth2RestTemplate oAuth2RestTemplate = new OAuth2RestTemplate(resource, new DefaultOAuth2ClientContext());
***/*error occurs here in RestTemplate.doExcute. error:org.springframework.security.oauth2.client.resource.UserRedirectRequiredException:
A redirect is required to get the users approval */***
ResponseEntity<String> response = oAuth2RestTemplate.exchange(path, HttpMethod.GET, null, String.class);
//AuthorizationCodeAccessTokenProvider provider = new //AuthorizationCodeAccessTokenProvider();
//Token Request
//AccessTokenRequest request = new DefaultAccessTokenRequest();
//String code = provider.obtainAuthorizationCode(resource, request);
//request.setAuthorizationCode(code);
//OAuth2AccessToken oAuth2AccessToken = //provider.obtainAccessToken(resource, request);
//Token Response
//String tokenValue = oAuth2AccessToken.getValue();
//return tokenValue;
}
//Call when ready to send token Request
private OAuth2ProtectedResourceDetails resource() {
AuthorizationCodeResourceDetails authorizationCodeResourceDetails = new AuthorizationCodeResourceDetails();
authorizationCodeResourceDetails.setClientId(clientId);
authorizationCodeResourceDetails.setClientSecret(clientSecret);
authorizationCodeResourceDetails.setAccessTokenUri(accessTokenUri);
authorizationCodeResourceDetails.setUserAuthorizationUri(userAuthorizationUri);
//authorizationCodeResourceDetails.setScope(scopes);
authorizationCodeResourceDetails.setPreEstablishedRedirectUri(redirectUri);
return authorizationCodeResourceDetails;
}
}
application.yml
security:
oauth2:
client:
clientId: clientid
clientSecret: secret
accessTokenUri: http://localhost:8080/test-auth/token
userAuthorizationUri: http://localhost:8080/test-auth/authorize
apiUrl: http://localhost:8080/test-fhir-cdr/data
redirectUri: http://localhost:8080/test-examples-access-token
I finally found the solution here: https://projects.spring.io/spring-security-oauth/docs/oauth2.html
I needed to add the below code to the controller:
#Autowired
private OAuth2ClientContext oauth2Context;
#Bean
public OAuth2RestTemplate getOauth2RestTemplate() {
return new OAuth2RestTemplate(resource(), oauth2Context);
}
Then call getOauth2RestTemplate() like below:
#RequestMapping("/ex-1")
public String retrievePatientByIdUsingRestTemplate(#RequestParam String empi) {
String path = apiUrl + "/Patient/" + empi;
OAuth2RestTemplate oAuth2RestTemplate = getOauth2RestTemplate();
ResponseEntity<String> response = oAuth2RestTemplate.exchange(path, HttpMethod.GET, null, String.class);
return response.getBody();
}

Resources