In the previous blog post, we built the Spring API that responds with Profile information. Continuing on the path to building authentication with JWT, in this blog post, we will create a login mechanism that issues a JWT when the user presents the correct credentials.
These are the blog posts in this series:
- Part 1 - Discussion of JWT and implementation
- Part 2 - A Spring User Profiles API
- Part 3 - Issuing a token from the server
- Part 4 - Verifying the token sent back by the client
- Part 5 - Securing the front end
Our fist step is to configure Spring Security to allow access to the login end point we will be building. This can be done as follows:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().ignoringAntMatchers("/login");
http.authorizeRequests()
.antMatchers("/login")
.permitAll()
.antMatchers("/**/*")
.denyAll();
}
}
We are turning off authentication and CSRF token checking for the /login
end point.
Next, we build a LoginController
to issue tokens up on a user presenting valid credentials:
@RestController
@RequestMapping(path = "/login")
public class LoginController {
private final LoginService loginService;
private final JwtService jwtService;
@SuppressWarnings("unused")
public LoginController() {
this(null, null);
}
@Autowired
public LoginController(LoginService loginService, JwtService jwtService) {
this.loginService = loginService;
this.jwtService = jwtService;
}
@RequestMapping(path = "",
method = POST,
produces = APPLICATION_JSON_VALUE)
public MinimalProfile login(@RequestBody LoginCredentials credentials,
HttpServletResponse response) {
return loginService.login(credentials)
.map(minimalProfile -> {
try {
response.setHeader("Token", jwtService.tokenFor(minimalProfile));
} catch (Exception e) {
throw new RuntimeException(e);
}
return minimalProfile;
})
.orElseThrow(() -> new FailedToLoginException(credentials.getUsername()));
}
}
We use the login service to verify the credentials and it returns an Optional<MinimalProfile>
. If there is a valid MinimalProfile
, we ask the JwtService
to issue a token.
The LoginService
uses the ProfileService
to load a profile matching the user name and the password presented by the user.
@Component
public class LoginService {
private ProfileService profileService;
@SuppressWarnings("unused")
public LoginService() {
this(null);
}
@Autowired
public LoginService(ProfileService profileService) {
this.profileService = profileService;
}
public Optional<MinimalProfile> login(LoginCredentials credentials) {
return profileService.get(credentials.getUsername())
.filter(profile -> profile.getLogin().getPassword().equals(credentials.getPassword()))
.map(profile -> new MinimalProfile(profile));
}
}
Please note that in real applications, you never want to do this. You should be comparing the hashed version of the password presented by the user with the hashed version of the password stored in the database.
The JwtService
creates a token using the profile information and an expiration date of 2 hours with the HMASHA256
algorithm. It uses the key provided by SecretKeyProvider
. For creating the JWT token, we use the excellent jjwt 1 library we introduced in part 1.
@Component
public class JwtService {
private static final String ISSUER = "in.sdqali.jwt";
private SecretKeyProvider secretKeyProvider;
@SuppressWarnings("unused")
public JwtService() {
this(null);
}
@Autowired
public JwtService(SecretKeyProvider secretKeyProvider) {
this.secretKeyProvider = secretKeyProvider;
}
public String tokenFor(MinimalProfile minimalProfile) throws IOException, URISyntaxException {
byte[] secretKey = secretKeyProvider.getKey();
Date expiration = Date.from(LocalDateTime.now().plusHours(2).toInstant(UTC));
return Jwts.builder()
.setSubject(minimalProfile.getUsername())
.setExpiration(expiration)
.setIssuer(ISSUER)
.signWith(SignatureAlgorithm.HS512, secretKey)
.compact();
}
}
The SecretKeyProvider
in this example will simply load the secret key from a file, where it is stored in plain text. In a real application, you may store and encrypted version of it and decrypt it when required.
@Component
public class SecretKeyProvider {
public byte[] getKey() throws URISyntaxException, IOException {
return Files.readAllBytes(Paths.get(this.getClass().getResource("/jwt.key").toURI()));
}
}
With this code, a client can authenticate and receive a JWT. When request is made with correct username and password:
$ curl -v -X POST "http://localhost:8080/login" -d '{"username":"greenrabbit948", "password":"celeste"}' --header "Content-Type: application/json" | jq .
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Trying ::1...
* Connected to localhost (::1) port 8080 (#0)
> POST /login HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.43.0
> Accept: */*
> Content-Type: application/json
> Content-Length: 51
>
} [51 bytes data]
* upload completely sent off: 51 out of 51 bytes
< HTTP/1.1 200 OK
< Server: Apache-Coyote/1.1
< X-Content-Type-Options: nosniff
< X-XSS-Protection: 1; mode=block
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
< Pragma: no-cache
< Expires: 0
< X-Frame-Options: DENY
< Token: eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJqd3QtZGVtbyIsImV4cCI6MTQ2Nzc2Njk3MSwiaXNzIjoiaW4uc2RxYWxpLmp3dCJ9.eu_OuBIkc4BfcTsTu4t_6TCwyLkH4HcuQzvWIMzNQYdxXiWA77SfvwCe4mdc7C17mXdtBAsvFGDj7A9fzI0M1w
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Wed, 06 Jul 2016 06:02:51 GMT
<
{ [164 bytes data]
100 211 0 160 100 51 15071 4804 --:--:-- --:--:-- --:--:-- 16000
* Connection #0 to host localhost left intact
{
"username": "greenrabbit948",
"name": {
"title": "miss",
"first": "dionaura",
"last": "rodrigues"
},
"thumbnail": "https://randomuser.me/api/portraits/thumb/women/78.jpg"
}
We can see the Token
header has value eyJhbGciOiJIUzUxMiJ9.
eyJzdWIiOiJqd3QtZGVtbyIsImV4cCI6MTQ2Nzc2Njk3MSwiaXNzIjoiaW4uc2RxYWxpLmp3dCJ9.
eu_OuBIkc4BfcTsTu4t_6TCwyLkH4HcuQzvWIMzNQYdxXiWA77SfvwCe4mdc7C17mXdtBAsvFGDj
7A9fzI0M1w
.
If we were to present invalid credentials, the API will return a 401
:
$ curl -i -X POST "http://localhost:8080/login" -d '{"username":"greenrabbit948", "password":"wrongpassword"}' --header "Content-Type: application/json"
HTTP/1.1 401 Unauthorized
Server: Apache-Coyote/1.1
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Length: 0
Date: Wed, 06 Jul 2016 06:05:46 GMT
In the next blog post, the fourth is this series, we will move on to verifying the token presented by the client for subsequent requests. The source code for this example for the progress made from part 1 through part 3 is available on GitHub.