Using Apache Shiro
So in part 1 I got a basic servlet that can be used to determine if a user has logged in via Facebook, and get hold of details like the Facebook users’s ID and name etc.
Now comes configuring and extending Shiro to handle Facebook login.
I’ve taken the approach of creating a new realm for Facebook, which will use a custom CredentialsMatcher (which won’t actually have to do much as it’s Facebook that handles any credentials matching). So with two new classes to be written uk.co.mrdw.security.facebook.FacebookCredentialsMatcher and uk.co.mrdw.security.facebook.FacebookRealm the config in shiro.ini (or embedded in web.xml in the tutorial) will have the following “main” section.
[main] realmA = name.brucephillips.somesecurity.dao.RoleSecurityJdbcRealm fbCredentialsMatcher = uk.co.mrdw.shiro.facebook.FacebookCredentialsMatcher realmB = uk.co.mrdw.security.facebook.FacebookRealm realmB.credentialsMatcher = $fbCredentialsMatcher securityManager.realms = $realmA, $realmB
(Maybe realmA and and realmB could be better named)
So shiro will first try realmA (the jdbc realm, backed by a users table), and if that realm doesn’t support the token being passed in to the SecurityUtils.getSubject().login(token) it will try realmB, the Facebook realm. So we’ll need a new token class for Facebook to identify facebook logins.
The code in the authenticate method in the FacebookLoginServlet will be moved to the FacebookRealm class, and the FacebookLoginServlet will just create a Facebook token and call SecurityUtils.getSubject().login(token).
So we end up with a FacebookRealm class like this
package uk.co.mrdw.shiro.facebook; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.net.MalformedURLException; import java.net.URL; import java.util.HashMap; import java.util.Map; import java.util.Properties; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; import uk.co.mrdw.shiroexp.servlets.FacebookProperties; public class FacebookRealm extends AuthorizingRealm { private static final Properties props = new FacebookProperties().getProperties(); private static final String APP_SECRET = props.get("fbAppSecret").toString(); private static final String APP_ID = props.get("fbAppId").toString(); private static final String REDIRECT_URL = props.get("fbLoginRedirectURL").toString(); @Override public boolean supports(AuthenticationToken token) { if (token instanceof FacebookToken) { return true; } return false; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { return new FacebookAuthorizationInfo(); } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { FacebookToken facebookToken = (FacebookToken) token; // do all the facebook gubbins if (facebookToken.getCode() != null && facebookToken.getCode().trim().length() > 0) { URL authUrl; try { authUrl = new URL("https://graph.facebook.com/oauth/access_token?" + "client_id=" + APP_ID + "&redirect_uri=" + REDIRECT_URL + "&client_secret=" + APP_SECRET + "&code=" + facebookToken.getCode()); String authResponse = readURL(authUrl); System.out.println(authResponse); String accessToken = getPropsMap(authResponse).get("access_token"); URL url = new URL("https://graph.facebook.com/me?access_token=" + accessToken); String fbResponse = readURL(url); FacebookUserDetails fud = new FacebookUserDetails(fbResponse); return new FacebookAuthenticationInfo(fud, this.getName()); } catch (MalformedURLException e1) { e1.printStackTrace(); throw new AuthenticationException(e1); } catch (IOException ioe) { ioe.printStackTrace(); throw new AuthenticationException(ioe); } catch (Throwable e) { e.printStackTrace(); } } return null; } // ------------------------------------------------------------ // STUFF here should be in a more generic place TODO // ------------------------------------------------------------ private String readURL(URL url) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); InputStream is = url.openStream(); int r; while ((r = is.read()) != -1) { baos.write(r); } return new String(baos.toByteArray()); } private Map<String, String> getPropsMap(String someString) { String[] pairs = someString.split("&"); Map<String, String> props = new HashMap<String, String>(); for (String propPair : pairs) { String[] pair = propPair.split("="); props.put(pair[0], pair[1]); } return props; } }
I have also added some getters to the FacebookUserDetails class – just extracting data from the json String it’s created with, will be refined as required in the future.
<pre> package uk.co.mrdw.shiro.facebook; import org.json.JSONException; import org.json.JSONObject; /** * Simple class for holding data relating to a facebook user * * @author Mike * */ public class FacebookUserDetails { private String id; private String firstName; private String lastName; private String email; // jsonString Expected to be something like this // { // "education": [{ // "school": { // "id": "123456789012345", // "name": "University of Sheffield" // }, // "type": "Graduate School", // "with": [{ // "id": "123456789", // "name": "Daffy Duck" // }] // }], // "first_name": "Mike", // "id": "121212121", // "last_name": "Warren", // "link": // "http://www.facebook.com/profile.php?id=121212121", // "locale": "en_US", // "name": "Mike Warren", // "updated_time": "2011-08-15T14:51:05+0000", // "verified": true // } private String jsonString; public FacebookUserDetails(String fbResponse){ jsonString = fbResponse; JSONObject respjson; try { respjson = new JSONObject(fbResponse); this.id = respjson.getString("id"); this.firstName = respjson.has("first_name") ? respjson.getString("first_name") : " no name" + id; this.lastName = respjson.has("last_name") ? respjson.getString("last_name") : ""; this.email = respjson.has("email") ? respjson.getString("email") : "-no email-"; } catch (JSONException e) { System.out.println( "fbResponse:"+fbResponse ); e.printStackTrace(); throw new RuntimeException(e); } } public String toString(){ return jsonString; } public String getId() { return id; } public void setId(String id) { this.id = id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } }
The CredentialsMatcher class shouldn’t need to do anything, as Facebook is doing the credentials matching for us.
package uk.co.mrdw.shiro.facebook; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.credential.CredentialsMatcher; public class FacebookCredentialsMatcher implements CredentialsMatcher { /** * Just confirms that token is the right type - credentials checking is done by facebook OAuth */ @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { if(info instanceof FacebookAuthenticationInfo){ return true; } return false; } }
create a facebook token class, will just be used to hold the “code” provided by Facebook
package uk.co.mrdw.shiro.facebook; import org.apache.shiro.authc.AuthenticationToken; public class FacebookToken implements AuthenticationToken { private static final long serialVersionUID = 1L; private String code; public FacebookToken(String code){ this.code = code; } @Override public Object getPrincipal() { return null;// not known - facebook does the login } @Override public Object getCredentials() { return null;// credentials handled by facebook - we don't need them } public String getCode() { return code; } public void setCode(String code) { this.code = code; } }
And a FacebookAuthenticationInfo class
package uk.co.mrdw.shiro.facebook; import java.util.ArrayList; import java.util.Collection; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.SimplePrincipalCollection; public class FacebookAuthenticationInfo implements AuthenticationInfo { private static final long serialVersionUID = 1L; private PrincipalCollection principalCollection; public FacebookAuthenticationInfo(FacebookUserDetails facebookUserDetails, String realmName){ Collection<String> principals = new ArrayList<String>(); principals.add(facebookUserDetails.getId()); principals.add(facebookUserDetails.getFirstName()+" "+facebookUserDetails.getLastName()); // Is this appropriate is the name not really a Principal ? this.principalCollection = new SimplePrincipalCollection(principals, realmName); } @Override public PrincipalCollection getPrincipals() { return principalCollection; } @Override public Object getCredentials() { return null;// no credentials required } }
The FacebookLoginServlet then becomes much simpler – basically just use’s Shiro’s login mechanism.
package uk.co.mrdw.shiroexp.servlets; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import uk.co.mrdw.shiro.facebook.FacebookToken; /** * Simple Facebook Login Handling, doesn't actually do anything except display page confirming login * successfull. * * * @author Mike * */ public class FacebookLoginServlet extends HttpServlet { private static final long serialVersionUID = 1L; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("FacebookLoginServlet getting.."); String code = request.getParameter("code"); FacebookToken facebookToken = new FacebookToken(code); try{ SecurityUtils.getSubject().login(facebookToken); response.sendRedirect(response.encodeRedirectURL("index.jsp")); } catch(AuthenticationException ae){ throw new ServletException(ae); } } /** * @see HttpServlet#doPost(HttpServletRequest request, HttpServletResponse * response) */ protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { System.out.println("Unexpected doPost ..."); } }
I’ve added a few lines to index.jsp, just to give some feedback on the result of the login via facebook
<%@ page import="org.apache.shiro.SecurityUtils"%> <%@ page import="org.apache.shiro.subject.PrincipalCollection"%> ... <p>Is authenticated? <%= SecurityUtils.getSubject().isAuthenticated() %></p> <p>Principal:<%= SecurityUtils.getSubject().getPrincipal() %></p> <p>Principals:</p> <% PrincipalCollection principalCollection = SecurityUtils.getSubject().getPrincipals(); if(principalCollection!=null){ for(Object principal : principalCollection.asList()){ //request.out.println("<p>"+principal +"</p>"); %><p><%=principal %></p><% } } %>
which ends up with output like this
</pre> Is authenticated? true Principal:121212121 Principals: 121212121 Mike Warren <pre>
And that’s it – have just tested it and it works, I can login via facebook and get access to the “secure” page, get a display of the facebook ID as the principal, and can logout via the standard logout link.
Think I may need to read up on Principals and Roles a bit to see how to integrate shiro authorization capabilities with what I’ve got, but it may be that this is enough for my currrent needs.
I have a few doubts about whether this is the right way to use Shiro, so may well post a follow up post – “Using Shiro properly” to correct mistakes here.
Have added a question related to this to the Shiro user’s forum
http://shiro-user.582556.n2.nabble.com/Implementing-Facebook-Login-td7038905.html
hello
I enjoy what you guʏs tend tօ be up too. This sort of clever work and exposure!
Ҝeeƿ սp the terrific ѡorks guʏs I’ve adɗed you guys to my personal Ьlogroll.
Hello, i have successfully tried out your example. But now i’m facing a problem on how to automatically create shiro native session upon the manual subject.login(token). Do you have any ideas on how to do this ? Thank you !
The session creation works well. It was my problem with noSessionCreation filter. Thanks for the great help. Your solution works better than any other options out there !
Hi Mike, I tried to implement your code, but I am stucked at currentUser.login(token) method. The following error is reported. Please suggest what am i doing wrong. Either their is configuration mistake or else what !!
Realm [FacebookRealm@52039826] does not support authentication token [FacebookToken@132d9844]. Please ensure that the appropriate Realm implementation is configured correctly or that the realm accepts AuthenticationTokens of this type.
You never posted the code for FacebookAuthorizationInfo. Anything special there?
This blog post is too old to be of much use now.