Unable to call nuxeo rest api from rest client when integrated with Keycloak authentication plugin - oauth-2.0

I wanted to integrate keycloak as authentication plugin for nuxeo platform both running on my local machine
Set up details
Nuxeo platform version: 10.10 (runs on tomcat 9)
Keycloak version: 6.0.1
keycloak tomcat adapter distribution: keycloak-tomcat8-adapter-dist
I followed the steps mentioned in link https://github.com/nuxeo/nuxeo/tree/master/nuxeo-services/login/nuxeo-platform-login-keycloak.
Here, I built the nuxeo-platform-login-keycloak plugin for keycloak 6.0.1 version.
On keycloak,
I set up a auth client under newly created realm 'demo'
Client details available in
client configuration
I created role as 'Members' and added admin role to it
I created a user 'keycloakuser' and added to 'Members'.
When nuxeo ui is hit from browser, the authentication flow works fine. It redirects me to login page of keycloak, on valid credentials, it redirects me to nuxeo ui.
The user created along with 'Members' group assigned to it.
Error Scenario
To call rest api from postman, I configured Oauth2 for authentication.
Auth url: http://localhost:8080/auth/realms/demo/protocol/openid-connect/auth
Token Url: http://localhost:8080/auth/realms/demo/protocol/openid-connect/token
Client: testclient
Client secret: *****
Scope: openid
I used access_token obtained using Oauth2 flow, to make API call as http://localhost:8190/nuxeo/api/v1/id/document_id. It is failing with
java.lang.ClassCastException: class org.apache.catalina.core.ApplicationHttpRequest cannot be cast to class org.apache.catalina.connector.RequestFacade (org.apache.catalina.core.ApplicationHttpRequest and org.apache.catalina.connector.RequestFacade are in unnamed module of loader java.net.URLClassLoader #39aeed2f)
at org.nuxeo.ecm.platform.ui.web.keycloak.DeploymentResult.invokeOn(DeploymentResult.java:79) [nuxeo-platform-login-keycloak-10.10.jar:?]
at org.nuxeo.ecm.platform.ui.web.keycloak.KeycloakAuthenticatorProvider.provide(KeycloakAuthenticatorProvider.java:56) [nuxeo-platform-login-keycloak-10.10.jar:?]
at org.nuxeo.ecm.platform.ui.web.keycloak.KeycloakAuthenticationPlugin.handleRetrieveIdentity(KeycloakAuthenticationPlugin.java:113) [nuxeo-platform-login-keycloak-10.10.jar:?]
at org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter.handleRetrieveIdentity(NuxeoAuthenticationFilter.java:1137) [nuxeo-platform-web-common-10.10.jar:?]
at org.nuxeo.ecm.platform.ui.web.auth.NuxeoAuthenticationFilter.doFilterInternal(NuxeoAuthenticationFilter.java:548) [nuxeo-platform-web-common-10.10.jar:?]
Observation:
1. The API request call is not hitting the keycloak endpoint
2. I tried to print the reqqest type (actually the request wrapper type) in both scenarios.
For browser request, it was org.apache.catalina.connector.RequestFacade and for api request it was org.apache.catalina.core.ApplicationHttpRequest which is not extending org.apache.catalina.connector.RequestFacade
Questions:
1. Does above behavior (mentioend in point 2) differ in tomcat versions earlier to tomcat 9?
2. Is the problem with compatibility issues with tomcat version and keycloak adapters jar version?

Late answer, but it may come handy for new readers. I had the exact same issue some months ago. It seems to be due to a bug in the nuxeo-platform-login-keycloak plugin.
I endend making the following changes to org.nuxeo.ecm.platform.ui.web.keycloak.DeploymentResult:
public class DeploymentResult {
final static Logger LOGGER = LoggerFactory.getLogger(DeploymentResult.class);
private boolean isOk;
private static KeycloakDeployment keycloakDeployment;
private HttpServletRequest httpServletRequest;
private HttpServletResponse httpServletResponse;
private Request request;
private CatalinaHttpFacade facade;
public DeploymentResult(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
this.httpServletRequest = httpServletRequest;
this.httpServletResponse = httpServletResponse;
}
boolean isOk() {
return isOk;
}
public static KeycloakDeployment getKeycloakDeployment() {
return keycloakDeployment;
}
public Request getRequest() {
return request;
}
public CatalinaHttpFacade getFacade() {
return facade;
}
public DeploymentResult invokeOn(AdapterDeploymentContext deploymentContext) {
// In Tomcat, a HttpServletRequest and a HttpServletResponse are wrapped in a Facades
if (httpServletRequest instanceof RequestFacade) {
// Received upon logout.
request = unwrapRequest(httpServletRequest);
} else {
request = unwrapRequest(((ServletRequestWrapper) httpServletRequest).getRequest());
}
facade = new CatalinaHttpFacade(httpServletResponse, request);
if (keycloakDeployment == null) {
keycloakDeployment = deploymentContext.resolveDeployment(facade);
}
if (keycloakDeployment.isConfigured()) {
isOk = true;
return this;
}
isOk = false;
return this;
}
/**
* Get the wrapper {#link Request} hidden in a {#link ServletRequest} object
*
* #param servletRequest, the main ServletRequest object
* #return the wrapper {#link Request} in {#link ServletRequest}
*/
private Request unwrapRequest(final ServletRequest servletRequest) {
try {
final Field f = servletRequest.getClass().getDeclaredField("request");
f.setAccessible(true); // grant access to (protected) field
return (Request) f.get(servletRequest);
} catch (final NoSuchFieldException | IllegalAccessException e) {
LOGGER.error("Couldn't unwrap request", e);
throw new RuntimeException(e);
} catch (final Exception e) {
LOGGER.error("Couldn't unwrap request", e);
throw e;
}
}
}
After building and deploying the plugin with these changes, I was allowed to call Nuxeo's REST API with bearer authentication using access token obtained through Keycloak.
Disclaimer: I focused on making it work, not on making it clean...

Related

Spring authorization server authenticate for each client

I'm trying to build an Identity Provider using Spring authorization-server that third party applications are going to use for FIM (federated identity management).
We want each OAuth client to require authentication (if a user tries to login with a different client they would need to authenticate for each client).
Out of the box the flow looks like this:
So there's 2 issues.
The /oauth2/authorize endpoint just checks whether or not the sessions principal is authenticated, it doesn't care or know which client the principal was meant for.
There's just a single /login endpoint, so during authentication it doesn't know which client is used.
My best bet here is that I should:
Make the oauth2/authorize endpoint redirection to /login include the query parameter client_id
Create a custom AuthenticationFilter that also adds the client_id to the User principal
Override the authorizationRequestConverter for the oauth2/authorize endpoint and validate that the client in the request is the same as the client stored on the authenticated principal
Am I missing anything or do anyone know of a simpler way of doing this?
Based on your last comment, it seems one possibility is to simply require authentication every time, or at least every time an authorization is requested. In that case, you could clear out the authentication after the authorization code is issued to the client, using a Filter. This doesn't seem ideal and will result in a poor user experience, but may achieve your requirement.
#Configuration
#EnableWebSecurity
public class SecurityConfig {
#Bean
#Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// ...
// Add filter to remove the SecurityContext after successful authorization
http.addFilterAfter(new RemoveSecurityContextOnAuthorizationFilter(), LogoutFilter.class);
return http.build();
}
private static final class RemoveSecurityContextOnAuthorizationFilter extends OncePerRequestFilter {
private SecurityContextHolderStrategy securityContextHolderStrategy =
SecurityContextHolder.getContextHolderStrategy();
private final LogoutHandler logoutHandler = new CompositeLogoutHandler(
new CookieClearingLogoutHandler("JSESSIONID"),
new SecurityContextLogoutHandler()
);
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} finally {
String locationHeader = response.getHeader(HttpHeaders.LOCATION);
if (locationHeader != null) {
UriComponents uriComponents = UriComponentsBuilder.fromUriString(locationHeader).build();
if (uriComponents.getQueryParams().containsKey("code")) {
Authentication authentication = this.securityContextHolderStrategy.getContext().getAuthentication();
this.logoutHandler.logout(request, response, authentication);
}
}
}
}
}
// ...
}

How to provide an OAuth2 token to a Feign client using Spring Security for the client_credentials workflow

Overview
I am trying to write a program that accesses a public REST API. In order for me to be able to consume it, I need to provide an OAuth2 token.
My App uses Spring Boot 2.4.2 and Spring Cloud version 2020.0.1. The app itself does call the REST API once every 24h, download the data, and stores it in a database. A different microservice consumes this data at some other point and needs the data to have been refreshed daily.
My approach to this is to use OpenFeign to declare the REST Client that consumes the REST API and provide it an OAuth2 token. This is a problem that is quite common, so I assume that machine to machine client_credentials workflow is well documented.
And indeed, I did find a simple example to do this with OpenFeign - here: https://github.com/netshoes/sample-feign-oauth2-interceptor/blob/master/src/main/java/com/sample/feign/oauth2/interceptor/OrderFeignClientConfiguration.java
TL;DR: Trying to write a machine-to-machine microservice requiring an OAuth2 token (client_credentials grant type).
Problem
This was my first try, but unfortunately with the new Spring Security release, I can't seem to get the OAuth2FeignRequestInterceptor instantiated, I might have a package problem. I then went on to study the documentation for Spring Security and the new OAuth2 rewrite, which can be found here: https://docs.spring.io/spring-security/site/docs/5.1.2.RELEASE/reference/htmlsingle/#oauth2client.
Approaches
My approach is to use a RequestInterceptor which injects the current OAuth2 token into the request of the OpenFeign client, by adding an Authorization Bearer header. My assumption is that I can retrieve this, more or less automagically, using the Spring Security OAuth2 layer.
Using the documentation I tried providing a bean of OAuth2RegisteredClient to my interceptor, as well as a bean of type OAuth2AccessToken - which both didn't work. My last try looked like this and is to be seen, as a sort of hail mary, kind of approach:
#Bean
public OAuth2AccessToken apiAccessToken(
#RegisteredOAuth2AuthorizedClient("MY_AWESOME_PROVIDER") OAuth2AuthorizedClient authorizedClient) {
return authorizedClient.getAccessToken();
}
This doesn't work because RegisteredOAuth2AuthorizedClient requires a user session, lest it is null. I also saw someone else on Stackoverflow trying the same approach, but they actually did it in a Controller (=> Resolving OAuth2AuthorizedClient as a Spring bean)
I also tried some approaches that I have found here on SO:
Feign and Spring Security 5 - Client Credentials (Provided answer uses Spring Boot 2.2.4 - thus not relevant anymore)
Alternative For OAuth2FeignRequestInterceptor as it is deprecated NOW another gentleman looking for an alternative for OAuth2FeignRequestInterceptor
OAuth2FeignRequestInterceptor class deprecated in Spring Boot 2.3 - solution here again required an active user-session
https://github.com/jgrandja/spring-security-oauth-5-2-migrate this Github repo pops up every now and then, I studied it, but I deem it irrelevant to my question - maybe I missed something? From what I understood, this sample application has multiple providers using multiple scopes - but still a user that triggers a login and thus the automagic generation of an OAuth2 token through Spring Security. (also featured in this question: Migrating from Spring Boot Oauth2 to Spring Security 5) [1]
https://github.com/spring-cloud/spring-cloud-openfeign/issues/417 -> as of right now there is no replacement for OAuth2FeignRequestInterceptor
My assumption is that I can somehow use Spring Security 5 to solve this, but I simply can't wrap my head around how to actually do it. It seems to me that most of the tutorials and code samples I have found actually require a user-session, or are outdated with Spring Security 5.
It really seems that I am missing something and I hope that somebody can point me in the right direction, towards a tutorial or written documentation on how to achieve this.
In depth example
I tried supplying an OAuth2AuthorizedClientManager as seen in this example (https://github.com/jgrandja/spring-security-oauth-5-2-migrate).
For this, I registered an OAuth2AuthorizedClientManager following the example code:
#Bean
public OAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,
OAuth2AuthorizedClientRepository authorizedClientRepository) {
OAuth2AuthorizedClientProvider authorizedClientProvider =
OAuth2AuthorizedClientProviderBuilder.builder()
.authorizationCode()
.refreshToken()
.clientCredentials()
.password()
.build();
DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(
clientRegistrationRepository, authorizedClientRepository);
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);
return authorizedClientManager;
}
and provided it for my RequestInterceptor as can be seen here:
#Bean
public RequestInterceptor requestInterceptor(OAuth2AuthorizedClientManager clientManager) {
return new OAuthRequestInterceptor(clientManager);
}
Finally I wrote the interceptor, which looks like this:
private String getAccessToken() {
OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest.withClientRegistrationId(appClientId)
// .principal(appClientId) // if this is not set, I receive "principal cannot be null" (or empty)
.build();
return Optional.ofNullable(authorizedClientManager)
.map(clientManager -> clientManager.authorize(request))
.map(OAuth2AuthorizedClient::getAccessToken)
.map(AbstractOAuth2Token::getTokenValue)
.orElseThrow(OAuth2AccessTokenRetrievalException::failureToRetrieve);
}
#Override
public void apply(RequestTemplate template) {
log.debug("FeignClientInterceptor -> apply CALLED");
String token = getAccessToken();
if (token != null) {
String bearerString = String.format("%s %s", BEARER, token);
template.header(HttpHeaders.AUTHORIZATION, bearerString);
log.debug("set the template header to this bearer string: {}", bearerString);
} else {
log.error("No bearer string.");
}
}
When I run the code, I can see "FeignClientInterceptor -> apply called" output in the console, followed by an Exception:
Caused by: java.lang.IllegalArgumentException: servletRequest cannot be null
My assumption is that I receive this, because I don't have an active user session. It seems to me thus, that I absolutely need one to fix this problem - which I don't have in machine-to-machine communcations.
This is a common use-case so I am sure I must have made a mistake at some point.
Used packages
Maybe I made a mistake with my packages?
implementation 'org.springframework.boot:spring-boot-starter-amqp'
implementation 'org.springframework.boot:spring-boot-starter-jooq'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
According documentation need use AuthorizedClientServiceOAuth2AuthorizedClientManager instead of DefaultOAuth2AuthorizedClientManager
When operating outside of the context of a HttpServletRequest, use AuthorizedClientServiceOAuth2AuthorizedClientManager instead.
So. I was playing with your solution in my free time. And found the simple solution:
just add SecurityContextHolder.getContext().authentication principle to your code OAuth2AuthorizeRequest request = OAuth2AuthorizeRequest.withClientRegistrationId(appClientId).build();
Should be like this:
val request = OAuth2AuthorizeRequest
.withClientRegistrationId("keycloak") // <-- here your registered client from application.yaml
.principal(SecurityContextHolder.getContext().authentication)
.build()
Used packages:
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
implementation("org.springframework.boot:spring-boot-starter-oauth2-client")
application.yaml:
spring:
security:
oauth2:
client:
registration:
keycloak: # <--- It's your custom client. I am using keycloak
client-id: ${SECURITY_CLIENT_ID}
client-secret: ${SECURITY_CLIENT_SECRET}
authorization-grant-type: client_credentials
scope: openid # your scopes
provider:
keycloak: # <--- Here Registered my custom provider
authorization-uri: ${SECURITY_HOST}/auth/realms/${YOUR_REALM}/protocol/openid-connect/authorize
token-uri: ${SECURITY_HOST}/auth/realms/${YOUR_REALM}/protocol/openid-connect/token
feign:
compression:
request:
enabled: true
mime-types: application/json
response:
enabled: true
client.config.default:
connectTimeout: 1000
readTimeout: 60000
decode404: false
loggerLevel: ${LOG_LEVEL_FEIGN:basic}
SecurityConfiguration:
#Configuration
#EnableWebSecurity
#EnableGlobalMethodSecurity(prePostEnabled = true)
class SecurityConfiguration() : WebSecurityConfigurerAdapter() {
#Throws(Exception::class)
override fun configure(http: HttpSecurity) {
// #formatter:off
http
.authorizeRequests { authorizeRequests ->
authorizeRequests
.antMatchers(HttpMethod.GET, "/test").permitAll() // Here my public endpoint which do logic with secured client enpoint
.anyRequest().authenticated()
}.cors().configurationSource(corsConfigurationSource()).and()
.csrf().disable()
.cors().disable()
.httpBasic().disable()
.formLogin().disable()
.logout().disable()
.oauth2Client()
// #formatter:on
}
#Bean
fun authorizedClientManager(
clientRegistration: ClientRegistrationRepository?,
authorizedClient: OAuth2AuthorizedClientRepository?
): OAuth2AuthorizedClientManager? {
val authorizedClientProvider = OAuth2AuthorizedClientProviderBuilder
.builder()
.clientCredentials()
.build()
val authorizedClientManager = DefaultOAuth2AuthorizedClientManager(clientRegistration, authorizedClient)
authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider)
return authorizedClientManager
}
}
FeignClientConfiguration:
private val logger = KotlinLogging.logger {}
class FeignClientConfiguration(private val authorizedClientManager: OAuth2AuthorizedClientManager) {
#Bean
fun requestInterceptor(): RequestInterceptor = RequestInterceptor { template ->
if (template.headers()["Authorization"].isNullOrEmpty()) {
val accessToken = getAccessToken()
logger.debug { "ACCESS TOKEN TYPE: ${accessToken?.tokenType?.value}" }
logger.debug { "ACCESS TOKEN: ${accessToken?.tokenValue}" }
template.header("Authorization", "Bearer ${accessToken?.tokenValue}")
}
}
private fun getAccessToken(): OAuth2AccessToken? {
val request = OAuth2AuthorizeRequest
.withClientRegistrationId("keycloak") // <- Here you load your registered client
.principal(SecurityContextHolder.getContext().authentication)
.build()
return authorizedClientManager.authorize(request)?.accessToken
}
}
TestClient:
#FeignClient(
name = "test",
url = "http://localhost:8080",
configuration = [FeignClientConfiguration::class]
)
interface TestClient {
#GetMapping("/test")
fun test(): ResponseEntity<Void> // Here my secured resource server endpoint. Expect 204 status
}

Zuul reverse proxy with Keycloak server

I'm configuring a Spring Cloud (Angel.SR6) application using the Zuul reverse proxy utility, in order to hide the internal service ports. My zuul (edge) service is published in the 8765 port and my organizations service is in the 8083 one. Everything goes smoothly when I access the application with no security, http://localhost:8765/organization/organizations returns the JSON with all the organizations.
However, now I want to integrate a Keycloak SSO (OAuth2) server for authorization purposes. I have added the Spring Security adapter in my organization service and configured it to authenticate in http://localhost:8080/auth. Everything goes well, except that zuul performs a redirection instead of proxying. So when authentication is successful, I get redirected to http://localhost:8083/organizations instead of http://localhost:8765/organization/organizations. Here there are my browser requests:
That's because the keycloak adapter creates a token verification endpoint in the http://localhost:8083/sso/login, from which it performs a redirection to the authorization server in order to validate the token. When authorization server acknowledges it, a redirection is sent to the organization service, with the /organization path, so the end url being loaded is http://localhost:8083/organizations. But I would like the first requested url to be loaded instead.
Which choice do I have?
Recently I've had the same problem. I've solved it by:
Add to application.properties in Zuul
zuul.sensitive-headers=Cookie,Set-Cookie
Introduce KeycloakFilterRoute in Zuul
class KeycloakFilterRoute extends ZuulFilter {
private static final String AUTHORIZATION_HEADER = "authorization";
#Override
public String filterType() {
return "route";
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() {
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.getRequest().getHeader(AUTHORIZATION_HEADER) == null) {
addKeycloakTokenToHeader(ctx);
}
return null;
}
private void addKeycloakTokenToHeader(RequestContext ctx) {
RefreshableKeycloakSecurityContext securityContext = getRefreshableKeycloakSecurityContext(ctx);
if (securityContext != null) {
ctx.addZuulRequestHeader(AUTHORIZATION_HEADER, buildBearerToken(securityContext));
}
}
private RefreshableKeycloakSecurityContext getRefreshableKeycloakSecurityContext(RequestContext ctx) {
if (ctx.getRequest().getUserPrincipal() instanceof KeycloakAuthenticationToken) {
KeycloakAuthenticationToken token = (KeycloakAuthenticationToken) ctx.getRequest().getUserPrincipal();
return (RefreshableKeycloakSecurityContext) token.getCredentials();
}
return null;
}
private String buildBearerToken(RefreshableKeycloakSecurityContext securityContext) {
return "Bearer " + securityContext.getTokenString();
}
}
(Migrated from comment to answer)
I ended up making a Github project in order to explain my problem to the keycloak team, and got a pull request from one of the development team members trying to help me out. Following their recommendations, I came into the conclusion that zuul is good to hide stateless services (bearer only ones), but not the ones that user directly interacts with. Here it is the whole thread in the mailing list.

Spring Zuul API Gateway with Spring Session / Redis Authenticate and Route in same Request

I have been really been searching high and low for the last few days on how to do this and have finally decided to admit defeat and ask for help, please!!!
I have followed Dr Dave Syer's tutorial on Angular and Spring Security specifically the Zuul Proxy as an api gateway and using Spring Session with Redis (https://github.com/spring-guides/tut-spring-security-and-angular-js/tree/master/double#_sso_with_oauth2_angular_js_and_spring_security_part_v)
The issue I am having is that I am calling resource rest services via the gateway from an external application with the following header:
String plainCreds = "user:password";
byte[] plainCredsBytes = plainCreds.getBytes();
byte[] base64CredsBytes = Base64.getEncoder().encode(plainCredsBytes);
String base64Creds = new String(base64CredsBytes);
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Basic " + base64Creds);
to be authenticated and then routed by zuul and then the resource to have access to the authenticated session via redis.
The issue is that the session seems to only commit to redis in the gateway after the request has responded. So what is happening is that when I call a resource service with the header, I can see the successful authentication occurring in the gateway and session being created, however I am getting a 403 in the resource due to the session not being in redis after its been routed via zuul.
However if I get the error, grab the session id and add it to the header and try again it works because now my authenticated session is available for the resource project after its been routed.
Please could someone point me in the direction of how I go about getting my calls via the gateway to authenticate and route in the same request please?
Thanks
Justin
I followed Justin Taylor's posts on different pages so this is his solution. It makes me sense to have solution with source code here:
Make Spring Session commit eagerly - since spring-session v1.0 there is annotation property #EnableRedisHttpSession(redisFlushMode = RedisFlushMode.IMMEDIATE) which saves session data into Redis immediately. Documentation here.
Simple Zuul filter for adding session into current request's header:
#Component
public class SessionSavingZuulPreFilter extends ZuulFilter {
#Autowired
private SessionRepository repository;
#Override
public String filterType() {
return "pre";
}
#Override
public int filterOrder() {
return 0;
}
#Override
public Object run() {
RequestContext context = RequestContext.getCurrentContext();
HttpSession httpSession = context.getRequest().getSession();
Session session = repository.getSession(httpSession.getId());
context.addZuulRequestHeader("Cookie", "SESSION=" + httpSession.getId());
log.info("ZuulPreFilter session proxy: {}", session.getId());
return null;
}
}
Once more - this is not my solution - credentials go to Justin Taylor.
I am so sorry about the delayed response here, one of the great things about South Africa is our great telecoms hehe, I have had no internet at home for a while and my source code for this is on my home pc.
Yes Steve is on the right track. There are two issues that you need to be resolve here:
Spring session only commits the authenticated session to redis on response to the initial incoming request. So the first step is to follow that link steve provided to ensure spring session commits to redis whenever the session changes.
Zuul doesn't propagate this newly authenticated session on the initial routing. So what you need to do is to use a zuul pre filter (lots of examples around) that gets the authenticated session id and then adds it to the zuul request to the resource behind the gateway. You will see a setter method on the zuul request to set the session id.
If you don't do this, you will need to do two calls, one to authenticate and get a valid session id which would be in redis from spring session, and then the subsequent call with your authenticated session id.
I did battle with this for a while, but when I got it working it was spot on. I extended this solution to not only work for http basic, but added in a jwt token implementation.
Hopefully this helps, as soon as I am connected at home I can post the source.
Good Luck!
Justin
My APIGateway (Zuul) is proxied by Apache Httpd and protected by Mellon module (SAML 2.0). After a successfully authentication on the identity provider, mellon module inject correctly some headers read into the SAML response, but the first request fails with a 403 status code.
I'm also using SpringSecurity, to solve the problem I'm using a simple filter added on the security filter chain that ensure the correct creation of SecurityContext:
#Component
public class MellonFilter extends OncePerRequestFilter {
private final Logger log = LoggerFactory.getLogger(MellonFilter.class);
#Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String mellonId=req.getHeader("mellon-nameid");
if(mellonId==null||mellonId.isEmpty())
;//do filterchain
else {
UserWithRoles userWithRoles = new UserWithRoles();
userWithRoles.setUsername(mellonId);
SilUserDetails details = new SilUserDetails(userWithRoles);
SilAuthenticationPrincipal silPrincipal = null;
Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
authorities.add(new SimpleGrantedAuthority("Some roles");
silPrincipal = new SilAuthenticationPrincipal(details, true, authorities);
SecurityContextHolder.clearContext();
SecurityContextHolder.getContext().setAuthentication(silPrincipal);
}
filterChain.doFilter(req,httpServletResponse);
}
#Override
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
if(SecurityContextHolder.getContext().getAuthentication()!=null&&SecurityContextHolder.getContext().getAuthentication() instanceof SilAuthenticationPrincipal)
return true;
return false;
}
}
Then I need a ZuulFilter to save the session (on Redis) and to propagate the actual session id:
public class ZuulSessionCookieFilter extends ZuulFilter {
private final Logger log = LoggerFactory.getLogger(ZuulSessionCookieFilter.class);
#Autowired
private SessionRepository repository;
#Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
#Override
public int filterOrder() {
return 0;
}
#Override
public boolean shouldFilter() {
return true;
}
#Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpSession httpSession = context.getRequest().getSession();
httpSession.setAttribute(
HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY,
SecurityContextHolder.getContext()
);
Session session = repository.findById(httpSession.getId());
context.addZuulRequestHeader("cookie", "SESSION=" + base64Encode(httpSession.getId()));
log.debug("ZuulPreFilter session proxy: {} and {}", session.getId(),httpSession.getId());
return null;
}
private static String base64Encode(String value) {
byte[] encodedCookieBytes = Base64.getEncoder().encode(value.getBytes());
return new String(encodedCookieBytes);
}
}
I hope this solution will be helpful to everyone.

Integrate Spring Security OAuth2 and Spring Social

I'm working with a Spring Boot + Spring Security OAuth2 application that I believe was inspired by examples from Dave Syer. The application is configured to be an OAuth2 authorization server, with a single public client using the Resource Owner Password Credentials flow. A successful token is configured to be a JWT.
The public Angular client sends a POST request to /oauth/token with a basic auth header containing the client id and secret (this was the easiest way to get the client to authenticate, even though the secret is not private). The body of the request contains username, password, and grant type of "password".
In addition to being an authentication server, the application is a RESTful resource server for users, teams, and organizations.
I'm trying to add an additional SSO authentication flow using Spring Social. I've got Spring Social configured to authenticate through external providers via /auth/[provider]; however, following requests no longer have the SecurityContext correctly set. Possibly, Spring Security OAuth server or client is overriding the SecurityContext?
If I can get the SecurityContext correctly set after the Spring Social flow, I've got a new TokenGranter that allows a new grant type of "social" that would check the SecurityContextHolder for the pre authenticated user.
I'm interested in both a solution to my specific problem with the SecurityContext (I believe it's an issue with Spring OAuth + Social integration), or a different approach for authenticating with external providers and getting a valid JWT from our own auth server.
Thanks!
I had a similar problem on a JHipster-generated web application. Finally I decided to go with the SocialAuthenticationFilter option from Spring Social (via the SpringSocialConfigurer). After a successful social login, the server automatically generates and returns the "own" access token via redirection to the client app.
Here's my try:
#Configuration
#EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter implements EnvironmentAware {
//...
#Inject
private AuthorizationServerTokenServices authTokenServices;
#Override
public void configure(HttpSecurity http) throws Exception {
SpringSocialConfigurer socialCfg = new SpringSocialConfigurer();
socialCfg
.addObjectPostProcessor(new ObjectPostProcessor<SocialAuthenticationFilter>() {
#SuppressWarnings("unchecked")
public SocialAuthenticationFilter postProcess(SocialAuthenticationFilter filter){
filter.setAuthenticationSuccessHandler(
new SocialAuthenticationSuccessHandler(
authTokenServices,
YOUR_APP_CLIENT_ID
)
);
return filter;
}
});
http
//... lots of other configuration ...
.apply(socialCfg);
}
}
And the SocialAuthenticationSuccessHandler class:
public class SocialAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
public static final String REDIRECT_PATH_BASE = "/#/login";
public static final String FIELD_TOKEN = "access_token";
public static final String FIELD_EXPIRATION_SECS = "expires_in";
private final Logger log = LoggerFactory.getLogger(getClass());
private final AuthorizationServerTokenServices authTokenServices;
private final String localClientId;
public SocialAuthenticationSuccessHandler(AuthorizationServerTokenServices authTokenServices, String localClientId){
this.authTokenServices = authTokenServices;
this.localClientId = localClientId;
}
#Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
log.debug("Social user authenticated: " + authentication.getPrincipal() + ", generating and sending local auth");
OAuth2AccessToken oauth2Token = authTokenServices.createAccessToken(convertAuthentication(authentication)); //Automatically checks validity
String redirectUrl = new StringBuilder(REDIRECT_PATH_BASE)
.append("?").append(FIELD_TOKEN).append("=")
.append(encode(oauth2Token.getValue()))
.append("&").append(FIELD_EXPIRATION_SECS).append("=")
.append(oauth2Token.getExpiresIn())
.toString();
log.debug("Sending redirection to " + redirectUrl);
response.sendRedirect(redirectUrl);
}
private OAuth2Authentication convertAuthentication(Authentication authentication) {
OAuth2Request request = new OAuth2Request(null, localClientId, null, true, null,
null, null, null, null);
return new OAuth2Authentication(request,
//Other option: new UsernamePasswordAuthenticationToken(authentication.getPrincipal(), "N/A", authorities)
new PreAuthenticatedAuthenticationToken(authentication.getPrincipal(), "N/A")
);
}
private String encode(String in){
String res = in;
try {
res = UriUtils.encode(in, GeneralConstants.ENCODING_UTF8);
} catch(UnsupportedEncodingException e){
log.error("ERROR: unsupported encoding: " + GeneralConstants.ENCODING_UTF8, e);
}
return res;
}
}
This way your client app will receive your web app's access token via redirection to /#/login?access_token=my_access_token&expires_in=seconds_to_expiration, as long as you set the corresponding REDIRECT_PATH_BASE in SocialAuthenticationSuccessHandler.
I hope it helps.
First, I would strongly recommend you to move away from the password grant for such a use case.
Public clients (JavaScript, installed applications) cannot keep their client secret confidential, that's why they MUST NOT be assigned one : any visitor inspecting your JavaScript code can discover the secret, and thus implement the same authentication page you have, storing your users passwords in the process.
The implicit grant has been created exactly for what you are doing.
Using a redirection-based flow has the advantage of leaving the authentication mechanism up to the authorization server, instead of having each of your applications have a piece of it : that's mostly the definition of Single Sign On (SSO).
With that said, your question is tightly related to this one I just answered : Own Spring OAuth2 server together with 3rdparty OAuth providers
To sum up the answer :
In the end, it's about how your authorization server secures the AuthorizationEndpoint : /oauth/authorize. Since your authorization server works, you already have a configuration class extending WebSecurityConfigurerAdapter that handles the security for /oauth/authorize with formLogin. That's where you need to integrate social stuff.
You simply cannot use a password grant for what you're trying to achieve, you must have your public client redirect to the authorization server. The authorization server will then redirect to the social login as its security mechanism for the /oauth/authorize endpoint.
I was starting with the good answer of above (https://stackoverflow.com/a/33963286/3351474) however with my version of Spring Security (4.2.8.RELEASE) this fails. The reason is that in org.springframework.security.access.intercept.AbstractSecurityInterceptor#authenticateIfRequired the PreAuthenticatedAuthenticationToken of the answer is not authenticated. Some GrantedAuthorities have to be passed.
In addition sharing the token in an URL parameter is not good, it should always be hidden in an HTTPs payload or header. Instead a HTML template is loaded and the token value is inserted into a ${token} placeholder field.
Here the revised version:
NOTE: The used UserDetails here is implementing org.springframework.security.core.userdetails.UserDetails
#Component
public class SocialAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
#Autowired
private OAuth2TokenStore tokenStore;
#Qualifier("tokenServices")
#Autowired
private AuthorizationServerTokenServices authTokenServices;
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
IClient user = ((SocialUserDetails) authentication.getPrincipal()).getUser();
// registration is not finished, forward the user, a marker interface
// IRegistration is used here, remove this if there no two step approach to
// create a user from a social network
if (user instanceof IRegistration) {
response.sendRedirect(subscriberRegistrationUrl + "/" + user.getId());
}
OAuth2AccessToken token = loginUser(user);
// load a HTML template from the class path and replace the token placeholder within, the HTML should contain a redirect to the actual page, but must store the token in a safe place, e.g. for preventing CSRF in the `sessionStorage` JavaScript storage.
String html = IOUtils.toString(getClass().getResourceAsStream("/html/socialLoginRedirect.html"));
html = html.replace("${token}", token.getValue());
response.getOutputStream().write(html.getBytes(StandardCharsets.UTF_8));
}
private OAuth2Authentication convertAuthentication(Authentication authentication) {
OAuth2Request request = new OAuth2Request(null, authentication.getName(),
authentication.getAuthorities(), true, null,
null, null, null, null);
// note here the passing of the authentication.getAuthorities()
return new OAuth2Authentication(request,
new PreAuthenticatedAuthenticationToken(authentication.getPrincipal(), "N/A", authentication.getAuthorities())
);
}
/**
* Logs in a user.
*/
public OAuth2AccessToken loginUser(IClient user) {
SecurityContext securityContext = SecurityContextHolder.getContext();
UserDetails userDetails = new UserDetails(user);
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, "N/A", userDetails.getAuthorities());
securityContext.setAuthentication(authentication);
OAuth2Authentication oAuth2Authentication = convertAuthentication(authentication);
// delete the token because the client id in the DB is calculated as hash of the username and client id (here also also identical to username), this would be identical to the
// to an existing user. This existing one can come from a user registration or a previous user with the same name.
// If a new entity with a different ID is used the stored token hash would differ and the the wrong token would be retrieved
tokenStore.deleteTokensForUserId(user.getUsername());
OAuth2AccessToken oAuth2AccessToken = authTokenServices.createAccessToken(oAuth2Authentication);
// the DB id of the created user is returned as additional data, can be
// removed if not needed
((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(new HashMap<>());
oAuth2AccessToken.getAdditionalInformation().put("userId", user.getId());
return oAuth2AccessToken;
}
}
Example socialLoginRedirect.html:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Example App</title>
<meta http-equiv="Refresh" content="0; url=/index.html#/home"/>
</head>
<script>
window.sessionStorage.setItem('access_token', '${token}');
</script>
<body>
<p>Please follow this link.</p>
</body>
</html>
The configuration wiring in a WebSecurityConfigurerAdapter:
#Configuration
#EnableWebSecurity
#EnableWebMvc
#Import(WebServiceConfig.class)
public class AuthenticationConfig extends WebSecurityConfigurerAdapter {
#Value("${registrationUrl}")
private String registrationUrl;
#Autowired
private SocialAuthenticationSuccessHandler socialAuthenticationSuccessHandler;
#Value("${loginUrl}")
private String loginUrl;
#Override
protected void configure(HttpSecurity http) throws Exception {
List<String> permitAllUrls = new ArrayList<>();
// permit social log in
permitAllUrls.add("/auth/**");
http.authorizeRequests().antMatchers(permitAllUrls.toArray(new String[0])).permitAll();
SpringSocialConfigurer springSocialConfigurer = new SpringSocialConfigurer();
springSocialConfigurer.signupUrl(registrationUrl);
springSocialConfigurer.postFailureUrl(loginUrl);
springSocialConfigurer
.addObjectPostProcessor(new ObjectPostProcessor<SocialAuthenticationFilter>() {
#SuppressWarnings("unchecked")
public SocialAuthenticationFilter postProcess(SocialAuthenticationFilter filter){
filter.setAuthenticationSuccessHandler(socialAuthenticationSuccessHandler);
return filter;
}
});
http.apply(springSocialConfigurer);
http.logout().disable().csrf().disable();
http.requiresChannel().anyRequest().requiresSecure();
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
I implemented spring oauth2 to secure my rest services and additionally add social login and implicit signup for first time login . for user user you can generate the token using username and password only problem with generate the token for social user . for that you have to implement the Filter that will intercept your /oauth/token request before processing . here if you want to generate the the token for social user pass the username and facebook token , here you can use facebook token as password and generate the token for facebook user also . if facebook token updated then you have to write a db trigger also to update you token in user table .... may be it will help you

Resources