Skip to content

Commit 2e56af9

Browse files
committed
add device specific token generation
- remove token expiration check - refactor token helper class - refactor login flow - add device checker - add longer expiration time for mobile device - ui login page bug fix
1 parent bd32aac commit 2e56af9

15 files changed

+438
-193
lines changed

README.md

+1-2
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,7 @@ springboot-jwt-starter/
7171
│ │ │ │ └──UserController.java * REST controller to handle User related requests
7272
│ │ │ ├──security * Security related folder(JWT, filters)
7373
│ │ │ │ ├──auth
74-
│ │ │ │ │ ├──AuthenticationFailureHandler.java * login fail handler, configrued in WebSecurityConfig
75-
│ │ │ │ │ ├──AuthenticationSuccessHandler.java * login success handler, configrued in WebSecurityConfig
74+
│ │ │ │ │ ├──JwtAuthenticationRequest.java * login request object, contains username and password
7675
│ │ │ │ │ ├──LogoutSuccess.java * controls the behavior after sign out.
7776
│ │ │ │ │ ├──RestAuthenticationEntryPoint.java * handle auth exceptions, like invalid token etc.
7877
│ │ │ │ │ ├──TokenAuthenticationFilter.java * the JWT token filter, configured in WebSecurityConfig

pom.xml

+4
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,10 @@
3737
<groupId>org.springframework.boot</groupId>
3838
<artifactId>spring-boot-starter-data-jpa</artifactId>
3939
</dependency>
40+
<dependency>
41+
<groupId>org.springframework.boot</groupId>
42+
<artifactId>spring-boot-starter-mobile</artifactId>
43+
</dependency>
4044
<dependency>
4145
<groupId>io.jsonwebtoken</groupId>
4246
<artifactId>jjwt</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package com.bfwg.common;
2+
3+
import org.springframework.stereotype.Component;
4+
5+
import java.io.Serializable;
6+
import java.util.Date;
7+
8+
/**
9+
* Created by fanjin on 2017-10-07.
10+
*/
11+
@Component
12+
public class TimeProvider implements Serializable {
13+
14+
private static final long serialVersionUID = -3301695478208950415L;
15+
16+
public Date now() {
17+
return new Date();
18+
}
19+
}

src/main/java/com/bfwg/config/WebSecurityConfig.java

+4-11
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,6 @@ public void configureGlobal( AuthenticationManagerBuilder auth ) throws Exceptio
5353
.passwordEncoder( passwordEncoder() );
5454
}
5555

56-
@Autowired
57-
private AuthenticationSuccessHandler authenticationSuccessHandler;
58-
59-
@Autowired
60-
private AuthenticationFailureHandler authenticationFailureHandler;
61-
62-
6356
@Autowired
6457
TokenHelper tokenHelper;
6558

@@ -72,6 +65,8 @@ protected void configure(HttpSecurity http) throws Exception {
7265
.antMatchers(
7366
HttpMethod.GET,
7467
"/",
68+
"/auth/**",
69+
"/webjars/**",
7570
"/*.html",
7671
"/favicon.ico",
7772
"/**/*.html",
@@ -81,10 +76,6 @@ protected void configure(HttpSecurity http) throws Exception {
8176
.antMatchers("/auth/**").permitAll()
8277
.anyRequest().authenticated().and()
8378
.addFilterBefore(new TokenAuthenticationFilter(tokenHelper, userDetailsService), BasicAuthenticationFilter.class)
84-
.formLogin()
85-
.loginPage("/auth/login")
86-
.successHandler(authenticationSuccessHandler)
87-
.failureHandler(authenticationFailureHandler).and()
8879
.logout()
8980
.logoutRequestMatcher(new AntPathRequestMatcher("/auth/logout"))
9081
.logoutSuccessHandler(logoutSuccess)
@@ -104,6 +95,8 @@ public void configure(WebSecurity web) throws Exception {
10495
web.ignoring().antMatchers(
10596
HttpMethod.GET,
10697
"/",
98+
"/auth/**",
99+
"/webjars/**",
107100
"/*.html",
108101
"/favicon.ico",
109102
"/**/*.html",

src/main/java/com/bfwg/rest/AuthenticationController.java

+62-2
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
package com.bfwg.rest;
22

3+
import com.bfwg.model.User;
34
import com.bfwg.model.UserTokenState;
45
import com.bfwg.security.TokenHelper;
6+
import com.bfwg.security.auth.JwtAuthenticationRequest;
7+
import com.fasterxml.jackson.databind.ObjectMapper;
58
import org.springframework.beans.factory.annotation.Autowired;
69
import org.springframework.beans.factory.annotation.Value;
710
import org.springframework.http.MediaType;
811
import org.springframework.http.ResponseEntity;
12+
import org.springframework.mobile.device.Device;
13+
import org.springframework.mobile.device.DeviceUtils;
14+
import org.springframework.security.authentication.AuthenticationManager;
15+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
16+
import org.springframework.security.core.Authentication;
17+
import org.springframework.security.core.AuthenticationException;
18+
import org.springframework.security.core.context.SecurityContextHolder;
19+
import org.springframework.web.bind.annotation.RequestBody;
920
import org.springframework.web.bind.annotation.RequestMapping;
1021
import org.springframework.web.bind.annotation.RequestMethod;
1122
import org.springframework.web.bind.annotation.RestController;
1223

1324
import javax.servlet.http.Cookie;
1425
import javax.servlet.http.HttpServletRequest;
1526
import javax.servlet.http.HttpServletResponse;
27+
import java.io.IOException;
1628

1729
/**
1830
* Created by fan.jin on 2017-05-10.
@@ -25,24 +37,72 @@ public class AuthenticationController {
2537
@Autowired
2638
TokenHelper tokenHelper;
2739

40+
@Autowired
41+
ObjectMapper objectMapper;
42+
43+
@Autowired
44+
private AuthenticationManager authenticationManager;
45+
2846
@Value("${jwt.expires_in}")
2947
private int EXPIRES_IN;
3048

49+
@Value("${jwt.mobile_expires_in}")
50+
private int MOBILE_EXPIRES_IN;
51+
3152
@Value("${jwt.cookie}")
3253
private String TOKEN_COOKIE;
3354

55+
@RequestMapping(value = "/login", method = RequestMethod.POST)
56+
public ResponseEntity<?> createAuthenticationToken(
57+
@RequestBody JwtAuthenticationRequest authenticationRequest,
58+
HttpServletResponse response,
59+
Device device
60+
) throws AuthenticationException, IOException {
61+
62+
// Perform the security
63+
final Authentication authentication = authenticationManager.authenticate(
64+
new UsernamePasswordAuthenticationToken(
65+
authenticationRequest.getUsername(),
66+
authenticationRequest.getPassword()
67+
)
68+
);
69+
SecurityContextHolder.getContext().setAuthentication(authentication);
70+
71+
User user = (User)authentication.getPrincipal();
72+
73+
String jws = tokenHelper.generateToken( user.getUsername(), device);
74+
75+
UserTokenState userTokenState;
76+
if (device.isMobile() || device.isTablet()) {
77+
userTokenState = new UserTokenState(jws, MOBILE_EXPIRES_IN);
78+
} else {
79+
// Create token auth Cookie
80+
Cookie authCookie = new Cookie( TOKEN_COOKIE, ( jws ) );
81+
authCookie.setPath( "/" );
82+
authCookie.setHttpOnly( true );
83+
authCookie.setMaxAge( EXPIRES_IN );
84+
// Add cookie to response
85+
response.addCookie( authCookie );
86+
userTokenState = new UserTokenState(jws, EXPIRES_IN);
87+
}
88+
// Return the token
89+
return ResponseEntity.ok(userTokenState);
90+
}
91+
3492
@RequestMapping(value = "/refresh", method = RequestMethod.GET)
3593
public ResponseEntity<?> refreshAuthenticationToken(HttpServletRequest request, HttpServletResponse response) {
3694

3795
String authToken = tokenHelper.getToken( request );
96+
Device currentDevice = DeviceUtils.getCurrentDevice(request);
97+
3898
if (authToken != null && tokenHelper.canTokenBeRefreshed(authToken)) {
3999
// TODO check user password last update
40-
String refreshedToken = tokenHelper.refreshToken(authToken);
100+
String refreshedToken = tokenHelper.refreshToken(authToken, currentDevice);
41101

42102
Cookie authCookie = new Cookie( TOKEN_COOKIE, ( refreshedToken ) );
43103
authCookie.setPath( "/" );
44104
authCookie.setHttpOnly( true );
45-
authCookie.setMaxAge( EXPIRES_IN );
105+
// authCookie.setMaxAge( EXPIRES_IN );
46106
// Add cookie to response
47107
response.addCookie( authCookie );
48108

src/main/java/com/bfwg/security/TokenHelper.java

+97-39
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,18 @@
11
package com.bfwg.security;
22

3+
import com.bfwg.common.TimeProvider;
34
import io.jsonwebtoken.Claims;
45
import io.jsonwebtoken.Jwts;
56
import io.jsonwebtoken.SignatureAlgorithm;
6-
import org.joda.time.DateTime;
77
import org.springframework.beans.factory.annotation.Autowired;
88
import org.springframework.beans.factory.annotation.Value;
9-
import org.springframework.security.core.userdetails.UserDetailsService;
9+
import org.springframework.mobile.device.Device;
10+
import org.springframework.security.core.userdetails.UserDetails;
1011
import org.springframework.stereotype.Component;
1112

1213
import javax.servlet.http.Cookie;
1314
import javax.servlet.http.HttpServletRequest;
1415
import java.util.Date;
15-
import java.util.Map;
16-
import java.util.function.Function;
1716

1817

1918
/**
@@ -32,74 +31,133 @@ public class TokenHelper {
3231
@Value("${jwt.expires_in}")
3332
private long EXPIRES_IN;
3433

34+
@Value("${jwt.mobile_expires_in}")
35+
private long MOBILE_EXPIRES_IN;
36+
3537
@Value("${jwt.header}")
3638
private String AUTH_HEADER;
3739

3840
@Value("${jwt.cookie}")
3941
private String AUTH_COOKIE;
4042

43+
static final String AUDIENCE_UNKNOWN = "unknown";
44+
static final String AUDIENCE_WEB = "web";
45+
static final String AUDIENCE_MOBILE = "mobile";
46+
static final String AUDIENCE_TABLET = "tablet";
47+
4148
@Autowired
42-
UserDetailsService userDetailsService;
49+
TimeProvider timeProvider;
4350

4451
private SignatureAlgorithm SIGNATURE_ALGORITHM = SignatureAlgorithm.HS512;
4552

4653
public String getUsernameFromToken(String token) {
47-
return getClaimsFromToken(token, Claims::getSubject);
54+
String username;
55+
try {
56+
final Claims claims = this.getAllClaimsFromToken(token);
57+
username = claims.getSubject();
58+
} catch (Exception e) {
59+
username = null;
60+
}
61+
return username;
4862
}
4963

50-
public Boolean canTokenBeRefreshed(String token) {
51-
final Date expirationDate = getClaimsFromToken(token, Claims::getExpiration);
52-
return expirationDate.compareTo(generateCurrentDate()) > 0;
64+
public Date getIssuedAtDateFromToken(String token) {
65+
Date issueAt;
66+
try {
67+
final Claims claims = this.getAllClaimsFromToken(token);
68+
issueAt = claims.getIssuedAt();
69+
} catch (Exception e) {
70+
issueAt = null;
71+
}
72+
return issueAt;
73+
}
74+
75+
public String getAudienceFromToken(String token) {
76+
String audience;
77+
try {
78+
final Claims claims = this.getAllClaimsFromToken(token);
79+
audience = claims.getAudience();
80+
} catch (Exception e) {
81+
audience = null;
82+
}
83+
return audience;
5384
}
5485

55-
public String refreshToken(String token) {
56-
final Claims claims = getAllClaimsFromToken(token);
57-
claims.setIssuedAt(generateCurrentDate());
58-
return generateToken(claims);
86+
public String refreshToken(String token, Device device) {
87+
String refreshedToken;
88+
try {
89+
final Claims claims = this.getAllClaimsFromToken(token);
90+
claims.setIssuedAt(timeProvider.now());
91+
refreshedToken = Jwts.builder()
92+
.setClaims(claims)
93+
.setExpiration(generateExpirationDate(device))
94+
.signWith( SIGNATURE_ALGORITHM, SECRET )
95+
.compact();
96+
} catch (Exception e) {
97+
refreshedToken = null;
98+
}
99+
return refreshedToken;
59100
}
60101

61-
public String generateToken(String username) {
102+
public String generateToken(String username, Device device) {
103+
String audience = generateAudience(device);
62104
return Jwts.builder()
63105
.setIssuer( APP_NAME )
64106
.setSubject(username)
65-
.setIssuedAt(generateCurrentDate())
66-
.setExpiration(generateExpirationDate())
107+
.setAudience(audience)
108+
.setIssuedAt(timeProvider.now())
109+
.setExpiration(generateExpirationDate(device))
67110
.signWith( SIGNATURE_ALGORITHM, SECRET )
68111
.compact();
69112
}
70113

71-
72-
private <T> T getClaimsFromToken(String token, Function<Claims, T> claimsResolver) {
73-
Claims claims = getAllClaimsFromToken(token);
74-
return claimsResolver.apply(claims);
114+
private String generateAudience(Device device) {
115+
String audience = AUDIENCE_UNKNOWN;
116+
if (device.isNormal()) {
117+
audience = AUDIENCE_WEB;
118+
} else if (device.isTablet()) {
119+
audience = AUDIENCE_TABLET;
120+
} else if (device.isMobile()) {
121+
audience = AUDIENCE_MOBILE;
122+
}
123+
return audience;
75124
}
76125

77126
private Claims getAllClaimsFromToken(String token) {
78-
return Jwts.parser()
79-
.setSigningKey(SECRET)
80-
.parseClaimsJws(token)
81-
.getBody();
82-
}
83-
84-
String generateToken(Map<String, Object> claims) {
85-
return Jwts.builder()
86-
.setClaims(claims)
87-
.setExpiration(generateExpirationDate())
88-
.signWith( SIGNATURE_ALGORITHM, SECRET )
89-
.compact();
127+
Claims claims;
128+
try {
129+
claims = Jwts.parser()
130+
.setSigningKey(SECRET)
131+
.parseClaimsJws(token)
132+
.getBody();
133+
} catch (Exception e) {
134+
claims = null;
135+
}
136+
return claims;
90137
}
91138

92-
private long getCurrentTimeMillis() {
93-
return DateTime.now().getMillis();
139+
private Date generateExpirationDate(Device device) {
140+
long expiresIn = device.isTablet() || device.isMobile() ? MOBILE_EXPIRES_IN : EXPIRES_IN;
141+
return new Date(timeProvider.now().getTime() + expiresIn * 1000);
94142
}
95143

96-
private Date generateCurrentDate() {
97-
return new Date(getCurrentTimeMillis());
144+
public Boolean canTokenBeRefreshed(String token) {
145+
final Date created = getIssuedAtDateFromToken(token);
146+
if (created == null) {
147+
return false;
148+
} else {
149+
return true;
150+
}
98151
}
99152

100-
private Date generateExpirationDate() {
101-
102-
return new Date(getCurrentTimeMillis() + this.EXPIRES_IN * 1000);
153+
public Boolean validateToken(String token, UserDetails userDetails) {
154+
final String username = getUsernameFromToken(token);
155+
// final Date created = getIssuedAtDateFromToken(token);
156+
return (
157+
username != null &&
158+
username.equals(userDetails.getUsername())
159+
// && !isCreatedBeforeLastPasswordReset(created, user.getLastPasswordResetDate())
160+
);
103161
}
104162

105163
public String getToken( HttpServletRequest request ) {

0 commit comments

Comments
 (0)