Authorization on the site through the social networks API with integration into Spring Security

I decided to implement authorization (registration) and user identification on the developed portal using the Social Networks REST API tool - the topic is far from innovative, actively used and very convenient to use. I’m not going to list all the conveniences and advantages of using such functionality on my sites, but I’ll notice that I’m very happy not to remember passwords for each site (even if I have a couple of standard ones), not to participate in tedious registrations with mail forwarding and confirmations, as well as once again not to encounter captchas.

The functionality of the API data is quite primitive, the technology is simple, and the implementation is quite the same and simple. But when you get acquainted with the technology, documentation and API examples of a social network is not enough. In addition, as the topic says, the language used is Java, which automatically reduces the amount of useful information. And in RuNet descriptions are not so many. You can go the path of least resistance and use third-party RESTful products, but a) this does not give a complete understanding of the process; b) reduces the switching properties of the required process; c) often researching a third-party product can be more difficult than developing its implementation. Although the ease of use of such a third-party product can greatly facilitate the development. However, I personally in this review put the emphasis on in order to maximally control all processes, even to the detriment of universality (we “fasten” specific functionality to a specific site, and only a few make this a universal product “for all occasions”). In addition, I am interested not only in implementing user authorization, but also in implementing the project security system provided by the Spring Security 3 framework.

Used set of platforms and tools: Spring Core 3.1 , Spring MVC 3.1 , Spring Security 3.1 , Hibernate 4.1 . The implementation project is foreign, therefore the set of implemented social networks is standard “for them” - Facebook , Twitter , Google+ , LinkedIn .

I want to note that the Spring package has a ready-made project out of the box - Spring Social(today release 1.0.2), which is remarkably encapsulated in the Spring framework of the product and is designed for use with other Spring products. Surely this would be a professional solution, but our task is to control everything and make the process as transparent as possible for understanding. And with Social itself, not everything is so smooth.

1. Model.


I took a somewhat risky and contradictory path, combining POJO , UserDetails , and Entity in the user object . From the point of view of programming approach this is wrong, but a) it is very convenient; and b) it saves the creation of several layers, saving us to do separately POJO + Entity, separately UserDetails and separately DTO, which actually duplicates the contents.

The proposed model building scheme is as follows:
image
I selected two layers (AuthUser and DataUser) so as not to interfere with the authorization logic and the business logic of the project: both the visitor and the administrator, and whoever else are logged in the same way, but have their own set of properties. For example, I have Jobseekers and Employers in my project, they go to the site the same way, but they have a completely different model structure.

As for the separation of the structure within the layers, this is obvious - the set of received fields from Facebook, Twitter, etc., and especially with standard authorization, is so different that creating one terribly stretched structure for everything is just silly from the point of view of building the database - excessively. As for scalability, when adding a new service provider, working with such a structure would be extremely inconvenient.

Listings of some of the listed objects, as well as the enum classes used.

AuthUser.java:

@ Entity
	@ Table(name = "auth_user")
	@ Inheritance(strategy = InheritanceType.JOINED)
	public class AuthUser implements Serializable, UserDetails {
		@ Id
		@ Column(name = "id")
		@ GeneratedValue(strategy = GenerationType.AUTO)
		private Long id;
		@ Column(name = "identification_name", length = 64, nullable = false)
		private String identificationName;
		@ Enumerated(EnumType.STRING)
		@ Column(name = "type", nullable = false)
		private AuthorityType type;
		@ Column(name = "binary_authorities", nullable = false)
		private Long binaryAuthorities;
		@ Column(name = "enabled", nullable = false, columnDefinition = "tinyint")
		private Boolean enabled;
		@ Transient
		private Set authorities;
		@ OneToOne(fetch = FetchType.LAZY, orphanRemoval = true)
		@ Cascade({CascadeType.ALL})
		@ JoinColumn(name="user_id")
		private User user;
		@ Override
		public Collection getAuthorities() {
			authorities = EnumSet.noneOf(Authority.class);
			for (Authority authority : Authority.values())
	        		if ((binaryAuthorities & (1 << authority.ordinal())) != 0)
					authorities.add(authority);
			return authorities;
		}
		public void setAuthority(Set authorities) {
			binaryAuthorities = 0L;
			for (Authority authority : authorities)
				binaryAuthorities |= 1 << authority.ordinal();
		}
		@ Override
		public String getPassword() {
			return type.name();
		}
		@ Override
		public String getUsername() {
			return identificationName;
		}
		@ Override
		public boolean isAccountNonExpired() {
			return true;
		}
		@ Override
		public boolean isAccountNonLocked() {
			return true;
		}
		@ Override
		public boolean isCredentialsNonExpired() {
			return true;
		}
		//getters/setters
	}

AuthorityType.java:

public enum AuthorityType implements Serializable {
		SIMPLE, FACEBOOK, TWITTER, GOOGLE, LINKEDIN;
	}

Authority.java:

public enum Authority implements GrantedAuthority {
		NEW_CUSTOMER, CUSTOMER, ADMINISTRATOR;
		@ Override
		public String getAuthority() {
			return toString();
		}
	}

FacebookAuthUser.java:

@ Entity
	@ Table(name = "facebook_auth_user")
	public class FacebookAuthUser extends AuthUser {
		@ Column(name = "first_name", length = 32)
		private String firstName;
		@ Column(name = "last_name", length = 32)
		private String lastName;
		@ Column(name = "email", length = 64)
		private String email;
		@ Column(name = "token", length = 128)
		private String token;
		//any number of available properties
		//getters/setters
	}

TwitterAuthUser.java:

@ Entity
	@ Table(name = "twitter_auth_user")
	public class TwitterAuthUser extends AuthUser {
		@ Column(name = "screen_name", length = 64)
		private String screenName;
		@ Column(name = "oauth_token", length = 80)
		private String oauthToken;
		@ Column(name = "oauth_token_secret", length = 80)
		private String oauthTokenSecret;
		//any number of available properties
		//getters/setters
	}

SimpleAuthUser.java:

@ Entity
	@ Table(name = "simple_auth_user")
	public class SimpleAuthUser extends AuthUser {
		@ Column(name = "password", length = 40, nullable = false)
		private String password;
		@ Column(name = "uuid", length = 36, nullable = false)
		private String uuid;
		@ Override
		public String getPassword() {
			return password;
		}
		//getters/setters
	}

As you can see, it was not without a little “chemistry”:
  • I don’t want to do another structure (table) for storing user roles, and to deprive myself of the ability to endow a user with several roles by changing the set of roles to one is silly. Therefore, in the database I store the binary representation of the set of roles, and Spring Security feeds the set. The main thing to remember when reading from the base is to make a transformation. Ideologically, the translation mechanism is wrong to keep in the POJO, you need to leave it in the controller or DAO, but we will consider this a publication cost.
  • The type field (enum AuthorityType) - it does not carry any special need, rather for visualizing data in the database, plus it’s easier to use user.getType () in the view than exploring the membership of one of the classes - user instanseof TwitterAuthUser , although this is completely unprincipled.
  • The UserDetails interface requires a number of methods to be implemented. In particular, identificationName (and getUsername () ) is the field where identifiers are stored: for Facebook it is FacebookID , for Twitter it is TwitterID , for standard authorization it is nickname or email. The getPassword () method in my case returns the same type, then it will be used by Spring Security to create a hash for cookies in the RememberMe mechanism . To increase the project security in each of the classes, you can override this method by assigning it really security data, as I did in the SimpleAuthUser class. For others, it can be tokens or secret tokens. What to do next with methods and potentially corresponding properties isAccountNonExpired , isAccountNonLocked , isCredentialsNonExpired - decide to use such data as needed, in this review I do not use it, drowning return true .

As you can see in the diagram and in the code, I decided to use lazythe relationship between objects of different layers, even though they are correlated as one-to-one. There are two goals: 1) AuthUser often jerks the framework in the controller and view, and there is not much desire to drag the dependent structure everywhere, especially since it can be very extended and massive (in my project for Jobseeker tables there are only EAGER dependencies of pieces 5- 6, not counting LAZY - these are both telephones, and address, and professions, and others), therefore, in my opinion, reinsurance does not hurt. 2) we must not forget that these layers belong to different layers of logic: AuthUser is twitching with the Spring Security framework, and at the same time, changes in DataUser may occur, but I don’t want to constantly monitor and update. I agree that this decision is controversial, and does not pretend to be final. Perhaps you need to connect the other way around, thus, the above problems go away, and the authorization bean can always be pulled from the business logic. This remains at the discretion of the developers.

As for the DataUser class and the dependent ones, these are simple POJO classes, directly the DataUser contains properties common to all (say, id, firstName, lastName, email, location), and the rest extends it by adding properties specific to itself (listing is impractical) .

2. The controller.


In principle, in the terminology of authentication and authorizationthere is not much difference - authorization is authorization, moreover, different network providers incline these terms in their own way. However, in my report I clearly distinguish between 2 concepts - registration and direct authorization or login (both are based on authorization from a social network provider). Say, when participating in a forum, or submitting a comment, you just need to log in - whether it be in the first entry, or in the hundredth. I pursue the separation of registration and simple authorization by the need to create a user model when applying for registration. And although this could be implemented easier - at the entrance we check whether there is such a person or not - and create a user structure in the case of the first login. But a) there is a standard registration and it’s logical to make a visual separation “here is one, but here is another” (the notorioususability ); b) no matter how offensive, but the social networking APIs are not unanimous in providing information about their customers - say, the Facebook API provides an email, first name, last name, gender, location; Twitter API - gives screen_name, which may not be “first name last name”, does not give email (they have a position to clearly distinguish between real and virtual); The Google+ API provides a first name, last name, email, but nothing about the location; LinkedIn API- name, surname, gender, location, but does not give an email. Since my project is very closely tied to the visitor’s personal data (project for a recruitment company), along with registration, I indicate the need to fill in some fields (initially, apart from Facebook users, everyone had to at least specify something, now it’s simplified and only such a need exists Twitter users, although I do not exclude a complete refusal, and filling in the fields when the need arises - say, when trying to go to the "zone", where such information will be simply necessary). Therefore, my implementation is somewhat inflated, although this only helps to understand the authorization mechanism more.

I want to remind you that for work (or testing) you need to create your own application in each of the social networks, and use its settings to work. For Facebook, thisdevelopers.facebook.com/apps , for Twitter - dev.twitter.com/apps , for Google+ - code.google.com/apis/console , for LinkedIn - www.linkedin.com/secure/developer . When creating an application, 3 parameters are important that each provider has: a key (or API key, Consumer key, Client ID), a secret key (App Secret, Consumer secret, Client secret, Secret key) and a redirect address (they say, until recently time, one of the providers did not have a redirect to localhost, but today it’s verified that everyone works with an address like http: // localhost: 8080 / myproject ). There you can configure other parameters, including the application logo, however, LinkedIn requires that the link to the image be SSL (incomprehensible wish).

Facebook and Google+ have been using the new OAuth 2 protocol for a long time , Twitter and LinkedIn are still using the older OAuth protocol (Google+ also supported the first version of OAuth until April 20, 2012). At my discretion (although I can’t imagine that there was a different opinion), working with OAuth 2 is incomparably simpler and more convenient, although, despite the fact that it is quite popular, it is still not approved as a standard. The principle of operation is quite primitive (the most popular scheme):
image
So, the user clicks on one of the registration buttons on the web page:
image
(and I don’t leave any “extra” functionality on the page, only a button with an address like www.myproject.com/registration / facebook etc.), the request goes to the controller (for Facebook):
@ RequestMapping(value = "/registrate/facebook", method = RequestMethod.POST)
	public ModelAndView facebookRegistration() throws Exception {
		return new ModelAndView(new RedirectView(FACEBOOK_URL + "?client_id=" + FACEBOOK_API_KEY +
			+ "&redirect_uri=" + FACEBOOK_URL_CALLBACK_REGISTRATION +
			+ "&scope=email,user_location&state=registration", true, true, true));
	}

Parameters for scope can be found at developers.facebook.com/docs/authentication/permissions (for Twitter - dev.twitter.com/docs/platform-objects/users , Google+ - developers.google.com/accounts/docs/OAuth2Login# userinfocall , LinkedIn - developer.linkedin.com/documents/profile-fields ), I just brought a couple here. The redirect_uri domain must match the registered address of the application. state is a “free” parameter, I use it as a semaphore for further actions - registration, signin, autosignin.

Next, the user “redirects” to the Facebook login page of authorization, where he will allow the application to use his data, and if the permissions are outside the basic, then they will be listed in the authorization window.

After authorization, our controller with mapping FACEBOOK_IRL_CALLBACK_REGISTRATION receives a call (at any decision by the client - log in, cancel, return). Spring MVC allows us to filter requests by mapping (in this case, mapping of my project is given):
@ RequestMapping(value = "/callback/facebook", method = RequestMethod.GET)
	public class FacebookController extends ExternalController implements Constants {
		@ RequestMapping(value = "/registration", params = "code")
		public ModelAndView registrationAccessCode(@ RequestParam("code") String code, HttpServletRequest request) throws Exception {
			String authRequest = Utils.sendHttpRequest("GET", FACEBOOK_URL_ACCESS_TOKEN, new String[]{"client_id", "redirect_uri", "client_secret", "code"}, new String[]{FACEBOOK_API_KEY, FACEBOOK_URL_CALLBACK_REGISTRATION, FACEBOOK_API_SECRET, code});
			String token = Utils.parseURLQuery(authRequest).get("access_token");
	String tokenRequest = Utils.sendHttpRequest("GET", FACEBOOK_URL_ME, new String[]{"access_token"}, new String[]{token})
			Map userInfoResponse = Json.read(tokenRequest).asJsonMap();
			String email = userInfoResponse.get("email").asString().toLowerCase();
			String id = userInfoResponse.get("id").asString();
			//verifying ... is new? is email in DB?
			//creating objects
			Customer customer = new Customer();
			customer.setEmail(email);
			//...
			customerer = (Customerer) userDAO.put(customer);
			FacebookAuthUser user = new FacebookAuthUser();
			user.setFirstName(firstName);
			//...
			user.setIdentificationName(id);
			user.setToken(token);
			user.setType(AuthenticationType.FACEBOOK);
			user.setEnabled(true);
			user.setAuthority(EnumSet.of(Authority.CUSTOMER));
			user.setUser(customer);
			authenticationDAO.put(user);
			return new ModelAndView(new RedirectView("/registrate.complete", true, true, false));
		}
		@ RequestMapping(value = "/registration", params = "error_reason")
		public ModelAndView registrationError(@ RequestParam("error_description") String errorDescription, HttpServletRequest request, HttpServletResponse response) {
			//return client to registration page with errorDescription
			return new ModelAndView(new RedirectView("/registrate", true, true, false));
		}
		//will signin and signinError
	}

For convenience and unitary use, a couple of static methods of the Utils class used in this listing:
public static String sendHttpRequest(String methodName, String url, String[] names, String[] values) throws HttpException, IOException {
		if (names.length != values.length) return null;
		if (!methodName.equalsIgnoreCase("GET") && !methodName.equalsIgnoreCase("POST")) return null;
		HttpMethod method;
		if (methodName.equalsIgnoreCase("GET")) {
			String[] parameters = new String[names.length];
			for (int i = 0; i < names.length; i++)
				parameters[i] = names[i] + "=" + values[i];
			method = new GetMethod(url + "?" + StringUtils.join(parameters, "&"));
		} else {
			method = new PostMethod(url);
			for (int i = 0; i < names.length; i++)
				((PostMethod) method).addParameter(names[i], values[i]);
			method.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
		}
		new HttpClient().executeMethod(method);
		return getStringFromStream(method.getResponseBodyAsStream());
	}
	public static Map parseURLQuery(String query) {
		Map result = new HashMap();
		String params[] = query.split("&");
		for (String param : params) {
			String temp[] = param.split("=");
			try {
				result.put(temp[0], URLDecoder.decode(temp[1], "UTF-8"));
			} catch (UnsupportedEncodingException exception) {
				exception.printStackTrace();
			}
		}
		return result;
	}

Constants:
final public static String FACEBOOK_API_KEY = "XXXXXXXXXXXXXXXX";
	final public static String FACEBOOK_API_SECRET = "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX";
	final public static String FACEBOOK_URL = "https://www.facebook.com/dialog/oauth";
	final public static String FACEBOOK_URL_ACCESS_TOKEN = "https://graph.facebook.com/oauth/access_token";
	final public static String FACEBOOK_URL_ME = "https://graph.facebook.com/me";
	final public static String FACEBOOK_URL_CALLBACK_REGISTRATION = SITE_ADDRESS + "/callback/facebook/registration";
	final public static String FACEBOOK_URL_CALLBACK_SIGNIN = SITE_ADDRESS + "/callback/facebook/signin";

Everyone can use the JSON libraries at their discretion, I used the mjson library ( http://sharegov.blogspot.com/2011/06/json-library.html ) - small, convenient and without serialization.

As you can see, the process is simple and should not cause special questions. I also want to note that Facebook gives a location parameter, the values ​​from which you can “slip” the Google Maps API (at http://maps.googleapis.com/maps/api/geocode/json ) and pull out the geolocation in a convenient form (by standards Google Maps). It’s clear that this can only be done if the client in his Facebook account indicated not only the country of location.

Sign up for Google+ in the same way, with the only difference being thatThe callback URL in their system must match the one specified in the application settings. Thus, all redirects will fall on only one mapping. To separate processes, it is convenient to use the returned state parameter :
@ RequestMapping(value = "/callback/google", method = RequestMethod.GET)
	public class GoogleController extends ExternalController implements Constants {
		@ RequestMapping(value = {"/", ""}, params = "code")
		public ModelAndView googleProxy(@ RequestParam("code") String code, @ RequestParam("state") String state, HttpServletRequest request, HttpServletResponse response) throws Exception { ... }
		@ RequestMapping(value = {"/", ""}, params = "error")
		public ModelAndView googleErrorProxy(@RequestParam("error") String error, @RequestParam("state") String state, HttpServletRequest request) throws Exception { ... }
	}

The rest of the actions, with the exception of addresses and return parameters, are identical.

The situation is different with OAuth authentication (Twitter and LinkedIn). I went through the entire authorization chain, but this is very inconvenient due to the formation of a request with tokens - they need to be “glued” in a special way, pack base64, add parameters over time and other manipulations. And what is most surprising - sections for developers of these social networks do not display these processes. Although this is a standard, therefore, the calculation goes for a standard approach. In any case, authorization in this way, implemented “manually”, is not of interest for developing your application. I recommend using third-party free libraries that make this task easier. For example, there is a library specifically for Twitter -twitter4j.jar . I used the scribe-java library ( http://github.com/fernandezpablo85/scribe-java ), which is distributed under the rights of the MIT license . The package includes work with the Digg API , Facebook API , Flickr API , Freelancer API , Google API , LinkedIn API , Skyrock API , Tumblr API , Twitter API , Vkontakte API , Yahoo API and a dozen 2 others.

The registration process for Twitter using the scribe librarywill look like this. Client request controller for authorization from the registration page:
@ RequestMapping(value = "/registrate/twitter", params = "action", method = RequestMethod.POST)
	public ModelAndView twitterRegistrationJobseeker(HttpServletRequest request) throws Exception {
		OAuthService service = new ServiceBuilder().provider(TwitterApi.class)
			.apiKey(TWITTER_CONSUMER_KEY).apiSecret(TWITTER_CONSUMER_SECRET)
			.callback(TWITTER_URL_CALLBACK_REGISTRATION).build();
		Token requestToken = service.getRequestToken();
		request.getSession().setAttribute("twitter", service);
		request.getSession().setAttribute("request_token", requestToken);
		return new ModelAndView(new RedirectView(service.getAuthorizationUrl(requestToken), true, true, true));
	}

Twitter callback controller:
@ RequestMapping(value = "/callback/twitter", method = RequestMethod.GET)
	public class TwitterController extends ExternalController implements Constants {
		@ RequestMapping(value = "/registration", params = "oauth_verifier")
		public ModelAndView registrationAccessCode(@ RequestParam("oauth_verifier") String verifier, HttpServletRequest request, HttpServletResponse response) throws Exception {
			OAuthService service = (OAuthService) request.getSession().getAttribute("twitter");
			Token accessToken = service.getAccessToken((Token) request.getSession().getAttribute("request_token"), new Verifier(verifier));
			OAuthRequest oauthRequest = new OAuthRequest(Verb.GET, TWITTER_URL_CREDENTIALS);
			service.signRequest(accessToken, oauthRequest);
			Map userInfoResponse = Json.read(oauthRequest.send().getBody()).asJsonMap();
			String twitterId = userInfoResponse.get("id").asString();
			//verifying ...
			Customer customer = new Customer();
			customer.setFirstName((String) request.getSession().getAttribute("pageValueFirstName"));
			//...
			customer = (Customer) userDAO.put(customer);
			TwitterAuthUser user = new TwitterAuthUser();
			user.setAuthority(EnumSet.of(Authority.CUSTOMER));
			user.setIdentificationName(twitterId);
			//...
			user.setOauthToken(accessToken.getToken());
			user.setOauthTokenSecret(accessToken.getSecret());
			user.setType(AuthenticationType.TWITTER);
			user.setUser(customer);
			authenticationDAO.put(user);
			return new ModelAndView(new RedirectView("/registrate.complete", true, true, false));
		}
		@ RequestMapping(value = "/registration", params = "denied")
		public ModelAndView registrationError(HttpServletRequest request) {
			//response does not contain the error text
			return new ModelAndView(new RedirectView("/registrate", true, true, false));
		}
		//will signin and signinError
	}

Again, everything is very simple and affordable. Registration through the LinkedIn API is done in exactly the same way.

Last - registration in a standard way. Standard - that’s why it’s standard, I won’t give the code, I’ll just clarify that as a result we create an object of type SimpleAuthUser inherited from AuthUser:
	SimpleAuthUser user = new SimpleAuthUser();
		user.setAuthority(EnumSet.of(Authority.NEW_CUSTOMER));
		user.setEnabled(false);
		user.setIdentificationName(email);
		user.setPassword(passwordEncoder.encodePassword(password, email));
		user.setType(AuthenticationType.SIMPLE);
		user.setUser(customer);
		user.setUuid(uuid);
		authenticationDAO.put(user);

For this case, authority NEW_CUSTOMER was needed - a registered user needs confirmation of registration (standard practice), and therefore a) has a different role; b) it is not allowed to authorize Spring Security (enabled = false).

Authorization on the site


A simple spring application-context-security.xml :

CustomUserDetailsManager.java:

public class CustomUserDetailsManager implements UserDetailsService {
		@ Resource private AuthenticationDAO authenticationDAO;
		@ Override
		public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
			return authenticationDAO.findAuthUser(username);
		}
	}

CustomUserAuthentication.java:

public class CustomUserAuthentication implements Authentication {
		private String name;
		private Object details;
		private UserDetails user;
		private boolean authenticated;
		private Collection authorities;
		public CustomUserAuthentication(UserDetails user, Object details) {
			this.name = user.getUsername();
			this.details = details;
			this.user = user;
			this.authorities = user.getAuthorities();
			authenticated = true;
		}
		@ Override
		public String getName() {
			return name;
		}
		@ Override
		public Collection getAuthorities() {
			return authorities;
		}
		@ Override
		public Object getCredentials() {
			return user.getPassword();
		}
		@ Override
		public Object getDetails() {
			return details;
		}
		@ Override
		public Object getPrincipal() {
			return user;
		}
		@ Override
		public boolean isAuthenticated() {
			return authenticated;
		}
		@ Override
		public void setAuthenticated(boolean authenticated) throws IllegalArgumentException {
			this.authenticated = authenticated;
		}
	}

CustomAuthenticationProvider.java
(the class is completely stupid, but Spring Security needs to feed the successor of the AuthenticationProvider interface , but the closest PreAuthenticatedAuthenticationProvider in terms of meaning is not suitable):
public class CustomAuthenticationProvider implements AuthenticationProvider {
		@ Override
		public Authentication authenticate(Authentication authentication) throws AuthenticationException {
			//тут могут быть доп. проверки
			return authentication;
		}
		@ Override
		public boolean supports(Class authentication) {
			return PreAuthenticatedAuthenticationToken.class.isAssignableFrom(authentication);
		}
		public Authentication trust(UserDetails user) {
			Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
			Authentication trustedAuthentication = new CustomUserAuthentication(user, authentication.getDetails());
			authentication = authenticate(trustedAuthentication);
			SecurityContextHolder.getContext().setAuthentication(authentication);
			return authentication;
		}
	}

And perhaps the most “bottleneck” place for organizing security is the implementation of the RememberMe mechanism . In principle, everything is already organized so that the operation of the RememberMe service is fully consistent with the implementation of TokenBasedRememberMeServices, with one clarification: all the data for the automatic authorization of the client is in our database, however, there may be a situation where the user registered on the site and then deleted the account on the used social network. And it turns out a conflict - we authorize the user, but in fact he is not there, that is, the main principles of authorization through third-party services are violated. That is, when the RememberMe mechanism is triggered, we must check the automatically incoming client. There are such mechanisms in the API of each network provider, but we must "wedge" into the work of Spring's RememberMe in order to check at the right stage. Unfortunately, it will not work to extend some class ( AbstractRememberMeServices has the required method set as final), so you need to completely redefine the class. My method is more cumbersome, I went from the end, and the banal human laziness does not allow to remodel into a simpler option. I completely redefined the AbstractRememberMeServices class to include the code of the TokenBasedRememberMeServices class by adding a couple of lines to the public Authentication autoLogin method (HttpServletRequest request, HttpServletResponse response) - after checking the values ​​in the method, but before the authorization I inserted a call to the client’s “reality” verification method:
Class controller = externalControllers.get(user.getPassword()); 
	if (controller != null && !controller.newInstance().checkAccount(user)) return null;

And earlier, in the constructor, I define a static list:
private Map> externalControllers;
	public CustomRememberMeService() {
		externalControllers = new HashMap>(){{
			put(AuthenticationType.FACEBOOK.name(), FacebookController.class);
			put(AuthenticationType.TWITTER.name(), TwitterController.class);
			put(AuthenticationType.GOOGLE.name(), GoogleController.class);
			put(AuthenticationType.LINKEDIN.name(), LinkedinController.class);
		}};
	}

(This implementation of the code will not affect RememberMe for standard authorization in any way).

A simpler method involves replacing the REMEMBER_ME_FILTER filter with your own, where you need to put the same code after calling the above autoLogin method, but before direct authorization. It is less expensive by code and more logical to understand, however, it requires intervention in the config. Everyone will decide which way to take, but the second, in my opinion, is ideologically more “pure”.

We also need to clarify about the ExternalController class and the call to checkAccount (user) . All my callback controllers extend the ExternalController class :
public abstract class ExternalController {
		public abstract boolean checkAccount(UserDetails user) throws Exception;
	}

and each controller overrides this single method. For example, for Facebook it is:
public boolean сheckAccount(UserDetails user) throws Exception {
		FacebookAuthUser facebookUser = (FacebookAuthUser) user;
		String authRequest = Utils.sendHttpRequest("GET", FACEBOOK_URL_ME, new String[]{"access_token"}, new String[]{facebookUser.getToken()});
		Map tokenInfoResponse = Json.read(authRequest).asJsonMap();
		return tokenInfoResponse.get("error") == null && tokenInfoResponse.get("id").asString().equalsIgnoreCase(facebookUser.getIdentificationName());
	}

and for Twitter:
public boolean checkAccount(UserDetails user) throws Exception {
		TwitterAuthUser twitterUser = (TwitterAuthUser) user;
		OAuthService service = new ServiceBuilder().provider(TwitterApi.class).apiKey(TWITTER_CONSUMER_KEY).apiSecret(TWITTER_CONSUMER_SECRET).build();
		OAuthRequest oauthRequest = new OAuthRequest(Verb.GET, TWITTER_URL_CREDENTIALS);
		service.signRequest(new Token(twitterUser.getOauthToken(), twitterUser.getOauthTokenSecret()), oauthRequest);
		String response = oauthRequest.send().getBody();
		Map info = Json.read(request).asJsonMap();
		return info.get("id").asString().equalsIgnoreCase(twitterUser.getIdentificationName());
	}

etc.

The actual authorization on the site itself (login, sign in) is very similar to registration. The user goes to the page, clicks "Login" and also redirects him to authorization:
image

The only thing I pass to the server is the parameter - "signin" or "autosignin", depending on whether the "sign in automatically" checkbox is clicked. Further, everything happens according to the scenario similar to registration, only the parameter changes, callback URL and remove all scope or permission- We need to get only the client ID and its tokens. After appropriate checks in the controller methods, I advise you to rewrite the tokens in the database. And although, for example, Facebook did not change the client token during my tests, but Google+ does it every time. I don’t know how often a “change” occurs, so I rewrite it after each access_token (in fact, at each non-automatic authorization by the provider).

And the most important point is the direct authorization of the user in Spring Security (after checking, of course, for compliance and obtaining rights from the provider’s API), using the Facebook controller as an example:
@ RequestMapping(value = "/signin", params = "code")
	public ModelAndView signInAccessCode(@ RequestParam("code") String code, @ RequestParam("state") String state, HttpServletRequest request, HttpServletResponse response) throws Exception {
		String accessRequest = Utils.sendHttpRequest("GET", FACEBOOK_URL_ACCESS_TOKEN, new String[]{"client_id", "redirect_uri", "client_secret", "code"}, new String[]{FACEBOOK_API_KEY, FACEBOOK_URL_CALLBACK_SIGNIN, FACEBOOK_API_SECRET, code});
		String token = Utils.parseURLQuery(accessRequest).get("access_token");
		Map userInfoResponse = Json.read(Utils.sendHttpRequest("GET", FACEBOOK_URL_ME, new String[]{"access_token"}, new String[]{token})).asJsonMap();
		FacebookAuthUser user = (FacebookAuthUser) authenticationDAO.findAuthUser(userInfoResponse.get("id").asString(), AuthenticationType.FACEBOOK);
		if (user == null) {
			//что-то пошло не так ...
			return new ModelAndView(new RedirectView("/signin", true, true, false));
		} else {
			if (!token.equals(user.getToken())) {
				user.setToken(token);
				user = (FacebookAuthUser) authenticationDAO.put(user);
			}
			Authentication authentication = customAuthenticationProvider.trust(user);
			if (state.equalsIgnoreCase("autosignin")) customRememberMeService.onLoginSuccess(request, response, authentication);
			else customRememberMeService.logout(request, response, authentication); //очистить куки RememberMe
			return new ModelAndView(new RedirectView("/signin.complete", true, true, false));
		}
	}

Now, with the auto-login checkbox selected, the client will be automatically logged in. Accordingly, if there is no checkmark, a call to the logout method of the RememberMe service will erase the cookies (it does nothing else). By the way, clicking on the "/ logout" link removes authorization and clears cookies automatically. This is provided by the corresponding line in the Spring Security config above.

Using this method can also be “screwed” for standard authorization: after passing the checks (finding the user in the table, reconciling the password hash, etc.), we manually authorize it:
Authentication authentication = customAuthenticationProvider.trust(user);
	if (autosignin) customRememberMeService.onLoginSuccess(request, response, authentication);
	else customRememberMeService.logout(request, response, authentication);

There is no difference in use. The only thing that distinguishes is that when the RememberMe mechanism is triggered there will be no extraneous checks, in fact, it will completely coincide with the operation of the TokenBasedRememberMeServices service.

Further, the use of authorization is similar to the use of the usual Spring Security roles, with the only difference being that you cannot use the @Secured annotation ("CUSTOM_ROLE") , it is designed for standard roles (although there seems to be a mechanism for redefining them, which I did not go into). But Spring Security has another mechanism - using the same annotations @PreAuthorize , @PostFilter : @PreAuthorize ("hasRole ('ADMINISTRATOR')"), @PreAuthorize ("hasRole ({'CUSTOMER', 'ADMINISTRATOR'})”)). It is only necessary to specify this in the Spring Security config in the security: global-method-security parameter .

Similarly, you can take advantage of Spring Security in the view (in JSP ). For example:
<%@ taglib prefix="core" uri="http://java.sun.com/jsp/jstl/core" %>
	<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
	...
	
Welcome, !

Such constructions allow you not to transfer the model from the controller to the view, but leave the mechanism for removing the model to the view (it will itself apply for the model in the DAO).

You can also use jsp-scriptlets on jsp-pages (although the use of scriptlets has a lot of opponents, mainly because of the position "bean - god, Caesar - cesarean", the programmer is engaged in programming, and the layout and / or designer are in design; but this a moot point, I personally am not a supporter of any one or the other concept - yes, ugly, yes, sometimes it’s very convenient):
<%@ page import="org.springframework.security.core.context.SecurityContextHolder" %>
	<%@ page import="myproject.auth.AuthUser" %>
	<%	Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
		AuthUser authUser = null;
		if (!(principal instanceof String)) authUser = (AuthUser) principal; %>
	...
	 name="your_name" value="<%= authUser == null ? "" : authUser.getUser().getFirstName()%>"/>
	...
	<% if (authUser == null) { %>
		
...
<% } %>

The given page code only reflects the possibilities of using the context of security, but does not pretend to any meaningfulness of the logic of the page.

I want to focus on the bottleneck caused by the lazy dependency between the authentication object and its parameters (the principal object): without modifications, both pieces of the page code will cause a Runtime Exception upon opening, since the user field (calling the getUser () method will contain default object with all fields filled as null. Using the OpenSessionInView Patternwithout additional loading of the dependent object in this case will not help, since the HTTP sessions here are different. Therefore, you must either load the dependent object immediately upon loading, but this contradicts the approach that caused the lazy connection to be assigned - the object will be loaded and changing the dependent object will not update the loaded one, in this case it is easier to establish an EAGER connection. I decided this in authenticationDAO by replacing the commonly used sessionFactory.getCurrentSession () with the opening of a new session: SessionFactoryUtils.openSession (sessionFactory) . Perhaps this is not the most economical solution in terms of memory, but I have not yet asked this question and did not delve into this topic. I think that by checking for the presence of the current session, you can refuse the filter or interceptorOpenSessionInView , actually replacing its work.

The text turned out to be more than necessary, for sure there are controversial or even erroneous moments, but I tried to reflect the solution of the difficulties that I myself encountered in implementing the conceived mechanism.

Also popular now: