package org.dataone.portal; import java.io.IOException; import java.net.URL; import java.security.cert.Certificate; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.text.ParseException; import java.util.Arrays; import java.util.Calendar; import java.util.Date; import java.util.Timer; import java.util.TimerTask; import javax.net.ssl.HttpsURLConnection; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.dataone.client.auth.AuthTokenSession; import org.dataone.client.auth.CertificateManager; import org.dataone.client.v1.itk.D1Client; import org.dataone.configuration.Settings; import org.dataone.service.exceptions.BaseException; import org.dataone.service.types.v1.Person; import org.dataone.service.types.v1.Session; import org.dataone.service.types.v1.Subject; import org.dataone.service.types.v1.SubjectInfo; import org.dataone.service.util.DateTimeMarshaller; import com.nimbusds.jose.JOSEException; import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.JWSHeader; import com.nimbusds.jose.JWSSigner; import com.nimbusds.jose.JWSVerifier; import com.nimbusds.jose.crypto.RSASSASigner; import com.nimbusds.jose.crypto.RSASSAVerifier; import com.nimbusds.jwt.JWTClaimsSet; import com.nimbusds.jwt.SignedJWT; /** * Class for generating JSON web tokens for authenticated users. * Targeting this for use with AnnotateIt.org. * @see "http://docs.annotatorjs.org/en/latest/authentication.html" * @author leinfelder * */ public class TokenGenerator { public static Log log = LogFactory.getLog(TokenGenerator.class); private static TokenGenerator instance = null; private String consumerKey = null; private RSAPublicKey publicKey = null; private RSAPrivateKey privateKey = null; // 18 hour default, like certificates, in seconds private int TTL_SECONDS = Settings.getConfiguration().getInt("token.ttl", 18 * 60 * 60); public static TokenGenerator getInstance() throws IOException { if (instance == null) { instance = new TokenGenerator(); } return instance; } /* * Construct a token generator * @throws IOException an I/O exeption if the certificates cannot be read */ private TokenGenerator() throws IOException { setPrivateKey(); setConsumerKey(); setPublicKey(); /* Create a timer to monitor the signing certificate every five minutes */ Timer timer = new Timer("Signing Certificate Monitor"); long certMonitorPeriod = 5 * 60 * 1000; timer.scheduleAtFixedRate(new TimerTask() { /** * Check the server certificate's public key modulus for changes * Update the TokenGenerator singleton if it has changed */ @Override public void run() { try { Certificate certificate = fetchServerCertificate(); if ( certificate != null ) { RSAPublicKey currentKey = (RSAPublicKey) certificate.getPublicKey(); // Replace the singleton in-memory key if it does not match the fetched key if ( ! currentKey.getModulus().equals(publicKey.getModulus()) ) { setPublicKey(); setPrivateKey(); setConsumerKey(); log.info("Portal reset the private key and public certificate after the certificate was renewed. The new certificate has the mudulus " + publicKey.getModulus().toString(16)); } } } catch (Exception e) { log.warn("Couldn't fetch the server certificate for change comparison. " + e.getMessage()); } } }, new Date(), certMonitorPeriod); } /** * fetches the server certificates from the remote CN using the configured * CN baseurl from d1_libclient_java. Returns the first server certificate. * * @return either the Certificate or null (if problem) */ public Certificate fetchServerCertificate() { try { String baseUrl = D1Client.getCN().getNodeBaseServiceUrl(); log.debug("fetching cert from server: " + baseUrl); URL url = new URL(baseUrl); HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); conn.connect(); Certificate serverCertificate = conn.getServerCertificates()[0]; return serverCertificate; } catch (Exception e) { log.error(e.getMessage(), e); } return null; } public String getJWT(String userId, String fullName) throws JOSEException, ParseException, IOException { // Create RSA-signer with the private key JWSSigner signer = new RSASSASigner(privateKey); // Calendar instances are associated with a fixed Date, confusingly // accessed with getTime() method. Calendar now = Calendar.getInstance(); Calendar expires = Calendar.getInstance(); expires.setTime(now.getTime()); expires.add(Calendar.SECOND, TTL_SECONDS); // Prepare JWT with claims set JWTClaimsSet claimsSet = new JWTClaimsSet(); // claims for annotator: http://docs.annotatorjs.org/en/v1.2.x/authentication.html claimsSet.setClaim("consumerKey", consumerKey); claimsSet.setClaim("userId", userId); claimsSet.setClaim("issuedAt", DateTimeMarshaller.serializeDateToUTC(now.getTime())); claimsSet.setClaim("ttl", TTL_SECONDS); claimsSet.setClaim("fullName", fullName); // standard JWT fields: https://tools.ietf.org/html/rfc7519#section-4.1.4 claimsSet.setSubject(userId); claimsSet.setIssueTime(now.getTime()); claimsSet.setExpirationTime(expires.getTime()); // purposefully skipping setting the claimsSet.setNotBeforeTime(nbf) to // avoid fussiness related to clock skew. SignedJWT signedJWT = new SignedJWT(new JWSHeader(JWSAlgorithm.RS256), claimsSet); // Compute the RSA signature signedJWT.sign(signer); // To serialize to compact form, produces something like // eyJhbGciOiJIUzI1NiJ9.SGVsbG8sIHdvcmxkIQ.onO9Ihudz3WkiauDO2Uhyuz0Y18UASXlSc1eS0NkWyA String token = signedJWT.serialize(); return token; } /* * Set the private key * @throws IOException IO exception */ private void setPrivateKey() throws IOException { String privateKeyFileName = Settings.getConfiguration().getString("cn.server.privatekey.filename"); String privateKeyPassword = null; CertificateManager cmInst = CertificateManager.getInstance(); // consumers do not need the private key if (privateKeyFileName != null) { privateKey = (RSAPrivateKey) cmInst.loadPrivateKeyFromFile(privateKeyFileName, privateKeyPassword); } } /* * Set the public key * @throws IOException */ private void setPublicKey() throws IOException { // use either the configured certificate, or fetch it from the CN String certificateFileName = Settings.getConfiguration().getString("cn.server.publiccert.filename"); CertificateManager cmInst = CertificateManager.getInstance(); log.debug("certificateFileName=" + certificateFileName); if (certificateFileName != null && certificateFileName.length() > 0) { publicKey = (RSAPublicKey) cmInst.loadCertificateFromFile(certificateFileName).getPublicKey(); } else { Certificate cert = fetchServerCertificate(); log.debug("using certificate from server: " + cert); if (cert != null) { publicKey = (RSAPublicKey) cert.getPublicKey(); } // what happens if publicKey is null? } } /* * Set the consumer key */ private void setConsumerKey() { consumerKey = Settings.getConfiguration().getString("annotator.consumerKey"); } /** * Extracts the subject from the token string, and attempts to get the * SubjectInfo from the CN. If not able to, builds a SubjectInfo entry * from the token subject. * @param token the given JWT token string * @return a Session or null if Exceptions raised (they are logged as Warnings) */ public Session getSession(String token) { AuthTokenSession session = null; try { // parse the JWS and verify it SignedJWT signedJWT = SignedJWT.parse(token); // verify the signing JWSVerifier verifier = new RSASSAVerifier(publicKey); if (!signedJWT.verify(verifier)) { log.info("public key: " + publicKey); log.warn("Could not use public key to verify provided token: " + token); // Reload the certificate keys in case they changed, and retry setPrivateKey(); setPublicKey(); setConsumerKey(); verifier = new RSASSAVerifier(publicKey); if ( ! signedJWT.verify(verifier)) { log.info("public key: " + publicKey); log.warn("Again, could not use public key to verify provided token: " + token); return null; } } // check the expiration Calendar now = Calendar.getInstance(); Date expDate = signedJWT.getJWTClaimsSet().getExpirationTime(); if (!expDate.after(now.getTime())) { log.warn("Token expiration date has passed: " + expDate); return null; } // we only accept tokens generated in this class, and since we don't // generate a NotBeforeTime claim, we don't need to process it. // extract user info String userId = signedJWT.getJWTClaimsSet().getSubject(); Subject subject = new Subject(); subject.setValue(userId); session = new AuthTokenSession(token); session.setSubject(subject); SubjectInfo subjectInfo = null; try { subjectInfo = D1Client.getCN().getSubjectInfo(subject); } catch (Exception be) { log.warn(be.getMessage(), be); } // TODO: fill in more subject info if we didn't retrieve it if (subjectInfo == null) { subjectInfo = new SubjectInfo(); Person person = new Person(); person.setSubject(subject); person.setFamilyName("Unknown"); person.addGivenName("Unknown"); subjectInfo.setPersonList(Arrays.asList(person)); } session.setSubjectInfo(subjectInfo); } catch (Exception e) { // if we got here, we don't have a good session log.warn("Could not get session from provided token: " + token, e); // e.printStackTrace(); return null; } return session; } /** * For generating custom tokens outside of the portal workflow. * These properties should be set in portal.properties: * token.ttl=31536000 * cn.server.privatekey.filename=/Users/leinfelder/Downloads/dataone_org.key * cn.server.publiccert.filename=/Users/leinfelder/Downloads/_.dataone.org.crt * The main class should be called with and parameters. * The token will be printed to System.out * @param args */ public static void main(String[] args) { String userId = args[0]; String fullName = "Unknown"; if (args.length > 1) { fullName = args[1]; } String token = null; try { token = TokenGenerator.getInstance().getJWT(userId, fullName); } catch (JOSEException | ParseException | IOException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(token); } }