Created
December 12, 2014 20:46
-
-
Save ccashwell/dfc05dd8bd1a75d189d1 to your computer and use it in GitHub Desktop.
RESTful Authentication via Spring (http://stackoverflow.com/questions/10826293/restful-authentication-via-spring)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.some.api.support; | |
import com.some.domain.User; | |
public class AuthenticationResult { | |
private String token; | |
private User user; | |
public String getToken() { | |
return token; | |
} | |
public void setToken(String token) { | |
this.token = token; | |
} | |
public User getUser() { | |
return user; | |
} | |
public void setUser(User user) { | |
this.user = user; | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.some.api.support.spring; | |
import com.some.api.support.TokenProvider; | |
import com.some.domain.User; | |
import java.io.IOException; | |
import javax.servlet.FilterChain; | |
import javax.servlet.ServletException; | |
import javax.servlet.ServletRequest; | |
import javax.servlet.ServletResponse; | |
import javax.servlet.http.HttpServletRequest; | |
import org.apache.commons.logging.Log; | |
import org.apache.commons.logging.LogFactory; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.security.authentication.AuthenticationManager; | |
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | |
import org.springframework.security.core.context.SecurityContext; | |
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; | |
import org.springframework.web.filter.GenericFilterBean; | |
public class AuthenticationTokenProcessingFilter extends GenericFilterBean { | |
private static Log log = LogFactory.getLog(AuthenticationTokenProcessingFilter.class); | |
@Autowired TokenProvider tokenProvider; | |
AuthenticationManager authManager; | |
SecurityContextProvider securityContextProvider; | |
WebAuthenticationDetailsSource webAuthenticationDetailsSource = new WebAuthenticationDetailsSource(); | |
public AuthenticationTokenProcessingFilter(AuthenticationManager authManager) { | |
this.authManager = authManager; | |
this.securityContextProvider = new SecurityContextProvider(); | |
} | |
@Override | |
public void doFilter(ServletRequest request, ServletResponse response, | |
FilterChain chain) throws IOException, ServletException { | |
log.debug("Checking headers and parameters for authentication token..."); | |
String token = null; | |
HttpServletRequest httpServletRequest = (HttpServletRequest) request; | |
if (httpServletRequest.getParameter("token") != null) { | |
token = httpServletRequest.getParameter("token"); | |
log.debug("Found token '" + token + "' in request parameters"); | |
} else if (httpServletRequest.getHeader("Authentication-token") != null) { | |
token = httpServletRequest.getHeader("Authentication-token"); | |
log.debug("Found token '" + token + "' in request headers"); | |
} | |
if (token != null) { | |
if (tokenProvider.isTokenValid(token)) { | |
User user = tokenProvider.getUserFromToken(token); | |
authenticateUser(httpServletRequest, user); | |
} | |
} | |
chain.doFilter(request, response); | |
} | |
private void authenticateUser(HttpServletRequest request, User user) { | |
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(user.getEmail(), user.getHashedPassword()); | |
authentication.setDetails(webAuthenticationDetailsSource.buildDetails(request)); | |
SecurityContext sc = securityContextProvider.getSecurityContext(); | |
sc.setAuthentication(authManager.authenticate(authentication)); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.some.api.support.spring; | |
import com.some.domain.User; | |
import com.some.services.UserService; | |
import java.util.Collections; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.security.authentication.BadCredentialsException; | |
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; | |
import org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider; | |
import org.springframework.security.core.AuthenticationException; | |
import org.springframework.security.core.GrantedAuthority; | |
import org.springframework.security.core.userdetails.UserDetails; | |
public class CustomApiAuthProvider extends AbstractUserDetailsAuthenticationProvider { | |
@Autowired UserService userService; | |
@Override | |
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) | |
throws AuthenticationException { | |
} | |
@Override | |
protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException { | |
User user = userService.findUserByHashedCredentials(authentication.getName(), (String)authentication.getCredentials()); | |
if(null == user) { | |
throw new BadCredentialsException("Username not found"); | |
} | |
return new org.springframework.security.core.userdetails.User(user.getEmail(), user.getHashedPassword(), Collections.<GrantedAuthority>emptySet()); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.some.api.support.spring; | |
import java.io.IOException; | |
import javax.servlet.ServletException; | |
import javax.servlet.http.HttpServletRequest; | |
import javax.servlet.http.HttpServletResponse; | |
import org.springframework.security.core.AuthenticationException; | |
import org.springframework.security.web.AuthenticationEntryPoint; | |
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { | |
@Override | |
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { | |
response.sendError( HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized" ); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.some.api.support.spring; | |
import org.springframework.security.core.context.SecurityContext; | |
import org.springframework.security.core.context.SecurityContextHolder; | |
public class SecurityContextProvider { | |
public SecurityContext getSecurityContext() { | |
return SecurityContextHolder.getContext(); | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.some.api.support; | |
import com.some.domain.User; | |
import com.some.services.UserService; | |
import java.security.MessageDigest; | |
import java.security.NoSuchAlgorithmException; | |
import org.apache.commons.codec.binary.Base64; | |
import org.apache.commons.lang.StringUtils; | |
import org.joda.time.DateTime; | |
import org.springframework.beans.factory.annotation.Autowired; | |
public class TokenProvider { | |
@Autowired UserService userService; | |
MessageDigest md5er; | |
private final String secretKey; | |
static String FENCE_POST = "!!!"; | |
private static final String NEWLINE = "\r\n"; | |
public TokenProvider(String secretKey) { | |
try { | |
md5er = MessageDigest.getInstance("MD5"); | |
} catch (NoSuchAlgorithmException e) { | |
throw new RuntimeException("Cannot find MD5 algorithm",e); | |
} | |
if(StringUtils.isEmpty(secretKey)){ | |
throw new IllegalArgumentException("Secret key must be set"); | |
} | |
this.secretKey = secretKey; | |
} | |
public String getToken(User user) { | |
return getToken(user, DateTime.now().plusDays(1).getMillis()); | |
} | |
public String getToken(User user, long expirationDateInMillis) { | |
StringBuilder tokenBuilder = new StringBuilder(); | |
byte[] token = tokenBuilder | |
.append(user.getEmail()) | |
.append(FENCE_POST) | |
.append(expirationDateInMillis) | |
.append(FENCE_POST) | |
.append(new String(buildTokenKey(expirationDateInMillis, user))) | |
.toString().getBytes(); | |
// returns a value ending in a newline, remove it | |
return Base64.encodeBase64String(token).replace(NEWLINE, ""); | |
} | |
public boolean isTokenValid(String encodedToken) { | |
String[] components = decodeAndDissectToken(encodedToken); | |
if (components == null || components.length != 3) { | |
return false; | |
} | |
String externalUser = components[0]; | |
Long externalDate = Long.parseLong(components[1]); | |
String externalKey = components[2]; | |
User user = userService.findUserByEmail(externalUser); | |
String expectedKey = new String(buildTokenKey(externalDate, user)); | |
byte[] expectedKeyBytes = expectedKey.getBytes(); | |
byte[] externalKeyBytes = externalKey.getBytes(); | |
if (!MessageDigest.isEqual(expectedKeyBytes, externalKeyBytes)) { | |
return false; | |
} | |
if (new DateTime(externalDate).isBeforeNow()) { | |
return false; | |
} | |
return true; | |
} | |
private byte[] buildTokenKey(long expirationDateInMillis, User user) { | |
StringBuilder keyBuilder = new StringBuilder(); | |
String key = keyBuilder | |
.append(user.getEmail()) | |
.append(FENCE_POST) | |
.append(user.getHashedPassword()) | |
.append(FENCE_POST) | |
.append(expirationDateInMillis) | |
.append(FENCE_POST) | |
.append(secretKey).toString(); | |
byte[] keyBytes = key.getBytes(); | |
return md5er.digest(keyBytes); | |
} | |
public User getUserFromToken(String token) { | |
if (!isTokenValid(token)) { return null; } | |
String[] components = decodeAndDissectToken(token); | |
if (components == null || components.length != 3) { return null; } | |
String email = components[0]; | |
return userService.findUserByEmail(email); | |
} | |
private String[] decodeAndDissectToken(String encodedToken) { | |
if(StringUtils.isBlank(encodedToken) || !Base64.isArrayByteBase64(encodedToken.getBytes())) { | |
return null; | |
} | |
// Apache Commons Base64 expects encoded strings to end with a newline, add one | |
if(!encodedToken.endsWith(NEWLINE)) { encodedToken = encodedToken + NEWLINE; } | |
String token = new String(Base64.decodeBase64(encodedToken)); | |
if(!token.contains(FENCE_POST) || token.split(FENCE_POST).length != 3) { | |
return null; | |
} | |
return token.split(FENCE_POST); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment