googleapis / google-auth-library-java

Open source Auth client library for Java
https://developers.google.com/identity
BSD 3-Clause "New" or "Revised" License
411 stars 225 forks source link

Authenticate with Service Account to send Gmail #1270

Open ybhwang opened 1 year ago

ybhwang commented 1 year ago

Thanks for stopping by to let us know something could be better!

PLEASE READ: If you have a support contract with Google, please create an issue in the support console instead of filing on GitHub. This will ensure a timely response.

Please run down the following list and make sure you've tried the usual "quick fixes":

If you are still having issues, please include as much information as possible:

Environment details

  1. Specify the API at the beginning of the title: Gmail (Google Workspace)
  2. OS type and version: Spring Boot 2.3.7.RELEASE
  3. Java version: OpenJDK 11
  4. version(s):

    implementation 'com.google.api-client:google-api-client:2.0.0'
    implementation 'com.google.apis:google-api-services-gmail:v1-rev20230123-2.0.0'
    
    implementation 'org.springframework.boot:spring-boot-starter-mail' // javax.mail.*

Steps to reproduce

  1. Gmail API activated
  2. Create Service Account
  3. Set Domain Wide Delegation (expect to send email with any addresses in domains / Not for a specific user's address)
  4. RESTful API with Springboot
  5. Test API
    • POST /send-gmail (application/json)
  6. Error Occurred

Code example

...
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.google.api.services.gmail.Gmail;
import javax.mail.internet.MimeMessage;

@RestController
public class GmailController {

    Gmail service;

    @Autowired
    private GmailService gmailService;

    @PostMapping("/send-gmail")
    public ResponseEntity<String> sendEmail(@RequestBody Email email) {
        try {
            MimeMessage message = gmailHelper.createMessage(email.getTo(), email.getFrom(), email.getSubject(), email.getBody());
            gmailService.sendMessage("me", message);
            return ResponseEntity.ok().body("Email sent successfully!");
        } catch (Exception e) {
            return ResponseEntity.badRequest().body("Failed to send email: " + e.getMessage());
        }
    }

}
...
import org.springframework.stereotype.Service;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.json.gson.GsonFactory;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.http.HttpTransport;
import com.google.api.services.gmail.Gmail;
import com.google.api.services.gmail.model.Message;
import com.google.auth.http.HttpCredentialsAdapter;
import com.google.auth.oauth2.GoogleCredentials;
import org.apache.commons.codec.binary.Base64;
import javax.mail.internet.MimeMessage;
import javax.mail.internet.InternetAddress;
import javax.mail.Session;
import javax.mail.MessagingException;
import java.io.IOException;
import java.io.ByteArrayOutputStream;
import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.util.Properties;

@Service
public class GmailService {

    private static final JsonFactory jsonFactory = GsonFactory.getDefaultInstance();
    private static HttpTransport httpTransport;

    public MimeMessage createMessage(String to, String from, String subject, String body) throws MessagingException, UnsupportedEncodingException {
        Properties props = new Properties();
        Session session = Session.getDefaultInstance(props, null);

        MimeMessage message = new MimeMessage(session);
        message.setFrom(new InternetAddress(from));
        message.addRecipient(javax.mail.Message.RecipientType.TO, new InternetAddress(to));
        message.setSubject(subject);
        message.setText(body);

        return message;
    }

    public Message sendMessage(String userId, MimeMessage message) throws MessagingException, IOException, GeneralSecurityException {
        Gmail service = getGmailService();

        Message sendMessage = createMessageWithEmail(message);
        sendMessage = service.users().messages().send(userId, sendMessage).execute();

        return sendMessage;
    }

    public Message createMessageWithEmail(MimeMessage email) throws MessagingException, IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        email.writeTo(baos);

        String encodedEmail = Base64.encodeBase64URLSafeString(baos.toByteArray());
        Message message = new Message();
        message.setRaw(encodedEmail);

        return message;
    }

    public Gmail getGmailService() throws IOException, GeneralSecurityException {
        GoogleCredentials credentials = GoogleCredentials.getApplicationDefault();
        httpTransport = GoogleNetHttpTransport.newTrustedTransport();
        Gmail service =  new Gmail.Builder(httpTransport, jsonFactory, new HttpCredentialsAdapter(credentials))
            .setApplicationName("APPLICATION NAME")
            .build();

        return service;
    }

}

Stack trace

{
    "error": {
        "code": 401,
        "message": "Request is missing required authentication credential. Expected OAuth 2 access token, login cookie or other valid authentication credential. See https://developers.google.com/identity/sign-in/web/devconsole-project.",
        "errors": [
            {
                "message": "Login Required.",
                "domain": "global",
                "reason": "required",
                "location": "Authorization",
                "locationType": "header"
            }
        ],
        "status": "UNAUTHENTICATED",
        "details": [
            {
                "@type": "type.googleapis.com/google.rpc.ErrorInfo",
                "reason": "CREDENTIALS_MISSING",
                "domain": "googleapis.com",
                "metadata": {
                    "service": "gmail.googleapis.com",
                    "method": "caribou.api.proto.MailboxService.GetMessage"
                }
            }
        ]
    }
}

External references such as API reference guides

Any additional information below

Creating and authenticating the OAuth 2.0 Client is confirmed as an official document, but I remember that in the past, only Service Account was authenticated and normal service was possible.

If the OAuth 2.0 Client allows the service to be built through offline settings, the server should be able to authenticate with Service Account because it seems that there is a problem with continuing the normal Gmail delivery service when the first authenticated user information is discarded (deleted through Workspace Admin).

In addition, if Client Token is implemented randomly to configure the logic of continuing authentication, it is expected that unnecessary resources will be generated in the part where the token is issued again through the authentication when the server is re-run.

In conclusion, if you look inside the key of the Service Account, I would like you to provide a way to use Gmail only with Service Account in the latest 2.0.0 version of Google API Service, where all the values required for authentication exist, including Client ID.

meltsufin commented 1 year ago

@TimurSadykov Any thoughts on why this isn't working? The README suggests that it should work.

TimurSadykov commented 1 year ago

Looking