working on sending mails: prerequisite mail configuration in progress
Signed-off-by: Stephan Richter <s.richter@srsoftware.de>
This commit is contained in:
@@ -27,6 +27,7 @@ public class Constants {
|
||||
public static final String INVALID_REQUEST = "invalid_request";
|
||||
public static final String INVALID_REQUEST_OBJECT = "invalid_request_object";
|
||||
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 NONCE = "nonce";
|
||||
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 SCOPE = "scope";
|
||||
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 START_TLS = "start_tls";
|
||||
public static final String TOKEN = "token";
|
||||
public static final String TOKEN_TYPE = "token_type";
|
||||
public static final String UNAUTHORIZED_CLIENT = "unauthorized_client";
|
||||
public static final String USER = "user";
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
/* © SRSoftware 2024 */
|
||||
package de.srsoftware.oidc.api.data;
|
||||
|
||||
public enum Permission { MANAGE_CLIENTS, MANAGE_USERS }
|
||||
public enum Permission { MANAGE_CLIENTS, MANAGE_SMTP, MANAGE_USERS }
|
||||
|
||||
@@ -3,8 +3,7 @@ package de.srsoftware.oidc.app;
|
||||
|
||||
|
||||
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.MANAGE_USERS;
|
||||
import static de.srsoftware.oidc.api.data.Permission.*;
|
||||
import static de.srsoftware.utils.Optionals.emptyIfBlank;
|
||||
import static de.srsoftware.utils.Paths.configDir;
|
||||
import static de.srsoftware.utils.Strings.uuid;
|
||||
@@ -33,6 +32,7 @@ public class Application {
|
||||
public static final String API_CLIENT = "/api/client";
|
||||
private static final String API_TOKEN = "/api/token";
|
||||
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_PASS = "admin";
|
||||
public static final String FIRST_UUID = uuid();
|
||||
@@ -53,7 +53,7 @@ public class Application {
|
||||
var keyDir = storageFile.getParentFile().toPath().resolve("keys");
|
||||
var passwordHasher = new UuidHasher();
|
||||
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);
|
||||
KeyManager keyManager = new RotatingKeyManager(keyStore);
|
||||
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 Forward(INDEX).bindPath(ROOT).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
|
||||
new TokenController(fileStore, fileStore, keyManager, fileStore, tokenControllerconfig).bindPath(API_TOKEN).on(server);
|
||||
new ClientController(fileStore, fileStore, fileStore).bindPath(API_CLIENT).on(server);
|
||||
new KeyStoreController(keyStore).bindPath(JWKS).on(server);
|
||||
new EmailController(fileStore, fileStore).bindPath(API_EMAIL).on(server);
|
||||
server.setExecutor(Executors.newCachedThreadPool());
|
||||
server.start();
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ dependencies {
|
||||
implementation project(':de.srsoftware.utils')
|
||||
implementation 'org.json:json:20240303'
|
||||
implementation 'org.bitbucket.b_c:jose4j:0.9.6'
|
||||
implementation 'com.sun.mail:jakarta.mail:2.0.1'
|
||||
|
||||
}
|
||||
|
||||
test {
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,8 @@ import de.srsoftware.http.SessionToken;
|
||||
import de.srsoftware.oidc.api.*;
|
||||
import de.srsoftware.oidc.api.data.Session;
|
||||
import de.srsoftware.oidc.api.data.User;
|
||||
import jakarta.mail.*;
|
||||
import jakarta.mail.internet.*;
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
@@ -19,10 +21,19 @@ import org.json.JSONObject;
|
||||
|
||||
public class UserController extends Controller {
|
||||
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);
|
||||
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 {
|
||||
@@ -115,6 +126,29 @@ public class UserController extends Controller {
|
||||
|
||||
private void senPasswordLink(User user) {
|
||||
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 {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* © 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.utils.Optionals.nullable;
|
||||
import static de.srsoftware.utils.Strings.uuid;
|
||||
@@ -20,17 +20,14 @@ import java.time.temporal.ChronoUnit;
|
||||
import java.util.*;
|
||||
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 CLIENTS = "clients";
|
||||
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 SECRET = "secret";
|
||||
private static final String SESSIONS = "sessions";
|
||||
private static final String USERS = "users";
|
||||
private static final String USER = "user";
|
||||
private static final List<String> KEYS = List.of(USERNAME, EMAIL, REALNAME);
|
||||
|
||||
private final Path storageFile;
|
||||
@@ -87,6 +84,7 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
||||
public FileStore init(User defaultUser) {
|
||||
if (!json.has(AUTHORIZATIONS)) json.put(AUTHORIZATIONS, 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(USERS)) save(defaultUser);
|
||||
return this;
|
||||
@@ -318,4 +316,83 @@ public class FileStore implements AuthorizationService, ClientService, SessionSe
|
||||
private AuthResult unauthorized(Collection<String> scopes) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,8 @@ function setText(id, text){
|
||||
|
||||
|
||||
function setValue(id,newVal){
|
||||
get(id).value = newVal;
|
||||
var elem = get(id);
|
||||
if (elem) elem.value = newVal;
|
||||
}
|
||||
|
||||
function show(id){
|
||||
|
||||
@@ -10,40 +10,6 @@ function fillForm(){
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleResponse(response){
|
||||
if (response.ok){
|
||||
hide('update_error')
|
||||
setText('updateBtn', 'saved.');
|
||||
} else {
|
||||
show('update_error');
|
||||
setText('updateBtn', 'Update failed!');
|
||||
}
|
||||
enable('updateBtn');
|
||||
setTimeout(function(){
|
||||
setText('updateBtn','Update');
|
||||
},10000);
|
||||
}
|
||||
|
||||
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…');
|
||||
}
|
||||
|
||||
|
||||
async function handlePasswordResponse(response){
|
||||
if (response.ok){
|
||||
hide('wrong_password');
|
||||
@@ -62,6 +28,38 @@ async function handlePasswordResponse(response){
|
||||
},10000);
|
||||
}
|
||||
|
||||
function handleResponse(response){
|
||||
if (response.ok){
|
||||
hide('update_error')
|
||||
setText('updateBtn', 'saved.');
|
||||
} else {
|
||||
show('update_error');
|
||||
setText('updateBtn', 'Update failed!');
|
||||
}
|
||||
enable('updateBtn');
|
||||
setTimeout(function(){
|
||||
setText('updateBtn','Update');
|
||||
},10000);
|
||||
}
|
||||
|
||||
async function handleSettings(response){
|
||||
console.log('handleSettings(…)',response);
|
||||
if (response.ok){
|
||||
var json = await response.json();
|
||||
for (var key in json){
|
||||
setValue(key,json[key]);
|
||||
}
|
||||
show('mail_settings');
|
||||
} else {
|
||||
hide('mail_settings');
|
||||
}
|
||||
}
|
||||
|
||||
function passKeyDown(ev){
|
||||
if (event.keyCode == 13) updatePass();
|
||||
}
|
||||
|
||||
|
||||
|
||||
function updatePass(){
|
||||
disable('passBtn');
|
||||
@@ -80,8 +78,25 @@ function updatePass(){
|
||||
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);
|
||||
fetch("/api/email/settings").then(handleSettings);
|
||||
@@ -78,6 +78,32 @@
|
||||
</tr>
|
||||
</table>
|
||||
</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>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user