Browse Source

working on sending mails: prerequisite mail configuration in progress

Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
sqlite
Stephan Richter 4 months ago
parent
commit
f3c4c098c0
  1. 7
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java
  2. 49
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/MailConfig.java
  3. 2
      de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Permission.java
  4. 9
      de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java
  5. 2
      de.srsoftware.oidc.backend/build.gradle
  6. 38
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/EmailController.java
  7. 36
      de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java
  8. 89
      de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java
  9. 3
      de.srsoftware.oidc.web/src/main/resources/en/scripts/common.js
  10. 83
      de.srsoftware.oidc.web/src/main/resources/en/scripts/settings.js
  11. 26
      de.srsoftware.oidc.web/src/main/resources/en/settings.html

7
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/Constants.java

@ -27,6 +27,7 @@ public class Constants {
public static final String INVALID_REQUEST = "invalid_request"; public static final String INVALID_REQUEST = "invalid_request";
public static final String INVALID_REQUEST_OBJECT = "invalid_request_object"; public static final String INVALID_REQUEST_OBJECT = "invalid_request_object";
public static final String INVALID_SCOPE = "invalid_scope"; public static final String INVALID_SCOPE = "invalid_scope";
public static final String MAILCONFIG = "mail_config";
public static final String NAME = "name"; public static final String NAME = "name";
public static final String NONCE = "nonce"; public static final String NONCE = "nonce";
public static final String OPENID = "openid"; public static final String OPENID = "openid";
@ -36,8 +37,14 @@ public class Constants {
public static final String RESPONSE_TYPE = "response_type"; public static final String RESPONSE_TYPE = "response_type";
public static final String SCOPE = "scope"; public static final String SCOPE = "scope";
public static final String SECRET = "secret"; public static final String SECRET = "secret";
public static final String SENDER_ADDRESS = "sender_address";
public static final String SMTP_AUTH = "smtp_auth";
public static final String SMTP_HOST = "smtp_host";
public static final String SMTP_PORT = "smtp_port";
public static final String STATE = "state"; public static final String STATE = "state";
public static final String START_TLS = "start_tls";
public static final String TOKEN = "token"; public static final String TOKEN = "token";
public static final String TOKEN_TYPE = "token_type"; public static final String TOKEN_TYPE = "token_type";
public static final String UNAUTHORIZED_CLIENT = "unauthorized_client"; public static final String UNAUTHORIZED_CLIENT = "unauthorized_client";
public static final String USER = "user";
} }

49
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/MailConfig.java

@ -0,0 +1,49 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.api;
import static de.srsoftware.oidc.api.Constants.*;
import java.util.Map;
import java.util.Properties;
public interface MailConfig {
public String smtpHost();
public MailConfig smtpHost(String newValue);
public int smtpPort();
public MailConfig smtpPort(int newValue);
public String senderAddress();
public MailConfig senderAddress(String newValue);
public String senderPassword();
public MailConfig senderPassword(String newValue);
default boolean startTls() {
return true;
}
MailConfig startTls(boolean newValue);
default boolean smtpAuth() {
return true;
}
MailConfig smtpAuth(boolean newValue);
public default Properties props() {
Properties props = new Properties();
props.put("mail.smtp.host", smtpHost());
props.put("mail.smtp.port", smtpPort());
props.put("mail.smtp.auth", smtpAuth() ? "true" : "false");
props.put("mail.smtp.starttls.enable", startTls() ? "true" : "false");
return props;
}
default Map<String, Object> map() {
return Map.of( //
SMTP_HOST, smtpHost(), //
SMTP_PORT, smtpPort(), //
SMTP_AUTH, smtpAuth(), //
SENDER_ADDRESS, senderAddress(), //
START_TLS, startTls());
}
}

2
de.srsoftware.oidc.api/src/main/java/de/srsoftware/oidc/api/data/Permission.java

@ -1,4 +1,4 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.oidc.api.data; package de.srsoftware.oidc.api.data;
public enum Permission { MANAGE_CLIENTS, MANAGE_USERS } public enum Permission { MANAGE_CLIENTS, MANAGE_SMTP, MANAGE_USERS }

9
de.srsoftware.oidc.app/src/main/java/de/srsoftware/oidc/app/Application.java

@ -3,8 +3,7 @@ package de.srsoftware.oidc.app;
import static de.srsoftware.oidc.api.Constants.*; import static de.srsoftware.oidc.api.Constants.*;
import static de.srsoftware.oidc.api.data.Permission.MANAGE_CLIENTS; import static de.srsoftware.oidc.api.data.Permission.*;
import static de.srsoftware.oidc.api.data.Permission.MANAGE_USERS;
import static de.srsoftware.utils.Optionals.emptyIfBlank; import static de.srsoftware.utils.Optionals.emptyIfBlank;
import static de.srsoftware.utils.Paths.configDir; import static de.srsoftware.utils.Paths.configDir;
import static de.srsoftware.utils.Strings.uuid; import static de.srsoftware.utils.Strings.uuid;
@ -33,6 +32,7 @@ public class Application {
public static final String API_CLIENT = "/api/client"; public static final String API_CLIENT = "/api/client";
private static final String API_TOKEN = "/api/token"; private static final String API_TOKEN = "/api/token";
public static final String API_USER = "/api/user"; public static final String API_USER = "/api/user";
public static final String API_EMAIL = "/api/email";
public static final String FIRST_USER = "admin"; public static final String FIRST_USER = "admin";
public static final String FIRST_USER_PASS = "admin"; public static final String FIRST_USER_PASS = "admin";
public static final String FIRST_UUID = uuid(); public static final String FIRST_UUID = uuid();
@ -53,7 +53,7 @@ public class Application {
var keyDir = storageFile.getParentFile().toPath().resolve("keys"); var keyDir = storageFile.getParentFile().toPath().resolve("keys");
var passwordHasher = new UuidHasher(); var passwordHasher = new UuidHasher();
var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID); var firstHash = passwordHasher.hash(FIRST_USER_PASS, FIRST_UUID);
var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID).add(MANAGE_CLIENTS, MANAGE_USERS); var firstUser = new User(FIRST_USER, firstHash, FIRST_USER, "%s@internal".formatted(FIRST_USER), FIRST_UUID).add(MANAGE_CLIENTS, MANAGE_SMTP, MANAGE_USERS);
KeyStorage keyStore = new PlaintextKeyStore(keyDir); KeyStorage keyStore = new PlaintextKeyStore(keyDir);
KeyManager keyManager = new RotatingKeyManager(keyStore); KeyManager keyManager = new RotatingKeyManager(keyStore);
FileStore fileStore = new FileStore(storageFile, passwordHasher).init(firstUser); FileStore fileStore = new FileStore(storageFile, passwordHasher).init(firstUser);
@ -61,11 +61,12 @@ public class Application {
new StaticPages(basePath).bindPath(STATIC_PATH, FAVICON).on(server); new StaticPages(basePath).bindPath(STATIC_PATH, FAVICON).on(server);
new Forward(INDEX).bindPath(ROOT).on(server); new Forward(INDEX).bindPath(ROOT).on(server);
new WellKnownController().bindPath(WELL_KNOWN).on(server); new WellKnownController().bindPath(WELL_KNOWN).on(server);
new UserController(fileStore, fileStore).bindPath(API_USER).on(server); new UserController(fileStore, fileStore, fileStore).bindPath(API_USER).on(server);
var tokenControllerconfig = new TokenController.Configuration("https://lightoidc.srsoftware.de", 10); // TODO configure or derive from hostname var tokenControllerconfig = new TokenController.Configuration("https://lightoidc.srsoftware.de", 10); // TODO configure or derive from hostname
new TokenController(fileStore, fileStore, keyManager, fileStore, tokenControllerconfig).bindPath(API_TOKEN).on(server); new TokenController(fileStore, fileStore, keyManager, fileStore, tokenControllerconfig).bindPath(API_TOKEN).on(server);
new ClientController(fileStore, fileStore, fileStore).bindPath(API_CLIENT).on(server); new ClientController(fileStore, fileStore, fileStore).bindPath(API_CLIENT).on(server);
new KeyStoreController(keyStore).bindPath(JWKS).on(server); new KeyStoreController(keyStore).bindPath(JWKS).on(server);
new EmailController(fileStore, fileStore).bindPath(API_EMAIL).on(server);
server.setExecutor(Executors.newCachedThreadPool()); server.setExecutor(Executors.newCachedThreadPool());
server.start(); server.start();
} }

2
de.srsoftware.oidc.backend/build.gradle

@ -18,6 +18,8 @@ dependencies {
implementation project(':de.srsoftware.utils') implementation project(':de.srsoftware.utils')
implementation 'org.json:json:20240303' implementation 'org.json:json:20240303'
implementation 'org.bitbucket.b_c:jose4j:0.9.6' implementation 'org.bitbucket.b_c:jose4j:0.9.6'
implementation 'com.sun.mail:jakarta.mail:2.0.1'
} }
test { test {

38
de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/EmailController.java

@ -0,0 +1,38 @@
/* © SRSoftware 2024 */
package de.srsoftware.oidc.backend;
import static de.srsoftware.oidc.api.data.Permission.MANAGE_SMTP;
import static java.net.HttpURLConnection.HTTP_FORBIDDEN;
import static java.net.HttpURLConnection.HTTP_UNAUTHORIZED;
import com.sun.net.httpserver.HttpExchange;
import de.srsoftware.oidc.api.MailConfig;
import de.srsoftware.oidc.api.SessionService;
import de.srsoftware.oidc.api.data.User;
import java.io.IOException;
public class EmailController extends Controller {
private final MailConfig mailConfig;
public EmailController(MailConfig mailConfig, SessionService sessionService) {
super(sessionService);
this.mailConfig = mailConfig;
}
@Override
public boolean doGet(String path, HttpExchange ex) throws IOException {
var optSession = getSession(ex);
if (optSession.isEmpty()) return sendEmptyResponse(HTTP_UNAUTHORIZED, ex);
var user = optSession.get().user();
switch (path) {
case "/settings":
return provideSettings(ex, user);
}
return notFound(ex);
}
private boolean provideSettings(HttpExchange ex, User user) throws IOException {
if (!user.hasPermission(MANAGE_SMTP)) return sendEmptyResponse(HTTP_FORBIDDEN, ex);
return sendContent(ex, mailConfig.map());
}
}

36
de.srsoftware.oidc.backend/src/main/java/de/srsoftware/oidc/backend/UserController.java

@ -12,6 +12,8 @@ import de.srsoftware.http.SessionToken;
import de.srsoftware.oidc.api.*; import de.srsoftware.oidc.api.*;
import de.srsoftware.oidc.api.data.Session; import de.srsoftware.oidc.api.data.Session;
import de.srsoftware.oidc.api.data.User; import de.srsoftware.oidc.api.data.User;
import jakarta.mail.*;
import jakarta.mail.internet.*;
import java.io.IOException; import java.io.IOException;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@ -19,10 +21,19 @@ import org.json.JSONObject;
public class UserController extends Controller { public class UserController extends Controller {
private final UserService users; private final UserService users;
private final MailConfig mailConfig;
private final Authenticator auth;
public UserController(SessionService sessionService, UserService userService) { public UserController(MailConfig mailConfig, SessionService sessionService, UserService userService) {
super(sessionService); super(sessionService);
users = userService; users = userService;
this.mailConfig = mailConfig;
auth = new Authenticator() {
// override the getPasswordAuthentication method
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(mailConfig.senderAddress(), mailConfig.senderPassword());
}
};
} }
private boolean addUser(HttpExchange ex, Session session) throws IOException { private boolean addUser(HttpExchange ex, Session session) throws IOException {
@ -115,6 +126,29 @@ public class UserController extends Controller {
private void senPasswordLink(User user) { private void senPasswordLink(User user) {
LOG.log(WARNING, "Sending password link to {0}", user.email()); LOG.log(WARNING, "Sending password link to {0}", user.email());
try {
var session = jakarta.mail.Session.getDefaultInstance(mailConfig.props(), auth);
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(mailConfig.senderAddress()));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(user.email()));
message.setSubject("Mail Subject");
String msg = "This is my first email using JavaMailer";
MimeBodyPart mimeBodyPart = new MimeBodyPart();
mimeBodyPart.setContent(msg, "text/html; charset=utf-8");
Multipart multipart = new MimeMultipart();
multipart.addBodyPart(mimeBodyPart);
message.setContent(multipart);
Transport.send(message);
} catch (AddressException e) {
throw new RuntimeException(e);
} catch (MessagingException e) {
throw new RuntimeException(e);
}
} }
private boolean sendUserAndCookie(HttpExchange ex, Session session) throws IOException { private boolean sendUserAndCookie(HttpExchange ex, Session session) throws IOException {

89
de.srsoftware.oidc.datastore.file/src/main/java/de/srsoftware/oidc/datastore/file/FileStore.java

@ -1,6 +1,6 @@
/* © SRSoftware 2024 */ /* © SRSoftware 2024 */
package de.srsoftware.oidc.datastore.file; /* © SRSoftware 2024 */ package de.srsoftware.oidc.datastore.file; /* © SRSoftware 2024 */
import static de.srsoftware.oidc.api.Constants.EXPIRATION; import static de.srsoftware.oidc.api.Constants.*;
import static de.srsoftware.oidc.api.data.User.*; import static de.srsoftware.oidc.api.data.User.*;
import static de.srsoftware.utils.Optionals.nullable; import static de.srsoftware.utils.Optionals.nullable;
import static de.srsoftware.utils.Strings.uuid; import static de.srsoftware.utils.Strings.uuid;
@ -20,17 +20,14 @@ import java.time.temporal.ChronoUnit;
import java.util.*; import java.util.*;
import org.json.JSONObject; import org.json.JSONObject;
public class FileStore implements AuthorizationService, ClientService, SessionService, UserService { public class FileStore implements AuthorizationService, ClientService, SessionService, UserService, MailConfig {
private static final System.Logger LOG = System.getLogger(FileStore.class.getSimpleName());
private static final String AUTHORIZATIONS = "authorizations"; private static final String AUTHORIZATIONS = "authorizations";
private static final String CLIENTS = "clients"; private static final String CLIENTS = "clients";
private static final String CODES = "codes"; private static final String CODES = "codes";
private static final System.Logger LOG = System.getLogger(FileStore.class.getSimpleName());
private static final String NAME = "name";
private static final String REDIRECT_URIS = "redirect_uris"; private static final String REDIRECT_URIS = "redirect_uris";
private static final String SECRET = "secret";
private static final String SESSIONS = "sessions"; private static final String SESSIONS = "sessions";
private static final String USERS = "users"; private static final String USERS = "users";
private static final String USER = "user";
private static final List<String> KEYS = List.of(USERNAME, EMAIL, REALNAME); private static final List<String> KEYS = List.of(USERNAME, EMAIL, REALNAME);
private final Path storageFile; private final Path storageFile;
@ -87,6 +84,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
public FileStore init(User defaultUser) { public FileStore init(User defaultUser) {
if (!json.has(AUTHORIZATIONS)) json.put(AUTHORIZATIONS, new JSONObject()); if (!json.has(AUTHORIZATIONS)) json.put(AUTHORIZATIONS, new JSONObject());
if (!json.has(CLIENTS)) json.put(CLIENTS, new JSONObject()); if (!json.has(CLIENTS)) json.put(CLIENTS, new JSONObject());
if (!json.has(MAILCONFIG)) json.put(MAILCONFIG, new JSONObject());
if (!json.has(SESSIONS)) json.put(SESSIONS, new JSONObject()); if (!json.has(SESSIONS)) json.put(SESSIONS, new JSONObject());
if (!json.has(USERS)) save(defaultUser); if (!json.has(USERS)) save(defaultUser);
return this; return this;
@ -318,4 +316,83 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
private AuthResult unauthorized(Collection<String> scopes) { private AuthResult unauthorized(Collection<String> scopes) {
return new AuthResult(null, new HashSet<>(scopes), null); return new AuthResult(null, new HashSet<>(scopes), null);
} }
/*** MailConfig implementation ***/
private String mailConfig(String key) {
var config = json.getJSONObject(MAILCONFIG);
if (config.has(key)) return config.getString(key);
return "";
}
private FileStore mailConfig(String key, Object newValue) {
var config = json.getJSONObject(MAILCONFIG);
config.put(key, newValue);
return this;
}
@Override
public String smtpHost() {
return mailConfig("smtp_host");
}
@Override
public MailConfig smtpHost(String newValue) {
return mailConfig("smtp_host", newValue);
}
@Override
public int smtpPort() {
try {
return Integer.parseInt(mailConfig("smtp_port"));
} catch (NumberFormatException nfe) {
return 0;
}
}
@Override
public MailConfig smtpPort(int newValue) {
return mailConfig("smtp_port", newValue);
}
@Override
public String senderAddress() {
return mailConfig("sender_address");
}
@Override
public MailConfig senderAddress(String newValue) {
return mailConfig("sender_address", newValue);
}
@Override
public String senderPassword() {
return mailConfig("smtp_password");
}
@Override
public MailConfig senderPassword(String newValue) {
return mailConfig("smtp_password", newValue);
}
@Override
public boolean startTls() {
return "true".equals(mailConfig("start_tls"));
}
@Override
public MailConfig startTls(boolean newValue) {
return mailConfig("start_tls", newValue);
}
@Override
public boolean smtpAuth() {
return "true".equals(mailConfig("smtp_auth"));
}
@Override
public MailConfig smtpAuth(boolean newValue) {
return mailConfig("smtp_auth", newValue);
}
} }

3
de.srsoftware.oidc.web/src/main/resources/en/scripts/common.js

@ -39,7 +39,8 @@ function setText(id, text){
function setValue(id,newVal){ function setValue(id,newVal){
get(id).value = newVal; var elem = get(id);
if (elem) elem.value = newVal;
} }
function show(id){ function show(id){

83
de.srsoftware.oidc.web/src/main/resources/en/scripts/settings.js

@ -10,6 +10,23 @@ function fillForm(){
} }
} }
async function handlePasswordResponse(response){
if (response.ok){
hide('wrong_password');
hide('password_mismatch');
setText('passBtn', 'saved.');
} else {
setText('passBtn', 'Update failed!');
var text = await response.text();
if (text == 'wrong password') show('wrong_password');
if (text == 'password mismatch') show('password_mismatch');
}
enable('passBtn');
setTimeout(function(){
setText('passBtn','Update');
},10000);
}
function handleResponse(response){ function handleResponse(response){
if (response.ok){ if (response.ok){
@ -25,43 +42,24 @@ function handleResponse(response){
},10000); },10000);
} }
function update(){ async function handleSettings(response){
disable('updateBtn'); console.log('handleSettings(…)',response);
var newData = {
username : getValue('username'),
email : getValue('email'),
realname : getValue('realname'),
uuid : getValue('uuid')
}
fetch(user_controller+'/update',{
method : 'POST',
headers : {
'Content-Type': 'application/json'
},
body : JSON.stringify(newData)
}).then(handleResponse)
setText('updateBtn','sent…');
}
async function handlePasswordResponse(response){
if (response.ok){ if (response.ok){
hide('wrong_password'); var json = await response.json();
hide('password_mismatch'); for (var key in json){
setText('passBtn', 'saved.'); setValue(key,json[key]);
}
show('mail_settings');
} else { } else {
setText('passBtn', 'Update failed!'); hide('mail_settings');
var text = await response.text();
if (text == 'wrong password') show('wrong_password');
if (text == 'password mismatch') show('password_mismatch');
} }
enable('passBtn');
setTimeout(function(){
setText('passBtn','Update');
},10000);
} }
function passKeyDown(ev){
if (event.keyCode == 13) updatePass();
}
function updatePass(){ function updatePass(){
disable('passBtn'); disable('passBtn');
@ -80,8 +78,25 @@ function updatePass(){
setText('passBtn','sent…'); setText('passBtn','sent…');
} }
function passKeyDown(ev){
if (event.keyCode == 13) updatePass();
function update(){
disable('updateBtn');
var newData = {
username : getValue('username'),
email : getValue('email'),
realname : getValue('realname'),
uuid : getValue('uuid')
}
fetch(user_controller+'/update',{
method : 'POST',
headers : {
'Content-Type': 'application/json'
},
body : JSON.stringify(newData)
}).then(handleResponse)
setText('updateBtn','sent…');
} }
setTimeout(fillForm,100); setTimeout(fillForm,100);
fetch("/api/email/settings").then(handleSettings);

26
de.srsoftware.oidc.web/src/main/resources/en/settings.html

@ -78,6 +78,32 @@
</tr> </tr>
</table> </table>
</fieldset> </fieldset>
<fieldset id="mail_settings" style="display: none">
<legend>
Mail settings
</legend>
<table>
<tr>
<th>Smtp host</th>
<td><input type="text" id="smtp_host"></td>
</tr>
<tr>
<th>Smtp port</th>
<td><input type="text" id="smtp_port"></td>
</tr>
<tr>
<th>Sender email address</th>
<td><input type="text" id="sender_mail"></td>
</tr>
<tr>
<th>Sender password</th>
<td><input type="password" id="sender_password"></td>
</tr>
<td></td>
<td><button id="smtpBtn" type="button" onClick="updateSmtp()">Update</button></td>
</tr>
</table>
</fieldset>
</form> </form>
</div> </div>
</body> </body>

Loading…
Cancel
Save