I’m trying to implement a solution with the encoder/decoder from the org.springframework.security.oauth2.jwt package with a shared secret.
But my attempt fails when I try to encode a token with a JwtEncodingException.
I have asked this question in another form, but here I include a simple ready to execute example, to verify the problem.
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.jwk.source.ImmutableSecret;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.security.oauth2.jwt.*;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
public class EncoderDecoderTest {
public static String secret = "j8IoV1jF67";
public JwtEncoder jwtEncoder() throws JOSEException {
SecretKey originalKey = new SecretKeySpec(secret.getBytes(), "AES");
JWKSource<SecurityContext> immutableSecret = new ImmutableSecret<SecurityContext>(originalKey);
return new NimbusJwtEncoder(immutableSecret);
}
public JwtDecoder jwtDecoder() {
SecretKey originalKey = new SecretKeySpec(secret.getBytes(), "AES");
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(originalKey).build();
return jwtDecoder;
}
public void tester() throws JOSEException {
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self") //Only this for simplicity
.build();
var encoder = jwtEncoder();
//This line throws the exception
String token = encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
System.out.println(token);
var decoder = jwtDecoder();
Jwt jwt = decoder.decode(token);
System.out.println(jwt.getIssuer());
}
public static void main(String[] args) throws JOSEException {
new EncoderDecoderTest().tester();
}
}
jps has come up with a an explanation to why this doesn't work (AES is not a valid signature algorithm), thanks :-)
I guess my question could then be rephrased into: how do I implement a shared secret version of these two encoders/decoders, for example with HMAC using SHA-256?
org.springframework.security.oauth2.jwt.NimbusJwtDecoder
org.springframework.security.oauth2.jwt.NimbusJwtEncoder
I tried (a lot) but have not yet succeeded ;-(
Any suggestions for how to fix this problem?
With the input from jps (AES is actually OK for encrypted tokens, but not signed) and Github Copilot I came up with a working solution using HMAC-SHA256:
//Don't hardcode like this for real
public static String secret = "s/4KMb61LOrMYYAn4rfaQYSgr+le5SMrsMzKw8G6bXc=";
public JwtEncoder jwtEncoder() throws JOSEException {
SecretKey key = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
JWKSource<SecurityContext> immutableSecret = new ImmutableSecret<SecurityContext>(key);
return new NimbusJwtEncoder(immutableSecret);
}
public JwtDecoder jwtDecoder() {
SecretKey originalKey = new SecretKeySpec(secret.getBytes(), "HmacSHA256");
NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(originalKey).build();
return jwtDecoder;
}
public void tester() throws JOSEException {
//Create the TOKEN
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("https://somewhere.com") //Only this for simplicity
.build();
var encoder = jwtEncoder();
JwsHeader jwsHeader = JwsHeader.with(() -> "HS256").build();
String token = encoder.encode(JwtEncoderParameters.from(jwsHeader, claims)).getTokenValue();
//Decode the TOKEN
var decoder = jwtDecoder();
Jwt jwt = decoder.decode(token);
System.out.println(jwt.getIssuer());
}
Related
I have a JWT with the secret key :
private final static Algorithm ALGORITHM = Algorithm.HMAC256("secret");
public String createToken(#NonNull String username) {
Timestamp timestamp = Timestamp.from(Instant.from(ZonedDateTime.now(ZoneId.of("Z"))));
Timestamp expTime = Timestamp.from(Instant.from(ZonedDateTime.now(ZoneId.of("Z")).plusMinutes(10)));
try {
String token = JWT.create()
.withIssuer("auth0")
.withClaim("username", username)
.withClaim("time", timestamp).withExpiresAt(expTime)
.sign(ALGORITHM);
return token;
and the verify method:
public DecodedJWT verifyToken(String token) {
DecodedJWT decodedJWT = JWT.decode(token);
try {
JWTVerifier verifier = JWT
.require(ALGORITHM)
.withIssuer("auth0")
.build();
DecodedJWT jwt = verifier.verify(token);
return jwt;
} catch (JWTVerificationException exception) {
System.out.println("token not verified");
}
My problem is that the token returned from the method has an invaild signature as by https://jwt.io/ . Also the verify method is not working because of that.
I read some blogs in which they said that you have to encode your secret, so I tried it like this:
private final static Algorithm ALGORITHM = Algorithm.HMAC256(Base64.getEncoder().encodeToString("secret".getBytes()));
but it didnt work the signature was also invalid. Has anyone an idea on how can I fix that?
Thanks in advance
MongooseIM has a provision to use JWT instead of username and password for authorization.
On the server-side, the docs suggest to modify the mongooseim.toml file (can be found at /etc/mongooseim/mongooseim.toml)
[auth]
methods = ["jwt"]
[auth.jwt]
secret.value = "top-secret123"
algorithm = "HS256"
username_key = "user"
But how does then one authenticate from Gajim or from Java code?
Let's first understand what is happening behind the scenes.
Instead of passing the username-password pair. We create a JWT token and send that. JWT tokens are stateless, which means if one has the secret key, one can decode and encode the token to/from the original message.
Here is a working code in Java. We generate the JWT token and send that token instead of the password. To generate the JWT token, we have used Auth0 (you will need to add this in classpath). Link to the maven project.
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.ConnectionConfiguration;
import org.jivesoftware.smack.chat2.Chat;
import org.jivesoftware.smack.chat2.ChatManager;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import javax.net.ssl.X509TrustManager;
import java.net.InetAddress;
import java.util.Date;
public class JWTMain {
private final static String senderUsername = "jatin";
private final static String senderPassword = "abcd";
private final static String sendTo = "dad";
public static void main(String[] args) throws Exception {
Algorithm algorithm = Algorithm.HMAC256("top-secret123");
String token = JWT.create()
.withClaim("user", senderUsername) // they key needs to match with `username_key` in mongooseim.toml file
.withClaim(senderUsername, senderPassword)
.sign(algorithm);
System.out.println("Token generated: " + token);
XMPPTCPConnectionConfiguration config = XMPPTCPConnectionConfiguration.builder()
.setSecurityMode(ConnectionConfiguration.SecurityMode.required)
.setUsernameAndPassword("jatin", token)
.setXmppDomain(JidCreate.domainBareFrom("localhost"))
.setHostAddress(InetAddress.getByName("localhost"))
.setPort(5222)
.setCustomX509TrustManager(new TrustAllManager())
.addEnabledSaslMechanism("PLAIN")
.build();
AbstractXMPPConnection connection = new XMPPTCPConnection(config);
AbstractXMPPConnection connect = connection.connect();
connection.login();
sendMessage("This message is being sent programmatically? " + new Date(), sendTo + "#localhost", connect);
}
private static void sendMessage(String body, String toJid, AbstractXMPPConnection mConnection) throws Exception {
Jid jid = JidCreate.from(toJid);
Chat chat = ChatManager.getInstanceFor(mConnection)
.chatWith(jid.asEntityBareJidIfPossible());
chat.send(body);
System.out.println("Message sent to : " + toJid);
}
}
class TrustAllManager implements X509TrustManager {
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return null;
}
public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {
}
}
If you wish to login to Gajim with the JWT token:
The above program outputs the JWT token. You can use that token and provide the token in the password field.
I have an old application running in PHP that uses the function base64_encode(hash_hmac(“sha512”, $p_password, $p_salt, true)) to encode passwords in our database.
I am migrating this application to Java Spring Boot and want to encode the passwords during authentification in the exact same way.
I have found how to make the same hashing method with Java in this post Compute HMAC-SHA512 with secret key in java and I also learnt that we can have several password encoders for old and new users with https://spring.io/blog/2017/11/01/spring-security-5-0-0-rc1-released#constructing-delegatingpasswordencoder
But I still cannot find an example of how I can integrate this hasing method in Spring authentication process. I have to create a PasswordEncoder bean and I don't know what to put inside.
I tried Pbkdf2PasswordEncoder because it can make some SHA-512 hash like in my app but I get the error Detected a Non-hex character at 1 or 2 position.
It is probably due to the fact that the passwords are not prefixed by {pbkdf2} in the database. The following code is what I am currently using as PasswordEncoder
#Bean
public PasswordEncoder passwordEncoder() {
Pbkdf2PasswordEncoder passwordEncoder = new Pbkdf2PasswordEncoder("salt");
passwordEncoder.setAlgorithm(Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA512);
return passwordEncoder;
}
I need help to set up the right password encoder to use HMAC-SHA512 in my authentification process with Java Spring and in a second time, combine it with BCrytPasswordEncoder (for new users) with DelegatingPasswordEncoder.
Maybe it requires to update the passwords in DB to prefix them with the right encoder ?
If my question is not accurate enough or missing information, please ask me for more details :)
You need to add a DelegatingPasswordEncoder to your project configuration file.
The DelegatingPasswordEncoder acts as a PasswordEncoder, and we use it when we have to choose from a collection of implementations.:
#Configuration
public class ProjectConfig extends WebSecurityConfigurerAdapter {
#Bean
public PasswordEncoder passwordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
Pbkdf2PasswordEncoder bcryprPe = new Pbkdf2PasswordEncoder("salt");
bcryprPe.setAlgorithm(
Pbkdf2PasswordEncoder.SecretKeyFactoryAlgorithm.PBKDF2WithHmacSHA512);
encoders.put("pbkdf2", pbkdf2Pe);
// add other PasswordEncoder here:
encoders.put("scrypt", new SCryptPasswordEncoder());
return new DelegatingPasswordEncoder("pbkdf2", encoders);
}
}
With
return new DelegatingPasswordEncoder("pbkdf2", encoders);
we are saying to Spring Security: "Use the 'pbkdf2' as default password encoder".
If the provided hash is {scrypt}12345, the DelegatingPasswordEncoder delegates to the SCryptPasswordEncoder, if there is no prefix, the application will use default one.
I finally got what I wanted. I created an implementation of PasswordEncoder inspired by https://github.com/lathspell/java_test/blob/master/java_test_openldap/src/main/java/LdapSha512PasswordEncoder.java
in WebSecurityConfig.java
#Bean
public PasswordEncoder passwordEncoder() {
Map<String, PasswordEncoder> encoders = new HashMap<>();
encoders.put("SSHA-512", new Hmac512PasswordEncoder("salt"));
encoders.put("bcrypt", new BCryptPasswordEncoder());
return new DelegatingPasswordEncoder("SSHA-512", encoders);
}
in Hmac512PasswordEncoder.java
public class Hmac512PasswordEncoder implements PasswordEncoder {
private static final String SSHA512_PREFIX = "{SSHA-512}";
private static final String HMAC_SHA512 = "HmacSHA512";
private final String salt;
public Hmac512PasswordEncoder(String salt) {
if (salt == null) {
throw new IllegalArgumentException("salt cannot be null");
}
this.salt = salt;
}
public String encode(CharSequence rawPassword) {
String result = null;
try {
Mac sha512Hmac = Mac.getInstance(HMAC_SHA512);
final byte[] byteKey = Utf8.encode(salt);
SecretKeySpec keySpec = new SecretKeySpec(byteKey, HMAC_SHA512);
sha512Hmac.init(keySpec);
byte[] macData = sha512Hmac.doFinal(Utf8.encode(rawPassword.toString()));
result = SSHA512_PREFIX + Base64.getEncoder().encodeToString(macData);
//result = bytesToHex(macData);
} catch (InvalidKeyException | NoSuchAlgorithmException e) {
e.printStackTrace();
}
return result;
}
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null || encodedPassword == null) {
return false;
}
String encodedRawPass = encode(rawPassword);
return MessageDigest.isEqual(Utf8.encode(encodedRawPass), Utf8.encode(encodedPassword));
}
}
I am facing a issue that whenever I am signing a token also I parse it and it is not throwing any signature exception.
You can see the key are different still it giving me the proper response.
public class JwtUtil {
public String parseToken(String token) {
try {
Jws<Claims> jwt = Jwts.parser()
.setSigningKey("Test#12")
.parseClaimsJws(token);
System.out.println(jwt.getBody().getSubject());
return "Valid";
} catch (SignatureException jwtException) {
jwtException.printStackTrace();
return null;
}
}
public String generateToken() {
Claims claim = Jwts.claims();
claim.put("GivenName", "Johnny");
claim.put("Surname", "Rocket");
claim.put("Email", "jrocket#example.com");
return Jwts.builder().setHeaderParam("typ", "JWT").setClaims(claim)
.setIssuer("Online JWT Builder")
.setAudience("www.example.com").setSubject("jrocket#example.com")
.signWith(SignatureAlgorithm.HS256, "Test#123").compact();
}
public static void main(String[] args) {
JwtUtil jwtUtil = new JwtUtil();
String token = jwtUtil.generateToken();
System.out.println(token);
JwtUtil jwtUtil1 = new JwtUtil();
jwtUtil1.parseToken(token);
}
}
Really Test#12 and Test#123 are the same key
It is due to JwtBuilder.signWith(SignatureAlgorithm alg, String base64EncodedSecretKey). assumes that you are providing a key in base64 and your keys are not base64. When the method decodes from base64 to byte[] the java converter used by jjwt provides a representation of the string. Test#12 and Test#123 are encoded with the byte array
See https://stackoverflow.com/a/38269014/6371459
You can test yourself with
System.out.println(
javax.xml.bind.DatatypeConverter.printBase64Binary(
javax.xml.bind.DatatypeConverter.parseBase64Binary("Test#12")));
System.out.println(
javax.xml.bind.DatatypeConverter.printBase64Binary(
javax.xml.bind.DatatypeConverter.parseBase64Binary("Test#123")));
Try a (more) different key and the SignatureException will be thrown
I am using amazon s3 android low level sdk to upload a file and want to set md5 checksum to upload data.
1)Following is the code to create credentials:
BasicAWSCredentials lAwsCredentials = new BasicAWSCredentials(
Constants.ACCESS_KEY_ID, Constants.SECRET_KEY);
AmazonS3Client lS3Client = new AmazonS3Client(lAwsCredentials);
2)Following is the code to calculate md5
public class MD5CheckSum {
public static byte[] createChecksum(String pFilepath) throws Exception {
InputStream lFis = new FileInputStream(pFilepath);
byte[] lBuffer = new byte[1024];
MessageDigest lMessageDigest = MessageDigest.getInstance("MD5");
int lNumRead;
do {
lNumRead = lFis.read(lBuffer);
if (lNumRead > 0) {
lMessageDigest.update(lBuffer, 0, lNumRead);
}
} while (lNumRead != -1);
lFis.close();
return lMessageDigest.digest();
}
public static String getMD5Checksum(String pFilepath) throws Exception {
byte[] lBytes = createChecksum(pFilepath);
return Base64.encodeToString(lBytes, Base64.DEFAULT);
}
}
3)Following is the code to set md5 using metadata:
try {
lMd5 = MD5CheckSum.getMD5Checksum(pFile.getAbsolutePath());
Log.v(TAG, "CheckSum:====" + lMd5);
} catch (Exception lException) {
lException.printStackTrace();
}
ObjectMetadata lObjectMetadata = new ObjectMetadata();
if (lMd5 != null) {
lObjectMetadata.setContentMD5(lMd5);
}`
InitiateMultipartUploadResult mInitResponse = mS3Client.initiateMultipartUpload(new InitiateMultipartUploadRequest(mBucketName, mKeyName,
lObjectMetadata);
But a exception is thrown by amazon when i set md5:
Caused by: com.amazonaws.services.s3.model.AmazonS3Exception: Anonymous users cannot initiate multipart uploads. Please authenticate. (Service: Amazon S3; Status Code: 403; Error Code: AccessDenied; Request ID: BA0C68FC884703FD), S3 Extended Request ID: re2sdbzf8MMqGAyrNQOoqYJ8EdXERoWE7cjG+UpfAtFuP5IeAbXmk6Riw+PX8Uw3Jcspn1rSQvI=
Is this the correct way to set md5?
Note: When md5 is not set(ie objectmetadata is not set) then upload works without any exception
I have also faced this type of issue..
I fixed by adding Base64.DEFAULT instead of others such as WRAP and NO_WRAP.