JwtService.java

1
package com.rbctcsworld.ecommerce.auth;
2
3
import com.rbctcsworld.ecommerce.common.SharedEnvironment;
4
5
import io.jsonwebtoken.Jwts;
6
import io.jsonwebtoken.security.Keys;
7
import org.springframework.beans.factory.annotation.Value;
8
import org.springframework.core.env.Environment;
9
import org.springframework.stereotype.Service;
10
11
import javax.crypto.SecretKey;
12
import java.nio.charset.StandardCharsets;
13
import java.util.Date;
14
15
/**
16
 * Issues and verifies HS256 tokens. Only the e-mail (subject) is trusted from the token: the ROLE is read
17
 * from the database on every request (JwtFilter), so editing the token to "role: ADMIN" gives nothing.
18
 *
19
 * Verification rejects: a wrong/edited signature, "alg: none" (unsigned) tokens, expired tokens and
20
 * tokens from another issuer.
21
 */
22
@Service
23
public class JwtService {
24
25
    public static final String ISSUER = "rbctcsworld-ecommerce";
26
    /** The development default in application.yml. It is public (in Git), so it must never run in production. */
27
    static final String DEV_DEFAULT_SECRET = "change-this-development-secret-key-which-must-be-long-enough-123456789";
28
29
    private final String secret;
30
    private final long expirationMs;
31
    private final Environment env;
32
    private SecretKey key;
33
34
    public JwtService(@Value("${app.jwt.secret}") String secret,
35
                      @Value("${app.jwt.expiration-ms}") long expirationMs,
36
                      Environment env) {
37
        this.secret = secret;
38
        this.expirationMs = expirationMs;
39
        this.env = env;
40 1 1. <init> : removed call to com/rbctcsworld/ecommerce/auth/JwtService::validateSecret → KILLED
        validateSecret();
41
    }
42
43
    /** Fails the application start instead of running with an unsafe key. */
44
    private void validateSecret() {
45 1 1. validateSecret : removed conditional - replaced equality check with false → KILLED
        byte[] bytes = secret == null ? new byte[0] : secret.getBytes(StandardCharsets.UTF_8);
46 2 1. validateSecret : removed conditional - replaced comparison check with false → KILLED
2. validateSecret : changed conditional boundary → KILLED
        if (bytes.length < 32) {
47
            throw new IllegalStateException("app.jwt.secret must be at least 32 bytes (256 bits) for HS256");
48
        }
49 2 1. validateSecret : removed conditional - replaced equality check with false → KILLED
2. validateSecret : removed conditional - replaced equality check with false → KILLED
        if (SharedEnvironment.isShared(env) && DEV_DEFAULT_SECRET.equals(secret)) {
50
            throw new IllegalStateException("Refusing to start: the public development JWT secret is used with the 'prod' profile"
51
                    + " or the public demo."
52
                    + " Set the JWT_SECRET environment variable.");
53
        }
54
        key = Keys.hmacShaKeyFor(bytes);
55
    }
56
57
    public String generate(String email) {
58
        long now = System.currentTimeMillis();
59 1 1. generate : replaced return value with "" for com/rbctcsworld/ecommerce/auth/JwtService::generate → KILLED
        return Jwts.builder()
60
                .issuer(ISSUER)
61
                .subject(email)
62 1 1. generate : Replaced long addition with subtraction → KILLED
                .issuedAt(new Date(now))
63
                .expiration(new Date(now + expirationMs))
64
                .signWith(key)
65
                .compact();
66
    }
67
68
    /** Lifetime of a new access token, sent to the client as expiresIn. */
69
    public long expiresInSeconds() {
70 2 1. expiresInSeconds : Replaced long division with multiplication → KILLED
2. expiresInSeconds : replaced long return with 0 for com/rbctcsworld/ecommerce/auth/JwtService::expiresInSeconds → KILLED
        return expirationMs / 1000;
71
    }
72
73
    /** Returns the e-mail of a valid token; throws for anything else (JwtFilter then treats the caller as anonymous). */
74
    public String email(String token) {
75 1 1. email : replaced return value with "" for com/rbctcsworld/ecommerce/auth/JwtService::email → KILLED
        return Jwts.parser()
76
                .verifyWith(key)
77
                .requireIssuer(ISSUER)
78
                .build()
79
                .parseSignedClaims(token)
80
                .getPayload()
81
                .getSubject();
82
    }
83
}

Mutations

40

1.1
Location : <init>
Killed by : com.rbctcsworld.ecommerce.auth.JwtServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.JwtServiceTest]/[method:secretOfExactly32BytesIsTheMinimum()]
removed call to com/rbctcsworld/ecommerce/auth/JwtService::validateSecret → KILLED

45

1.1
Location : validateSecret
Killed by : com.rbctcsworld.ecommerce.auth.JwtServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.JwtServiceTest]/[method:secretOfExactly32BytesIsTheMinimum()]
removed conditional - replaced equality check with false → KILLED

46

1.1
Location : validateSecret
Killed by : com.rbctcsworld.ecommerce.auth.JwtServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.JwtServiceTest]/[method:secretOfExactly32BytesIsTheMinimum()]
removed conditional - replaced comparison check with false → KILLED

2.2
Location : validateSecret
Killed by : com.rbctcsworld.ecommerce.auth.JwtServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.JwtServiceTest]/[method:secretOfExactly32BytesIsTheMinimum()]
changed conditional boundary → KILLED

49

1.1
Location : validateSecret
Killed by : com.rbctcsworld.ecommerce.auth.JwtServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.JwtServiceTest]/[method:publicDevSecretIsRefusedInProduction()]
removed conditional - replaced equality check with false → KILLED

2.2
Location : validateSecret
Killed by : com.rbctcsworld.ecommerce.auth.JwtServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.JwtServiceTest]/[method:publicDevSecretIsRefusedInProduction()]
removed conditional - replaced equality check with false → KILLED

59

1.1
Location : generate
Killed by : com.rbctcsworld.ecommerce.auth.JwtServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.JwtServiceTest]/[method:validTokenRoundTrip()]
replaced return value with "" for com/rbctcsworld/ecommerce/auth/JwtService::generate → KILLED

62

1.1
Location : generate
Killed by : com.rbctcsworld.ecommerce.auth.JwtServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.JwtServiceTest]/[method:validTokenRoundTrip()]
Replaced long addition with subtraction → KILLED

70

1.1
Location : expiresInSeconds
Killed by : com.rbctcsworld.ecommerce.auth.JwtServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.JwtServiceTest]/[method:expiresInIsTheLifetimeInSeconds()]
Replaced long division with multiplication → KILLED

2.2
Location : expiresInSeconds
Killed by : com.rbctcsworld.ecommerce.auth.JwtServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.JwtServiceTest]/[method:expiresInIsTheLifetimeInSeconds()]
replaced long return with 0 for com/rbctcsworld/ecommerce/auth/JwtService::expiresInSeconds → KILLED

75

1.1
Location : email
Killed by : com.rbctcsworld.ecommerce.auth.JwtServiceTest.[engine:junit-jupiter]/[class:com.rbctcsworld.ecommerce.auth.JwtServiceTest]/[method:validTokenRoundTrip()]
replaced return value with "" for com/rbctcsworld/ecommerce/auth/JwtService::email → KILLED

Active mutators

Tests examined


Report generated by PIT 1.30.0 support