JavaMail 与 Oauth 和 Office365

Yve*_* V. 5 imap oauth jakarta-mail office365

我正在用 Java 构建一个简单的命令行应用程序,它可以登录我的电子邮件箱 (IMAP) 并下载所有附件。我使用了基本身份验证,但 Microsoft 正在禁用它,因此我尝试将我的应用程序转换为使用 OAuth。

在阅读了不同的 OAuth 流程后,似乎对于我的简单独立命令行应用程序来说,简单地硬编码密码没有问题,资源所有者密码凭据 Grand(如此处所述将是最佳(或良好)选择。我进一步根据此来源的说明,其中描述了如何使用最新版本的 Javamail 启用 OAuth。

将它们放在一起似乎有点困难,而且我不断收到 AUTHENTICATE Failed 错误。

那么,我尝试了什么?我首先按如下方式检索我的授权令牌:

public String getAuthToken() {
        try {
            CloseableHttpClient client = HttpClients.createDefault();
            HttpPost loginPost =  new HttpPost("https://login.microsoftonline.com/organizations/oauth2/v2.0/token");
            String clientId = "some client UUID";
            String scopes = "email openid IMAP.AccessAsUser.All offline_access";
            String client_secret = "My client secret, not base64 encoded";
            String username = "my emailadress";
            String password = "my password, not base64 encoded";

           String encodedBody = "client_id=" + clientId
                    + "&scope=" + scopes
                    + "&client_secret=" + client_secret
                    + "&username=" + username
                    + "&password=" + password
                    + "&grant_type=password";

            loginPost.setEntity(new StringEntity(encodedBody, ContentType.APPLICATION_FORM_URLENCODED));

            loginPost.addHeader(new BasicHeader("cache-control", "no-cache"));
            CloseableHttpResponse loginResponse = client.execute(loginPost);
            byte[] response = loginResponse.getEntity().getContent().readAllBytes();
            ObjectMapper objectMapper = new ObjectMapper();
            JavaType type = objectMapper.constructType(objectMapper.getTypeFactory()
                    .constructParametricType(Map.class, String.class, String.class));
            Map<String, String> parsed = new ObjectMapper().readValue(response, type);
            return parsed.get("access_token");
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
Run Code Online (Sandbox Code Playgroud)

来自 oauth 服务的响应实际上是一个 json 对象,其中包含以下字段:

令牌的缩写屏幕截图

显然令牌更长,但这里不共享。access_token 本身采用三个 base64 编码字符串的形式,以 . 第一个,解码时包含

{
  "typ": "JWT",
  "nonce": "Vobb8bI7E...",
  "alg": "RS256",
  "x5t": "2ZQpJ3Up...",
  "kid": "2ZQpJ3Up..."
}
Run Code Online (Sandbox Code Playgroud)

第二部分是一个更大的对象,包含以下字段(也经过编辑):

{
  "aud": "someuuid",
  "iss": "https://sts.windows.net/someuuid/",
  "iat": 1658397625,
  "nbf": 1658397625,
  "exp": 1658402597,
  "acct": 0,
  "acr": "1",
  "aio": "ASQ....",
  "amr": [
    "pwd"
  ],
  "app_displayname": "myapp",
  "appid": "some uuid",
  "appidacr": "1",
  "family_name": "My Last Name",
  "given_name": "My First Name",
  "idtyp": "user",
  "ipaddr": "some.ip.address.here",
  "name": "My Full name",
  "oid": "someuuid",
  "platf": "14",
  "puid": "10032...",
  "rh": "0.AToA....",
  "scp": "email IMAP.AccessAsUser.All openid profile",
  "sub": "enaKK...",
  "tenant_region_scope": "EU",
  "tid": "someuuid",
  "unique_name": "my email",
  "upn": "my email",
  "uti": "1cc...",
  "ver": "1.0",
  "wids": [
    "some uuid",
    "some uuid"
  ],
  "xms_st": {
    "sub": "02n7h..."
  },
  "xms_tcdt": 1571393936
}
Run Code Online (Sandbox Code Playgroud)

最后一部分只是二进制数据。我目前只是将整个 access_token 传递给 JavaMail,如下所示:

        String accesstoken = new OauthTokenFetcher().getAuthToken();
        imapReader =  new ImapMailBoxReader(
                "outlook.office365.com",
                "my email",
                accesstoken);
        LocalDate startDate = LocalDate.of(2022,4,1);
        LocalDate endDate = LocalDate.of(2022,7,1);
        imapReader.processOnMessages("Inbox", startDate, endDate,this::processMessage);
Run Code Online (Sandbox Code Playgroud)

与 ImapMailBoxReader 如下:

public class ImapMailBoxReader {

    private String host;
    private String username;
    private String password;

    public ImapMailBoxReader(String host, String username, String password) {
        this.host = host;
        this.username = username;
        this.password = password;
    }
    public void processOnMessages(String folder, LocalDate since, LocalDate until, Consumer<Message> mailconsumer) {
        try {
            System.out.println("Password:" + password);
            Properties prop = new Properties();
            MailSSLSocketFactory sf = new MailSSLSocketFactory();
            sf.setTrustAllHosts(true);

            prop.put("mail.debug.auth", "true");
            prop.put("mail.imap.sasl.enable", "true");
            prop.put("mail.imap.sasl.mechanisms", "XOAUTH2");
            prop.put("mail.imap.auth.login.disable", "true");
            prop.put("mail.imap.auth.plain.disable", "true");
            prop.put("mail.imap.ssl.enable", "true");

            // Create the session
            //Connect to the server
            Session session = Session.getDefaultInstance(prop, null);
            session.setDebug(true);
            Store store = session.getStore("imap");
            store.connect(host, username, password);

            //open the inbox folder
            Folder inbox = store.getFolder(folder);
            inbox.open(Folder.READ_ONLY);

            Message[] messages;
            if (since != null) {
                Date startDate = Date.from(since.atStartOfDay(ZoneId.systemDefault()).toInstant());
                SearchTerm newerThan = new ReceivedDateTerm(ComparisonTerm.GE, startDate);
                if (until != null) {
                    Date endDate = Date.from(until.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
                    SearchTerm olderThan = new ReceivedDateTerm(ComparisonTerm.LT, endDate);
                    SearchTerm both = new AndTerm(olderThan, newerThan);
                    messages = inbox.search(both);
                } else {
                    messages = inbox.search(newerThan);
                }
            } else if (until != null) {
                Date endDate = Date.from(until.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
                SearchTerm olderThan = new ReceivedDateTerm(ComparisonTerm.LT, endDate);
                messages = inbox.search(olderThan);
            } else {
                messages = inbox.getMessages();
            }
            for (Message m: messages) {
                mailconsumer.accept(m);
            }
            inbox.close(false);
            store.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
Run Code Online (Sandbox Code Playgroud)

上述语句在 store.connect 语句处失败,并显示 AUTHENTICATE FAILED。我可能错误地传递了令牌?上面的 JavaMail 文档指出我不应该对令牌进行 Base64 编码,但我确实收到了它。我应该只发送一部分吗?那是哪一部分呢?

任何帮助,将不胜感激。

Yve*_* V. 5

在一位没有 stackoverflow 帐户的同事的提示下,我终于让它工作了。关键是我用于 OAuth 令牌的范围显然不允许应用程序使用。这段信息隐藏在本页的底部。

总结一下,解决办法就是:

  • 您必须配置 IMAP.AccessAsApp 权限,而不是 IMAP.AccessAsUser.All 。此权限无法在与 AccessAsUser.All 权限相同的位置找到,但隐藏在“Office 365 Exchange Online”权限下。
  • 与您期望的不同,您必须使用https://outlook.office365.com/.default访问令牌请求的主体有效负载中的范围。

这就成功了。可笑的是,我使用搜索引擎在文档页面中找到这些信息有多么困难。