I am struggling for a few hours to create a small authentication system using Spring Security but I have not had luck yet.
What I want is a login form which goes through AngularJS to a database and searches with the provided data. For the purpose of making it work, I tried to use an in memory database.
Here is the code I used:
#RestController
#RequestMapping("/authentication")
public class AuthenticationController {
#RequestMapping("/user")
public Principal user(Principal user) {
return user;
}
#Configuration
#Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
protected static class SecurityConfiguration extends WebSecurityConfigurerAdapter {
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/index.html", "/home.html", "/login.html", "/lib/**", "/app/**", "/css/**", "/js/**").permitAll().anyRequest()
.authenticated();
http.formLogin().loginPage("/login.html").defaultSuccessUrl("/index.html")
.failureUrl("/login?error").permitAll();
// Logout
http.logout().logoutUrl("/logout").logoutSuccessUrl("/login?logout")
.permitAll();
}
#Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
// Authorization
auth.inMemoryAuthentication().withUser("user").password("p1")
.roles("USER");
auth.inMemoryAuthentication().withUser("root").password("p2")
.roles("USER", "ADMIN");
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
}
}
This is the authenticate function from Angular:
function($rootScope, $scope, $http, $location) {
var authenticate = function(credentials, callback) {
var headers = credentials ? {
authorization : "Basic "
+ btoa(credentials.username + ":"
+ credentials.password)
} : {};
$http.get('authentication/user', {
headers : headers
}).success(function(data) {
console.log(data.name);
if (data.name) {
$rootScope.authenticated = true;
} else {
$rootScope.authenticated = false;
}
callback && callback();
}).error(function() {
$rootScope.authenticated = false;
callback && callback();
});
}
authenticate();
$scope.credentials = {};
$scope.login = function() {
authenticate($scope.credentials, function() {
if ($rootScope.authenticated) {
$location.path("/index.html");
$scope.error = false;
} else {
//$location.path("/login");
$scope.error = true;
}
});
};
});
Do you have any idea why this isn't working? The provided code I took from a few tutorials which are very similar. I tried a slightly different way previous to this which used httpBasic but still am not getting the desired result.
Any advice? Thank you!
It is quite late in my timezone and cannot test & execute your code :), but from my point of view you do not have a chance to run your code properly.
You are inappropriately mixing basic authentication with form login authentication. You declare form login:
http.formLogin().loginPage("/login.html").defaultSuccessUrl("/index.html")
.failureUrl("/login?error").permitAll();
which briefly means that you require form login with POST to /login.html page.
Form login means that you should post a form using angular with username & password fields included (as if you posted a normal form using POST method). Therefore you should have something like this in authenticate function:
var authenticate = function(credentials, callback) {
$http.post('login.html', {username: username, password: password} ).success(function(data) {
// do something
}).error(function() {
// do something
});
}
Just please be forgiving if something does not compile above because I simply cannot test it/verify it.
Related
I have the simplest oauth2 client:
#EnableAutoConfiguration
#Configuration
#EnableOAuth2Sso
#RestController
public class ClientApplication {
#RequestMapping("/")
public String home(Principal user, HttpServletRequest request, HttpServletResponse response) throws ServletException {
return "Hello " + user.getName();
}
public static void main(String[] args) {
new SpringApplicationBuilder(ClientApplication.class)
.properties("spring.config.name=application").run(args);
}
}
I also have the following application.yml:
server:
port: 9999
servlet:
context-path: /client
security:
oauth2:
client:
client-id: acme
client-secret: acmesecret
access-token-uri: http://localhost:8080/oauth/token
user-authorization-uri: http://localhost:8080/oauth/authorize
resource:
user-info-uri: http://localhost:8080/me
logging:
level:
org.springframework.security: DEBUG
org.springframework.web: DEBUG
It is the full code. I don't have any additional source code. It works properly.
But now I want to add a logout feature. I've added an endpoint but it doesn't work. I tried to do the following:
#RequestMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response) throws ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
authentication.setAuthenticated(false);
new SecurityContextLogoutHandler().logout(request,response,authentication);
SecurityContextHolder.clearContext();
request.logout();
request.getSession().invalidate();
}
But I am still logged in and can access / url and it responds to me with the username.
Can you help me fix this issue?
Update
I tried the approach described here https://spring.io/guides/tutorials/spring-boot-oauth2/#_social_login_logout :
#EnableAutoConfiguration
#Configuration
#EnableOAuth2Sso
#Controller
public class ClientApplication extends WebSecurityConfigurerAdapter {
private Logger logger = LoggerFactory.getLogger(ClientApplication.class);
#RequestMapping("/hello")
public String home(Principal user, HttpServletRequest request, HttpServletResponse response, Model model) throws ServletException {
model.addAttribute("name", user.getName());
return "hello";
}
#Override
protected void configure(HttpSecurity http) throws Exception {
// #formatter:off
http.antMatcher("/**")
.authorizeRequests()
.antMatchers( "/login**", "/webjars/**", "/error**").permitAll()
.anyRequest()
.authenticated()
.and().logout().logoutSuccessUrl("/").permitAll()
.and()
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
// #formatter:on
}
public static void main(String[] args) {
new SpringApplicationBuilder(ClientApplication.class)
.properties("spring.config.name=application").run(args);
}
}
and on FE I wrote:
<script type="text/javascript">
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (settings.type == 'POST' || settings.type == 'PUT'
|| settings.type == 'DELETE') {
if (!(/^http:.*/.test(settings.url) || /^https:.*/
.test(settings.url))) {
// Only send the token to relative URLs i.e. locally.
xhr.setRequestHeader("X-XSRF-TOKEN",
Cookies.get('XSRF-TOKEN'));
}
}
}
});
var logout = function () {
$.post("/client/logout", function () {
$("#user").html('');
$(".unauthenticated").show();
$(".authenticated").hide();
});
return true;
};
$(function() {
$("#logoutButton").on("click", function () {
logout();
});
});
</script>
and
<input type="button" id="logoutButton" value="Logout"/>
But it still doesn't work. It results in the following behavior:
Post http://localhost:9999/client/logout redirects to the http://localhost:9999/client but this page doesn't exist
source code on gitub:
client - https://github.com/gredwhite/logour_social-auth-client (use localhost:9999/client/hello url)
server - https://github.com/gredwhite/logout_social-auth-server
You can delete the refresh token as well as access token from database to save space.
#PostMapping("/oauth/logout")
public ResponseEntity<String> revoke(HttpServletRequest request) {
try {
String authorization = request.getHeader("Authorization");
if (authorization != null && authorization.contains("Bearer")) {
String tokenValue = authorization.replace("Bearer", "").trim();
OAuth2AccessToken accessToken = tokenStore.readAccessToken(tokenValue);
tokenStore.removeAccessToken(accessToken);
//OAuth2RefreshToken refreshToken = tokenStore.readRefreshToken(tokenValue);
OAuth2RefreshToken refreshToken = accessToken.getRefreshToken();
tokenStore.removeRefreshToken(refreshToken);
}
} catch (Exception e) {
return ResponseEntity.badRequest().body("Invalid access token");
}
return ResponseEntity.ok().body("Access token invalidated successfully");
}
The URL to logout will be : http://localhost:9999/oauth/logout
Also, pass the access token in the Authorization header, as
Authorization: Bearer 0cb72897-c4f7-4f01-aed9-2f3f79a75484
where, 0cb72897-c4f7-4f01-aed9-2f3f79a75484 is the access token.
Since, its Spring security, don't forget to bypass /oauth/logout url from authorize access, as
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/hello", "/oauth/logout");
}
Hope, it will solve your logout problem in Springboot2+Oauth2. Its working for me.
Add following code snippet to your ClientApplication class. This will also clear your session details.
Replace below code with the configure method of your web security adapter class.
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.authorizeRequests()
.antMatchers( "/login**", "/webjars/**", "/error**").permitAll()
.anyRequest()
.authenticated()
.and().logout().invalidateHttpSession(true)
.clearAuthentication(true).logoutSuccessUrl("/login?logout").deleteCookies("JSESSIONID").permitAll().and().csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse());
}
You probably want to use the Spring Security built-in support for the /logout endpoint which will do the right thing (clear the session and invalidate the cookie). To configure the endpoint extend the existing configure() method in our WebSecurityConfigurer:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.antMatcher("/**")
.and().logout().logoutSuccessUrl("/").permitAll();
}
You can change Post to
Get http://localhost:9999/client/logout
it works for me
Try to add logout url to your security configuration.
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.permitAll();
I'm working on a Spring MVC web app and I'm using AngularJS, I want to protect my views based on roles, example if I login with and admin role I can watch the delete users partial page, and if I login with user role I can't watch the delete user partial page.
I knew how to do this with spring security but with a normal web app not with a restful Single page app with angularJS, since with angular you are not really requesting a view from the server instead you just load a partial html page in a ng-view tag.
I'm also using ngRoute to route my partials.
Here is my ngRoute.jS file
angular.module('MyApp')
.config(['$routeProvider', 'USER_ROLES' ,function ($routeProvider, USER_ROLES) {
$routeProvider.
when('/', {
templateUrl: '/SpringMVCTemplateAnn/resources/angular/templates/dashboardTemplates/dashboardTemplate.html',
controller: 'DashBoardCtrl',
access: {
loginRequired: true,
authorizedRoles: [USER_ROLES.all]
}
}).
when('/login', {
templateUrl: '/SpringMVCTemplateAnn/resources/angular/templates/loginTemplate/login.html',
controller: 'LoginController',
access: {
loginRequired: false,
authorizedRoles: [USER_ROLES.all]
}
}).
when('/createUser', {
templateUrl: '/SpringMVCTemplateAnn/views/partials/userTemplates/createUser.html',
controller: 'UserCtrl'
}).
when('/deleteUser', {
templateUrl: '/SpringMVCTemplateAnn/views/partials/userTemplates/deleteUser.html',
controller: 'UserCtrl'
}).
}]);
here is my spring security configuration I'm using java annotation configuration, I have adapted the configuration to convert it so it could fit a rest application
this is my Security class that extends WebSecuirtyConfigureAdapter
public class SegurityConfig extends WebSecurityConfigurerAdapter {
public SegurityConfig() {
super();
}
#Autowired
private UserDetailsService userDetailsService;
#Autowired
private RestUnauthorizedEntryPoint restAuthenticationEntryPoint;
#Autowired
private AccessDeniedHandler restAccessDeniedHandler;
#Autowired
private AuthenticationSuccessHandler restAuthenticationSuccessHandler;
#Autowired
private AuthenticationFailureHandler restAuthenticationFailureHandler;
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
#Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/resources/**", "/index.jsp", "/login.jsp",
"/template/**", "/", "/error/**");
}
//
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.headers().disable()
.csrf().disable()
.authorizeRequests()
.antMatchers("/failure").permitAll()
.antMatchers("/v2/api-docs").hasAnyAuthority("admin")
.antMatchers("/users/**").hasAnyAuthority("admin")
.antMatchers("/views/partials/userTemplates/createUser.html").access("hasRole('create users')")
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(restAuthenticationEntryPoint)
.accessDeniedHandler(restAccessDeniedHandler)
.and()
.formLogin()
.loginProcessingUrl("/login")
.successHandler(restAuthenticationSuccessHandler)
.failureHandler(restAuthenticationFailureHandler)
.usernameParameter("username")
.passwordParameter("password")
.permitAll()
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
.deleteCookies("JSESSIONID")
.permitAll()
.and();
}
}
I'm also using the http-auth-interceptor plugin for angularjs
here is MyApp module
angular.module('MyApp', ['ngRoute', 'ui.bootstrap', 'smart-table', 'http-auth-interceptor']);
and here is my js file that prevents users for navigating the site without being authenticated
angular.module('MyApp')
.run(function ($rootScope, $location, $http, AuthSharedService, Session,
USER_ROLES, $q, $timeout) {
$rootScope.$on('$routeChangeStart', function (event, next) {
if (next.originalPath === "/login" && $rootScope.authenticated) {
event.preventDefault();
console.log('registrese');
} else if (next.access && next.access.loginRequired && !$rootScope.authenticated) {
event.preventDefault();
$rootScope.$broadcast("event:auth-loginRequired", {});
} else if (next.access && !AuthSharedService.isAuthorized(next.access.authorizedRoles)) {
event.preventDefault();
$rootScope.$broadcast("event:auth-forbidden", {});
}
});
// Call when the the client is confirmed
$rootScope.$on('event:auth-loginConfirmed', function (event, data) {
console.log('login confirmed start ' + data);
$rootScope.loadingAccount = false;
var nextLocation = ($rootScope.requestedUrl ? $rootScope.requestedUrl : "/home");
var delay = ($location.path() === "/loading" ? 1500 : 0);
$timeout(function () {
Session.create(data);
$rootScope.account = Session;
$rootScope.authenticated = true;
$location.path(nextLocation).replace();
}, delay);
});
// Call when the 401 response is returned by the server
$rootScope.$on('event:auth-loginRequired', function (event, data) {
if ($rootScope.loadingAccount && data.status !== 401) {
$rootScope.requestedUrl = $location.path()
$location.path('/loading');
} else {
Session.invalidate();
$rootScope.authenticated = false;
$rootScope.loadingAccount = false;
$location.path('/login');
}
});
// Call when the 403 response is returned by the server
$rootScope.$on('event:auth-forbidden', function (rejection) {
$rootScope.$evalAsync(function () {
$location.path('/error/403').replace();
});
});
});
and here is my USER_ROLES constant js file
angular.module('MyApp')
.constant('USER_ROLES', {
all: '*',
admin: 'admin',
user: 'user'
});
I want to protect my partials pages if I login with a user that have the normal user role I want to be able to not watch the delete or create users partial page,
I tried moving my partials outside of the resources folder and putting this in my secuirty class .antMatchers("/views/partials/userTemplates/createUser.html").access("hasRole('create users')") but what this does if that it blocks the partial even if I login with a user that have that role I still get the 403 error, I would like to this to happen but if I login with a user that doesn't have that role but why this happens when I login with a user that have the role is like it doesn't recognize that I have that role.
Is there a way to protect those partials based on roles like in a normal web app because that config worked in a normal web app that I worked but it looks like it doesn't work in a Single page rest app.
If it possible to protect those partials from the server side, I know that I can protect my Rest methods in the server-side using #PreAuthorize("hasRole('create users')") but is there a way to put like an .antMatchers("/my partials/**").hasAnyAuthority("admin") or something similar in the security config class to protect the partials?
My folder structure
SpringMVCProject
--Web-Pages
--WEB-INF
--resources
--angular
--controllers
userController.js
--services
userService.js
--partials
--userPartials
deleteuser.html
app.js
permissionConstants.js
routes.js
--css
--views
--partials (I experimented putting this the folder but I 403 error)
--userpartials
deleteuser.html
index.html
--Source Packages
--controllers
--userControllers
UserController.java
--secuirty
SecuirtyConfig.java
I want to notify all logged in users that how many users are logged-in/online in my app, that was so simple I have following code to do so in configure method os WebSecurityConfigurerAdapter I have simply
#Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry()).expiredUrl("/");
}
and at controller side I have
#Autowired
#Qualifier("sessionRegistry")
private SessionRegistry sessionRegistry;
#Secured("ROLE_USER")
#RequestMapping(value = "/users/{userId}/{userName}", method = RequestMethod.GET)
public String showUserProfile(Model model){
List<Object> principals = sessionRegistry.getAllPrincipals();
List<String> usersNamesList = new ArrayList<String>();
for (Object principal: principals) {
if (principal instanceof org.springframework.security.core.userdetails.User) {
usersNamesList.add(((org.springframework.security.core.userdetails.User) principal).getUsername());
}
}
model.addAttribute("onlineUsers", "Online users are " + usersNamesList.size());
return "layout_user_profile";
}
I get number of online users, but not real time, I want to notify all users that some new user is logged-on/logged-off of that App, I have #EnableWebSocketMessageBroker annotated class
#Configuration
#EnableWebSocketMessageBroker
public class WebSocketBrokerConfigurer extends AbstractWebSocketMessageBrokerConfigurer{
#Override
public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
stompEndpointRegistry.addEndpoint("/users")
.setHandshakeHandler(new CurrentLoggeInUserHandshakeHandler())
.withSockJS();
}
#Override
public void configureMessageBroker(MessageBrokerRegistry registry) {
registry.setApplicationDestinationPrefixes("/app")
.enableSimpleBroker("/topic", "/queue");
}
class CurrentLoggeInUserHandshakeHandler extends DefaultHandshakeHandler{
#Override
protected Principal determineUser(ServerHttpRequest request,
WebSocketHandler wsHandler, Map<String, Object> attributes) {
User user = (User)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String username = user.getUsername();
return new UsernamePasswordAuthenticationToken(username, null);
}
}
}
and my client side is
<script type="text/javascript">
var webSocketC;
var stompClient;
$(document).ready(function(){
webSocketC = new SockJS('/app/users');
stompClient = Stomp.over(webSocketC);
stompClient.connect({}, function(frame){
stompClient.subscribe('/topic/users', function(message){
//TODO Here
});
}, function(error){
console.log("error " + error);
});
});
</script>
with this code I get browser console output as
Opening Web Socket...
stomp.js:134 Web Socket Opened...
stomp.js:134 >>> CONNECT
accept-version:1.1,1.0
heart-beat:10000,10000
<<< CONNECTED
version:1.1
heart-beat:0,0
user-name:john-doe#mail.com
connected to server undefined
stomp.js:134 >>> SUBSCRIBE
id:sub-0
destination:/topic/users
Conclusion
I can not find a way to send messages to all users, whether I use #EnableWebSocketMessageBroker annotated class send that notification or or the client side code is the place to get the desired trick done? and how that could be done?
#Service
public class MessageService {
#Autowired
public SimpMessageSendingOperations messagingTemplate;
public void sendMessage( String message ) {
messagingTemplate.convertAndSend( "/topic/greetings", new Greeting("Some message") );
}
}
Something like this should do.
I try implement Spring Security on back-end and Angularjs on front-end web app work together. But when I try authenticate user via /user method I got:
java.lang.IllegalArgumentException: No converter found for return value of type: class org.springframework.security.authentication.UsernamePasswordAuthenticationToken
at org.springframework.util.Assert.isTrue(Assert.java:68) ...
anglarjs authenticate function :
var authenticate = function(credentials, callback) {
var headers = credentials ? {authorization : "Basic "
+ btoa(credentials.username + ":" + credentials.password)
} : {};
console.log("header",headers);
$http.get('user', {headers : headers}).success(function(data) {
if (data.name) {
$rootScope.authenticated = true;
} else {
$rootScope.authenticated = false;
}
callback && callback();
}).error(function() {
$rootScope.authenticated = false;
callback && callback();
});
}
Spring rest controller:
#RestController
public class RestApiController {
#RequestMapping("/user")
public Principal user(Principal user) {
return user;
}
...
Security config:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{
#Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.inMemoryAuthentication()
.withUser("user").password("password").roles("USER");
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic()
.and()
.authorizeRequests()
.antMatchers("/index.jsp", "/resources/**","/").permitAll()
.anyRequest().authenticated().and()
.addFilterAfter(new CsrfHeaderFilter(), CsrfFilter.class)
.csrf().csrfTokenRepository(csrfTokenRepository());;
}
private CsrfTokenRepository csrfTokenRepository() {
HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository();
repository.setHeaderName("X-XSRF-TOKEN");
return repository;
}
}
Spring uses a set HttpMessageConverts to serialize/deserialize the request/response in suitable format ie. JSON/HTML etc. With your implementation It seems that you don't have proper message converters which can serialize your principal object in response body. To resolve this you need to include jackson-mapper-asl*.jar and jackson-databind*.jar in your classpath
I have a front end written in AngularJS, and a Spring MVC backend. The idea I had was to only secure the REST API services and use an interceptor in AngularJS to redirect the user to the login page when an unauthorized service call is made. The problem I'm facing now is that, while a service is called, the page is briefly displayed before the user is redirected. Is there anything I can do about that? Or is this approach fundamentally flawed?
This is the interceptor:
$httpProvider.interceptors.push(function ($q, $location) {
return {
'responseError': function(rejection) {
var status = rejection.status;
if (status == 401 || status == 403) {
$location.path( "/login" );
} else {
}
return $q.reject(rejection);
}
};});
My security configuration:
#Configuration
#EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
#Autowired
private CustomUserDetailsService customUserDetailsService;
#Autowired
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserDetailsService);
}
#Override
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/api/**")
.authorizeRequests()
.anyRequest().authenticated();
}
#Bean(name="myAuthenticationManager")
#Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
The login controller:
#RequestMapping(value = "/login", method = RequestMethod.POST, produces="application/json")
#ResponseBody
public String login(#RequestBody User user) {
JSONObject result = new JSONObject();
UsernamePasswordAuthenticationToken token =
new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
try {
Authentication auth = authenticationManager.authenticate(token);
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(auth);
ServletRequestAttributes attr = (ServletRequestAttributes) RequestContextHolder.currentRequestAttributes();
HttpSession session = attr.getRequest().getSession(true);
session.setAttribute("SPRING_SECURITY_CONTEXT", securityContext);
result.put("isauthenticated", true);
} catch (BadCredentialsException e) {
result.put("isauthenticated", false);
}
return result.toString();
}
I think this approach is OK, but you may have to live with the page flash, which in turn means you will have to handle it gracefully.
I guess the page flush happens roughly as follows:
A navigation takes place, rendering a template and activating a controller for the new route
The controller calls a service; this is asynchronous, so the page without any data is displayed
The service returns 401/403, it is intercepted and the new navigation to the login page occurs
You may want to try:
Collecting all data required by the page in the resolve configuration of the route (supported both by ngRoute and angular-ui-router), so that the navigation will not complete before all data is fetched.
Handle it from within the page: while the service call is still pending, display a spinner/message whatever to let the user know that some background activity is going on.
When the interceptor catches a 401/403, have it open a modal popup, explaining the situation and offering the user to login or navigate to the login page as a single option. Combine this with the spinner/message.