From 89c1a7732fa861015514c345d7687397fcfd7dba Mon Sep 17 00:00:00 2001 From: vinv Date: Fri, 18 Oct 2024 16:00:08 +0700 Subject: [PATCH 01/27] init oauth2 feature. --- .../connector/oauth/OAuth2Feature.java | 185 ++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 mailstore-connector/src/com/axonivy/connector/oauth/OAuth2Feature.java diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/OAuth2Feature.java b/mailstore-connector/src/com/axonivy/connector/oauth/OAuth2Feature.java new file mode 100644 index 0000000..6ecf6a4 --- /dev/null +++ b/mailstore-connector/src/com/axonivy/connector/oauth/OAuth2Feature.java @@ -0,0 +1,185 @@ +package com.axonivy.connector.oauth; + +import java.net.URI; +import java.util.Optional; + +import javax.ws.rs.Priorities; +import javax.ws.rs.client.Entity; +import javax.ws.rs.core.Feature; +import javax.ws.rs.core.FeatureContext; +import javax.ws.rs.core.Form; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.UriBuilder; + +import ch.ivyteam.ivy.bpm.error.BpmPublicErrorBuilder; +import ch.ivyteam.ivy.rest.client.FeatureConfig; +import ch.ivyteam.ivy.rest.client.oauth2.OAuth2BearerFilter; +import ch.ivyteam.ivy.rest.client.oauth2.OAuth2RedirectErrorBuilder; +import ch.ivyteam.ivy.rest.client.oauth2.OAuth2TokenRequester.AuthContext; +import ch.ivyteam.ivy.rest.client.oauth2.uri.OAuth2CallbackUriBuilder; +import ch.ivyteam.ivy.rest.client.oauth2.uri.OAuth2UriProperty; + +/** + * Microsoft Graph AUTH flow implementation. + * + * + * + * @since 9.2 + */ +public class OAuth2Feature implements Feature { + + public static interface Default { + String AUTH_URI = "https://login.microsoftonline.com/common/oauth2/v2.0"; + String APP_SCOPE = "https://graph.microsoft.com/.default"; + String USER_SCOPE = "user.read"; + } + + public static interface Property { + String APP_ID = "AUTH.appId"; + String CLIENT_SECRET = "AUTH.secretKey"; + String SCOPE = "AUTH.scope"; + String AUTH_BASE_URI = "AUTH.baseUri"; + + + String USE_APP_PERMISSIONS = "AUTH.useAppPermissions"; + String USE_USER_PASS_FLOW = "AUTH.userPassFlow"; + + + String USER = "AUTH.user"; + String PASS = "AUTH.password"; + } + + @Override + public boolean configure(FeatureContext context) { + var config = new FeatureConfig(context.getConfiguration(), OAuth2Feature.class); + var graphUri = new OAuth2UriProperty(config, Property.AUTH_BASE_URI, Default.AUTH_URI); + var oauth2 = new OAuth2BearerFilter( + ctxt -> requestToken(ctxt, graphUri), + graphUri + ); + oauth2.tokenScopeProperty(Property.SCOPE); + oauth2.tokenSuffix(() -> GrantType.of(config).type); + context.register(oauth2, Priorities.AUTHORIZATION); + return true; + } + + private static Response requestToken(AuthContext ctxt, OAuth2UriProperty uriFactory) { + FeatureConfig config = ctxt.config; + var authCode = ctxt.authCode(); + var refreshToken = ctxt.refreshToken(); + GrantType grant = GrantType.of(config); + if (authCode.isEmpty() && refreshToken.isEmpty() && GrantType.AUTH_CODE == grant) { + authError(config, uriFactory) + .withMessage("missing permission from user to act in his name.") + .throwError(); + } + + Form form = createTokenPayload(config, authCode, refreshToken); + var response = ctxt.target.request() + .accept(MediaType.WILDCARD) + .post(Entity.form(form)); + return response; + } + + static Form createTokenPayload(FeatureConfig config, Optional authCode, Optional refreshToken) { + Form form = new Form(); + form.param("client_id", config.readMandatory(Property.APP_ID)); + form.param("client_secret", config.readMandatory(Property.CLIENT_SECRET)); + GrantType grant = GrantType.of(config); + form.param("grant_type", grant.type); + configureGrant(config, authCode, form, grant); + if (refreshToken.isPresent()) { + form.param("redirect_uri", ivyCallbackUri().toASCIIString()); + form.param("refresh_token", refreshToken.get()); + form.asMap().putSingle("grant_type", "refresh_token"); + } + return form; + } + + private static void configureGrant(FeatureConfig config, Optional authCode, Form form, GrantType grant) { + switch (grant) { + case APPLICATION: + form.param("scope", config.read(Property.SCOPE).orElse(Default.APP_SCOPE)); + break; + case PASSWORD: + form.param("scope", getPersonalScope(config)); + form.param("username", config.readMandatory(Property.USER)); + form.param("password", config.readMandatory(Property.PASS)); + break; + default: + case AUTH_CODE: + form.param("scope", getPersonalScope(config)); + form.param("redirect_uri", ivyCallbackUri().toASCIIString()); + authCode.ifPresent(code -> form.param("code", code)); + } + } + + private static BpmPublicErrorBuilder authError(FeatureConfig config, OAuth2UriProperty uriFactory) { + var uri = createMsAuthCodeUri(config, uriFactory); + return OAuth2RedirectErrorBuilder.create(uri) + .withMessage("Missing permission from user to act in his name."); + } + + private static URI createMsAuthCodeUri(FeatureConfig config, OAuth2UriProperty uriFactory) { + return UriBuilder.fromUri(uriFactory.getUri("authorize")) + .queryParam("client_id", config.readMandatory(Property.APP_ID)) + .queryParam("scope", getPersonalScope(config)) + .queryParam("redirect_uri", ivyCallbackUri()) + .queryParam("response_type", "code") + .queryParam("response_mode", "query") + .build(); + } + + private static URI ivyCallbackUri() { + return OAuth2CallbackUriBuilder.create().toUrl(); + } + + private static String getPersonalScope(FeatureConfig config) { + return config.read(Property.SCOPE).orElse(Default.USER_SCOPE); + } + + private static enum GrantType { + /** work in the name of a user: requires user consent **/ + AUTH_CODE("authorization_code"), + + APPLICATION("client_credentials"), + + /** weak security: app acts as pre-configured personal user! **/ + PASSWORD("password"); + + private String type; + + GrantType(String type) { + this.type = type; + } + + static GrantType of(FeatureConfig config) { + if (isAppAuth(config)){ + return GrantType.APPLICATION; + } + if (isUserPassAuth(config)) { + return GrantType.PASSWORD; + } + return GrantType.AUTH_CODE; + } + + private static boolean isAppAuth(FeatureConfig config) { + return bool(config.read(Property.USE_APP_PERMISSIONS)); + } + + private static boolean isUserPassAuth(FeatureConfig config) { + return bool(config.read(Property.USE_USER_PASS_FLOW)); + } + + private static boolean bool(Optional value) { + return value.map(Boolean::parseBoolean).orElse(Boolean.FALSE); + } + } + +} From dc4c9b12dabc97ffb52017d1441e1230f761c6df Mon Sep 17 00:00:00 2001 From: vinv Date: Fri, 18 Oct 2024 17:08:28 +0700 Subject: [PATCH 02/27] test authen email from user test --- .../config/variables.yaml | 12 ++--- .../mailstore/demo/MyTestData.ivyClass | 2 + .../processes/MyTest.p.json | 44 +++++++++++++++++++ .../connector/mailstore/demo/DemoService.java | 7 ++- mailstore-connector/config/variables.yaml | 3 +- .../connector/mailstore/MailStoreService.java | 9 ++++ 6 files changed, 69 insertions(+), 8 deletions(-) create mode 100644 mailstore-connector-demo/dataclasses/com/axonivy/connector/mailstore/demo/MyTestData.ivyClass create mode 100644 mailstore-connector-demo/processes/MyTest.p.json diff --git a/mailstore-connector-demo/config/variables.yaml b/mailstore-connector-demo/config/variables.yaml index ce73ff9..677a05c 100644 --- a/mailstore-connector-demo/config/variables.yaml +++ b/mailstore-connector-demo/config/variables.yaml @@ -9,16 +9,16 @@ Variables: mailstore-connector: localhost-imap: # [enum: pop3, pop3s, imap, imaps] - protocol: 'imap' + protocol: 'imaps' # Host for store connection - host: 'localhost' + host: 'outlook.office365.com' # Port for store connection (only needed if not default) - port: -1 + port: 993 # User name for store connection - user: 'debug@localdomain.test' + user: 'atomic-ivy@axonactive24.onmicrosoft.com' # Password for store connection # [password] - password: 'pass' + password: 'eyJ0eXAiOiJKV1QiLCJub25jZSI6InQwakdibVFlUHBzOG1UQmJZSnpFTWpEd3J6aUozZFZ6OGhNNGdYUmV2RzAiLCJhbGciOiJSUzI1NiIsIng1dCI6IjNQYUs0RWZ5Qk5RdTNDdGpZc2EzWW1oUTVFMCIsImtpZCI6IjNQYUs0RWZ5Qk5RdTNDdGpZc2EzWW1oUTVFMCJ9.eyJhdWQiOiJodHRwczovL291dGxvb2sub2ZmaWNlMzY1LmNvbSIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzAxNmE4YzVhLTViNDAtNDAzZC05ODE5LWYxYjMxNjJkNTVhYy8iLCJpYXQiOjE3MjkyNDQzMTUsIm5iZiI6MTcyOTI0NDMxNSwiZXhwIjoxNzI5MjQ4MjE1LCJhaW8iOiJrMkJnWUpqM3E2RjcvWm1tZDg4KzlML08zUEduR3dBPSIsImFwcF9kaXNwbGF5bmFtZSI6ImltYXAtb3V0bG9vay10ZXN0LW9hdXRoMiIsImFwcGlkIjoiM2M0NmQxMDctYTYxNC00ZmY4LTkzYjQtYjQ2MGQyZmExODlhIiwiYXBwaWRhY3IiOiIxIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvMDE2YThjNWEtNWI0MC00MDNkLTk4MTktZjFiMzE2MmQ1NWFjLyIsImlkdHlwIjoiYXBwIiwib2lkIjoiM2FlNzIzMDQtNzY1MS00ZTZhLTgwMDAtMjM4OTBkZWRhNWE4IiwicmgiOiIwLkFjWUFXb3hxQVVCYlBVQ1lHZkd6RmkxVnJBSUFBQUFBQVBFUHpnQUFBQUFBQUFER0FBQS4iLCJyb2xlcyI6WyJNYWlsLlJlYWQiLCJJTUFQLkFjY2Vzc0FzQXBwIl0sInNpZCI6IjI2MDg1ZTMzLTA1NDgtNDJjMy1iNjAxLTIwMjMxMzY2YzMxZiIsInN1YiI6IjNhZTcyMzA0LTc2NTEtNGU2YS04MDAwLTIzODkwZGVkYTVhOCIsInRpZCI6IjAxNmE4YzVhLTViNDAtNDAzZC05ODE5LWYxYjMxNjJkNTVhYyIsInV0aSI6Ik5kQWNwdDFaUlVPTDQtd1ZBSFdhQUEiLCJ2ZXIiOiIxLjAiLCJ3aWRzIjpbIjA5OTdhMWQwLTBkMWQtNGFjYi1iNDA4LWQ1Y2E3MzEyMWU5MCJdLCJ4bXNfaWRyZWwiOiI3IDgifQ.WHZjqS7DDsm8_q7xLrBQtmLsiz9YBYlAyMnulRHbhBrlgu3GnTnc-ib5VZfVsvb9Dzg4d3VVn6HFxhwMrpvBApgvshd9GPqFifuaokePxted1DhmS-hJAFh-gzRoLYzIDT3ZGgokDJeyPzZvmtRNyUQf0QkHCSkmz171g7eDyYGhtpYZRAH5pdPPehqpFK_8_SW2aQdJYqKsSyft_YA51IVzjiS8Si5s72l0hhlSJKjD1OHO3gboC0_aW0eQJ-AynuCH7v2cDwaQvba3OqOt2iTQoeP6eGV11qMLZBdGX5pod2GKO3jN__hXOG6ZiKTShQUb9HN4gVo-eZEGOfFeHQ' # show debug output for connection debug: true # Additional properties for store connection, @@ -26,3 +26,5 @@ Variables: properties: mail.imaps.ssl.checkserveridentity: false mail.imaps.ssl.trust: '*' + mail.imaps.auth.mechanisms: 'XOAUTH2' + mail.imaps.sasl.mechanisms: 'XOAUTH2' \ No newline at end of file diff --git a/mailstore-connector-demo/dataclasses/com/axonivy/connector/mailstore/demo/MyTestData.ivyClass b/mailstore-connector-demo/dataclasses/com/axonivy/connector/mailstore/demo/MyTestData.ivyClass new file mode 100644 index 0000000..cb3b25e --- /dev/null +++ b/mailstore-connector-demo/dataclasses/com/axonivy/connector/mailstore/demo/MyTestData.ivyClass @@ -0,0 +1,2 @@ +MyTestData #class +com.axonivy.connector.mailstore.demo #namespace diff --git a/mailstore-connector-demo/processes/MyTest.p.json b/mailstore-connector-demo/processes/MyTest.p.json new file mode 100644 index 0000000..d94e434 --- /dev/null +++ b/mailstore-connector-demo/processes/MyTest.p.json @@ -0,0 +1,44 @@ +{ + "format" : "10.0.0", + "id" : "1929EF19B2B61825", + "config" : { + "data" : "com.axonivy.connector.mailstore.demo.MyTestData" + }, + "elements" : [ { + "id" : "f0", + "type" : "RequestStart", + "name" : "start.ivp", + "config" : { + "callSignature" : "start", + "outLink" : "start.ivp", + "case" : { } + }, + "visual" : { + "at" : { "x" : 208, "y" : 264 } + }, + "connect" : { "id" : "f4", "to" : "f3" } + }, { + "id" : "f1", + "type" : "TaskEnd", + "visual" : { + "at" : { "x" : 544, "y" : 264 } + } + }, { + "id" : "f3", + "type" : "Script", + "name" : "execute", + "config" : { + "security" : "system", + "output" : { + "code" : [ + "import com.axonivy.connector.mailstore.demo.DemoService;", + "DemoService.handleMessages();" + ] + } + }, + "visual" : { + "at" : { "x" : 360, "y" : 264 } + }, + "connect" : { "id" : "f2", "to" : "f1" } + } ] +} \ No newline at end of file diff --git a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java index d360264..d711158 100644 --- a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java +++ b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java @@ -23,12 +23,15 @@ public class DemoService { private static final Logger LOG = Ivy.log(); public static void handleMessages() throws MessagingException, IOException { - MessageIterator iterator = MailStoreService.messageIterator("localhost-imap", "INBOX", null, false, MailStoreService.subjectMatches(".*test [0-9]+.*"), new MessageComparator()); + MessageIterator iterator = MailStoreService.messageIterator("localhost-imap", "INBOX", null, false, MailStoreService.subjectMatches(".*"), new MessageComparator()); while (iterator.hasNext()) { Message message = iterator.next(); - boolean handled = handleMessage(message); + + Ivy.log().info("---------> "+message.getSubject()); + + boolean handled = true; iterator.handledMessage(handled); } } diff --git a/mailstore-connector/config/variables.yaml b/mailstore-connector/config/variables.yaml index b246085..4baa3e0 100644 --- a/mailstore-connector/config/variables.yaml +++ b/mailstore-connector/config/variables.yaml @@ -26,4 +26,5 @@ Variables: properties: mail.imaps.ssl.checkserveridentity: false mail.imaps.ssl.trust: '*' - \ No newline at end of file + mail.imaps.auth.mechanisms: 'XOAUTH2' + mail.imaps.sasl.mechanisms: 'XOAUTH2' \ No newline at end of file diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index a0e151a..0593eac 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -679,6 +679,9 @@ private static String getVar(String store, String var) { } private static Properties getProperties() { + + Ivy.log().info("-----------> getProperties"); + Properties properties = System.getProperties(); String propertiesPrefix = PROPERTIES_VAR + "."; @@ -691,6 +694,12 @@ private static Properties getProperties() { properties.setProperty(name, value); } } + + properties.setProperty("mail.imap.ssl.trust", "*"); + properties.setProperty("mail.imap.ssl.checkserveridentity", "true"); + properties.setProperty("mail.imap.ssl.enable", "true"); + properties.setProperty("mail.imap.auth.mechanisms", "XOAUTH2"); + properties.setProperty("mail.imaps.sasl.mechanisms", "XOAUTH2"); return properties; } From cfb4ffa4865e8859acc6bcbaf7a701f8543fa4fc Mon Sep 17 00:00:00 2001 From: vinv Date: Mon, 21 Oct 2024 11:59:07 +0700 Subject: [PATCH 03/27] adding callable method to get token --- .../mailstore/demo/MyTestData.ivyClass | 4 + .../processes/MyTest.p.json | 61 ++++++++++++- mailstore-connector/config/rest-clients.yaml | 5 ++ .../mailstore/OAuth2FeatureData.ivyClass | 6 ++ .../processes/OAuth2Feature.p.json | 90 +++++++++++++++++++ .../connector/mailstore/MailStoreService.java | 29 +++--- .../com/axonivy/connector/oauth/FormDTO.java | 36 ++++++++ .../com/axonivy/connector/oauth/TokenDTO.java | 50 +++++++++++ 8 files changed, 265 insertions(+), 16 deletions(-) create mode 100644 mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass create mode 100644 mailstore-connector/processes/OAuth2Feature.p.json create mode 100644 mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java create mode 100644 mailstore-connector/src/com/axonivy/connector/oauth/TokenDTO.java diff --git a/mailstore-connector-demo/dataclasses/com/axonivy/connector/mailstore/demo/MyTestData.ivyClass b/mailstore-connector-demo/dataclasses/com/axonivy/connector/mailstore/demo/MyTestData.ivyClass index cb3b25e..9b39c69 100644 --- a/mailstore-connector-demo/dataclasses/com/axonivy/connector/mailstore/demo/MyTestData.ivyClass +++ b/mailstore-connector-demo/dataclasses/com/axonivy/connector/mailstore/demo/MyTestData.ivyClass @@ -1,2 +1,6 @@ MyTestData #class com.axonivy.connector.mailstore.demo #namespace +domain String #field +endpoint String #field +form com.axonivy.connector.oauth.FormDTO #field +token com.axonivy.connector.oauth.TokenDTO #field diff --git a/mailstore-connector-demo/processes/MyTest.p.json b/mailstore-connector-demo/processes/MyTest.p.json index d94e434..0a5bd96 100644 --- a/mailstore-connector-demo/processes/MyTest.p.json +++ b/mailstore-connector-demo/processes/MyTest.p.json @@ -21,7 +21,7 @@ "id" : "f1", "type" : "TaskEnd", "visual" : { - "at" : { "x" : 544, "y" : 264 } + "at" : { "x" : 1288, "y" : 264 } } }, { "id" : "f3", @@ -31,13 +31,66 @@ "security" : "system", "output" : { "code" : [ - "import com.axonivy.connector.mailstore.demo.DemoService;", - "DemoService.handleMessages();" + "//import com.axonivy.connector.mailstore.demo.DemoService;", + "//DemoService.handleMessages();", + "", + "in.domain = \"login.microsoftonline.com\";", + "in.endpoint = \"016a8c5a-5b40-403d-9819-f1b3162d55ac/oauth2/v2.0/token\";", + "", + "in.form.clientId = \"3c46d107-a614-4ff8-93b4-b460d2fa189a\";", + "in.form.clientSecret = \"SgE8Q~0KvozzjtBm4YATST3A-BTtlmb3eo0snasi\";", + "in.form.grantType = \"client_credentials\";", + "in.form.scope = \"https://outlook.office365.com/.default\";" ] } }, "visual" : { - "at" : { "x" : 360, "y" : 264 } + "at" : { "x" : 392, "y" : 264 } + }, + "connect" : { "id" : "f6", "to" : "f5" } + }, { + "id" : "f5", + "type" : "SubProcessCall", + "name" : "OAuth2Feature", + "config" : { + "processCall" : "OAuth2Feature:call(String,String,com.axonivy.connector.oauth.FormDTO)", + "output" : { + "map" : { + "out" : "in", + "out.token" : "result.token" + } + }, + "call" : { + "params" : [ + { "name" : "domain", "type" : "String" }, + { "name" : "endpoint", "type" : "String" }, + { "name" : "form", "type" : "com.axonivy.connector.oauth.FormDTO" } + ], + "map" : { + "param.domain" : "in.domain", + "param.endpoint" : "in.endpoint", + "param.form" : "in.form" + } + } + }, + "visual" : { + "at" : { "x" : 656, "y" : 264 } + }, + "connect" : { "id" : "f8", "to" : "f7" } + }, { + "id" : "f7", + "type" : "Script", + "config" : { + "security" : "system", + "output" : { + "code" : [ + "", + "ivy.log.info(\"result access token: \"+in.token.accessToken);" + ] + } + }, + "visual" : { + "at" : { "x" : 1008, "y" : 264 } }, "connect" : { "id" : "f2", "to" : "f1" } } ] diff --git a/mailstore-connector/config/rest-clients.yaml b/mailstore-connector/config/rest-clients.yaml index 8e85296..887c405 100644 --- a/mailstore-connector/config/rest-clients.yaml +++ b/mailstore-connector/config/rest-clients.yaml @@ -1 +1,6 @@ RestClients: + Retrieve Token: + UUID: a37c499d-82de-4d21-a021-06844835fb2d + Url: https://{domain}/ + Features: + - ch.ivyteam.ivy.rest.client.mapper.JsonFeature diff --git a/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass b/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass new file mode 100644 index 0000000..0cd883f --- /dev/null +++ b/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass @@ -0,0 +1,6 @@ +OAuth2FeatureData #class +com.axonivy.connector.mailstore #namespace +domain String #field +endpoint String #field +tokenDTO com.axonivy.connector.oauth.TokenDTO #field +form com.axonivy.connector.oauth.FormDTO #field diff --git a/mailstore-connector/processes/OAuth2Feature.p.json b/mailstore-connector/processes/OAuth2Feature.p.json new file mode 100644 index 0000000..46d3f0d --- /dev/null +++ b/mailstore-connector/processes/OAuth2Feature.p.json @@ -0,0 +1,90 @@ +{ + "format" : "10.0.0", + "id" : "192AD1C7ED09AF64", + "kind" : "CALLABLE_SUB", + "config" : { + "data" : "com.axonivy.connector.mailstore.OAuth2FeatureData" + }, + "elements" : [ { + "id" : "f0", + "type" : "CallSubStart", + "name" : "call(String,String,FormDTO)", + "config" : { + "callSignature" : "call", + "input" : { + "params" : [ + { "name" : "domain", "type" : "String" }, + { "name" : "endpoint", "type" : "String" }, + { "name" : "form", "type" : "com.axonivy.connector.oauth.FormDTO" } + ], + "map" : { + "out.domain" : "param.domain", + "out.endpoint" : "param.endpoint", + "out.form" : "param.form" + } + }, + "result" : { + "params" : [ + { "name" : "token", "type" : "com.axonivy.connector.oauth.TokenDTO" } + ], + "map" : { + "result.token" : "in.#tokenDTO" + } + } + }, + "visual" : { + "at" : { "x" : 96, "y" : 64 } + }, + "connect" : { "id" : "f4", "to" : "f3" } + }, { + "id" : "f1", + "type" : "CallSubEnd", + "visual" : { + "at" : { "x" : 776, "y" : 64 } + } + }, { + "id" : "f3", + "type" : "RestClientCall", + "name" : "Get Token", + "config" : { + "bodyForm" : { + "client_id" : "in.form.clientId", + "client_secret" : "in.form.clientSecret", + "scope" : "in.form.scope", + "grant_type" : "in.form.grantType" + }, + "path" : "/{endpoint}", + "clientId" : "a37c499d-82de-4d21-a021-06844835fb2d", + "clientErrorCode" : "ivy:error:rest:client", + "method" : "POST", + "statusErrorCode" : "ivy:error:rest:client", + "responseMapping" : { + "out.tokenDTO" : "result" + }, + "templateParams" : { + "domain" : "in.domain", + "endpoint" : "in.endpoint" + }, + "resultType" : "com.axonivy.connector.oauth.TokenDTO", + "bodyInputType" : "FORM", + "bodyMediaType" : "application/x-www-form-urlencoded" + }, + "visual" : { + "at" : { "x" : 400, "y" : 64 } + }, + "boundaries" : [ { + "id" : "f5", + "type" : "ErrorBoundaryEvent", + "config" : { + "output" : { + "code" : "ivy.log.error(error);" + } + }, + "visual" : { + "at" : { "x" : 432, "y" : 104 } + }, + "connect" : { "id" : "f6", "to" : "f1" } + } ], + "connect" : { "id" : "f2", "to" : "f1" } + } ] +} \ No newline at end of file diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index 0593eac..6de688a 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -679,30 +679,35 @@ private static String getVar(String store, String var) { } private static Properties getProperties() { - - Ivy.log().info("-----------> getProperties"); - Properties properties = System.getProperties(); String propertiesPrefix = PROPERTIES_VAR + "."; for (Variable variable : Ivy.var().all()) { String name = variable.name(); - if(name.startsWith(propertiesPrefix)) { - String propertyName = name.substring(propertiesPrefix.length()); + if(name.contains(propertiesPrefix)) { + String propertyName = getSubstringAfterProperties(name); + String value = variable.value(); LOG.info("Setting additional property {0}: ''{1}''", propertyName, value); - properties.setProperty(name, value); + properties.setProperty(propertyName, value); } } - - properties.setProperty("mail.imap.ssl.trust", "*"); - properties.setProperty("mail.imap.ssl.checkserveridentity", "true"); - properties.setProperty("mail.imap.ssl.enable", "true"); - properties.setProperty("mail.imap.auth.mechanisms", "XOAUTH2"); - properties.setProperty("mail.imaps.sasl.mechanisms", "XOAUTH2"); return properties; } + + private static String getSubstringAfterProperties(String input) { + String keyword = "properties"; + int index = input.indexOf(keyword); + + if (index != -1) { + // Get the substring starting right after "properties." + // Add length of "properties." (which is 12) to index to get the position after it + return input.substring(index + keyword.length() + 1); + } + + return null; // Return null if "properties" is not found + } private static BpmPublicErrorBuilder buildError(String code) { BpmPublicErrorBuilder builder = BpmError.create(ERROR_BASE + ":" + code); diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java b/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java new file mode 100644 index 0000000..b25d37c --- /dev/null +++ b/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java @@ -0,0 +1,36 @@ +package com.axonivy.connector.oauth; + +public class FormDTO { + + private String clientId; + private String clientSecret; + private String scope; + private String grantType; + + public String getClientId() { + return clientId; + } + public void setClientId(String clientId) { + this.clientId = clientId; + } + public String getClientSecret() { + return clientSecret; + } + public void setClientSecret(String clientSecret) { + this.clientSecret = clientSecret; + } + public String getScope() { + return scope; + } + public void setScope(String scope) { + this.scope = scope; + } + public String getGrantType() { + return grantType; + } + public void setGrantType(String grantType) { + this.grantType = grantType; + } + + +} diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/TokenDTO.java b/mailstore-connector/src/com/axonivy/connector/oauth/TokenDTO.java new file mode 100644 index 0000000..02c6d7f --- /dev/null +++ b/mailstore-connector/src/com/axonivy/connector/oauth/TokenDTO.java @@ -0,0 +1,50 @@ +package com.axonivy.connector.oauth; + +import com.fasterxml.jackson.annotation.JsonProperty; + +public class TokenDTO { + @JsonProperty("token_type") + private String tokenType; + + @JsonProperty("access_token") + private String accessToken; + + @JsonProperty("expires_in") + private int expiresIn; + + @JsonProperty("ext_expires_in") + private int extExpiresIn; + + public String getTokenType() { + return tokenType; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public int getExpiresIn() { + return expiresIn; + } + + public void setExpiresIn(int expiresIn) { + this.expiresIn = expiresIn; + } + + public int getExtExpiresIn() { + return extExpiresIn; + } + + public void setExtExpiresIn(int extExpiresIn) { + this.extExpiresIn = extExpiresIn; + } + +} From 588fc91d106bcbd9cf22681e9e18aa9d3373f31e Mon Sep 17 00:00:00 2001 From: vinv Date: Mon, 21 Oct 2024 18:38:51 +0700 Subject: [PATCH 04/27] adding function to get token --- mailstore-connector/config/variables.yaml | 20 ++++- .../connector/mailstore/MailStoreService.java | 78 ++++++++++++++++++- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/mailstore-connector/config/variables.yaml b/mailstore-connector/config/variables.yaml index 4baa3e0..c0c375a 100644 --- a/mailstore-connector/config/variables.yaml +++ b/mailstore-connector/config/variables.yaml @@ -26,5 +26,21 @@ Variables: properties: mail.imaps.ssl.checkserveridentity: false mail.imaps.ssl.trust: '*' - mail.imaps.auth.mechanisms: 'XOAUTH2' - mail.imaps.sasl.mechanisms: 'XOAUTH2' \ No newline at end of file + + # [basic, oauth2] basic: username and password, oauth2: OAuth2 Client Credentials grant flow, OAuth2 Authorization code flow, OAuth2 device authorization grant flow. + auth: 'basic' + + # tenant to use for OAUTH2 request. + # the default 'common' fits for user delegate requests. + # set the Azure Directory (tenant) ID, for application requests. + tenantId: '' + # Your Azure Application (client) ID + appId: '' + # Secret key from your applications "certificates & secrets" (client secret) + secretKey: '' + # permissions to request access to. + # you may exclude or add some, as your azure administrator allows or restricts them. + # for client_credentials: https://outlook.office365.com/.default + scope: '' + #[client_credentials, authorization_code] + grantType: '' \ No newline at end of file diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index 6de688a..7107eca 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Optional; import java.util.Properties; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -34,9 +35,14 @@ import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; +import com.axonivy.connector.oauth.FormDTO; +import com.axonivy.connector.oauth.TokenDTO; + import ch.ivyteam.ivy.bpm.error.BpmError; import ch.ivyteam.ivy.bpm.error.BpmPublicErrorBuilder; import ch.ivyteam.ivy.environment.Ivy; +import ch.ivyteam.ivy.process.call.SubProcessCall; +import ch.ivyteam.ivy.process.call.SubProcessCallResult; import ch.ivyteam.ivy.vars.Variable; import ch.ivyteam.log.Logger; @@ -53,6 +59,7 @@ public class MailStoreService { private static final String PROPERTIES_VAR = "properties"; private static final String ERROR_BASE = "mailstore:connector"; private static final Address[] EMPTY_ADDRESSES = new Address[0]; + private static final String AUTH = "auth"; public static MailStoreService get() { return INSTANCE; @@ -609,7 +616,17 @@ public static Store openStore(String storeName) throws MessagingException { String host = getVar(storeName, HOST_VAR); String portString = getVar(storeName, PORT_VAR); String user = getVar(storeName, USER_VAR); - String password = getVar(storeName, PASSWORD_VAR); + + boolean isBasicAuth = isBasicAuth(storeName); + String password; + if(isBasicAuth) { + Ivy.log().info("---> basic auth"); + password = getVar(storeName, PASSWORD_VAR); + } else { + Ivy.log().info("---> oauth2"); + password = getToken(storeName); + } + String debugString = getVar(storeName, DEBUG_VAR); LOG.debug("Creating mail store connection, protocol: {0} host: {1} port: {2} user: {3} password: {4} debug: {5}", @@ -622,7 +639,7 @@ public static Store openStore(String storeName) throws MessagingException { boolean debug = true; try { - Session session = getSession(); + Session session = getSession(storeName); debug = Boolean.parseBoolean(debugString); int port = Integer.parseInt(portString); @@ -650,6 +667,12 @@ public static Store openStore(String storeName) throws MessagingException { return store; } + private static Session getSession(String storeName) { + Properties properties = getProperties(storeName); + Session session = Session.getDefaultInstance(properties, null); + return session; + } + private static Session getSession() { Properties properties = getProperties(); Session session = Session.getDefaultInstance(properties, null); @@ -677,11 +700,62 @@ private static Folder openFolder(Store store, String folderName, int mode) throw private static String getVar(String store, String var) { return Ivy.var().get(String.format("%s.%s.%s", MAIL_STORE_VAR, store, var)); } + + private static boolean isBasicAuth(String store) { + String auth = Ivy.var().get(String.format("%s.%s.%s", MAIL_STORE_VAR, store, AUTH)); + return auth.equalsIgnoreCase("basic"); + } + + private static String getToken(String store) { + + + + TokenDTO result = null; + BpmError error = null; + SubProcessCallResult callResult = SubProcessCall.withPath("OAuth2Feature") + .withStartName("call") + .withParam("domain", domain) + .withParam("endpoint", endpoint) + .withParam("form", form).call(); + + if (callResult != null) { + Optional o = Optional.ofNullable(callResult.get("token")); + if (o.isPresent()) { + result = (TokenDTO) o.get(); + } else { + Optional e = Optional.ofNullable(callResult.get("error")); + if (e.isPresent()) { + error = (BpmError) e.get(); + Ivy.log().error(error); + throw error; + } + } + } + return result.getAccessToken(); + } + private static Properties getProperties() { Properties properties = System.getProperties(); String propertiesPrefix = PROPERTIES_VAR + "."; + for (Variable variable : Ivy.var().all()) { + String name = variable.name(); + if(name.startsWith(propertiesPrefix)) { + String propertyName = name.substring(propertiesPrefix.length()); + String value = variable.value(); + LOG.info("Setting additional property {0}: ''{1}''", propertyName, value); + properties.setProperty(name, value); + } + } + + return properties; + } + + private static Properties getProperties(String storeName) { + Properties properties = System.getProperties(); + String propertiesPrefix = MAIL_STORE_VAR + "." +storeName +"." + PROPERTIES_VAR + "."; + for (Variable variable : Ivy.var().all()) { String name = variable.name(); if(name.contains(propertiesPrefix)) { From 84d0e691a0f1c4122f73f9512eb91a1e29ec8e94 Mon Sep 17 00:00:00 2001 From: vinv Date: Tue, 22 Oct 2024 14:08:55 +0700 Subject: [PATCH 05/27] remove redundant code --- .../config/variables.yaml | 14 ++++---- mailstore-connector/config/rest-clients.yaml | 2 +- .../mailstore/OAuth2FeatureData.ivyClass | 4 +-- .../processes/OAuth2Feature.p.json | 12 ++----- .../connector/mailstore/MailStoreService.java | 35 +++++++++++++------ .../com/axonivy/connector/oauth/FormDTO.java | 19 +++++++++- 6 files changed, 53 insertions(+), 33 deletions(-) diff --git a/mailstore-connector-demo/config/variables.yaml b/mailstore-connector-demo/config/variables.yaml index 677a05c..0e41688 100644 --- a/mailstore-connector-demo/config/variables.yaml +++ b/mailstore-connector-demo/config/variables.yaml @@ -9,22 +9,20 @@ Variables: mailstore-connector: localhost-imap: # [enum: pop3, pop3s, imap, imaps] - protocol: 'imaps' + protocol: 'imap' # Host for store connection - host: 'outlook.office365.com' + host: 'localhost' # Port for store connection (only needed if not default) - port: 993 + port: -1 # User name for store connection - user: 'atomic-ivy@axonactive24.onmicrosoft.com' + user: 'debug@localdomain.test' # Password for store connection # [password] - password: 'eyJ0eXAiOiJKV1QiLCJub25jZSI6InQwakdibVFlUHBzOG1UQmJZSnpFTWpEd3J6aUozZFZ6OGhNNGdYUmV2RzAiLCJhbGciOiJSUzI1NiIsIng1dCI6IjNQYUs0RWZ5Qk5RdTNDdGpZc2EzWW1oUTVFMCIsImtpZCI6IjNQYUs0RWZ5Qk5RdTNDdGpZc2EzWW1oUTVFMCJ9.eyJhdWQiOiJodHRwczovL291dGxvb2sub2ZmaWNlMzY1LmNvbSIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzAxNmE4YzVhLTViNDAtNDAzZC05ODE5LWYxYjMxNjJkNTVhYy8iLCJpYXQiOjE3MjkyNDQzMTUsIm5iZiI6MTcyOTI0NDMxNSwiZXhwIjoxNzI5MjQ4MjE1LCJhaW8iOiJrMkJnWUpqM3E2RjcvWm1tZDg4KzlML08zUEduR3dBPSIsImFwcF9kaXNwbGF5bmFtZSI6ImltYXAtb3V0bG9vay10ZXN0LW9hdXRoMiIsImFwcGlkIjoiM2M0NmQxMDctYTYxNC00ZmY4LTkzYjQtYjQ2MGQyZmExODlhIiwiYXBwaWRhY3IiOiIxIiwiaWRwIjoiaHR0cHM6Ly9zdHMud2luZG93cy5uZXQvMDE2YThjNWEtNWI0MC00MDNkLTk4MTktZjFiMzE2MmQ1NWFjLyIsImlkdHlwIjoiYXBwIiwib2lkIjoiM2FlNzIzMDQtNzY1MS00ZTZhLTgwMDAtMjM4OTBkZWRhNWE4IiwicmgiOiIwLkFjWUFXb3hxQVVCYlBVQ1lHZkd6RmkxVnJBSUFBQUFBQVBFUHpnQUFBQUFBQUFER0FBQS4iLCJyb2xlcyI6WyJNYWlsLlJlYWQiLCJJTUFQLkFjY2Vzc0FzQXBwIl0sInNpZCI6IjI2MDg1ZTMzLTA1NDgtNDJjMy1iNjAxLTIwMjMxMzY2YzMxZiIsInN1YiI6IjNhZTcyMzA0LTc2NTEtNGU2YS04MDAwLTIzODkwZGVkYTVhOCIsInRpZCI6IjAxNmE4YzVhLTViNDAtNDAzZC05ODE5LWYxYjMxNjJkNTVhYyIsInV0aSI6Ik5kQWNwdDFaUlVPTDQtd1ZBSFdhQUEiLCJ2ZXIiOiIxLjAiLCJ3aWRzIjpbIjA5OTdhMWQwLTBkMWQtNGFjYi1iNDA4LWQ1Y2E3MzEyMWU5MCJdLCJ4bXNfaWRyZWwiOiI3IDgifQ.WHZjqS7DDsm8_q7xLrBQtmLsiz9YBYlAyMnulRHbhBrlgu3GnTnc-ib5VZfVsvb9Dzg4d3VVn6HFxhwMrpvBApgvshd9GPqFifuaokePxted1DhmS-hJAFh-gzRoLYzIDT3ZGgokDJeyPzZvmtRNyUQf0QkHCSkmz171g7eDyYGhtpYZRAH5pdPPehqpFK_8_SW2aQdJYqKsSyft_YA51IVzjiS8Si5s72l0hhlSJKjD1OHO3gboC0_aW0eQJ-AynuCH7v2cDwaQvba3OqOt2iTQoeP6eGV11qMLZBdGX5pod2GKO3jN__hXOG6ZiKTShQUb9HN4gVo-eZEGOfFeHQ' + password: 'pass' # show debug output for connection debug: true # Additional properties for store connection, # see https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html properties: mail.imaps.ssl.checkserveridentity: false - mail.imaps.ssl.trust: '*' - mail.imaps.auth.mechanisms: 'XOAUTH2' - mail.imaps.sasl.mechanisms: 'XOAUTH2' \ No newline at end of file + mail.imaps.ssl.trust: '*' \ No newline at end of file diff --git a/mailstore-connector/config/rest-clients.yaml b/mailstore-connector/config/rest-clients.yaml index 887c405..2497672 100644 --- a/mailstore-connector/config/rest-clients.yaml +++ b/mailstore-connector/config/rest-clients.yaml @@ -1,6 +1,6 @@ RestClients: Retrieve Token: UUID: a37c499d-82de-4d21-a021-06844835fb2d - Url: https://{domain}/ + Url: https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token Features: - ch.ivyteam.ivy.rest.client.mapper.JsonFeature diff --git a/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass b/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass index 0cd883f..9c71552 100644 --- a/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass +++ b/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass @@ -1,6 +1,4 @@ OAuth2FeatureData #class com.axonivy.connector.mailstore #namespace -domain String #field -endpoint String #field -tokenDTO com.axonivy.connector.oauth.TokenDTO #field form com.axonivy.connector.oauth.FormDTO #field +tokenDTO com.axonivy.connector.oauth.TokenDTO #field diff --git a/mailstore-connector/processes/OAuth2Feature.p.json b/mailstore-connector/processes/OAuth2Feature.p.json index 46d3f0d..b0e5847 100644 --- a/mailstore-connector/processes/OAuth2Feature.p.json +++ b/mailstore-connector/processes/OAuth2Feature.p.json @@ -8,18 +8,14 @@ "elements" : [ { "id" : "f0", "type" : "CallSubStart", - "name" : "call(String,String,FormDTO)", + "name" : "call(FormDTO)", "config" : { "callSignature" : "call", "input" : { "params" : [ - { "name" : "domain", "type" : "String" }, - { "name" : "endpoint", "type" : "String" }, { "name" : "form", "type" : "com.axonivy.connector.oauth.FormDTO" } ], "map" : { - "out.domain" : "param.domain", - "out.endpoint" : "param.endpoint", "out.form" : "param.form" } }, @@ -53,7 +49,6 @@ "scope" : "in.form.scope", "grant_type" : "in.form.grantType" }, - "path" : "/{endpoint}", "clientId" : "a37c499d-82de-4d21-a021-06844835fb2d", "clientErrorCode" : "ivy:error:rest:client", "method" : "POST", @@ -62,8 +57,7 @@ "out.tokenDTO" : "result" }, "templateParams" : { - "domain" : "in.domain", - "endpoint" : "in.endpoint" + "tenant_id" : "in.form.tenantId" }, "resultType" : "com.axonivy.connector.oauth.TokenDTO", "bodyInputType" : "FORM", @@ -83,7 +77,7 @@ "visual" : { "at" : { "x" : 432, "y" : 104 } }, - "connect" : { "id" : "f6", "to" : "f1" } + "connect" : { "id" : "f6", "to" : "f1", "via" : [ { "x" : 776, "y" : 104 } ] } } ], "connect" : { "id" : "f2", "to" : "f1" } } ] diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index 7107eca..73f232c 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -59,7 +59,13 @@ public class MailStoreService { private static final String PROPERTIES_VAR = "properties"; private static final String ERROR_BASE = "mailstore:connector"; private static final Address[] EMPTY_ADDRESSES = new Address[0]; + private static final String AUTH = "auth"; + private static final String TENANT_ID = "tenantId"; + private static final String APP_ID = "appId"; + private static final String SECRET_KEY = "secretKey"; + private static final String GRANT_TYPE = "grantType"; + private static final String SCOPE = "scope"; public static MailStoreService get() { return INSTANCE; @@ -620,8 +626,8 @@ public static Store openStore(String storeName) throws MessagingException { boolean isBasicAuth = isBasicAuth(storeName); String password; if(isBasicAuth) { - Ivy.log().info("---> basic auth"); password = getVar(storeName, PASSWORD_VAR); + Ivy.log().info("---> basic auth: "+password); } else { Ivy.log().info("---> oauth2"); password = getToken(storeName); @@ -669,7 +675,9 @@ public static Store openStore(String storeName) throws MessagingException { private static Session getSession(String storeName) { Properties properties = getProperties(storeName); - Session session = Session.getDefaultInstance(properties, null); + + // Use getInstance instead of getDefaultInstance + Session session = Session.getInstance(properties, null); return session; } @@ -707,16 +715,14 @@ private static boolean isBasicAuth(String store) { } private static String getToken(String store) { - - + FormDTO form = new FormDTO(getVar(store, TENANT_ID), getVar(store, APP_ID), getVar(store, SECRET_KEY), getVar(store, SCOPE), getVar(store, GRANT_TYPE)); + TokenDTO result = null; BpmError error = null; SubProcessCallResult callResult = SubProcessCall.withPath("OAuth2Feature") .withStartName("call") - .withParam("domain", domain) - .withParam("endpoint", endpoint) .withParam("form", form).call(); if (callResult != null) { @@ -727,11 +733,17 @@ private static String getToken(String store) { Optional e = Optional.ofNullable(callResult.get("error")); if (e.isPresent()) { error = (BpmError) e.get(); - Ivy.log().error(error); + LOG.error(error); throw error; } } } + + if(null == result || StringUtils.isBlank(result.getAccessToken())) { + LOG.error("access token cannot be null or empty"); + throw buildError("load").withMessage("access token cannot be null or empty").build(); + } + return result.getAccessToken(); } @@ -743,9 +755,10 @@ private static Properties getProperties() { String name = variable.name(); if(name.startsWith(propertiesPrefix)) { String propertyName = name.substring(propertiesPrefix.length()); + String value = variable.value(); LOG.info("Setting additional property {0}: ''{1}''", propertyName, value); - properties.setProperty(name, value); + properties.setProperty(name, value); } } @@ -753,7 +766,7 @@ private static Properties getProperties() { } private static Properties getProperties(String storeName) { - Properties properties = System.getProperties(); + Properties properties = new Properties(); String propertiesPrefix = MAIL_STORE_VAR + "." +storeName +"." + PROPERTIES_VAR + "."; for (Variable variable : Ivy.var().all()) { @@ -766,7 +779,7 @@ private static Properties getProperties(String storeName) { properties.setProperty(propertyName, value); } } - + return properties; } @@ -780,7 +793,7 @@ private static String getSubstringAfterProperties(String input) { return input.substring(index + keyword.length() + 1); } - return null; // Return null if "properties" is not found + return null; } private static BpmPublicErrorBuilder buildError(String code) { diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java b/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java index b25d37c..62778f1 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java @@ -2,11 +2,29 @@ public class FormDTO { + private String tenantId; private String clientId; private String clientSecret; private String scope; private String grantType; + public FormDTO() { + } + + public FormDTO(String tenantId, String clientId, String clientSecret, String scope, String grantType) { + super(); + this.tenantId = tenantId; + this.clientId = clientId; + this.clientSecret = clientSecret; + this.scope = scope; + this.grantType = grantType; + } + public String getTenantId() { + return tenantId; + } + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } public String getClientId() { return clientId; } @@ -32,5 +50,4 @@ public void setGrantType(String grantType) { this.grantType = grantType; } - } From e903cfbd764db36f3787b504662c3145be9bb028 Mon Sep 17 00:00:00 2001 From: vinv Date: Tue, 22 Oct 2024 18:01:31 +0700 Subject: [PATCH 06/27] update config file --- mailstore-connector/config/variables.yaml | 14 +- .../processes/OAuth2Feature.p.json | 4 +- .../connector/mailstore/MailStoreService.java | 62 +++--- .../com/axonivy/connector/oauth/FormDTO.java | 2 +- .../connector/oauth/OAuth2Feature.java | 185 ------------------ 5 files changed, 42 insertions(+), 225 deletions(-) delete mode 100644 mailstore-connector/src/com/axonivy/connector/oauth/OAuth2Feature.java diff --git a/mailstore-connector/config/variables.yaml b/mailstore-connector/config/variables.yaml index c0c375a..31c2c55 100644 --- a/mailstore-connector/config/variables.yaml +++ b/mailstore-connector/config/variables.yaml @@ -26,21 +26,23 @@ Variables: properties: mail.imaps.ssl.checkserveridentity: false mail.imaps.ssl.trust: '*' + # only set below credential when you go with oauth2 + # mail.imaps.auth.mechanisms: 'XOAUTH2' + # mail.imaps.sasl.enable: 'true' + # mail.imaps.sasl.mechanisms: 'XOAUTH2' - # [basic, oauth2] basic: username and password, oauth2: OAuth2 Client Credentials grant flow, OAuth2 Authorization code flow, OAuth2 device authorization grant flow. + # [basic, oauth2] basic: username and password, oauth2: current only support OAuth2 Client Credentials grant flow auth: 'basic' + # only set below credential when you go with oauth2 # tenant to use for OAUTH2 request. - # the default 'common' fits for user delegate requests. # set the Azure Directory (tenant) ID, for application requests. tenantId: '' - # Your Azure Application (client) ID + # Your Azure Application (client) ID, used for OAuth2 authentication appId: '' # Secret key from your applications "certificates & secrets" (client secret) secretKey: '' - # permissions to request access to. - # you may exclude or add some, as your azure administrator allows or restricts them. # for client_credentials: https://outlook.office365.com/.default scope: '' - #[client_credentials, authorization_code] + #[client_credentials] grantType: '' \ No newline at end of file diff --git a/mailstore-connector/processes/OAuth2Feature.p.json b/mailstore-connector/processes/OAuth2Feature.p.json index b0e5847..e92e3c8 100644 --- a/mailstore-connector/processes/OAuth2Feature.p.json +++ b/mailstore-connector/processes/OAuth2Feature.p.json @@ -8,9 +8,9 @@ "elements" : [ { "id" : "f0", "type" : "CallSubStart", - "name" : "call(FormDTO)", + "name" : "getToken(FormDTO)", "config" : { - "callSignature" : "call", + "callSignature" : "getToken", "input" : { "params" : [ { "name" : "form", "type" : "com.axonivy.connector.oauth.FormDTO" } diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index 73f232c..ebdfad0 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -650,7 +650,6 @@ public static Store openStore(String storeName) throws MessagingException { debug = Boolean.parseBoolean(debugString); int port = Integer.parseInt(portString); - if(debug) { session.setDebug(debug); session.setDebugOut(debugStream); @@ -711,20 +710,20 @@ private static String getVar(String store, String var) { private static boolean isBasicAuth(String store) { String auth = Ivy.var().get(String.format("%s.%s.%s", MAIL_STORE_VAR, store, AUTH)); - return auth.equalsIgnoreCase("basic"); + return auth.equals("basic"); } private static String getToken(String store) { + FormDTO form = new FormDTO(getVar(store, TENANT_ID), getVar(store, APP_ID), getVar(store, SECRET_KEY), getVar(store, SCOPE), getVar(store, GRANT_TYPE)); - + TokenDTO result = null; BpmError error = null; - SubProcessCallResult callResult = SubProcessCall.withPath("OAuth2Feature") - .withStartName("call") + SubProcessCallResult callResult = SubProcessCall.withPath("OAuth2Feature").withStartName("getToken") .withParam("form", form).call(); - + if (callResult != null) { Optional o = Optional.ofNullable(callResult.get("token")); if (o.isPresent()) { @@ -738,12 +737,12 @@ private static String getToken(String store) { } } } - - if(null == result || StringUtils.isBlank(result.getAccessToken())) { + + if (null == result || StringUtils.isBlank(result.getAccessToken())) { LOG.error("access token cannot be null or empty"); - throw buildError("load").withMessage("access token cannot be null or empty").build(); + throw buildError("load").withMessage("access token cannot be null or empty").build(); } - + return result.getAccessToken(); } @@ -753,12 +752,12 @@ private static Properties getProperties() { String propertiesPrefix = PROPERTIES_VAR + "."; for (Variable variable : Ivy.var().all()) { String name = variable.name(); - if(name.startsWith(propertiesPrefix)) { + if (name.startsWith(propertiesPrefix)) { String propertyName = name.substring(propertiesPrefix.length()); - - String value = variable.value(); + + String value = variable.value(); LOG.info("Setting additional property {0}: ''{1}''", propertyName, value); - properties.setProperty(name, value); + properties.setProperty(name, value); } } @@ -767,34 +766,35 @@ private static Properties getProperties() { private static Properties getProperties(String storeName) { Properties properties = new Properties(); - String propertiesPrefix = MAIL_STORE_VAR + "." +storeName +"." + PROPERTIES_VAR + "."; - + String propertiesPrefix = MAIL_STORE_VAR + "." + storeName + "." + PROPERTIES_VAR + "."; + for (Variable variable : Ivy.var().all()) { String name = variable.name(); - if(name.contains(propertiesPrefix)) { + if (name.contains(propertiesPrefix)) { String propertyName = getSubstringAfterProperties(name); - - String value = variable.value(); + + String value = variable.value(); LOG.info("Setting additional property {0}: ''{1}''", propertyName, value); properties.setProperty(propertyName, value); } } - + return properties; } private static String getSubstringAfterProperties(String input) { - String keyword = "properties"; - int index = input.indexOf(keyword); - - if (index != -1) { - // Get the substring starting right after "properties." - // Add length of "properties." (which is 12) to index to get the position after it - return input.substring(index + keyword.length() + 1); - } - - return null; - } + String keyword = "properties"; + int index = input.indexOf(keyword); + + if (index != -1) { + // Get the substring starting right after "properties." + // Add length of "properties." (which is 12) to index to get the position after + // it + return input.substring(index + keyword.length() + 1); + } + + return null; + } private static BpmPublicErrorBuilder buildError(String code) { BpmPublicErrorBuilder builder = BpmError.create(ERROR_BASE + ":" + code); diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java b/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java index 62778f1..555fe9d 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java @@ -1,7 +1,6 @@ package com.axonivy.connector.oauth; public class FormDTO { - private String tenantId; private String clientId; private String clientSecret; @@ -19,6 +18,7 @@ public FormDTO(String tenantId, String clientId, String clientSecret, String sco this.scope = scope; this.grantType = grantType; } + public String getTenantId() { return tenantId; } diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/OAuth2Feature.java b/mailstore-connector/src/com/axonivy/connector/oauth/OAuth2Feature.java deleted file mode 100644 index 6ecf6a4..0000000 --- a/mailstore-connector/src/com/axonivy/connector/oauth/OAuth2Feature.java +++ /dev/null @@ -1,185 +0,0 @@ -package com.axonivy.connector.oauth; - -import java.net.URI; -import java.util.Optional; - -import javax.ws.rs.Priorities; -import javax.ws.rs.client.Entity; -import javax.ws.rs.core.Feature; -import javax.ws.rs.core.FeatureContext; -import javax.ws.rs.core.Form; -import javax.ws.rs.core.MediaType; -import javax.ws.rs.core.Response; -import javax.ws.rs.core.UriBuilder; - -import ch.ivyteam.ivy.bpm.error.BpmPublicErrorBuilder; -import ch.ivyteam.ivy.rest.client.FeatureConfig; -import ch.ivyteam.ivy.rest.client.oauth2.OAuth2BearerFilter; -import ch.ivyteam.ivy.rest.client.oauth2.OAuth2RedirectErrorBuilder; -import ch.ivyteam.ivy.rest.client.oauth2.OAuth2TokenRequester.AuthContext; -import ch.ivyteam.ivy.rest.client.oauth2.uri.OAuth2CallbackUriBuilder; -import ch.ivyteam.ivy.rest.client.oauth2.uri.OAuth2UriProperty; - -/** - * Microsoft Graph AUTH flow implementation. - * - *
    - *
  • Requires a registered application: - * https://docs.microsoft.com/en-us/graph/tutorials/java?tutorial-step=2
  • - *
  • Resolves accessTokens as described here: - * https://docs.microsoft.com/en-us/graph/auth-v2-user
  • - *
- * - * @since 9.2 - */ -public class OAuth2Feature implements Feature { - - public static interface Default { - String AUTH_URI = "https://login.microsoftonline.com/common/oauth2/v2.0"; - String APP_SCOPE = "https://graph.microsoft.com/.default"; - String USER_SCOPE = "user.read"; - } - - public static interface Property { - String APP_ID = "AUTH.appId"; - String CLIENT_SECRET = "AUTH.secretKey"; - String SCOPE = "AUTH.scope"; - String AUTH_BASE_URI = "AUTH.baseUri"; - - - String USE_APP_PERMISSIONS = "AUTH.useAppPermissions"; - String USE_USER_PASS_FLOW = "AUTH.userPassFlow"; - - - String USER = "AUTH.user"; - String PASS = "AUTH.password"; - } - - @Override - public boolean configure(FeatureContext context) { - var config = new FeatureConfig(context.getConfiguration(), OAuth2Feature.class); - var graphUri = new OAuth2UriProperty(config, Property.AUTH_BASE_URI, Default.AUTH_URI); - var oauth2 = new OAuth2BearerFilter( - ctxt -> requestToken(ctxt, graphUri), - graphUri - ); - oauth2.tokenScopeProperty(Property.SCOPE); - oauth2.tokenSuffix(() -> GrantType.of(config).type); - context.register(oauth2, Priorities.AUTHORIZATION); - return true; - } - - private static Response requestToken(AuthContext ctxt, OAuth2UriProperty uriFactory) { - FeatureConfig config = ctxt.config; - var authCode = ctxt.authCode(); - var refreshToken = ctxt.refreshToken(); - GrantType grant = GrantType.of(config); - if (authCode.isEmpty() && refreshToken.isEmpty() && GrantType.AUTH_CODE == grant) { - authError(config, uriFactory) - .withMessage("missing permission from user to act in his name.") - .throwError(); - } - - Form form = createTokenPayload(config, authCode, refreshToken); - var response = ctxt.target.request() - .accept(MediaType.WILDCARD) - .post(Entity.form(form)); - return response; - } - - static Form createTokenPayload(FeatureConfig config, Optional authCode, Optional refreshToken) { - Form form = new Form(); - form.param("client_id", config.readMandatory(Property.APP_ID)); - form.param("client_secret", config.readMandatory(Property.CLIENT_SECRET)); - GrantType grant = GrantType.of(config); - form.param("grant_type", grant.type); - configureGrant(config, authCode, form, grant); - if (refreshToken.isPresent()) { - form.param("redirect_uri", ivyCallbackUri().toASCIIString()); - form.param("refresh_token", refreshToken.get()); - form.asMap().putSingle("grant_type", "refresh_token"); - } - return form; - } - - private static void configureGrant(FeatureConfig config, Optional authCode, Form form, GrantType grant) { - switch (grant) { - case APPLICATION: - form.param("scope", config.read(Property.SCOPE).orElse(Default.APP_SCOPE)); - break; - case PASSWORD: - form.param("scope", getPersonalScope(config)); - form.param("username", config.readMandatory(Property.USER)); - form.param("password", config.readMandatory(Property.PASS)); - break; - default: - case AUTH_CODE: - form.param("scope", getPersonalScope(config)); - form.param("redirect_uri", ivyCallbackUri().toASCIIString()); - authCode.ifPresent(code -> form.param("code", code)); - } - } - - private static BpmPublicErrorBuilder authError(FeatureConfig config, OAuth2UriProperty uriFactory) { - var uri = createMsAuthCodeUri(config, uriFactory); - return OAuth2RedirectErrorBuilder.create(uri) - .withMessage("Missing permission from user to act in his name."); - } - - private static URI createMsAuthCodeUri(FeatureConfig config, OAuth2UriProperty uriFactory) { - return UriBuilder.fromUri(uriFactory.getUri("authorize")) - .queryParam("client_id", config.readMandatory(Property.APP_ID)) - .queryParam("scope", getPersonalScope(config)) - .queryParam("redirect_uri", ivyCallbackUri()) - .queryParam("response_type", "code") - .queryParam("response_mode", "query") - .build(); - } - - private static URI ivyCallbackUri() { - return OAuth2CallbackUriBuilder.create().toUrl(); - } - - private static String getPersonalScope(FeatureConfig config) { - return config.read(Property.SCOPE).orElse(Default.USER_SCOPE); - } - - private static enum GrantType { - /** work in the name of a user: requires user consent **/ - AUTH_CODE("authorization_code"), - - APPLICATION("client_credentials"), - - /** weak security: app acts as pre-configured personal user! **/ - PASSWORD("password"); - - private String type; - - GrantType(String type) { - this.type = type; - } - - static GrantType of(FeatureConfig config) { - if (isAppAuth(config)){ - return GrantType.APPLICATION; - } - if (isUserPassAuth(config)) { - return GrantType.PASSWORD; - } - return GrantType.AUTH_CODE; - } - - private static boolean isAppAuth(FeatureConfig config) { - return bool(config.read(Property.USE_APP_PERMISSIONS)); - } - - private static boolean isUserPassAuth(FeatureConfig config) { - return bool(config.read(Property.USE_USER_PASS_FLOW)); - } - - private static boolean bool(Optional value) { - return value.map(Boolean::parseBoolean).orElse(Boolean.FALSE); - } - } - -} From 994a18501e4a057250cd685237d156d5c374efa9 Mon Sep 17 00:00:00 2001 From: vinv Date: Wed, 23 Oct 2024 09:20:43 +0700 Subject: [PATCH 07/27] remove space --- .../src/com/axonivy/connector/mailstore/MailStoreService.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index ebdfad0..d628eb8 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -714,11 +714,8 @@ private static boolean isBasicAuth(String store) { } private static String getToken(String store) { - - FormDTO form = new FormDTO(getVar(store, TENANT_ID), getVar(store, APP_ID), getVar(store, SECRET_KEY), getVar(store, SCOPE), getVar(store, GRANT_TYPE)); - TokenDTO result = null; BpmError error = null; SubProcessCallResult callResult = SubProcessCall.withPath("OAuth2Feature").withStartName("getToken") From aea2301ec9d6256b8f2c7bb8b277df8dd5da0879 Mon Sep 17 00:00:00 2001 From: vinv Date: Wed, 23 Oct 2024 13:33:15 +0700 Subject: [PATCH 08/27] adding function to support oauth2 mailstore-connector --- mailstore-connector/config/rest-clients.yaml | 2 +- mailstore-connector/config/variables.yaml | 2 +- .../mailstore/OAuth2FeatureData.ivyClass | 4 +- .../processes/OAuth2Feature.p.json | 41 +++++++------- .../connector/mailstore/MailStoreService.java | 26 +++++++-- .../connector/mailstore/MessageService.java | 1 - .../com/axonivy/connector/oauth/FormDTO.java | 53 ------------------- .../axonivy/connector/oauth/OAuthUtils.java | 23 ++++++++ 8 files changed, 71 insertions(+), 81 deletions(-) delete mode 100644 mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java create mode 100644 mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java diff --git a/mailstore-connector/config/rest-clients.yaml b/mailstore-connector/config/rest-clients.yaml index 2497672..6414ecc 100644 --- a/mailstore-connector/config/rest-clients.yaml +++ b/mailstore-connector/config/rest-clients.yaml @@ -1,6 +1,6 @@ RestClients: Retrieve Token: UUID: a37c499d-82de-4d21-a021-06844835fb2d - Url: https://login.microsoftonline.com/{tenant_id}/oauth2/v2.0/token + Url: https://{tokenUrlPrefix}/ Features: - ch.ivyteam.ivy.rest.client.mapper.JsonFeature diff --git a/mailstore-connector/config/variables.yaml b/mailstore-connector/config/variables.yaml index 31c2c55..ffaf414 100644 --- a/mailstore-connector/config/variables.yaml +++ b/mailstore-connector/config/variables.yaml @@ -31,7 +31,7 @@ Variables: # mail.imaps.sasl.enable: 'true' # mail.imaps.sasl.mechanisms: 'XOAUTH2' - # [basic, oauth2] basic: username and password, oauth2: current only support OAuth2 Client Credentials grant flow + # [basic, oauth2] basic: username and password, oauth2: currently only support OAuth2 client credentials grant flow auth: 'basic' # only set below credential when you go with oauth2 diff --git a/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass b/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass index 9c71552..2c1d0db 100644 --- a/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass +++ b/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass @@ -1,4 +1,6 @@ OAuth2FeatureData #class com.axonivy.connector.mailstore #namespace -form com.axonivy.connector.oauth.FormDTO #field tokenDTO com.axonivy.connector.oauth.TokenDTO #field +form javax.ws.rs.core.Form #field +tokenUrlPrefix String #field +tokenUrlSuffix String #field diff --git a/mailstore-connector/processes/OAuth2Feature.p.json b/mailstore-connector/processes/OAuth2Feature.p.json index e92e3c8..de904b0 100644 --- a/mailstore-connector/processes/OAuth2Feature.p.json +++ b/mailstore-connector/processes/OAuth2Feature.p.json @@ -8,15 +8,19 @@ "elements" : [ { "id" : "f0", "type" : "CallSubStart", - "name" : "getToken(FormDTO)", + "name" : "getToken(Form,String,String)", "config" : { "callSignature" : "getToken", "input" : { "params" : [ - { "name" : "form", "type" : "com.axonivy.connector.oauth.FormDTO" } + { "name" : "form", "type" : "javax.ws.rs.core.Form" }, + { "name" : "tokenUrlPrefix", "type" : "String" }, + { "name" : "tokenUrlSuffix", "type" : "String" } ], "map" : { - "out.form" : "param.form" + "out.form" : "param.form", + "out.tokenUrlPrefix" : "param.tokenUrlPrefix", + "out.tokenUrlSuffix" : "param.tokenUrlSuffix" } }, "result" : { @@ -31,37 +35,36 @@ "visual" : { "at" : { "x" : 96, "y" : 64 } }, - "connect" : { "id" : "f4", "to" : "f3" } + "connect" : { "id" : "f9", "to" : "f3" } }, { "id" : "f1", "type" : "CallSubEnd", "visual" : { - "at" : { "x" : 776, "y" : 64 } + "at" : { "x" : 1048, "y" : 64 } } }, { "id" : "f3", "type" : "RestClientCall", "name" : "Get Token", "config" : { - "bodyForm" : { - "client_id" : "in.form.clientId", - "client_secret" : "in.form.clientSecret", - "scope" : "in.form.scope", - "grant_type" : "in.form.grantType" + "path" : "/{tokenUrlSuffix}", + "bodyObjectMapping" : { + "param" : "in.form" }, "clientId" : "a37c499d-82de-4d21-a021-06844835fb2d", "clientErrorCode" : "ivy:error:rest:client", "method" : "POST", "statusErrorCode" : "ivy:error:rest:client", - "responseMapping" : { - "out.tokenDTO" : "result" - }, "templateParams" : { - "tenant_id" : "in.form.tenantId" + "tokenUrlPrefix" : "in.tokenUrlPrefix", + "tokenUrlSuffix" : "in.tokenUrlSuffix" }, - "resultType" : "com.axonivy.connector.oauth.TokenDTO", - "bodyInputType" : "FORM", - "bodyMediaType" : "application/x-www-form-urlencoded" + "bodyInputType" : "ENTITY", + "bodyMediaType" : "application/x-www-form-urlencoded", + "responseCode" : [ + "import com.axonivy.connector.oauth.OAuthUtils;", + "in.tokenDTO = OAuthUtils.extractToken(response);" + ] }, "visual" : { "at" : { "x" : 400, "y" : 64 } @@ -77,8 +80,8 @@ "visual" : { "at" : { "x" : 432, "y" : 104 } }, - "connect" : { "id" : "f6", "to" : "f1", "via" : [ { "x" : 776, "y" : 104 } ] } + "connect" : { "id" : "f6", "to" : "f1", "via" : [ { "x" : 1048, "y" : 104 } ] } } ], - "connect" : { "id" : "f2", "to" : "f1" } + "connect" : { "id" : "f8", "to" : "f1" } } ] } \ No newline at end of file diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index d628eb8..a1085c4 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -31,11 +31,11 @@ import javax.mail.Session; import javax.mail.Store; import javax.mail.internet.MimeMessage; +import javax.ws.rs.core.Form; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import com.axonivy.connector.oauth.FormDTO; import com.axonivy.connector.oauth.TokenDTO; import ch.ivyteam.ivy.bpm.error.BpmError; @@ -714,12 +714,28 @@ private static boolean isBasicAuth(String store) { } private static String getToken(String store) { - FormDTO form = new FormDTO(getVar(store, TENANT_ID), getVar(store, APP_ID), getVar(store, SECRET_KEY), getVar(store, SCOPE), getVar(store, GRANT_TYPE)); - + Form form = new Form(); + form.param("client_id", getVar(store, APP_ID)); + form.param("client_secret", getVar(store, SECRET_KEY)); + form.param("scope", getVar(store, SCOPE)); + form.param("grant_type", getVar(store, GRANT_TYPE)); + + String tenantId = getVar(store, TENANT_ID); + String tokenUrlPrefix = getVar(store, "tokenUrl.tokenUrlPrefix"); + String tokenUrlSuffix = String.format(getVar(store, "tokenUrl.tokenUrlSuffix"), tenantId); + + if (StringUtils.isBlank(tokenUrlPrefix) || StringUtils.isBlank(tokenUrlSuffix)) { + LOG.error("url to get token cannot be null or empty"); + throw buildError("getToken").withMessage("url to get token cannot be null or empty").build(); + } + TokenDTO result = null; BpmError error = null; SubProcessCallResult callResult = SubProcessCall.withPath("OAuth2Feature").withStartName("getToken") - .withParam("form", form).call(); + .withParam("form", form) + .withParam("tokenUrlPrefix", tokenUrlPrefix) + .withParam("tokenUrlSuffix", tokenUrlSuffix) + .call(); if (callResult != null) { Optional o = Optional.ofNullable(callResult.get("token")); @@ -737,7 +753,7 @@ private static String getToken(String store) { if (null == result || StringUtils.isBlank(result.getAccessToken())) { LOG.error("access token cannot be null or empty"); - throw buildError("load").withMessage("access token cannot be null or empty").build(); + throw buildError("getToken").withMessage("access token cannot be null or empty").build(); } return result.getAccessToken(); diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MessageService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MessageService.java index 633c46e..ce88a75 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MessageService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MessageService.java @@ -19,7 +19,6 @@ public class MessageService { private static final MessageService INSTANCE = new MessageService(); - // private static final Logger LOG = Ivy.log(); private static final String ERROR_BASE = "mailstore:connector:message"; public static MessageService get() { diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java b/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java deleted file mode 100644 index 555fe9d..0000000 --- a/mailstore-connector/src/com/axonivy/connector/oauth/FormDTO.java +++ /dev/null @@ -1,53 +0,0 @@ -package com.axonivy.connector.oauth; - -public class FormDTO { - private String tenantId; - private String clientId; - private String clientSecret; - private String scope; - private String grantType; - - public FormDTO() { - } - - public FormDTO(String tenantId, String clientId, String clientSecret, String scope, String grantType) { - super(); - this.tenantId = tenantId; - this.clientId = clientId; - this.clientSecret = clientSecret; - this.scope = scope; - this.grantType = grantType; - } - - public String getTenantId() { - return tenantId; - } - public void setTenantId(String tenantId) { - this.tenantId = tenantId; - } - public String getClientId() { - return clientId; - } - public void setClientId(String clientId) { - this.clientId = clientId; - } - public String getClientSecret() { - return clientSecret; - } - public void setClientSecret(String clientSecret) { - this.clientSecret = clientSecret; - } - public String getScope() { - return scope; - } - public void setScope(String scope) { - this.scope = scope; - } - public String getGrantType() { - return grantType; - } - public void setGrantType(String grantType) { - this.grantType = grantType; - } - -} diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java b/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java new file mode 100644 index 0000000..e4d79d0 --- /dev/null +++ b/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java @@ -0,0 +1,23 @@ +package com.axonivy.connector.oauth; + +import java.util.Map; + +import javax.ws.rs.core.GenericType; +import javax.ws.rs.core.Response; + + +public class OAuthUtils { + + // get response entity from response + public static TokenDTO extractToken(Response response) { + GenericType> map = new GenericType<>(Map.class); + Map values = response.readEntity(map); + + TokenDTO tokenDto = new TokenDTO(); + tokenDto.setAccessToken(values.get("access_token").toString()); + + return tokenDto; + } + + +} From 6bab2fde79f571fa75c52d02234372d9ffb08702 Mon Sep 17 00:00:00 2001 From: vinv Date: Wed, 23 Oct 2024 14:06:50 +0700 Subject: [PATCH 09/27] remoreve redundant code --- mailstore-connector/config/variables.yaml | 6 +++++- .../com/axonivy/connector/mailstore/MailStoreService.java | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mailstore-connector/config/variables.yaml b/mailstore-connector/config/variables.yaml index ffaf414..2154448 100644 --- a/mailstore-connector/config/variables.yaml +++ b/mailstore-connector/config/variables.yaml @@ -45,4 +45,8 @@ Variables: # for client_credentials: https://outlook.office365.com/.default scope: '' #[client_credentials] - grantType: '' \ No newline at end of file + grantType: '' + # link url to get token + tokenUrl: + tokenUrlPrefix: 'login.microsoftonline.com' + tokenUrlSuffix: '%s/oauth2/v2.0/token' \ No newline at end of file diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index a1085c4..06eaeaa 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -591,7 +591,7 @@ public static InputStream saveMessage(Message message) { } return new ByteArrayInputStream(bos.toByteArray()); } - + /** * Create a mail from raw message data e.g. from loading. * @@ -777,6 +777,7 @@ private static Properties getProperties() { return properties; } + // Only retrieve properties from the store that belong to private static Properties getProperties(String storeName) { Properties properties = new Properties(); String propertiesPrefix = MAIL_STORE_VAR + "." + storeName + "." + PROPERTIES_VAR + "."; From 3f106808563952230bb8dbacbc93ff44d1b05eb8 Mon Sep 17 00:00:00 2001 From: vinv Date: Wed, 23 Oct 2024 14:14:29 +0700 Subject: [PATCH 10/27] remove test code --- .../config/variables.yaml | 26 ++++- .../mailstore/demo/MyTestData.ivyClass | 6 -- .../processes/MyTest.p.json | 97 ------------------- .../connector/mailstore/demo/DemoService.java | 7 +- .../connector/mailstore/MailStoreService.java | 4 +- 5 files changed, 29 insertions(+), 111 deletions(-) delete mode 100644 mailstore-connector-demo/dataclasses/com/axonivy/connector/mailstore/demo/MyTestData.ivyClass delete mode 100644 mailstore-connector-demo/processes/MyTest.p.json diff --git a/mailstore-connector-demo/config/variables.yaml b/mailstore-connector-demo/config/variables.yaml index 0e41688..3eab282 100644 --- a/mailstore-connector-demo/config/variables.yaml +++ b/mailstore-connector-demo/config/variables.yaml @@ -25,4 +25,28 @@ Variables: # see https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html properties: mail.imaps.ssl.checkserveridentity: false - mail.imaps.ssl.trust: '*' \ No newline at end of file + mail.imaps.ssl.trust: '*' + # only set below credential when you go with oauth2 + # mail.imaps.auth.mechanisms: 'XOAUTH2' + # mail.imaps.sasl.enable: 'true' + # mail.imaps.sasl.mechanisms: 'XOAUTH2' + + # [basic, oauth2] basic: username and password, oauth2: currently only support OAuth2 client credentials grant flow + auth: 'basic' + + # only set below credential when you go with oauth2 + # tenant to use for OAUTH2 request. + # set the Azure Directory (tenant) ID, for application requests. + tenantId: '' + # Your Azure Application (client) ID, used for OAuth2 authentication + appId: '' + # Secret key from your applications "certificates & secrets" (client secret) + secretKey: '' + # for client_credentials: https://outlook.office365.com/.default + scope: '' + #[client_credentials] + grantType: '' + # link url to get token + tokenUrl: + tokenUrlPrefix: 'login.microsoftonline.com' + tokenUrlSuffix: '%s/oauth2/v2.0/token' \ No newline at end of file diff --git a/mailstore-connector-demo/dataclasses/com/axonivy/connector/mailstore/demo/MyTestData.ivyClass b/mailstore-connector-demo/dataclasses/com/axonivy/connector/mailstore/demo/MyTestData.ivyClass deleted file mode 100644 index 9b39c69..0000000 --- a/mailstore-connector-demo/dataclasses/com/axonivy/connector/mailstore/demo/MyTestData.ivyClass +++ /dev/null @@ -1,6 +0,0 @@ -MyTestData #class -com.axonivy.connector.mailstore.demo #namespace -domain String #field -endpoint String #field -form com.axonivy.connector.oauth.FormDTO #field -token com.axonivy.connector.oauth.TokenDTO #field diff --git a/mailstore-connector-demo/processes/MyTest.p.json b/mailstore-connector-demo/processes/MyTest.p.json deleted file mode 100644 index 0a5bd96..0000000 --- a/mailstore-connector-demo/processes/MyTest.p.json +++ /dev/null @@ -1,97 +0,0 @@ -{ - "format" : "10.0.0", - "id" : "1929EF19B2B61825", - "config" : { - "data" : "com.axonivy.connector.mailstore.demo.MyTestData" - }, - "elements" : [ { - "id" : "f0", - "type" : "RequestStart", - "name" : "start.ivp", - "config" : { - "callSignature" : "start", - "outLink" : "start.ivp", - "case" : { } - }, - "visual" : { - "at" : { "x" : 208, "y" : 264 } - }, - "connect" : { "id" : "f4", "to" : "f3" } - }, { - "id" : "f1", - "type" : "TaskEnd", - "visual" : { - "at" : { "x" : 1288, "y" : 264 } - } - }, { - "id" : "f3", - "type" : "Script", - "name" : "execute", - "config" : { - "security" : "system", - "output" : { - "code" : [ - "//import com.axonivy.connector.mailstore.demo.DemoService;", - "//DemoService.handleMessages();", - "", - "in.domain = \"login.microsoftonline.com\";", - "in.endpoint = \"016a8c5a-5b40-403d-9819-f1b3162d55ac/oauth2/v2.0/token\";", - "", - "in.form.clientId = \"3c46d107-a614-4ff8-93b4-b460d2fa189a\";", - "in.form.clientSecret = \"SgE8Q~0KvozzjtBm4YATST3A-BTtlmb3eo0snasi\";", - "in.form.grantType = \"client_credentials\";", - "in.form.scope = \"https://outlook.office365.com/.default\";" - ] - } - }, - "visual" : { - "at" : { "x" : 392, "y" : 264 } - }, - "connect" : { "id" : "f6", "to" : "f5" } - }, { - "id" : "f5", - "type" : "SubProcessCall", - "name" : "OAuth2Feature", - "config" : { - "processCall" : "OAuth2Feature:call(String,String,com.axonivy.connector.oauth.FormDTO)", - "output" : { - "map" : { - "out" : "in", - "out.token" : "result.token" - } - }, - "call" : { - "params" : [ - { "name" : "domain", "type" : "String" }, - { "name" : "endpoint", "type" : "String" }, - { "name" : "form", "type" : "com.axonivy.connector.oauth.FormDTO" } - ], - "map" : { - "param.domain" : "in.domain", - "param.endpoint" : "in.endpoint", - "param.form" : "in.form" - } - } - }, - "visual" : { - "at" : { "x" : 656, "y" : 264 } - }, - "connect" : { "id" : "f8", "to" : "f7" } - }, { - "id" : "f7", - "type" : "Script", - "config" : { - "security" : "system", - "output" : { - "code" : [ - "", - "ivy.log.info(\"result access token: \"+in.token.accessToken);" - ] - } - }, - "visual" : { - "at" : { "x" : 1008, "y" : 264 } - }, - "connect" : { "id" : "f2", "to" : "f1" } - } ] -} \ No newline at end of file diff --git a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java index d711158..d360264 100644 --- a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java +++ b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java @@ -23,15 +23,12 @@ public class DemoService { private static final Logger LOG = Ivy.log(); public static void handleMessages() throws MessagingException, IOException { - MessageIterator iterator = MailStoreService.messageIterator("localhost-imap", "INBOX", null, false, MailStoreService.subjectMatches(".*"), new MessageComparator()); + MessageIterator iterator = MailStoreService.messageIterator("localhost-imap", "INBOX", null, false, MailStoreService.subjectMatches(".*test [0-9]+.*"), new MessageComparator()); while (iterator.hasNext()) { Message message = iterator.next(); - - Ivy.log().info("---------> "+message.getSubject()); - - boolean handled = true; + boolean handled = handleMessage(message); iterator.handledMessage(handled); } } diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index 06eaeaa..f2b5319 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -627,9 +627,9 @@ public static Store openStore(String storeName) throws MessagingException { String password; if(isBasicAuth) { password = getVar(storeName, PASSWORD_VAR); - Ivy.log().info("---> basic auth: "+password); + LOG.debug("connect to mail server with basic auth type"); } else { - Ivy.log().info("---> oauth2"); + LOG.debug("connect to mail server with oauth2 type"); password = getToken(storeName); } From e58fb4265d4a9fffda2e1b1a9e7a6c006c5f5036 Mon Sep 17 00:00:00 2001 From: vinv Date: Wed, 23 Oct 2024 14:21:00 +0700 Subject: [PATCH 11/27] refractor: extract method --- .../axonivy/connector/mailstore/MailStoreService.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index f2b5319..4048c08 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -638,7 +638,6 @@ public static Store openStore(String storeName) throws MessagingException { LOG.debug("Creating mail store connection, protocol: {0} host: {1} port: {2} user: {3} password: {4} debug: {5}", protocol, host, portString, user, StringUtils.isNotBlank(password) ? "is set" : "is not set", debugString); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); PrintStream debugStream = new PrintStream(stream); @@ -713,12 +712,18 @@ private static boolean isBasicAuth(String store) { return auth.equals("basic"); } - private static String getToken(String store) { + private static Form buildForm(String store) { Form form = new Form(); form.param("client_id", getVar(store, APP_ID)); form.param("client_secret", getVar(store, SECRET_KEY)); form.param("scope", getVar(store, SCOPE)); form.param("grant_type", getVar(store, GRANT_TYPE)); + + return form; + } + + private static String getToken(String store) { + Form form = buildForm(store); String tenantId = getVar(store, TENANT_ID); String tokenUrlPrefix = getVar(store, "tokenUrl.tokenUrlPrefix"); From 3ad274f630a2746f45dffba11823cdd8976dfced Mon Sep 17 00:00:00 2001 From: vinv Date: Thu, 24 Oct 2024 18:20:50 +0700 Subject: [PATCH 12/27] refractor: create auth provider depends on configuretion --- .../config/variables.yaml | 92 +++++++++++++++- .../processes/MailStoreDemo.p.json | 90 ++++++++++++++-- .../connector/mailstore/demo/DemoService.java | 43 +++++++- mailstore-connector/config/variables.yaml | 18 +++- .../mailstore/OAuth2FeatureData.ivyClass | 1 + .../processes/OAuth2Feature.p.json | 10 +- .../connector/mailstore/MailStoreService.java | 101 ++++-------------- .../AzureOauth2UserPasswordProvider.java | 83 ++++++++++++++ .../oauth/BasicUserPasswordProvider.java | 19 ++++ .../connector/oauth/UserPasswordProvider.java | 5 + 10 files changed, 359 insertions(+), 103 deletions(-) create mode 100644 mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java create mode 100644 mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java create mode 100644 mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java diff --git a/mailstore-connector-demo/config/variables.yaml b/mailstore-connector-demo/config/variables.yaml index 3eab282..4ad65e9 100644 --- a/mailstore-connector-demo/config/variables.yaml +++ b/mailstore-connector-demo/config/variables.yaml @@ -31,8 +31,10 @@ Variables: # mail.imaps.sasl.enable: 'true' # mail.imaps.sasl.mechanisms: 'XOAUTH2' - # [basic, oauth2] basic: username and password, oauth2: currently only support OAuth2 client credentials grant flow - auth: 'basic' + # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow + # com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider for Basic Authentication + # com.axonivy.connector.mailstore.demo.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider + userPasswordProvider: 'com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider' # only set below credential when you go with oauth2 # tenant to use for OAUTH2 request. @@ -47,6 +49,88 @@ Variables: #[client_credentials] grantType: '' # link url to get token + # example microsoft loutlook client_credentials: https://login.microsoftonline.com/{{tenant_id}}/oauth2/v2.0/token tokenUrl: - tokenUrlPrefix: 'login.microsoftonline.com' - tokenUrlSuffix: '%s/oauth2/v2.0/token' \ No newline at end of file + # prefix login url. + # ex: login.microsoftonline.com + tokenUrlPrefix: '' + # suffix login url + # ex: %s/oauth2/v2.0/token (with %s is tenant_id) + tokenUrlSuffix: '' + + localhost-imap-basic-authentication: + # [enum: pop3, pop3s, imap, imaps] + protocol: 'imap' + # Host for store connection + host: 'localhost' + # Port for store connection (only needed if not default) + port: -1 + # User name for store connection + user: 'debug@localdomain.test' + # Password for store connection + # [password] + password: 'pass' + # show debug output for connection + debug: true + # Additional properties for store connection, + # see https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html + properties: + mail.imaps.ssl.checkserveridentity: false + mail.imaps.ssl.trust: '*' + + # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow + # com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider for Basic Authentication + # com.axonivy.connector.mailstore.demo.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider + userPasswordProvider: 'com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider' + + + localhost-imap-azure-oauth2-authentication: + # [enum: pop3, pop3s, imap, imaps] + protocol: 'imap' + # Host for store connection + host: 'localhost' + # Port for store connection (only needed if not default) + port: -1 + # User name for store connection + user: 'debug@localdomain.test' + # Password for store connection + # [password] + password: '' + # show debug output for connection + debug: true + # Additional properties for store connection, + # see https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html + properties: + mail.imaps.ssl.checkserveridentity: false + mail.imaps.ssl.trust: '*' + # only set below credential when you go with oauth2 + mail.imaps.auth.mechanisms: 'XOAUTH2' + mail.imaps.sasl.enable: 'true' + mail.imaps.sasl.mechanisms: 'XOAUTH2' + + # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow + # com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider for Basic Authentication + # com.axonivy.connector.mailstore.demo.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider + userPasswordProvider: 'com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider' + + # only set below credential when you go with oauth2 + # tenant to use for OAUTH2 request. + # set the Azure Directory (tenant) ID, for application requests. + tenantId: '' + # Your Azure Application (client) ID, used for OAuth2 authentication + appId: '' + # Secret key from your applications "certificates & secrets" (client secret) + secretKey: '' + # for client_credentials: https://outlook.office365.com/.default + scope: '' + #[client_credentials] + grantType: '' + # link url to get token + # example microsoft loutlook client_credentials: https://login.microsoftonline.com/{{tenant_id}}/oauth2/v2.0/token + tokenUrl: + # prefix login url. + # ex: login.microsoftonline.com + tokenUrlPrefix: '' + # suffix login url + # ex: %s/oauth2/v2.0/token (with %s is tenant_id) + tokenUrlSuffix: '' \ No newline at end of file diff --git a/mailstore-connector-demo/processes/MailStoreDemo.p.json b/mailstore-connector-demo/processes/MailStoreDemo.p.json index 6667497..2ed6da5 100644 --- a/mailstore-connector-demo/processes/MailStoreDemo.p.json +++ b/mailstore-connector-demo/processes/MailStoreDemo.p.json @@ -21,7 +21,7 @@ "id" : "f1", "type" : "TaskEnd", "visual" : { - "at" : { "x" : 640, "y" : 48 } + "at" : { "x" : 888, "y" : 48 } } }, { "id" : "f3", @@ -49,7 +49,7 @@ } }, "visual" : { - "at" : { "x" : 512, "y" : 48 } + "at" : { "x" : 624, "y" : 48 } }, "connect" : { "id" : "f2", "to" : "f1" } }, { @@ -92,7 +92,7 @@ "id" : "f6", "type" : "TaskEnd", "visual" : { - "at" : { "x" : 640, "y" : 184 } + "at" : { "x" : 888, "y" : 184 } } }, { "id" : "f11", @@ -108,7 +108,7 @@ } }, "visual" : { - "at" : { "x" : 512, "y" : 184 } + "at" : { "x" : 624, "y" : 184 } }, "connect" : { "id" : "f9", "to" : "f6" } }, { @@ -121,14 +121,14 @@ "case" : { } }, "visual" : { - "at" : { "x" : 376, "y" : 328 } + "at" : { "x" : 384, "y" : 328 } }, "connect" : { "id" : "f14", "to" : "f13" } }, { "id" : "f10", "type" : "TaskEnd", "visual" : { - "at" : { "x" : 800, "y" : 328 } + "at" : { "x" : 888, "y" : 328 } } }, { "id" : "f13", @@ -144,8 +144,84 @@ } }, "visual" : { - "at" : { "x" : 584, "y" : 328 } + "at" : { "x" : 624, "y" : 328 } }, "connect" : { "id" : "f15", "to" : "f10" } + }, { + "id" : "f16", + "type" : "RequestStart", + "name" : "connectMailStoreWithBasicAuth.ivp", + "config" : { + "callSignature" : "connectMailStoreWithBasicAuth", + "outLink" : "connectMailStoreWithBasicAuth.ivp", + "case" : { } + }, + "visual" : { + "at" : { "x" : 384, "y" : 560 } + }, + "connect" : { "id" : "f19", "to" : "f18" } + }, { + "id" : "f17", + "type" : "TaskEnd", + "visual" : { + "at" : { "x" : 888, "y" : 560 } + } + }, { + "id" : "f18", + "type" : "Script", + "name" : "connectMailStoreWithBasicAuth", + "config" : { + "security" : "system", + "output" : { + "code" : [ + "import com.axonivy.connector.mailstore.demo.DemoService;", + "", + "DemoService.connectMailStoreWithBasicAuth();" + ] + } + }, + "visual" : { + "at" : { "x" : 632, "y" : 560 }, + "size" : { "width" : 232, "height" : 66 } + }, + "connect" : { "id" : "f20", "to" : "f17" } + }, { + "id" : "f21", + "type" : "RequestStart", + "name" : "connectMailStoreWithAzureOauth2.ivp", + "config" : { + "callSignature" : "connectMailStoreWithAzureOauth2", + "outLink" : "connectMailStoreWithAzureOauth2.ivp", + "case" : { } + }, + "visual" : { + "at" : { "x" : 384, "y" : 768 } + }, + "connect" : { "id" : "f24", "to" : "f22" } + }, { + "id" : "f22", + "type" : "Script", + "name" : "connectMailStoreWithAzureOauth2", + "config" : { + "security" : "system", + "output" : { + "code" : [ + "import com.axonivy.connector.mailstore.demo.DemoService;", + "", + "DemoService.connectMailStoreWithAzureOauth2();" + ] + } + }, + "visual" : { + "at" : { "x" : 632, "y" : 768 }, + "size" : { "width" : 232, "height" : 60 } + }, + "connect" : { "id" : "f25", "to" : "f23" } + }, { + "id" : "f23", + "type" : "TaskEnd", + "visual" : { + "at" : { "x" : 888, "y" : 768 } + } } ] } \ No newline at end of file diff --git a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java index d360264..9f8c293 100644 --- a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java +++ b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java @@ -13,6 +13,7 @@ import com.axonivy.connector.mailstore.MailStoreService; import com.axonivy.connector.mailstore.MailStoreService.MessageIterator; +import com.axonivy.connector.oauth.UserPasswordProvider; import com.axonivy.connector.mailstore.MessageService; import ch.ivyteam.ivy.environment.Ivy; @@ -63,6 +64,37 @@ public static void handleMessagesMultiDestinationFolder() throws MessagingExcept } } + public static void connectMailStoreWithBasicAuth() throws MessagingException, IOException { + String storeName = "localhost-imap-basic-authentication"; + + // get from variable mailstore-connector.localhost-imap.userPasswordProvider + String authProviderPath = "com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider"; + initAuthProvider(storeName, authProviderPath); + + MessageIterator iterator = MailStoreService.messageIterator(storeName, "INBOX", null, false, MailStoreService.subjectMatches(".*"), new MessageComparator()); + + while (iterator.hasNext()) { + Message message = iterator.next(); + boolean handled = handleMessage(message); + iterator.handledMessage(handled); + } + } + + public static void connectMailStoreWithAzureOauth2() throws MessagingException, IOException { + String storeName = "localhost-imap-azure-oauth2-authentication"; + + // get from variable mailstore-connector.localhost-imap.userPasswordProvider + String authProviderPath = "com.axonivy.connector.mailstore.demo.oauth.AzureOauth2UserPasswordProvider"; + initAuthProvider(storeName, authProviderPath); + + MessageIterator iterator = MailStoreService.messageIterator(storeName, "INBOX", null, false, MailStoreService.subjectMatches(".*"), new MessageComparator()); + + while (iterator.hasNext()) { + Message message = iterator.next(); + boolean handled = handleMessage(message); + iterator.handledMessage(handled); + } + } public static void handleAttachmentMessages() throws MessagingException, IOException { MessageIterator iterator = MailStoreService.messageIterator( @@ -71,7 +103,6 @@ public static void handleAttachmentMessages() throws MessagingException, IOExcep null, false, null); - // MailStoreService.hasAttachment(true)); while (iterator.hasNext()) { Message message = iterator.next(); @@ -86,6 +117,16 @@ public static void handleAttachmentMessages() throws MessagingException, IOExcep iterator.handledMessage(handled); } } + + private static void initAuthProvider(String storeName, String authProviderPath) { + try { + Class clazz = Class.forName(authProviderPath); + UserPasswordProvider userPasswordProvider = (UserPasswordProvider) clazz.getDeclaredConstructor().newInstance(); + MailStoreService.registerUserPasswordProvider(storeName, userPasswordProvider); + } catch(Exception ex) { + Ivy.log().error("exception during instantiate UserPasswordProvider"+ex); + } + } private static boolean logMessage(Message message) throws MessagingException, IOException { LOG.info("Working on message {0} received at {1} type {2}", message.getSubject(), message.getReceivedDate(), message.getContent().getClass()); diff --git a/mailstore-connector/config/variables.yaml b/mailstore-connector/config/variables.yaml index 2154448..526c5f0 100644 --- a/mailstore-connector/config/variables.yaml +++ b/mailstore-connector/config/variables.yaml @@ -30,10 +30,13 @@ Variables: # mail.imaps.auth.mechanisms: 'XOAUTH2' # mail.imaps.sasl.enable: 'true' # mail.imaps.sasl.mechanisms: 'XOAUTH2' - - # [basic, oauth2] basic: username and password, oauth2: currently only support OAuth2 client credentials grant flow - auth: 'basic' + # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow + # com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider for Basic Authentication + # com.axonivy.connector.mailstore.demo.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider + userPasswordProvider: 'com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider' + + # [basic, oauth2] basic: username and password, oauth2: currently only support OAuth2 client credentials grant flow # only set below credential when you go with oauth2 # tenant to use for OAUTH2 request. # set the Azure Directory (tenant) ID, for application requests. @@ -47,6 +50,11 @@ Variables: #[client_credentials] grantType: '' # link url to get token + # example microsoft loutlook client_credentials: https://login.microsoftonline.com/{{tenant_id}}/oauth2/v2.0/token tokenUrl: - tokenUrlPrefix: 'login.microsoftonline.com' - tokenUrlSuffix: '%s/oauth2/v2.0/token' \ No newline at end of file + # prefix login url. + # ex: login.microsoftonline.com + tokenUrlPrefix: '' + # suffix login url + # ex: %s/oauth2/v2.0/token (with %s is tenant_id) + tokenUrlSuffix: '' \ No newline at end of file diff --git a/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass b/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass index 2c1d0db..0d3aba5 100644 --- a/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass +++ b/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass @@ -4,3 +4,4 @@ tokenDTO com.axonivy.connector.oauth.TokenDTO #field form javax.ws.rs.core.Form #field tokenUrlPrefix String #field tokenUrlSuffix String #field +error ch.ivyteam.ivy.bpm.error.BpmError #field diff --git a/mailstore-connector/processes/OAuth2Feature.p.json b/mailstore-connector/processes/OAuth2Feature.p.json index de904b0..75fab77 100644 --- a/mailstore-connector/processes/OAuth2Feature.p.json +++ b/mailstore-connector/processes/OAuth2Feature.p.json @@ -25,10 +25,12 @@ }, "result" : { "params" : [ - { "name" : "token", "type" : "com.axonivy.connector.oauth.TokenDTO" } + { "name" : "token", "type" : "com.axonivy.connector.oauth.TokenDTO" }, + { "name" : "error", "type" : "ch.ivyteam.ivy.bpm.error.BpmError" } ], "map" : { - "result.token" : "in.#tokenDTO" + "result.token" : "in.#tokenDTO", + "result.error" : "in.#error" } } }, @@ -40,7 +42,7 @@ "id" : "f1", "type" : "CallSubEnd", "visual" : { - "at" : { "x" : 1048, "y" : 64 } + "at" : { "x" : 816, "y" : 64 } } }, { "id" : "f3", @@ -80,7 +82,7 @@ "visual" : { "at" : { "x" : 432, "y" : 104 } }, - "connect" : { "id" : "f6", "to" : "f1", "via" : [ { "x" : 1048, "y" : 104 } ] } + "connect" : { "id" : "f6", "to" : "f1", "via" : [ { "x" : 816, "y" : 104 } ] } } ], "connect" : { "id" : "f8", "to" : "f1" } } ] diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index 4048c08..d6c13df 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -8,12 +8,12 @@ import java.util.Arrays; import java.util.Collection; import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.NoSuchElementException; -import java.util.Optional; import java.util.Properties; import java.util.function.Predicate; import java.util.regex.Pattern; @@ -31,41 +31,32 @@ import javax.mail.Session; import javax.mail.Store; import javax.mail.internet.MimeMessage; -import javax.ws.rs.core.Form; import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.StringUtils; -import com.axonivy.connector.oauth.TokenDTO; +import com.axonivy.connector.oauth.BasicUserPasswordProvider; +import com.axonivy.connector.oauth.UserPasswordProvider; import ch.ivyteam.ivy.bpm.error.BpmError; import ch.ivyteam.ivy.bpm.error.BpmPublicErrorBuilder; import ch.ivyteam.ivy.environment.Ivy; -import ch.ivyteam.ivy.process.call.SubProcessCall; -import ch.ivyteam.ivy.process.call.SubProcessCallResult; import ch.ivyteam.ivy.vars.Variable; import ch.ivyteam.log.Logger; public class MailStoreService { private static final MailStoreService INSTANCE = new MailStoreService(); private static final Logger LOG = Ivy.log(); - private static final String MAIL_STORE_VAR = "mailstore-connector"; + public static final String MAIL_STORE_VAR = "mailstore-connector"; private static final String PROTOCOL_VAR = "protocol"; private static final String HOST_VAR = "host"; private static final String PORT_VAR = "port"; private static final String USER_VAR = "user"; - private static final String PASSWORD_VAR = "password"; private static final String DEBUG_VAR = "debug"; private static final String PROPERTIES_VAR = "properties"; private static final String ERROR_BASE = "mailstore:connector"; private static final Address[] EMPTY_ADDRESSES = new Address[0]; - - private static final String AUTH = "auth"; - private static final String TENANT_ID = "tenantId"; - private static final String APP_ID = "appId"; - private static final String SECRET_KEY = "secretKey"; - private static final String GRANT_TYPE = "grantType"; - private static final String SCOPE = "scope"; + private static Map userPasswordProviderRegister = new HashMap<>(); public static MailStoreService get() { return INSTANCE; @@ -120,7 +111,7 @@ public static MessageIterator messageIterator(String storeName, String srcFolder boolean delete, Predicate filter, Comparator comparator, List dstFolderNames) { return new MessageIterator(storeName, srcFolderName, dstFolderNames, delete, filter, comparator); } - + /** * Get a {@link Predicate} to match subjects against a regular expression. * @@ -367,6 +358,9 @@ public static Predicate alwaysFalse() { return m -> false; } + public static void registerUserPasswordProvider(String storeName, UserPasswordProvider userPasswordProvider) { + userPasswordProviderRegister.put(storeName, userPasswordProvider); + } /** * Iterate through the E-Mails of a store. @@ -623,15 +617,15 @@ public static Store openStore(String storeName) throws MessagingException { String portString = getVar(storeName, PORT_VAR); String user = getVar(storeName, USER_VAR); - boolean isBasicAuth = isBasicAuth(storeName); - String password; - if(isBasicAuth) { - password = getVar(storeName, PASSWORD_VAR); - LOG.debug("connect to mail server with basic auth type"); - } else { - LOG.debug("connect to mail server with oauth2 type"); - password = getToken(storeName); + + UserPasswordProvider userPasswordProvider = userPasswordProviderRegister.get(storeName); + // adapt exist project already use this connector, default is basic auth + if(null == userPasswordProvider) { + userPasswordProvider = new BasicUserPasswordProvider(); } + String password = userPasswordProvider.authenticate(storeName); + + String debugString = getVar(storeName, DEBUG_VAR); @@ -703,67 +697,10 @@ private static Folder openFolder(Store store, String folderName, int mode) throw return folder; } - private static String getVar(String store, String var) { + public static String getVar(String store, String var) { return Ivy.var().get(String.format("%s.%s.%s", MAIL_STORE_VAR, store, var)); } - private static boolean isBasicAuth(String store) { - String auth = Ivy.var().get(String.format("%s.%s.%s", MAIL_STORE_VAR, store, AUTH)); - return auth.equals("basic"); - } - - private static Form buildForm(String store) { - Form form = new Form(); - form.param("client_id", getVar(store, APP_ID)); - form.param("client_secret", getVar(store, SECRET_KEY)); - form.param("scope", getVar(store, SCOPE)); - form.param("grant_type", getVar(store, GRANT_TYPE)); - - return form; - } - - private static String getToken(String store) { - Form form = buildForm(store); - - String tenantId = getVar(store, TENANT_ID); - String tokenUrlPrefix = getVar(store, "tokenUrl.tokenUrlPrefix"); - String tokenUrlSuffix = String.format(getVar(store, "tokenUrl.tokenUrlSuffix"), tenantId); - - if (StringUtils.isBlank(tokenUrlPrefix) || StringUtils.isBlank(tokenUrlSuffix)) { - LOG.error("url to get token cannot be null or empty"); - throw buildError("getToken").withMessage("url to get token cannot be null or empty").build(); - } - - TokenDTO result = null; - BpmError error = null; - SubProcessCallResult callResult = SubProcessCall.withPath("OAuth2Feature").withStartName("getToken") - .withParam("form", form) - .withParam("tokenUrlPrefix", tokenUrlPrefix) - .withParam("tokenUrlSuffix", tokenUrlSuffix) - .call(); - - if (callResult != null) { - Optional o = Optional.ofNullable(callResult.get("token")); - if (o.isPresent()) { - result = (TokenDTO) o.get(); - } else { - Optional e = Optional.ofNullable(callResult.get("error")); - if (e.isPresent()) { - error = (BpmError) e.get(); - LOG.error(error); - throw error; - } - } - } - - if (null == result || StringUtils.isBlank(result.getAccessToken())) { - LOG.error("access token cannot be null or empty"); - throw buildError("getToken").withMessage("access token cannot be null or empty").build(); - } - - return result.getAccessToken(); - } - private static Properties getProperties() { Properties properties = System.getProperties(); @@ -815,7 +752,7 @@ private static String getSubstringAfterProperties(String input) { return null; } - private static BpmPublicErrorBuilder buildError(String code) { + public static BpmPublicErrorBuilder buildError(String code) { BpmPublicErrorBuilder builder = BpmError.create(ERROR_BASE + ":" + code); return builder; } diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java new file mode 100644 index 0000000..4b5c513 --- /dev/null +++ b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java @@ -0,0 +1,83 @@ +package com.axonivy.connector.oauth; + +import java.util.Optional; + +import javax.ws.rs.core.Form; + +import org.apache.commons.lang3.StringUtils; + +import com.axonivy.connector.mailstore.MailStoreService; + +import ch.ivyteam.ivy.bpm.error.BpmError; +import ch.ivyteam.ivy.environment.Ivy; +import ch.ivyteam.ivy.process.call.SubProcessCall; +import ch.ivyteam.ivy.process.call.SubProcessCallResult; +import ch.ivyteam.log.Logger; + +public class AzureOauth2UserPasswordProvider implements UserPasswordProvider { + private static final Logger LOG = Ivy.log(); + private static final String TENANT_ID = "tenantId"; + private static final String APP_ID = "appId"; + private static final String SECRET_KEY = "secretKey"; + private static final String GRANT_TYPE = "grantType"; + private static final String SCOPE = "scope"; + + @Override + public String authenticate(String storeName) { + LOG.debug("Connect to store {0} using OAuth2 Authentication.", storeName); + + return getToken(storeName); + } + + private Form buildForm(String storeName) { + Form form = new Form(); + form.param("client_id", MailStoreService.getVar(storeName, APP_ID)); + form.param("client_secret", MailStoreService.getVar(storeName, SECRET_KEY)); + form.param("scope", MailStoreService.getVar(storeName, SCOPE)); + form.param("grant_type", MailStoreService.getVar(storeName, GRANT_TYPE)); + + return form; + } + + private String getToken(String storeName) { + Form form = buildForm(storeName); + + String tenantId = MailStoreService.getVar(storeName, TENANT_ID); + String tokenUrlPrefix = MailStoreService.getVar(storeName, "tokenUrl.tokenUrlPrefix"); + String tokenUrlSuffix = String.format(MailStoreService.getVar(storeName, "tokenUrl.tokenUrlSuffix"), tenantId); + + if (StringUtils.isBlank(tokenUrlPrefix) || StringUtils.isBlank(tokenUrlSuffix)) { + LOG.error("url to get token cannot be null or empty"); + throw MailStoreService.buildError("getToken").withMessage("url to get token cannot be null or empty") + .build(); + } + + TokenDTO result = null; + BpmError error = null; + SubProcessCallResult callResult = SubProcessCall.withPath("OAuth2Feature").withStartName("getToken") + .withParam("form", form).withParam("tokenUrlPrefix", tokenUrlPrefix) + .withParam("tokenUrlSuffix", tokenUrlSuffix).call(); + + if (callResult != null) { + Optional o = Optional.ofNullable(callResult.get("token")); + if (o.isPresent()) { + result = (TokenDTO) o.get(); + } else { + Optional e = Optional.ofNullable(callResult.get("error")); + if (e.isPresent()) { + error = (BpmError) e.get(); + LOG.error(error); + throw error; + } + } + } + + if (null == result || StringUtils.isBlank(result.getAccessToken())) { + LOG.error("access token cannot be null or empty"); + throw MailStoreService.buildError("getToken").withMessage("access token cannot be null or empty").build(); + } + + return result.getAccessToken(); + } + +} \ No newline at end of file diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java new file mode 100644 index 0000000..4dbbb3d --- /dev/null +++ b/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java @@ -0,0 +1,19 @@ +package com.axonivy.connector.oauth; + +import com.axonivy.connector.mailstore.MailStoreService; + +import ch.ivyteam.ivy.environment.Ivy; +import ch.ivyteam.log.Logger; + +public class BasicUserPasswordProvider implements UserPasswordProvider { + private static final Logger LOG = Ivy.log(); + private static final String PASSWORD_VAR = "password"; + + @Override + public String authenticate(String storeName) { + LOG.debug("Connect to store {0} using Basic Authentication.", storeName); + + return MailStoreService.getVar(storeName, PASSWORD_VAR); + } + +} diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java new file mode 100644 index 0000000..e7f46e7 --- /dev/null +++ b/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java @@ -0,0 +1,5 @@ +package com.axonivy.connector.oauth; + +public interface UserPasswordProvider { + String authenticate(String storeName); +} From 5586739d6b78b9a6f3c408804da510550acb60f5 Mon Sep 17 00:00:00 2001 From: vinv Date: Thu, 24 Oct 2024 18:24:43 +0700 Subject: [PATCH 13/27] remove redundant code --- mailstore-connector-demo/config/variables.yaml | 3 ++- .../com/axonivy/connector/mailstore/MailStoreService.java | 5 +---- .../src/com/axonivy/connector/oauth/OAuthUtils.java | 2 -- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/mailstore-connector-demo/config/variables.yaml b/mailstore-connector-demo/config/variables.yaml index 4ad65e9..2560730 100644 --- a/mailstore-connector-demo/config/variables.yaml +++ b/mailstore-connector-demo/config/variables.yaml @@ -58,6 +58,7 @@ Variables: # ex: %s/oauth2/v2.0/token (with %s is tenant_id) tokenUrlSuffix: '' + localhost-imap-basic-authentication: # [enum: pop3, pop3s, imap, imaps] protocol: 'imap' @@ -83,7 +84,7 @@ Variables: # com.axonivy.connector.mailstore.demo.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider userPasswordProvider: 'com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider' - + localhost-imap-azure-oauth2-authentication: # [enum: pop3, pop3s, imap, imaps] protocol: 'imap' diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index d6c13df..6de714f 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -617,16 +617,13 @@ public static Store openStore(String storeName) throws MessagingException { String portString = getVar(storeName, PORT_VAR); String user = getVar(storeName, USER_VAR); - UserPasswordProvider userPasswordProvider = userPasswordProviderRegister.get(storeName); // adapt exist project already use this connector, default is basic auth if(null == userPasswordProvider) { userPasswordProvider = new BasicUserPasswordProvider(); } String password = userPasswordProvider.authenticate(storeName); - - - + String debugString = getVar(storeName, DEBUG_VAR); LOG.debug("Creating mail store connection, protocol: {0} host: {1} port: {2} user: {3} password: {4} debug: {5}", diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java b/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java index e4d79d0..f04ea07 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java @@ -5,7 +5,6 @@ import javax.ws.rs.core.GenericType; import javax.ws.rs.core.Response; - public class OAuthUtils { // get response entity from response @@ -19,5 +18,4 @@ public static TokenDTO extractToken(Response response) { return tokenDto; } - } From d0565a0f35baf23a05d469514d53f61e5ef98798 Mon Sep 17 00:00:00 2001 From: vinv Date: Fri, 25 Oct 2024 11:14:38 +0700 Subject: [PATCH 14/27] refractore: using rest inside java code instead of make function callable --- .../config/variables.yaml | 34 ++----- .../connector/mailstore/demo/DemoService.java | 4 +- mailstore-connector/config/rest-clients.yaml | 4 +- mailstore-connector/config/variables.yaml | 11 +-- .../mailstore/OAuth2FeatureData.ivyClass | 7 -- .../processes/OAuth2Feature.p.json | 89 ------------------- .../AzureOauth2UserPasswordProvider.java | 59 +++++------- .../axonivy/connector/oauth/OAuthUtils.java | 4 + 8 files changed, 40 insertions(+), 172 deletions(-) delete mode 100644 mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass delete mode 100644 mailstore-connector/processes/OAuth2Feature.p.json diff --git a/mailstore-connector-demo/config/variables.yaml b/mailstore-connector-demo/config/variables.yaml index 2560730..d0fcaff 100644 --- a/mailstore-connector-demo/config/variables.yaml +++ b/mailstore-connector-demo/config/variables.yaml @@ -32,8 +32,8 @@ Variables: # mail.imaps.sasl.mechanisms: 'XOAUTH2' # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow - # com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider for Basic Authentication - # com.axonivy.connector.mailstore.demo.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider + # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication + # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider userPasswordProvider: 'com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider' # only set below credential when you go with oauth2 @@ -48,15 +48,6 @@ Variables: scope: '' #[client_credentials] grantType: '' - # link url to get token - # example microsoft loutlook client_credentials: https://login.microsoftonline.com/{{tenant_id}}/oauth2/v2.0/token - tokenUrl: - # prefix login url. - # ex: login.microsoftonline.com - tokenUrlPrefix: '' - # suffix login url - # ex: %s/oauth2/v2.0/token (with %s is tenant_id) - tokenUrlSuffix: '' localhost-imap-basic-authentication: @@ -80,9 +71,9 @@ Variables: mail.imaps.ssl.trust: '*' # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow - # com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider for Basic Authentication - # com.axonivy.connector.mailstore.demo.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider - userPasswordProvider: 'com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider' + # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication + # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider + userPasswordProvider: 'com.axonivy.connector.oauth.BasicUserPasswordProvider' localhost-imap-azure-oauth2-authentication: @@ -110,9 +101,9 @@ Variables: mail.imaps.sasl.mechanisms: 'XOAUTH2' # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow - # com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider for Basic Authentication - # com.axonivy.connector.mailstore.demo.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider - userPasswordProvider: 'com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider' + # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication + # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider + userPasswordProvider: 'com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider' # only set below credential when you go with oauth2 # tenant to use for OAUTH2 request. @@ -126,12 +117,3 @@ Variables: scope: '' #[client_credentials] grantType: '' - # link url to get token - # example microsoft loutlook client_credentials: https://login.microsoftonline.com/{{tenant_id}}/oauth2/v2.0/token - tokenUrl: - # prefix login url. - # ex: login.microsoftonline.com - tokenUrlPrefix: '' - # suffix login url - # ex: %s/oauth2/v2.0/token (with %s is tenant_id) - tokenUrlSuffix: '' \ No newline at end of file diff --git a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java index 9f8c293..98392c5 100644 --- a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java +++ b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java @@ -68,7 +68,7 @@ public static void connectMailStoreWithBasicAuth() throws MessagingException, IO String storeName = "localhost-imap-basic-authentication"; // get from variable mailstore-connector.localhost-imap.userPasswordProvider - String authProviderPath = "com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider"; + String authProviderPath = "com.axonivy.connector.oauth.BasicUserPasswordProvider"; initAuthProvider(storeName, authProviderPath); MessageIterator iterator = MailStoreService.messageIterator(storeName, "INBOX", null, false, MailStoreService.subjectMatches(".*"), new MessageComparator()); @@ -84,7 +84,7 @@ public static void connectMailStoreWithAzureOauth2() throws MessagingException, String storeName = "localhost-imap-azure-oauth2-authentication"; // get from variable mailstore-connector.localhost-imap.userPasswordProvider - String authProviderPath = "com.axonivy.connector.mailstore.demo.oauth.AzureOauth2UserPasswordProvider"; + String authProviderPath = "com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider"; initAuthProvider(storeName, authProviderPath); MessageIterator iterator = MailStoreService.messageIterator(storeName, "INBOX", null, false, MailStoreService.subjectMatches(".*"), new MessageComparator()); diff --git a/mailstore-connector/config/rest-clients.yaml b/mailstore-connector/config/rest-clients.yaml index 6414ecc..2f6788e 100644 --- a/mailstore-connector/config/rest-clients.yaml +++ b/mailstore-connector/config/rest-clients.yaml @@ -1,6 +1,6 @@ RestClients: - Retrieve Token: + getTokenAzureOAuth: UUID: a37c499d-82de-4d21-a021-06844835fb2d - Url: https://{tokenUrlPrefix}/ + Url: https://login.microsoftonline.com Features: - ch.ivyteam.ivy.rest.client.mapper.JsonFeature diff --git a/mailstore-connector/config/variables.yaml b/mailstore-connector/config/variables.yaml index 526c5f0..85154fc 100644 --- a/mailstore-connector/config/variables.yaml +++ b/mailstore-connector/config/variables.yaml @@ -48,13 +48,4 @@ Variables: # for client_credentials: https://outlook.office365.com/.default scope: '' #[client_credentials] - grantType: '' - # link url to get token - # example microsoft loutlook client_credentials: https://login.microsoftonline.com/{{tenant_id}}/oauth2/v2.0/token - tokenUrl: - # prefix login url. - # ex: login.microsoftonline.com - tokenUrlPrefix: '' - # suffix login url - # ex: %s/oauth2/v2.0/token (with %s is tenant_id) - tokenUrlSuffix: '' \ No newline at end of file + grantType: '' \ No newline at end of file diff --git a/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass b/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass deleted file mode 100644 index 0d3aba5..0000000 --- a/mailstore-connector/dataclasses/com/axonivy/connector/mailstore/OAuth2FeatureData.ivyClass +++ /dev/null @@ -1,7 +0,0 @@ -OAuth2FeatureData #class -com.axonivy.connector.mailstore #namespace -tokenDTO com.axonivy.connector.oauth.TokenDTO #field -form javax.ws.rs.core.Form #field -tokenUrlPrefix String #field -tokenUrlSuffix String #field -error ch.ivyteam.ivy.bpm.error.BpmError #field diff --git a/mailstore-connector/processes/OAuth2Feature.p.json b/mailstore-connector/processes/OAuth2Feature.p.json deleted file mode 100644 index 75fab77..0000000 --- a/mailstore-connector/processes/OAuth2Feature.p.json +++ /dev/null @@ -1,89 +0,0 @@ -{ - "format" : "10.0.0", - "id" : "192AD1C7ED09AF64", - "kind" : "CALLABLE_SUB", - "config" : { - "data" : "com.axonivy.connector.mailstore.OAuth2FeatureData" - }, - "elements" : [ { - "id" : "f0", - "type" : "CallSubStart", - "name" : "getToken(Form,String,String)", - "config" : { - "callSignature" : "getToken", - "input" : { - "params" : [ - { "name" : "form", "type" : "javax.ws.rs.core.Form" }, - { "name" : "tokenUrlPrefix", "type" : "String" }, - { "name" : "tokenUrlSuffix", "type" : "String" } - ], - "map" : { - "out.form" : "param.form", - "out.tokenUrlPrefix" : "param.tokenUrlPrefix", - "out.tokenUrlSuffix" : "param.tokenUrlSuffix" - } - }, - "result" : { - "params" : [ - { "name" : "token", "type" : "com.axonivy.connector.oauth.TokenDTO" }, - { "name" : "error", "type" : "ch.ivyteam.ivy.bpm.error.BpmError" } - ], - "map" : { - "result.token" : "in.#tokenDTO", - "result.error" : "in.#error" - } - } - }, - "visual" : { - "at" : { "x" : 96, "y" : 64 } - }, - "connect" : { "id" : "f9", "to" : "f3" } - }, { - "id" : "f1", - "type" : "CallSubEnd", - "visual" : { - "at" : { "x" : 816, "y" : 64 } - } - }, { - "id" : "f3", - "type" : "RestClientCall", - "name" : "Get Token", - "config" : { - "path" : "/{tokenUrlSuffix}", - "bodyObjectMapping" : { - "param" : "in.form" - }, - "clientId" : "a37c499d-82de-4d21-a021-06844835fb2d", - "clientErrorCode" : "ivy:error:rest:client", - "method" : "POST", - "statusErrorCode" : "ivy:error:rest:client", - "templateParams" : { - "tokenUrlPrefix" : "in.tokenUrlPrefix", - "tokenUrlSuffix" : "in.tokenUrlSuffix" - }, - "bodyInputType" : "ENTITY", - "bodyMediaType" : "application/x-www-form-urlencoded", - "responseCode" : [ - "import com.axonivy.connector.oauth.OAuthUtils;", - "in.tokenDTO = OAuthUtils.extractToken(response);" - ] - }, - "visual" : { - "at" : { "x" : 400, "y" : 64 } - }, - "boundaries" : [ { - "id" : "f5", - "type" : "ErrorBoundaryEvent", - "config" : { - "output" : { - "code" : "ivy.log.error(error);" - } - }, - "visual" : { - "at" : { "x" : 432, "y" : 104 } - }, - "connect" : { "id" : "f6", "to" : "f1", "via" : [ { "x" : 816, "y" : 104 } ] } - } ], - "connect" : { "id" : "f8", "to" : "f1" } - } ] -} \ No newline at end of file diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java index 4b5c513..8f923ac 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java @@ -1,17 +1,14 @@ package com.axonivy.connector.oauth; -import java.util.Optional; - +import javax.ws.rs.client.Entity; import javax.ws.rs.core.Form; +import javax.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; import com.axonivy.connector.mailstore.MailStoreService; -import ch.ivyteam.ivy.bpm.error.BpmError; import ch.ivyteam.ivy.environment.Ivy; -import ch.ivyteam.ivy.process.call.SubProcessCall; -import ch.ivyteam.ivy.process.call.SubProcessCallResult; import ch.ivyteam.log.Logger; public class AzureOauth2UserPasswordProvider implements UserPasswordProvider { @@ -22,6 +19,8 @@ public class AzureOauth2UserPasswordProvider implements UserPasswordProvider { private static final String GRANT_TYPE = "grantType"; private static final String SCOPE = "scope"; + private static final String REST_CLIENT = "getTokenAzureOAuth"; + @Override public String authenticate(String storeName) { LOG.debug("Connect to store {0} using OAuth2 Authentication.", storeName); @@ -39,45 +38,33 @@ private Form buildForm(String storeName) { return form; } + private Response sendTokenRequest(String tenantId, Form form) { + return Ivy.rest().client(REST_CLIENT) + .path(tenantId) + .path("oauth2/v2.0/token") + .request() + .header("Content-Type", "application/x-www-form-urlencoded") + .post(Entity.form(form)); + } + private String getToken(String storeName) { Form form = buildForm(storeName); - String tenantId = MailStoreService.getVar(storeName, TENANT_ID); - String tokenUrlPrefix = MailStoreService.getVar(storeName, "tokenUrl.tokenUrlPrefix"); - String tokenUrlSuffix = String.format(MailStoreService.getVar(storeName, "tokenUrl.tokenUrlSuffix"), tenantId); - if (StringUtils.isBlank(tokenUrlPrefix) || StringUtils.isBlank(tokenUrlSuffix)) { - LOG.error("url to get token cannot be null or empty"); - throw MailStoreService.buildError("getToken").withMessage("url to get token cannot be null or empty") - .build(); - } - - TokenDTO result = null; - BpmError error = null; - SubProcessCallResult callResult = SubProcessCall.withPath("OAuth2Feature").withStartName("getToken") - .withParam("form", form).withParam("tokenUrlPrefix", tokenUrlPrefix) - .withParam("tokenUrlSuffix", tokenUrlSuffix).call(); + Response response = sendTokenRequest(tenantId, form); - if (callResult != null) { - Optional o = Optional.ofNullable(callResult.get("token")); - if (o.isPresent()) { - result = (TokenDTO) o.get(); - } else { - Optional e = Optional.ofNullable(callResult.get("error")); - if (e.isPresent()) { - error = (BpmError) e.get(); - LOG.error(error); - throw error; - } - } + if (null == response) { + LOG.error("response cannot be null"); + throw MailStoreService.buildError("getToken").withMessage("response cannot be null").build(); } - if (null == result || StringUtils.isBlank(result.getAccessToken())) { - LOG.error("access token cannot be null or empty"); - throw MailStoreService.buildError("getToken").withMessage("access token cannot be null or empty").build(); + TokenDTO tokenDto = OAuthUtils.extractToken(response); + + if (null == tokenDto || StringUtils.isEmpty(tokenDto.getAccessToken())) { + throw new IllegalStateException("Failed to read 'access_token' from " + response); } - return result.getAccessToken(); + return tokenDto.getAccessToken(); } - + } \ No newline at end of file diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java b/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java index f04ea07..9878a2c 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java @@ -11,6 +11,10 @@ public class OAuthUtils { public static TokenDTO extractToken(Response response) { GenericType> map = new GenericType<>(Map.class); Map values = response.readEntity(map); + + if(null == values) { + return null; + } TokenDTO tokenDto = new TokenDTO(); tokenDto.setAccessToken(values.get("access_token").toString()); From 46b7cb3b6c96cb4a50c8c7856b36c47bf5b37036 Mon Sep 17 00:00:00 2001 From: vinv Date: Fri, 25 Oct 2024 11:29:00 +0700 Subject: [PATCH 15/27] correct access modifier --- .../src/com/axonivy/connector/mailstore/MailStoreService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index 6de714f..1a539e9 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -47,7 +47,7 @@ public class MailStoreService { private static final MailStoreService INSTANCE = new MailStoreService(); private static final Logger LOG = Ivy.log(); - public static final String MAIL_STORE_VAR = "mailstore-connector"; + private static final String MAIL_STORE_VAR = "mailstore-connector"; private static final String PROTOCOL_VAR = "protocol"; private static final String HOST_VAR = "host"; private static final String PORT_VAR = "port"; From b19de44f76ea91e0f8b028013f6c71c343d34156 Mon Sep 17 00:00:00 2001 From: vinv Date: Fri, 25 Oct 2024 11:41:37 +0700 Subject: [PATCH 16/27] update demo test method --- .../com/axonivy/connector/mailstore/demo/DemoService.java | 4 ++-- .../com/axonivy/connector/mailstore/MailStoreService.java | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java index 98392c5..ca7436e 100644 --- a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java +++ b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java @@ -68,7 +68,7 @@ public static void connectMailStoreWithBasicAuth() throws MessagingException, IO String storeName = "localhost-imap-basic-authentication"; // get from variable mailstore-connector.localhost-imap.userPasswordProvider - String authProviderPath = "com.axonivy.connector.oauth.BasicUserPasswordProvider"; + String authProviderPath = MailStoreService.getVar(storeName, "userPasswordProvider"); initAuthProvider(storeName, authProviderPath); MessageIterator iterator = MailStoreService.messageIterator(storeName, "INBOX", null, false, MailStoreService.subjectMatches(".*"), new MessageComparator()); @@ -84,7 +84,7 @@ public static void connectMailStoreWithAzureOauth2() throws MessagingException, String storeName = "localhost-imap-azure-oauth2-authentication"; // get from variable mailstore-connector.localhost-imap.userPasswordProvider - String authProviderPath = "com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider"; + String authProviderPath = MailStoreService.getVar(storeName, "userPasswordProvider"); initAuthProvider(storeName, authProviderPath); MessageIterator iterator = MailStoreService.messageIterator(storeName, "INBOX", null, false, MailStoreService.subjectMatches(".*"), new MessageComparator()); diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index 1a539e9..33cab34 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -358,6 +358,12 @@ public static Predicate alwaysFalse() { return m -> false; } + /** + * Function to Register Authentication Provider + * + * client need to register authentication provider before they connect to mailstore, if not default basic authentication will be used + * + */ public static void registerUserPasswordProvider(String storeName, UserPasswordProvider userPasswordProvider) { userPasswordProviderRegister.put(storeName, userPasswordProvider); } From 5ad0b157755cc2aa5d0847e6f5de8dbb94562bd1 Mon Sep 17 00:00:00 2001 From: vinv Date: Fri, 25 Oct 2024 11:55:27 +0700 Subject: [PATCH 17/27] refractor remove redundant code --- .../AzureOauth2UserPasswordProvider.java | 20 ++++++-- .../axonivy/connector/oauth/OAuthUtils.java | 25 ---------- .../com/axonivy/connector/oauth/TokenDTO.java | 50 ------------------- 3 files changed, 17 insertions(+), 78 deletions(-) delete mode 100644 mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java delete mode 100644 mailstore-connector/src/com/axonivy/connector/oauth/TokenDTO.java diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java index 8f923ac..a93648c 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java @@ -1,7 +1,10 @@ package com.axonivy.connector.oauth; +import java.util.Map; + import javax.ws.rs.client.Entity; import javax.ws.rs.core.Form; +import javax.ws.rs.core.GenericType; import javax.ws.rs.core.Response; import org.apache.commons.lang3.StringUtils; @@ -58,13 +61,24 @@ private String getToken(String storeName) { throw MailStoreService.buildError("getToken").withMessage("response cannot be null").build(); } - TokenDTO tokenDto = OAuthUtils.extractToken(response); + String accessToken = extractToken(response); - if (null == tokenDto || StringUtils.isEmpty(tokenDto.getAccessToken())) { + if (StringUtils.isEmpty(accessToken)) { throw new IllegalStateException("Failed to read 'access_token' from " + response); } - return tokenDto.getAccessToken(); + return accessToken; } + // get response entity from response + private String extractToken(Response response) { + GenericType> map = new GenericType<>(Map.class); + Map values = response.readEntity(map); + + if(null == values) { + return null; + } + + return values.get("access_token").toString(); + } } \ No newline at end of file diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java b/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java deleted file mode 100644 index 9878a2c..0000000 --- a/mailstore-connector/src/com/axonivy/connector/oauth/OAuthUtils.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.axonivy.connector.oauth; - -import java.util.Map; - -import javax.ws.rs.core.GenericType; -import javax.ws.rs.core.Response; - -public class OAuthUtils { - - // get response entity from response - public static TokenDTO extractToken(Response response) { - GenericType> map = new GenericType<>(Map.class); - Map values = response.readEntity(map); - - if(null == values) { - return null; - } - - TokenDTO tokenDto = new TokenDTO(); - tokenDto.setAccessToken(values.get("access_token").toString()); - - return tokenDto; - } - -} diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/TokenDTO.java b/mailstore-connector/src/com/axonivy/connector/oauth/TokenDTO.java deleted file mode 100644 index 02c6d7f..0000000 --- a/mailstore-connector/src/com/axonivy/connector/oauth/TokenDTO.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.axonivy.connector.oauth; - -import com.fasterxml.jackson.annotation.JsonProperty; - -public class TokenDTO { - @JsonProperty("token_type") - private String tokenType; - - @JsonProperty("access_token") - private String accessToken; - - @JsonProperty("expires_in") - private int expiresIn; - - @JsonProperty("ext_expires_in") - private int extExpiresIn; - - public String getTokenType() { - return tokenType; - } - - public void setTokenType(String tokenType) { - this.tokenType = tokenType; - } - - public String getAccessToken() { - return accessToken; - } - - public void setAccessToken(String accessToken) { - this.accessToken = accessToken; - } - - public int getExpiresIn() { - return expiresIn; - } - - public void setExpiresIn(int expiresIn) { - this.expiresIn = expiresIn; - } - - public int getExtExpiresIn() { - return extExpiresIn; - } - - public void setExtExpiresIn(int extExpiresIn) { - this.extExpiresIn = extExpiresIn; - } - -} From 2ab96d36732dedd6209544d2e07a7ba054a066fd Mon Sep 17 00:00:00 2001 From: vinv Date: Fri, 25 Oct 2024 12:20:53 +0700 Subject: [PATCH 18/27] corect variable --- mailstore-connector-demo/config/variables.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mailstore-connector-demo/config/variables.yaml b/mailstore-connector-demo/config/variables.yaml index d0fcaff..8ea5d24 100644 --- a/mailstore-connector-demo/config/variables.yaml +++ b/mailstore-connector-demo/config/variables.yaml @@ -34,7 +34,7 @@ Variables: # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider - userPasswordProvider: 'com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider' + userPasswordProvider: 'com.axonivy.connector.oauth.BasicUserPasswordProvider' # only set below credential when you go with oauth2 # tenant to use for OAUTH2 request. From 07c7421695afdae13670ec3d310b385f9e9acc89 Mon Sep 17 00:00:00 2001 From: vinv Date: Fri, 25 Oct 2024 15:42:59 +0700 Subject: [PATCH 19/27] Add documentation to introduce the OAuth 2.0 feature. --- mailstore-connector-product/README_DE.md | 95 +++++++++++++++++++++++ mailstore-connector/config/variables.yaml | 6 +- 2 files changed, 98 insertions(+), 3 deletions(-) diff --git a/mailstore-connector-product/README_DE.md b/mailstore-connector-product/README_DE.md index 86fce31..2982ac6 100644 --- a/mailstore-connector-product/README_DE.md +++ b/mailstore-connector-product/README_DE.md @@ -90,3 +90,98 @@ Variables: # mail.imaps.ssl.checkserveridentity: false # mail.imaps.ssl.trust: '*' ``` + +OAuth 2.0 Support: Azure client_credential grant flow + +## Overview + +This document outlines the steps to configure OAuth 2.0 support using the Azure client credentials grant flow. + +### Configuration Steps +1. Ensure that the necessary properties are enabled for JavaMail to support OAuth 2.0. For more details, refer to the [JavaMail API documentation](https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html#:~:text=or%20confidentiality%20layer.-,OAuth%202.0%20Support,-Support%20for%20OAuth). + +```yaml + properties: + # only set below credential when you go with oauth2 + mail.imaps.auth.mechanisms: 'XOAUTH2' + mail.imaps.sasl.enable: 'true' + mail.imaps.sasl.mechanisms: 'XOAUTH2' +``` + +2. Add Credentials for Azure Authentication +Include your Azure credentials in the authentication configuration. +```yaml + # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow + # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication + # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider + userPasswordProvider: 'com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider' + + # only set below credential when you go with oauth2 + # tenant to use for OAUTH2 request. + # set the Azure Directory (tenant) ID, for application requests. + tenantId: '' + # Your Azure Application (client) ID, used for OAuth2 authentication + appId: '' + # Secret key from your applications "certificates & secrets" (client secret) + secretKey: '' + # for client_credentials: https://outlook.office365.com/.default + scope: '' + #[client_credentials] + grantType: ' +``` + +3. Provide a Complete YAML Configuration File +Ensure that a fully configured YAML file is available for the application. +```yaml +Variables: + mailstore-connector: + localhost-imap-azure-oauth2-authentication: + # [enum: pop3, pop3s, imap, imaps] + protocol: 'imap' + # Host for store connection + host: 'localhost' + # Port for store connection (only needed if not default) + port: -1 + # User name for store connection + user: 'debug@localdomain.test' + # Password for store connection + # [password] + password: '' + # show debug output for connection + debug: true + # Additional properties for store connection, + # see https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html + properties: + mail.imaps.ssl.checkserveridentity: false + mail.imaps.ssl.trust: '*' + # only set below credential when you go with oauth2 + mail.imaps.auth.mechanisms: 'XOAUTH2' + mail.imaps.sasl.enable: 'true' + mail.imaps.sasl.mechanisms: 'XOAUTH2' + + # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow + # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication + # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider + userPasswordProvider: 'com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider' + + # only set below credential when you go with oauth2 + # tenant to use for OAUTH2 request. + # set the Azure Directory (tenant) ID, for application requests. + tenantId: '' + # Your Azure Application (client) ID, used for OAuth2 authentication + appId: '' + # Secret key from your applications "certificates & secrets" (client secret) + secretKey: '' + # for client_credentials: https://outlook.office365.com/.default + scope: '' + #[client_credentials] + grantType: ' +``` + +4. Set Up the Authentication Provider +Before calling the mailstore connector, you need to provide an authentication provider. +```java + Class clazz = Class.forName("com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider"); + UserPasswordProvider userPasswordProvider = (UserPasswordProvider) clazz.getDeclaredConstructor().newInstance(); + MailStoreService.registerUserPasswordProvider(storeName, userPasswordProvider); +``` \ No newline at end of file diff --git a/mailstore-connector/config/variables.yaml b/mailstore-connector/config/variables.yaml index 85154fc..b5e37ad 100644 --- a/mailstore-connector/config/variables.yaml +++ b/mailstore-connector/config/variables.yaml @@ -32,9 +32,9 @@ Variables: # mail.imaps.sasl.mechanisms: 'XOAUTH2' # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow - # com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider for Basic Authentication - # com.axonivy.connector.mailstore.demo.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider - userPasswordProvider: 'com.axonivy.connector.mailstore.demo.oauth.BasicUserPasswordProvider' + # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication + # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider + userPasswordProvider: 'com.axonivy.connector.oauth.BasicUserPasswordProvider' # [basic, oauth2] basic: username and password, oauth2: currently only support OAuth2 client credentials grant flow # only set below credential when you go with oauth2 From 31cdeab8d1b1368b9f0bfd92f189d782d08fea0f Mon Sep 17 00:00:00 2001 From: vinv Date: Mon, 28 Oct 2024 17:18:30 +0700 Subject: [PATCH 20/27] refractor: getting credentials from Provider interface instead from inside mailstore-connector --- mailstore-connector-demo/config/variables.yaml | 9 +-------- .../connector/mailstore/demo/DemoService.java | 2 +- .../connector/mailstore/MailStoreService.java | 8 +++++--- .../oauth/AzureOauth2UserPasswordProvider.java | 15 ++++++++++++--- .../oauth/BasicUserPasswordProvider.java | 12 ++++++++++-- .../connector/oauth/UserPasswordProvider.java | 3 ++- 6 files changed, 31 insertions(+), 18 deletions(-) diff --git a/mailstore-connector-demo/config/variables.yaml b/mailstore-connector-demo/config/variables.yaml index 8ea5d24..1d79309 100644 --- a/mailstore-connector-demo/config/variables.yaml +++ b/mailstore-connector-demo/config/variables.yaml @@ -31,10 +31,9 @@ Variables: # mail.imaps.sasl.enable: 'true' # mail.imaps.sasl.mechanisms: 'XOAUTH2' - # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider - userPasswordProvider: 'com.axonivy.connector.oauth.BasicUserPasswordProvider' + userPasswordProvider: '' # only set below credential when you go with oauth2 # tenant to use for OAUTH2 request. @@ -70,9 +69,6 @@ Variables: mail.imaps.ssl.checkserveridentity: false mail.imaps.ssl.trust: '*' - # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow - # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication - # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider userPasswordProvider: 'com.axonivy.connector.oauth.BasicUserPasswordProvider' @@ -100,9 +96,6 @@ Variables: mail.imaps.sasl.enable: 'true' mail.imaps.sasl.mechanisms: 'XOAUTH2' - # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow - # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication - # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider userPasswordProvider: 'com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider' # only set below credential when you go with oauth2 diff --git a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java index ca7436e..102e047 100644 --- a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java +++ b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java @@ -124,7 +124,7 @@ private static void initAuthProvider(String storeName, String authProviderPath) UserPasswordProvider userPasswordProvider = (UserPasswordProvider) clazz.getDeclaredConstructor().newInstance(); MailStoreService.registerUserPasswordProvider(storeName, userPasswordProvider); } catch(Exception ex) { - Ivy.log().error("exception during instantiate UserPasswordProvider"+ex); + LOG.error("Exception during instatiation of UserPasswordProvider ''{0}''.",ex, authProviderPath); } } diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index 33cab34..4e88d58 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -51,7 +51,6 @@ public class MailStoreService { private static final String PROTOCOL_VAR = "protocol"; private static final String HOST_VAR = "host"; private static final String PORT_VAR = "port"; - private static final String USER_VAR = "user"; private static final String DEBUG_VAR = "debug"; private static final String PROPERTIES_VAR = "properties"; private static final String ERROR_BASE = "mailstore:connector"; @@ -621,14 +620,17 @@ public static Store openStore(String storeName) throws MessagingException { String protocol = getVar(storeName, PROTOCOL_VAR); String host = getVar(storeName, HOST_VAR); String portString = getVar(storeName, PORT_VAR); - String user = getVar(storeName, USER_VAR); + + //String user = getVar(storeName, USER_VAR); UserPasswordProvider userPasswordProvider = userPasswordProviderRegister.get(storeName); // adapt exist project already use this connector, default is basic auth if(null == userPasswordProvider) { userPasswordProvider = new BasicUserPasswordProvider(); } - String password = userPasswordProvider.authenticate(storeName); + + String user = userPasswordProvider.getUser(storeName); + String password = userPasswordProvider.getPassword(storeName); String debugString = getVar(storeName, DEBUG_VAR); diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java index a93648c..fd0d4a5 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java @@ -21,12 +21,20 @@ public class AzureOauth2UserPasswordProvider implements UserPasswordProvider { private static final String SECRET_KEY = "secretKey"; private static final String GRANT_TYPE = "grantType"; private static final String SCOPE = "scope"; + private static final String USER_VAR = "user"; private static final String REST_CLIENT = "getTokenAzureOAuth"; - + @Override - public String authenticate(String storeName) { - LOG.debug("Connect to store {0} using OAuth2 Authentication.", storeName); + public String getUser(String storeName) { + LOG.debug("Retrieving user for store: ''{0}''.", storeName); + + return MailStoreService.getVar(storeName, USER_VAR); + } + + @Override + public String getPassword(String storeName) { + LOG.debug("Retrieving password for store: ''{0}''.", storeName); return getToken(storeName); } @@ -81,4 +89,5 @@ private String extractToken(Response response) { return values.get("access_token").toString(); } + } \ No newline at end of file diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java index 4dbbb3d..d9a5948 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java @@ -8,10 +8,18 @@ public class BasicUserPasswordProvider implements UserPasswordProvider { private static final Logger LOG = Ivy.log(); private static final String PASSWORD_VAR = "password"; + private static final String USER_VAR = "user"; @Override - public String authenticate(String storeName) { - LOG.debug("Connect to store {0} using Basic Authentication.", storeName); + public String getUser(String storeName) { + LOG.debug("Retrieving user for store: ''{0}''.", storeName); + + return MailStoreService.getVar(storeName, USER_VAR); + } + + @Override + public String getPassword(String storeName) { + LOG.debug("Retrieving password for store: ''{0}''.", storeName); return MailStoreService.getVar(storeName, PASSWORD_VAR); } diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java index e7f46e7..8c76866 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java @@ -1,5 +1,6 @@ package com.axonivy.connector.oauth; public interface UserPasswordProvider { - String authenticate(String storeName); + String getUser(String storeName); + String getPassword(String storeName); } From 15f8b4080d9bc8fbb8a8dd683098fd83d244f1e9 Mon Sep 17 00:00:00 2001 From: vinv Date: Mon, 28 Oct 2024 17:28:56 +0700 Subject: [PATCH 21/27] remove redundant code --- mailstore-connector/config/variables.yaml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mailstore-connector/config/variables.yaml b/mailstore-connector/config/variables.yaml index b5e37ad..67976d1 100644 --- a/mailstore-connector/config/variables.yaml +++ b/mailstore-connector/config/variables.yaml @@ -35,8 +35,7 @@ Variables: # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider userPasswordProvider: 'com.axonivy.connector.oauth.BasicUserPasswordProvider' - - # [basic, oauth2] basic: username and password, oauth2: currently only support OAuth2 client credentials grant flow + # only set below credential when you go with oauth2 # tenant to use for OAUTH2 request. # set the Azure Directory (tenant) ID, for application requests. From 567b6606454a658c536e0c9d84e4b56f604912b6 Mon Sep 17 00:00:00 2001 From: vinv Date: Tue, 29 Oct 2024 17:16:25 +0700 Subject: [PATCH 22/27] adding featre to support oauth2 password granttype --- .../config/variables.yaml | 4 +-- mailstore-connector/config/variables.yaml | 4 +-- .../connector/mailstore/MailStoreService.java | 4 +-- .../AzureOauth2UserPasswordProvider.java | 31 +++++++++++++++++-- .../oauth/BasicUserPasswordProvider.java | 2 -- .../connector/oauth/UserPasswordProvider.java | 3 ++ 6 files changed, 37 insertions(+), 11 deletions(-) diff --git a/mailstore-connector-demo/config/variables.yaml b/mailstore-connector-demo/config/variables.yaml index 1d79309..9764c1a 100644 --- a/mailstore-connector-demo/config/variables.yaml +++ b/mailstore-connector-demo/config/variables.yaml @@ -106,7 +106,7 @@ Variables: appId: '' # Secret key from your applications "certificates & secrets" (client secret) secretKey: '' - # for client_credentials: https://outlook.office365.com/.default + # for client_credentials/password: https://outlook.office365.com/.default scope: '' - #[client_credentials] + #[client_credentials, password] grantType: '' diff --git a/mailstore-connector/config/variables.yaml b/mailstore-connector/config/variables.yaml index 67976d1..48bf366 100644 --- a/mailstore-connector/config/variables.yaml +++ b/mailstore-connector/config/variables.yaml @@ -34,7 +34,7 @@ Variables: # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider - userPasswordProvider: 'com.axonivy.connector.oauth.BasicUserPasswordProvider' + userPasswordProvider: '' # only set below credential when you go with oauth2 # tenant to use for OAUTH2 request. @@ -46,5 +46,5 @@ Variables: secretKey: '' # for client_credentials: https://outlook.office365.com/.default scope: '' - #[client_credentials] + #[client_credentials, password] grantType: '' \ No newline at end of file diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index 4e88d58..4ee0408 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -621,11 +621,9 @@ public static Store openStore(String storeName) throws MessagingException { String host = getVar(storeName, HOST_VAR); String portString = getVar(storeName, PORT_VAR); - //String user = getVar(storeName, USER_VAR); - UserPasswordProvider userPasswordProvider = userPasswordProviderRegister.get(storeName); // adapt exist project already use this connector, default is basic auth - if(null == userPasswordProvider) { + if (null == userPasswordProvider) { userPasswordProvider = new BasicUserPasswordProvider(); } diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java index fd0d4a5..3f84fb9 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java @@ -21,7 +21,6 @@ public class AzureOauth2UserPasswordProvider implements UserPasswordProvider { private static final String SECRET_KEY = "secretKey"; private static final String GRANT_TYPE = "grantType"; private static final String SCOPE = "scope"; - private static final String USER_VAR = "user"; private static final String REST_CLIENT = "getTokenAzureOAuth"; @@ -44,7 +43,17 @@ private Form buildForm(String storeName) { form.param("client_id", MailStoreService.getVar(storeName, APP_ID)); form.param("client_secret", MailStoreService.getVar(storeName, SECRET_KEY)); form.param("scope", MailStoreService.getVar(storeName, SCOPE)); - form.param("grant_type", MailStoreService.getVar(storeName, GRANT_TYPE)); + + String grantTypeValue = MailStoreService.getVar(storeName, GRANT_TYPE); + + LOG.debug("Grant type value retrieved for store {0}: {1}", storeName, grantTypeValue); + + form.param("grant_type", grantTypeValue); + + if (GrantType.isUserPassAuth(grantTypeValue)) { + form.param("username", getUser(storeName)); + form.param("password", MailStoreService.getVar(storeName, PASSWORD_VAR)); + } return form; } @@ -89,5 +98,23 @@ private String extractToken(Response response) { return values.get("access_token").toString(); } + + private static enum GrantType { + APPLICATION("client_credentials"), + + /** weak security: app acts as pre-configured personal user! **/ + PASSWORD("password"); + + private String type; + + GrantType(String type) { + this.type = type; + } + + private static boolean isUserPassAuth(String str) { + return str != null && (str.equals(PASSWORD.type)); + } + + } } \ No newline at end of file diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java index d9a5948..ce8dbf0 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java @@ -7,8 +7,6 @@ public class BasicUserPasswordProvider implements UserPasswordProvider { private static final Logger LOG = Ivy.log(); - private static final String PASSWORD_VAR = "password"; - private static final String USER_VAR = "user"; @Override public String getUser(String storeName) { diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java index 8c76866..e17a125 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java @@ -1,6 +1,9 @@ package com.axonivy.connector.oauth; public interface UserPasswordProvider { + String USER_VAR = "user"; + String PASSWORD_VAR = "password"; + String getUser(String storeName); String getPassword(String storeName); } From 88f0c9e9440e98adaced88227039552d91766c25 Mon Sep 17 00:00:00 2001 From: vinv Date: Tue, 29 Oct 2024 18:10:39 +0700 Subject: [PATCH 23/27] update document for market place --- mailstore-connector-product/README.md | 95 ++++++++++++++++++++++++ mailstore-connector-product/README_DE.md | 4 +- 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/mailstore-connector-product/README.md b/mailstore-connector-product/README.md index 117bc5b..a5f5096 100644 --- a/mailstore-connector-product/README.md +++ b/mailstore-connector-product/README.md @@ -88,3 +88,98 @@ Variables: # mail.imaps.ssl.checkserveridentity: false # mail.imaps.ssl.trust: '*' ``` + +OAuth 2.0 Support: Azure client_credential/password grant flow + +## Overview + +This document outlines the steps to configure OAuth 2.0 support using the Azure client credentials grant flow. + +### Configuration Steps +1. Ensure that the necessary properties are enabled for JavaMail to support OAuth 2.0. For more details, refer to the [JavaMail API documentation](https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html#:~:text=or%20confidentiality%20layer.-,OAuth%202.0%20Support,-Support%20for%20OAuth). + +```yaml + properties: + # only set below credential when you go with oauth2 + mail.imaps.auth.mechanisms: 'XOAUTH2' + mail.imaps.sasl.enable: 'true' + mail.imaps.sasl.mechanisms: 'XOAUTH2' +``` + +2. Add Credentials for Azure Authentication +Include your Azure credentials in the authentication configuration. +```yaml + # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow + # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication + # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider + userPasswordProvider: 'com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider' + + # only set below credential when you go with oauth2 + # tenant to use for OAUTH2 request. + # set the Azure Directory (tenant) ID, for application requests. + tenantId: '' + # Your Azure Application (client) ID, used for OAuth2 authentication + appId: '' + # Secret key from your applications "certificates & secrets" (client secret) + secretKey: '' + # for client_credentials: https://outlook.office365.com/.default + scope: '' + #[client_credentials] + grantType: ' +``` + +3. Provide a Complete YAML Configuration File +Ensure that a fully configured YAML file is available for the application. +```yaml +Variables: + mailstore-connector: + localhost-imap-azure-oauth2-authentication: + # [enum: pop3, pop3s, imap, imaps] + protocol: 'imap' + # Host for store connection + host: 'localhost' + # Port for store connection (only needed if not default) + port: -1 + # User name for store connection + user: 'debug@localdomain.test' + # Password for store connection + # [password] + password: '' + # show debug output for connection + debug: true + # Additional properties for store connection, + # see https://javaee.github.io/javamail/docs/api/com/sun/mail/imap/package-summary.html + properties: + mail.imaps.ssl.checkserveridentity: false + mail.imaps.ssl.trust: '*' + # only set below credential when you go with oauth2 + mail.imaps.auth.mechanisms: 'XOAUTH2' + mail.imaps.sasl.enable: 'true' + mail.imaps.sasl.mechanisms: 'XOAUTH2' + + # Basic: username and password, AzureOauth2UserPasswordProvider: currently only support OAuth2 client credentials grant flow + # com.axonivy.connector.oauth.BasicUserPasswordProvider for Basic Authentication + # com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider for AzureOauth2UserPasswordProvider + userPasswordProvider: 'com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider' + + # only set below credential when you go with oauth2 + # tenant to use for OAUTH2 request. + # set the Azure Directory (tenant) ID, for application requests. + tenantId: '' + # Your Azure Application (client) ID, used for OAuth2 authentication + appId: '' + # Secret key from your applications "certificates & secrets" (client secret) + secretKey: '' + # for client_credentials: https://outlook.office365.com/.default + scope: '' + #[client_credentials/password] + grantType: ' +``` + +4. Set Up the Authentication Provider +Before calling the mailstore connector, you need to provide an authentication provider. +```java + Class clazz = Class.forName("com.axonivy.connector.oauth.AzureOauth2UserPasswordProvider"); + UserPasswordProvider userPasswordProvider = (UserPasswordProvider) clazz.getDeclaredConstructor().newInstance(); + MailStoreService.registerUserPasswordProvider(storeName, userPasswordProvider); +``` \ No newline at end of file diff --git a/mailstore-connector-product/README_DE.md b/mailstore-connector-product/README_DE.md index 2982ac6..2c13c41 100644 --- a/mailstore-connector-product/README_DE.md +++ b/mailstore-connector-product/README_DE.md @@ -91,7 +91,7 @@ Variables: # mail.imaps.ssl.trust: '*' ``` -OAuth 2.0 Support: Azure client_credential grant flow +OAuth 2.0 Support: Azure client_credential/password grant flow ## Overview @@ -174,7 +174,7 @@ Variables: secretKey: '' # for client_credentials: https://outlook.office365.com/.default scope: '' - #[client_credentials] + #[client_credentials/password] grantType: ' ``` From e560d556783a51bc3a0f804ecb8c22fc5ffc3bd5 Mon Sep 17 00:00:00 2001 From: vinv Date: Tue, 29 Oct 2024 18:52:57 +0700 Subject: [PATCH 24/27] improve: make login url flexible depend on privider --- mailstore-connector-demo/config/variables.yaml | 4 ++++ mailstore-connector-product/README_DE.md | 5 ++++- mailstore-connector/config/rest-clients.yaml | 2 +- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/mailstore-connector-demo/config/variables.yaml b/mailstore-connector-demo/config/variables.yaml index 9764c1a..4d83b21 100644 --- a/mailstore-connector-demo/config/variables.yaml +++ b/mailstore-connector-demo/config/variables.yaml @@ -110,3 +110,7 @@ Variables: scope: '' #[client_credentials, password] grantType: '' + # login microsoft azure + azureOAuth: + loginUrl: 'login.microsoftonline.com' + diff --git a/mailstore-connector-product/README_DE.md b/mailstore-connector-product/README_DE.md index 2c13c41..2ea872a 100644 --- a/mailstore-connector-product/README_DE.md +++ b/mailstore-connector-product/README_DE.md @@ -175,7 +175,10 @@ Variables: # for client_credentials: https://outlook.office365.com/.default scope: '' #[client_credentials/password] - grantType: ' + grantType: '' + # login url microsoft zure + azureOAuth: + loginUrl: 'login.microsoftonline.com' ``` 4. Set Up the Authentication Provider diff --git a/mailstore-connector/config/rest-clients.yaml b/mailstore-connector/config/rest-clients.yaml index 2f6788e..49b0a1e 100644 --- a/mailstore-connector/config/rest-clients.yaml +++ b/mailstore-connector/config/rest-clients.yaml @@ -1,6 +1,6 @@ RestClients: getTokenAzureOAuth: UUID: a37c499d-82de-4d21-a021-06844835fb2d - Url: https://login.microsoftonline.com + Url: https://${ivy.var.azureOAuth.loginUrl} Features: - ch.ivyteam.ivy.rest.client.mapper.JsonFeature From 86126b4e3fcca66684616a81dc238a82d4bc0680 Mon Sep 17 00:00:00 2001 From: vinv Date: Thu, 31 Oct 2024 17:12:34 +0700 Subject: [PATCH 25/27] update: adding login url --- mailstore-connector/config/variables.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mailstore-connector/config/variables.yaml b/mailstore-connector/config/variables.yaml index 48bf366..040d9d3 100644 --- a/mailstore-connector/config/variables.yaml +++ b/mailstore-connector/config/variables.yaml @@ -47,4 +47,8 @@ Variables: # for client_credentials: https://outlook.office365.com/.default scope: '' #[client_credentials, password] - grantType: '' \ No newline at end of file + grantType: '' + + # login microsoft azure + azureOAuth: + loginUrl: '' \ No newline at end of file From cc73dc8eb9579f937cd7a8f4d71365559153a6fb Mon Sep 17 00:00:00 2001 From: vinv Date: Fri, 1 Nov 2024 13:44:07 +0700 Subject: [PATCH 26/27] reformat code and adding java docs for class and method --- .../connector/mailstore/MailStoreService.java | 18 +---- .../AzureOauth2UserPasswordProvider.java | 73 ++++++++++++++----- .../oauth/BasicUserPasswordProvider.java | 21 ++++++ .../connector/oauth/UserPasswordProvider.java | 31 +++++++- 4 files changed, 107 insertions(+), 36 deletions(-) diff --git a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java index 4ee0408..8b262ea 100644 --- a/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java +++ b/mailstore-connector/src/com/axonivy/connector/mailstore/MailStoreService.java @@ -712,7 +712,6 @@ private static Properties getProperties() { String name = variable.name(); if (name.startsWith(propertiesPrefix)) { String propertyName = name.substring(propertiesPrefix.length()); - String value = variable.value(); LOG.info("Setting additional property {0}: ''{1}''", propertyName, value); properties.setProperty(name, value); @@ -725,13 +724,12 @@ private static Properties getProperties() { // Only retrieve properties from the store that belong to private static Properties getProperties(String storeName) { Properties properties = new Properties(); - String propertiesPrefix = MAIL_STORE_VAR + "." + storeName + "." + PROPERTIES_VAR + "."; + String propertiesPrefix = String.format("%s.%s.%s.", MAIL_STORE_VAR, storeName, PROPERTIES_VAR); for (Variable variable : Ivy.var().all()) { String name = variable.name(); if (name.contains(propertiesPrefix)) { String propertyName = getSubstringAfterProperties(name); - String value = variable.value(); LOG.info("Setting additional property {0}: ''{1}''", propertyName, value); properties.setProperty(propertyName, value); @@ -742,17 +740,9 @@ private static Properties getProperties(String storeName) { } private static String getSubstringAfterProperties(String input) { - String keyword = "properties"; - int index = input.indexOf(keyword); - - if (index != -1) { - // Get the substring starting right after "properties." - // Add length of "properties." (which is 12) to index to get the position after - // it - return input.substring(index + keyword.length() + 1); - } - - return null; + int index = input.indexOf(PROPERTIES_VAR); + + return index != -1 ? input.substring(index + PROPERTIES_VAR.length() + 1) : null; } public static BpmPublicErrorBuilder buildError(String code) { diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java index 3f84fb9..d131172 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java @@ -1,6 +1,7 @@ package com.axonivy.connector.oauth; import java.util.Map; +import java.util.Optional; import javax.ws.rs.client.Entity; import javax.ws.rs.core.Form; @@ -14,6 +15,15 @@ import ch.ivyteam.ivy.environment.Ivy; import ch.ivyteam.log.Logger; +/** + * The {@code AzureOauth2UserPasswordProvider} class implements the + * {@link UserPasswordProvider} interface, providing methods to retrieve + * user credentials specifically for Azure OAuth2 authentication. + *

+ * This class retrieves user credentials from a mail store and handles + * interactions with Azure's OAuth2 token endpoint. + *

+ */ public class AzureOauth2UserPasswordProvider implements UserPasswordProvider { private static final Logger LOG = Ivy.log(); private static final String TENANT_ID = "tenantId"; @@ -23,7 +33,30 @@ public class AzureOauth2UserPasswordProvider implements UserPasswordProvider { private static final String SCOPE = "scope"; private static final String REST_CLIENT = "getTokenAzureOAuth"; - + private static final String TOKEN_PATH = "oauth2/v2.0/token"; + private static final String CONTENT_TYPE_HEADER = "Content-Type"; + private static final String CONTENT_TYPE_VALUE = "application/x-www-form-urlencoded"; + + /** + * The {@code FormProperty} interface defines constants for form property + * names used in the OAuth2 token request. + */ + public static interface FormProperty { + String CLIENT_ID = "client_id"; + String CLIENT_SECRET = "client_secret"; + String SCOPE = "scope"; + String GRANT_TYPE = "grant_type"; + String USERNAME = "username"; + String PASSWORD = "password"; + + } + + /** + * Retrieves the username associated with the specified store name. + * + * @param storeName the name of the store + * @return the username for the specified store + */ @Override public String getUser(String storeName) { LOG.debug("Retrieving user for store: ''{0}''.", storeName); @@ -31,6 +64,12 @@ public String getUser(String storeName) { return MailStoreService.getVar(storeName, USER_VAR); } + /** + * Retrieves the password associated with the specified store name. + * + * @param storeName the name of the store + * @return the password for the specified store + */ @Override public String getPassword(String storeName) { LOG.debug("Retrieving password for store: ''{0}''.", storeName); @@ -40,31 +79,31 @@ public String getPassword(String storeName) { private Form buildForm(String storeName) { Form form = new Form(); - form.param("client_id", MailStoreService.getVar(storeName, APP_ID)); - form.param("client_secret", MailStoreService.getVar(storeName, SECRET_KEY)); - form.param("scope", MailStoreService.getVar(storeName, SCOPE)); - + form.param(FormProperty.CLIENT_ID, MailStoreService.getVar(storeName, APP_ID)); + form.param(FormProperty.CLIENT_SECRET, MailStoreService.getVar(storeName, SECRET_KEY)); + form.param(FormProperty.SCOPE, MailStoreService.getVar(storeName, SCOPE)); + String grantTypeValue = MailStoreService.getVar(storeName, GRANT_TYPE); LOG.debug("Grant type value retrieved for store {0}: {1}", storeName, grantTypeValue); - form.param("grant_type", grantTypeValue); + form.param(FormProperty.GRANT_TYPE, grantTypeValue); if (GrantType.isUserPassAuth(grantTypeValue)) { - form.param("username", getUser(storeName)); - form.param("password", MailStoreService.getVar(storeName, PASSWORD_VAR)); + form.param(FormProperty.USERNAME, getUser(storeName)); + form.param(FormProperty.PASSWORD, MailStoreService.getVar(storeName, PASSWORD_VAR)); } return form; } private Response sendTokenRequest(String tenantId, Form form) { - return Ivy.rest().client(REST_CLIENT) - .path(tenantId) - .path("oauth2/v2.0/token") - .request() - .header("Content-Type", "application/x-www-form-urlencoded") - .post(Entity.form(form)); + return Ivy.rest() + .client(REST_CLIENT) + .path(tenantId) + .path(TOKEN_PATH).request() + .header(CONTENT_TYPE_HEADER, CONTENT_TYPE_VALUE) + .post(Entity.form(form)); } private String getToken(String storeName) { @@ -92,11 +131,7 @@ private String extractToken(Response response) { GenericType> map = new GenericType<>(Map.class); Map values = response.readEntity(map); - if(null == values) { - return null; - } - - return values.get("access_token").toString(); + return Optional.ofNullable(values).map(value -> values.get("access_token").toString()).orElse(null); } private static enum GrantType { diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java index ce8dbf0..1a3a326 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/BasicUserPasswordProvider.java @@ -5,9 +5,24 @@ import ch.ivyteam.ivy.environment.Ivy; import ch.ivyteam.log.Logger; +/** + * The {@code BasicUserPasswordProvider} class implements the + * {@link UserPasswordProvider} interface, providing a simple + * implementation for retrieving user credentials from a mail store. + *

+ * This class provides methods to obtain both the username and password + * for a specified store name, utilizing the {@link MailStoreService}. + *

+ */ public class BasicUserPasswordProvider implements UserPasswordProvider { private static final Logger LOG = Ivy.log(); + /** + * Retrieves the username associated with the specified store name. + * + * @param storeName the name of the store + * @return the username for the specified store + */ @Override public String getUser(String storeName) { LOG.debug("Retrieving user for store: ''{0}''.", storeName); @@ -15,6 +30,12 @@ public String getUser(String storeName) { return MailStoreService.getVar(storeName, USER_VAR); } + /** + * Retrieves the password associated with the specified store name. + * + * @param storeName the name of the store + * @return the password for the specified store + */ @Override public String getPassword(String storeName) { LOG.debug("Retrieving password for store: ''{0}''.", storeName); diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java index e17a125..3ef088a 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/UserPasswordProvider.java @@ -1,9 +1,34 @@ package com.axonivy.connector.oauth; +/** + * The {@code UserPasswordProvider} interface provides methods to retrieve + * user credentials such as username and password for a given store. + *

+ * Implementing classes should define the behavior of how user credentials + * are retrieved based on the store name. + *

+ */ public interface UserPasswordProvider { + + /** The variable name for the user. */ String USER_VAR = "user"; - String PASSWORD_VAR = "password"; - String getUser(String storeName); - String getPassword(String storeName); + /** The variable name for the password. */ + String PASSWORD_VAR = "password"; + + /** + * Retrieves the username associated with the specified store name. + * + * @param storeName the name of the store + * @return the username for the specified store + */ + String getUser(String storeName); + + /** + * Retrieves the password associated with the specified store name. + * + * @param storeName the name of the store + * @return the password for the specified store + */ + String getPassword(String storeName); } From d137676c2fbe8bb8d70cda245839962db9fb07d2 Mon Sep 17 00:00:00 2001 From: vinv Date: Fri, 1 Nov 2024 14:36:36 +0700 Subject: [PATCH 27/27] adding constact for variable --- .../connector/mailstore/demo/DemoService.java | 29 ++++++++++--------- .../AzureOauth2UserPasswordProvider.java | 7 +++-- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java index 102e047..309c4a5 100644 --- a/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java +++ b/mailstore-connector-demo/src/com/axonivy/connector/mailstore/demo/DemoService.java @@ -22,9 +22,14 @@ public class DemoService { private static final Logger LOG = Ivy.log(); + private static final String INBOX = "INBOX"; + private static final String USER_PASSWORD_PROVIDER = "userPasswordProvider"; + private static final String LOCALHOST_IMAP = "localhost-imap"; + private static final String LOCALHOST_IMAP_BASIC_AUTHENTICATION = "localhost-imap-basic-authentication"; + private static final String LOCALHOST_IMAP_AZURE_OAUTH2_AUTHENTICATION = "localhost-imap-azure-oauth2-authentication"; public static void handleMessages() throws MessagingException, IOException { - MessageIterator iterator = MailStoreService.messageIterator("localhost-imap", "INBOX", null, false, MailStoreService.subjectMatches(".*test [0-9]+.*"), new MessageComparator()); + MessageIterator iterator = MailStoreService.messageIterator(LOCALHOST_IMAP, INBOX, null, false, MailStoreService.subjectMatches(".*test [0-9]+.*"), new MessageComparator()); while (iterator.hasNext()) { Message message = iterator.next(); @@ -52,7 +57,7 @@ public static boolean handleMessage(Message message) throws MessagingException, } public static void handleMessagesMultiDestinationFolder() throws MessagingException, IOException { - MessageIterator iterator = MailStoreService.messageIterator("localhost-imap", "INBOX", true, MailStoreService.subjectMatches(".*test [0-9]+.*"), new MessageComparator(), Arrays.asList("Processed", "ErrorFolder")); + MessageIterator iterator = MailStoreService.messageIterator(LOCALHOST_IMAP, INBOX, true, MailStoreService.subjectMatches(".*test [0-9]+.*"), new MessageComparator(), Arrays.asList("Processed", "ErrorFolder")); int runner = 0; while (iterator.hasNext()) { @@ -65,13 +70,11 @@ public static void handleMessagesMultiDestinationFolder() throws MessagingExcept } public static void connectMailStoreWithBasicAuth() throws MessagingException, IOException { - String storeName = "localhost-imap-basic-authentication"; - // get from variable mailstore-connector.localhost-imap.userPasswordProvider - String authProviderPath = MailStoreService.getVar(storeName, "userPasswordProvider"); - initAuthProvider(storeName, authProviderPath); + String authProviderPath = MailStoreService.getVar(LOCALHOST_IMAP_BASIC_AUTHENTICATION, USER_PASSWORD_PROVIDER); + initAuthProvider(LOCALHOST_IMAP_BASIC_AUTHENTICATION, authProviderPath); - MessageIterator iterator = MailStoreService.messageIterator(storeName, "INBOX", null, false, MailStoreService.subjectMatches(".*"), new MessageComparator()); + MessageIterator iterator = MailStoreService.messageIterator(LOCALHOST_IMAP_BASIC_AUTHENTICATION, INBOX, null, false, MailStoreService.subjectMatches(".*"), new MessageComparator()); while (iterator.hasNext()) { Message message = iterator.next(); @@ -81,13 +84,11 @@ public static void connectMailStoreWithBasicAuth() throws MessagingException, IO } public static void connectMailStoreWithAzureOauth2() throws MessagingException, IOException { - String storeName = "localhost-imap-azure-oauth2-authentication"; - // get from variable mailstore-connector.localhost-imap.userPasswordProvider - String authProviderPath = MailStoreService.getVar(storeName, "userPasswordProvider"); - initAuthProvider(storeName, authProviderPath); + String authProviderPath = MailStoreService.getVar(LOCALHOST_IMAP_AZURE_OAUTH2_AUTHENTICATION, USER_PASSWORD_PROVIDER); + initAuthProvider(LOCALHOST_IMAP_AZURE_OAUTH2_AUTHENTICATION, authProviderPath); - MessageIterator iterator = MailStoreService.messageIterator(storeName, "INBOX", null, false, MailStoreService.subjectMatches(".*"), new MessageComparator()); + MessageIterator iterator = MailStoreService.messageIterator(LOCALHOST_IMAP_AZURE_OAUTH2_AUTHENTICATION, INBOX, null, false, MailStoreService.subjectMatches(".*"), new MessageComparator()); while (iterator.hasNext()) { Message message = iterator.next(); @@ -98,8 +99,8 @@ public static void connectMailStoreWithAzureOauth2() throws MessagingException, public static void handleAttachmentMessages() throws MessagingException, IOException { MessageIterator iterator = MailStoreService.messageIterator( - "localhost-imap", - "INBOX", + LOCALHOST_IMAP, + INBOX, null, false, null); diff --git a/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java index d131172..4b65842 100644 --- a/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java +++ b/mailstore-connector/src/com/axonivy/connector/oauth/AzureOauth2UserPasswordProvider.java @@ -48,7 +48,10 @@ public static interface FormProperty { String GRANT_TYPE = "grant_type"; String USERNAME = "username"; String PASSWORD = "password"; - + } + + public static interface ResponseProperty { + String ACCESS_TOKEN = "access_token"; } /** @@ -131,7 +134,7 @@ private String extractToken(Response response) { GenericType> map = new GenericType<>(Map.class); Map values = response.readEntity(map); - return Optional.ofNullable(values).map(value -> values.get("access_token").toString()).orElse(null); + return Optional.ofNullable(values).map(value -> values.get(ResponseProperty.ACCESS_TOKEN).toString()).orElse(null); } private static enum GrantType {