Started to secure some of my resful server resources using Spring Security.
My client is using ajax (jquery ajax) for the requests and I started by implementing the login functionality.
My Jersey web layer includes the following:
#Path("/login")
#Component
public class LoginResourceProvider extends ServiceResourceProvider {
/*--- Static ---*/
private final static ILogger logger = LogManager.getLogger(LoginResourceProvider.class);
/*--- Members ---*/
#Inject
#Qualifier("authenticationManager")
protected AuthenticationManager authenticationManager;
#Inject
protected SecurityContextRepository repository;
#Inject
protected RememberMeServices rememberMeServices;
/*--- Constructors ---*/
public LoginResourceProvider() {
super("Login");
}
/*--- Public Methods ---*/
#GET
public void login() {
}
/**
* A user login attempt
*
* #param username
* The user name
* #param password
* The password of the given user name
* #param request
* #param response
* #return A JSON string, indicating if the login is successful
*/
#POST
#Produces(MediaType.APPLICATION_JSON)
public String performLogin(#QueryParam("j_username") String username, #QueryParam("j_password") String password,
#Context HttpServletRequest request, #Context HttpServletResponse response) {
// Create a token
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);
SecurityContext securityContext = SecurityContextHolder.getContext();
try {
// Attempting to authenticate the user
Authentication auth = authenticationManager.authenticate(token);
// Updating the SecurityContext, which represents the user's
// secured, authenticated session
securityContext.setAuthentication(auth);
// If the user authenticates successfully then the authentication
// storing the security context in the HttpSession between requests
repository.saveContext(securityContext, request, response);
// object is passed to the remember-me service
rememberMeServices.loginSuccess(request, response, auth);
// Successfully authenticated
return "{\"status\": true}";
// Bad Credentials
} catch (BadCredentialsException ex) {
return "{\"status\": false, \"error\": \"Bad Credentials\"}";
}
}
}
My security-context.xml is pretty basic for now, just enough to test my the login process:
<http use-expressions="true">
<form-login />
<remember-me />
<intercept-url pattern="/**" access="permitAll" />
<intercept-url pattern="/secured/**" access="isAuthenticated()" />
</http>
<authentication-manager alias="authenticationManager">
<authentication-provider>
<user-service>
<user name="bob" password="bobspassword" authorities="ROLE_USER" />
</user-service>
</authentication-provider>
</authentication-manager>
I have 2 questions:
Is it a good practice? I mean, I could not find lot's of "non-auto" login for ajax style requests there.
I'm getting an exception when trying to save the security context to the SecurityContextRepository, in this line:
repository.saveContext(securityContext, request, response);
When I'm trying to log in using bob as username and bobspassword password the authentication goes smoothly but while debugging this specific line I'm jumping to a ClassCastException with the message:
$Proxy31 cannot be cast to org.springframework.security.web.context.SaveContextOnUpdateOrErrorResponseWrapper
Any help is appreciated!
Ok, I think I got it.
According to Spring documentation authentication is done using the following steps:
The username and password are obtained and combined into an instance of UsernamePasswordAuthenticationToken (an instance of the Authentication interface, which we saw earlier).
The token is passed to an instance of AuthenticationManager for validation.
The AuthenticationManager returns a fully populated Authentication instance onsuccessful authentication.
The security context is established by calling SecurityContextHolder.getContext().setAuthentication(...) , passing in the returned authentication object.
In addition to the above steps, I also tried to store the SecurityContext in between requests by saving it to the SecurityContextRepository.
The responsibility for storing the SecurityContext between requests should fall to the SecurityContextPersistenceFilter which in it's turn invokes this operation, so no need for me to do it manually, I guess I should only stick to the above 4 steps.
UPDATE: I guess I tried implementing on my own something that Spring-Security already implements for me. I do not recommend following this approach, Spring-Security offers a much more simple practice.
Related
We use Spring Security 4.0.x and I need to find the way to access the logged out user name.
I have configured LogoutSuccessHandler:
<logout logout-url="/logout" success-handler-ref="logoutSuccessHandler" />
I see the authentication object in the method signature:
onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
Unfortunately, the authentication object is empty.
I see that the LogoutHandler (SecurityContextLogoutHandler) clears the authentication before logoutSuccessHandler but I can not find the way how to configure LogoutHandler via <logout .. configuration.
How is possible to access to the logged out user name in Spring Security?
if (requiresLogout(request, response)) {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (logger.isDebugEnabled()) {
logger.debug("Logging out user '" + auth
+ "' and transferring to logout destination");
}
this.handler.logout(request, response, auth);
logoutSuccessHandler.onLogoutSuccess(request, response, auth);
return;
}
As you can see, the filter has got the Authentication, So even the SecurityContextLogoutHandler clears the Authentication in SecurityContextHolder, the auth still holds the Authentication, Do you have any other code that clear Authentication before LogoutFilter ?
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
I created a custom security filter that handles LOGOUT_FILTER. Debugging my app, it does reach to #Override public void onLogoutSuccess.
I need to be redirected to a different domain/context/url outside my app. However, response.sendRedirect doesnt redirect at all to that url.
In Spring Security 3.1 book from RWinch, apparently you can do this, here's the snippet:
<http ...>
...
<logout logout-url="/logout"
logout-success-url="https://${cas.server.host}/cas/logout"/>
</http>
CAS Server is in another context/domain which is different from context app.
You can use LogoutSuccessHandler :
pHttp.logout()
.logoutSuccessHandler(new LogoutSuccessHandler() {
#Override
public void onLogoutSuccess(HttpServletRequest pRequest,
HttpServletResponse pResponse,
Authentication pAuthentication) throws IOException,
ServletException {
pResponse.sendRedirect("https://${cas.server.host}/cas/logout");
}
});
I let you translate it in XML config ;)
I am trying to build a Spring MVC application and securing it with Spring Security OAuth2 and the provider is Google. I was able to get the web app working without security and with form login. However, I am not able to get OAuth with google to work. Google app setup is fine as I can get the call backs etc to work with a non Spring Security app.
My security config is as follows:
<?xml version="1.0" encoding="UTF-8"?>
<b:beans xmlns:sec="http://www.springframework.org/schema/security"
xmlns:b="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">
<sec:http use-expressions="true" entry-point-ref="clientAuthenticationEntryPoint">
<sec:http-basic/>
<sec:logout/>
<sec:anonymous enabled="false"/>
<sec:intercept-url pattern="/**" access="isFullyAuthenticated()"/>
<sec:custom-filter ref="oauth2ClientContextFilter" after="EXCEPTION_TRANSLATION_FILTER"/>
<sec:custom-filter ref="googleAuthenticationFilter" before="FILTER_SECURITY_INTERCEPTOR"/>
</sec:http>
<b:bean id="clientAuthenticationEntryPoint" class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint"/>
<sec:authentication-manager alias="alternateAuthenticationManager">
<sec:authentication-provider>
<sec:user-service>
<sec:user name="user" password="password" authorities="DOMAIN_USER"/>
</sec:user-service>
</sec:authentication-provider>
</sec:authentication-manager>
</b:beans>
The OAuth2 protected resource is as follows:
#Configuration
#EnableOAuth2Client
class ResourceConfiguration {
#Autowired
private Environment env;
#Resource
#Qualifier("accessTokenRequest")
private AccessTokenRequest accessTokenRequest;
#Bean
public OAuth2ProtectedResourceDetails googleResource() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setId("google-app");
details.setClientId(env.getProperty("google.client.id"));
details.setClientSecret(env.getProperty("google.client.secret"));
details.setAccessTokenUri(env.getProperty("google.accessTokenUri"));
details.setUserAuthorizationUri(env.getProperty("google.userAuthorizationUri"));
details.setTokenName(env.getProperty("google.authorization.code"));
String commaSeparatedScopes = env.getProperty("google.auth.scope");
details.setScope(parseScopes(commaSeparatedScopes));
details.setPreEstablishedRedirectUri(env.getProperty("google.preestablished.redirect.url"));
details.setUseCurrentUri(false);
details.setAuthenticationScheme(AuthenticationScheme.query);
details.setClientAuthenticationScheme(AuthenticationScheme.form);
return details;
}
private List<String> parseScopes(String commaSeparatedScopes) {
List<String> scopes = newArrayList();
Collections.addAll(scopes, commaSeparatedScopes.split(","));
return scopes;
}
#Bean
public OAuth2RestTemplate googleRestTemplate() {
return new OAuth2RestTemplate(googleResource(), new DefaultOAuth2ClientContext(accessTokenRequest));
}
#Bean
public AbstractAuthenticationProcessingFilter googleAuthenticationFilter() {
return new GoogleOAuthentication2Filter(new GoogleAppsDomainAuthenticationManager(), googleRestTemplate(), "https://accounts.google.com/o/oauth2/auth", "http://localhost:9000");
}
}
The custom authentication filter which I have written to throw a Redirect exception to get the OAuth2 authorization is as follows:
#Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
try {
logger.info("OAuth2 Filter Triggered!! for path {} {}", request.getRequestURI(), request.getRequestURL().toString());
logger.info("OAuth2 Filter hashCode {} request hashCode {}", this.hashCode(), request.hashCode());
String code = request.getParameter("code");
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
logger.info("Code is {} and authentication is {}", code, authentication == null ? null : authentication.isAuthenticated());
// not authenticated
if (requiresRedirectForAuthentication(code)) {
URI authURI = new URI(googleAuthorizationUrl);
logger.info("Posting to {} to trigger auth redirect", authURI);
String url = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + oauth2RestTemplate.getAccessToken();
logger.info("Getting profile data from {}", url);
// Should throw RedirectRequiredException
oauth2RestTemplate.getForEntity(url, GoogleProfile.class);
// authentication in progress
return null;
} else {
logger.info("OAuth callback received");
// get user profile and prepare the authentication token object.
String url = "https://www.googleapis.com/oauth2/v2/userinfo?access_token=" + oauth2RestTemplate.getAccessToken();
logger.info("Getting profile data from {}", url);
ResponseEntity<GoogleProfile> forEntity = oauth2RestTemplate.getForEntity(url, GoogleProfile.class);
GoogleProfile profile = forEntity.getBody();
CustomOAuth2AuthenticationToken authenticationToken = getOAuth2Token(profile.getEmail());
authenticationToken.setAuthenticated(false);
Authentication authenticate = getAuthenticationManager().authenticate(authenticationToken);
logger.info("Final authentication is {}", authenticate == null ? null : authenticate.isAuthenticated());
return authenticate;
}
} catch (URISyntaxException e) {
Throwables.propagate(e);
}
return null;
}
The filter chain sequence from the Spring web app is as follows:
o.s.b.c.e.ServletRegistrationBean - Mapping servlet: 'dispatcherServlet' to [/]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'metricFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'oauth2ClientContextFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'googleOAuthFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'org.springframework.security.filterChainProxy' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'org.springframework.security.web.access.intercept.FilterSecurityInterceptor#0' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'applicationContextIdFilter' to: [/*]
o.s.b.c.e.FilterRegistrationBean - Mapping filter: 'webRequestLoggingFilter' to: [/*]
The redirect to Google works fine and I get the callback to the filter and the authentication is successful. However after that, the request results in a redirect and it invokes the filter again (the request is the same, I have checked the hasCode). On the second call the authentication in the SecurityContext is null. As part of the first authentication call the Authentication object was populated in the security context, so why does it disappear?
I am working with Spring Security for the first time so may have made newbie mistake.
After playing around with Spring Security configuration and the filters I was finally able to get this working. I had to make couple of important changes
I used a standard Spring OAuth2 filter (org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter) instead of the custom filter I was using.
Change the intercept URL of the authentication filter to be /googleLogin and added an authentication entry point which redirects to this URL on authentication failure.
Overall the flow is as follows
Browser accesses / and the request passes through the OAuth2ClientContextFilter and OAuth2ClientAuthenticationProcessingFilter as the context does not match. The configured context path for login is /googleLogin
The security interceptor FilterSecurityInterceptor detects that the the user is anonymous and throws an access denied exception.
Spring security's ExceptionTranslationFilter catches the access denied exception and asks the configured authentication entry point to handle it which issues a redirect to /googleLogin.
For the request /googleLogin, the filter OAuth2AuthenticationProcessingFilter tries to access the Google protected resource and an UserRedirectRequiredException is thrown which is translated into a HTTP redirect to Google (with the OAuth2 details) by OAuth2ClientContextFilter.
On successful authentication from Google the browser is redirected back to /googleLogin with the OAuth code. The filter OAuth2AuthenticationProcessingFilter handles this and creates an Authentication object and updates the SecurityContext.
At this point the user is fully authenticated and redirect to / is issued by the OAuth2AuthenticationProcessingFilter.
FilterSecurityInterceptor allows the request to proceed as the SecurityContext contains an Authentication object which is authenticated.
Finally the application page which is secured using an expression like isFullyAuthenticated() or similar is rendered.
The security context xml is as follows:
<sec:http use-expressions="true" entry-point-ref="clientAuthenticationEntryPoint">
<sec:http-basic/>
<sec:logout/>
<sec:anonymous enabled="false"/>
<sec:intercept-url pattern="/**" access="isFullyAuthenticated()"/>
<!-- This is the crucial part and the wiring is very important -->
<!--
The order in which these filters execute are very important. oauth2ClientContextFilter must be invoked before
oAuth2AuthenticationProcessingFilter, that's because when a redirect to Google is required, oAuth2AuthenticationProcessingFilter
throws a UserRedirectException which the oauth2ClientContextFilter handles and generates a redirect request to Google.
Subsequently the response from Google is handled by the oAuth2AuthenticationProcessingFilter to populate the
Authentication object and stored in the SecurityContext
-->
<sec:custom-filter ref="oauth2ClientContextFilter" after="EXCEPTION_TRANSLATION_FILTER"/>
<sec:custom-filter ref="oAuth2AuthenticationProcessingFilter" before="FILTER_SECURITY_INTERCEPTOR"/>
</sec:http>
<b:bean id="oAuth2AuthenticationProcessingFilter" class="org.springframework.security.oauth2.client.filter.OAuth2ClientAuthenticationProcessingFilter">
<b:constructor-arg name="defaultFilterProcessesUrl" value="/googleLogin"/>
<b:property name="restTemplate" ref="googleRestTemplate"/>
<b:property name="tokenServices" ref="tokenServices"/>
</b:bean>
<!--
These token classes are mostly a clone of the Spring classes but have the structure modified so that the response
from Google can be handled.
-->
<b:bean id="tokenServices" class="com.rst.oauth2.google.security.GoogleTokenServices">
<b:property name="checkTokenEndpointUrl" value="https://www.googleapis.com/oauth2/v1/tokeninfo"/>
<b:property name="clientId" value="${google.client.id}"/>
<b:property name="clientSecret" value="${google.client.secret}"/>
<b:property name="accessTokenConverter">
<b:bean class="com.rst.oauth2.google.security.GoogleAccessTokenConverter">
<b:property name="userTokenConverter">
<b:bean class="com.rst.oauth2.google.security.DefaultUserAuthenticationConverter"/>
</b:property>
</b:bean>
</b:property>
</b:bean>
<!--
This authentication entry point is used for all the unauthenticated or unauthorised sessions to be directed to the
/googleLogin URL which is then intercepted by the oAuth2AuthenticationProcessingFilter to trigger authentication from
Google.
-->
<b:bean id="clientAuthenticationEntryPoint" class="org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint">
<b:property name="loginFormUrl" value="/googleLogin"/>
</b:bean>
Also the Java Config for the OAuth2 resources is as follows:
#Configuration
#EnableOAuth2Client
class OAuth2SecurityConfiguration {
#Autowired
private Environment env;
#Resource
#Qualifier("accessTokenRequest")
private AccessTokenRequest accessTokenRequest;
#Bean
#Scope("session")
public OAuth2ProtectedResourceDetails googleResource() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setId("google-oauth-client");
details.setClientId(env.getProperty("google.client.id"));
details.setClientSecret(env.getProperty("google.client.secret"));
details.setAccessTokenUri(env.getProperty("google.accessTokenUri"));
details.setUserAuthorizationUri(env.getProperty("google.userAuthorizationUri"));
details.setTokenName(env.getProperty("google.authorization.code"));
String commaSeparatedScopes = env.getProperty("google.auth.scope");
details.setScope(parseScopes(commaSeparatedScopes));
details.setPreEstablishedRedirectUri(env.getProperty("google.preestablished.redirect.url"));
details.setUseCurrentUri(false);
details.setAuthenticationScheme(AuthenticationScheme.query);
details.setClientAuthenticationScheme(AuthenticationScheme.form);
return details;
}
private List<String> parseScopes(String commaSeparatedScopes) {
List<String> scopes = newArrayList();
Collections.addAll(scopes, commaSeparatedScopes.split(","));
return scopes;
}
#Bean
#Scope(value = "session", proxyMode = ScopedProxyMode.INTERFACES)
public OAuth2RestTemplate googleRestTemplate() {
return new OAuth2RestTemplate(googleResource(), new DefaultOAuth2ClientContext(accessTokenRequest));
}
}
I had to override some of the Spring classes as the format of the token from Google and the one expected by Spring don't match. So there is some custom handiwork required there.
Not to impose a better answer, but another reason for this redirect looping is the session cookie handling when the session cookie sameSite attribute is set to Strict. Then, if the authority service has broken the redirect chain, the session cookie will not be transmitted.
See: How can I redirect after OAUTH2 with SameSite=Strict and still get my cookies?
This is a Spring Security question.
In my application, I have a User entity as a domain object. Users will be registered and will be logging in with credentials stored in the database. My User domain object contains implementation to support Spring UserDetails object.
The challenge is that I need an ability to log into the application even before the first user is created. In other words, I need to log in as 'admin' to create the 'admin' user.
To make sure my Spring setup is working, I'm currently returning the hardcoded admin user from SpringSecurityUserDetailsServiceImpl.loadUserByUsername(String userName).
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException, DataAccessException {
User user=null;
try {
if("admin".equalsIgnoreCase(userName)) {
user=new User();
user.setUserName("ADMIN");
user.setPassword("adsf"); // assume there's a hash of a true password here
user.setStatus(UserStatus.ACTIVE);
user.setAccessLevel(UserAccessLevel.ADMINISTRATOR);
} else {
//user = userDAO.getUserByUserName(userName);
}
} catch(Throwable t) {
throw new UsernameNotFoundException("Unable to locate User with user name \"" + userName + "\".", t);
}
return user;
}
This works, so now, I'm looking for the right way to do it. One would be to define this default admin user credentials in a properties file and read that properties file within loadUserByUsername(String userName) to construct the admn user object. However, I'm hoping there is a way to do this within the Spring Security xml configuration. I tried security:user name="admin" password="admin" authorities="ADMINISTRATOR" but that apparently does not work when you have security:authentication-provider user-service-ref="customUserDetailsService"
My spring-security.xml
<security:http auto-config="true" use-expressions="true" access-denied-page="/denied">
<security:intercept-url pattern="/login.html" access="permitAll"/>
<security:intercept-url pattern="/style/**" access="permitAll"/>
<security:intercept-url pattern="/user**" access="hasRole('ADMINISTRATOR')"/>
<security:intercept-url pattern="/**" access="hasRole('AUTHOR')"/>
<security:form-login login-page="/login.html"
login-processing-url="/j_spring_security_check"
authentication-failure-url="/login.html?failedAttempt=true"
default-target-url="/home.html"/>
<security:logout invalidate-session="true"
logout-success-url="/login"
logout-url="/logout"/>
</security:http>
<security:authentication-manager>
<security:authentication-provider user-service-ref="customUserDetailsService">
<security:password-encoder ref="passwordEncoder"/>
</security:authentication-provider>
</security:authentication-manager>
<bean class="org.springframework.security.authentication.encoding.Md5PasswordEncoder" id="passwordEncoder"/>
<bean id="customUserDetailsService" class="com.modelsite.services.impl.SpringSecurityUserDetailsServiceImpl"/>
So the question is: how do I define a default admin user that is able to log in and do stuff. Please note, I do not want to handle this with sql imports at set up times.
You can have multiple authentication providers:
Use the first like you already did.
Add a second with fixed name, password and role for the admin.
(The order of both authentication providers is important; the second is only taken into account if the authentication is not found in the first.)
<security:authentication-manager>
<security:authentication-provider user-service-ref="customUserDetailsService">
<security:password-encoder ref="passwordEncoder"/>
</security:authentication-provider>
<security:authentication-provider>
<security:user-service>
<security:user name="admin" password="admin" authorities="ROLE_USER, ROLE_ADMIN" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
#see also: Can I have multiple security contexts with spring security?
Personally, for the admin account I won't go with the basic Spring Security user service, mainly because it lacks the flexibility of a DB-based user management approach. Indeed, you probably don't want to have your admin credentials established once for all, since they can be guessed or stolen or simply forgotten.
Conversely, both password modification and recovery mechanisms should be put in place for all accounts, including the admin one (provided you use a trusted email account for password recovery, but this is a reasonable assumption).
Getting concrete, my approach is the following:
I use an AuthenticationManager where I inject a CustomUserDetailService
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="customUserDetailsService" >
<password-encoder ref="passwordEncoder" />
</authentication-provider>
</authentication-manager>
<b:bean id="passwordEncoder"
class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
which is the following
#Service
public class CustomUserDetailsService implements UserDetailsService{
#Autowired
#Qualifier("userDaoImpl")
private UserDao userDaoImpl;
#Override
#Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
User user = userDaoImpl.loadByUsername(username);
if (user != null)
return user;
else
throw new UsernameNotFoundException(username + " not found.");
}
}
this works for all users, not only the admin.
Now it comes the problem of having the admin account full functional when the application starts. This is accomplished by using an initialization bean to be executed at startup, detailed in the following
#Component
public class Initializer {
#Autowired
private HibernateTransactionManager transactionManager;
#Autowired
#Qualifier("userDaoImpl")
private UserDao userDao;
#Autowired
private CredentialsManager credentialsManager;
private String resetPassword = "makeItHardToGuess";
private String adminUsername = "admin";
#PostConstruct
private void init()
{
//since we are executing on startup, we need to use a TransactionTemplate directly as Spring may haven't setup transction capabilities yet
TransactionTemplate trxTemplate = new TransactionTemplate(transactionManager);
trxTemplate.execute(new TransactionCallbackWithoutResult() {
#Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
buildAdmin();
}
});
}
private void buildAdmin()
{
//here I try to retrieve the Admin from my persistence layer
ProfiledUser admin = userDao.loadByUsername(adminUsername);
try
{
//If the application is started for the first time (e.g., the admin is not in the DB)
if(admin==null)
{
//create a user for the admin
admin = new ProfiledUser();
//and fill her attributes accordingly
admin.setUsername(adminUsername);
admin.setPassword(credentialsManager.encodePassword(resetPassword));
admin.setAccountNonExpired(true);
admin.setAccountNonLocked(true);
admin.setCredentialsNonExpired(true);
admin.setEnabled(true);
admin.setEulaAccepted(true);
Authority authority = new Authority();
authority.setAuthority("ROLE_ADMIN");
admin.getAuthorities().add(authority);
}
//if the application has previously been started (e.g., the admin is already present in the DB)
else
{
//reset admin's attributes
admin.setPassword(credentialsManager.encodePassword(resetPassword));
admin.getAuthorities().clear();
Authority authority = new Authority();
authority.setAuthority("ROLE_ADMIN");
admin.getAuthorities().add(authority);
admin.setAccountNonExpired(true);
admin.setAccountNonLocked(true);
admin.setCredentialsNonExpired(true);
admin.setEnabled(true);
}
userDao.save(admin);
}
catch (Exception e)
{
e.printStackTrace();
System.out.println("Errors occurred during initialization. System verification is required.");
}
}
}
please note that the #PostConstruct annotation does not guarantee that spring has its transaction services available, that's why I had to manage the transaction my own. Please refer to this for more details.
The challenge is that I need an ability to log into the application even before the first user is created. In other words, I need to log in as 'admin' to create the 'admin' user.
The way I deal with this problem is to put some smarts into my custom UserDetailsService class and/or its DAO class. When it detects that it has been started with empty user details tables (or something), it initializes them with some user details entries that it reads from a configuration file. This allows you to:
load the initial admin account into your production system's user details store
load a bunch of test accounts into your test system's user details store for automated unit and system testing.
If that's too much work, just create some SQL statements to insert the relevant rows for the admin command and run them using your database's interactive SQL shell.
Embedding the admin account into your source code is a bad idea because:
anyone who can see your sourcecode can see the password (unless you use a hash),
it means that you need to modify and recompile the code to change the password, and
it means that you'll use the same password in testing and production (unless you add that distinction to your code as well).
These all raise security issues.
the answer by MaVVamaldo is cool (gave my +1 vote already) apart from the Initializer class. That class is great to initialise the database but it should avoid hard-coding the admin credentials which is unsafe as the source code can be easily retrieved (and it's what the original question asked to avoid in the first place).
A better solution IMHO would be to load the hashed credentials from a .properties file (to which you restrict the access via chmod or similar).
for this to work you need to have the following in your security-context.xml
<authentication-manager>
<authentication-provider>
<password-encoder hash="sha">
<salt-source user-property="username"/>
</password-encoder>
<user-service properties="classpath:/users.properties" />
</authentication-provider>
</authentication-manager>
where the .properties file looks like this:
bob=4f393f2314f75650ee50844d8e4f016ab5b3468f,ROLE_ADMIN,enabled
the salt is the username so you calculate it over the string password{username}
as explained.