3

In node.js, I want to send gmail by google api. but there are only examples of using credentials.json.

Credentials.json seems to be difficult to push github, difficult to make env, and difficult to use github action secrets.

is there any way call gmail api without credentials.json??? if is there is no way, how can i manage credentials.json??

Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449
fuzes
  • 1,777
  • 4
  • 20
  • 40

4 Answers4

1

In order to use Google APis you must first create a project on Google developer console. Once your project is created you will be able to enable which api you are looking at using in your project.

In order to access any data you will need to create credentials. These credentials identify your project to google and are used by your application to authorize and authenticate a user vai Oauth2.

No there is no way to use any google api accessing private user data without having a credeitnals.json file in your project.

Linda Lawton - DaImTo
  • 106,405
  • 32
  • 180
  • 449
  • so if there is no way, how can i manage credentials.json?? we can't put credentials.json in a project and push it to github because of security reasons – fuzes Dec 22 '20 at 13:48
  • I tried without credentials.json but offered that file's content as env. As result, you don't need to serve any files for deploy Please check below. – Siner Dec 23 '20 at 06:05
  • 1
    It is against TOS to share your credentials [Can I really not ship open source with Client ID?](https://stackoverflow.com/a/28109307/1841839) Instead instruct your users on how to create their own credentials, so that they will be able to use your code. – Linda Lawton - DaImTo Dec 23 '20 at 10:26
0

In nodejs, I wrote code like below.

Instead of exclude credentials.json you need to offer clientId, clientSecret, refreshToken, redirectUrl as other way (ex. environment)

export class GmailService {
  TOKEN_PATH: string = 'token.json';
  SCOPES: string[] = ['https://www.googleapis.com/auth/gmail.send'];
  refreshTokenUrl: string = 'https://oauth2.googleapis.com/token';
  ACCESS_TYPE: string = 'offline';
  oAuth2Client: any;
  gmailClient: gmail_v1.Gmail;

  constructor(private readonly configService: ConfigService) {
    this.oAuth2Client = new google.auth.OAuth2(
      this.configService.get(env.mailer.clientId),
      this.configService.get(env.mailer.clientSecret),
      this.configService.get(env.mailer.redirectUrl),
    );
    this.gmailClient = gmail({ version: 'v1', auth: this.oAuth2Client });
    this.authorize().catch((err: Error) => {
      throw err;
    });
  }

  private static encodeMessage(msg: Buffer): string {
    return Buffer.from(msg)
      .toString('base64')
      .replace(/\+/g, '-')
      .replace(/\//g, '_')
      .replace(/=+&/g, '');
  }

  async sendMail(mail: Mail.Options): Promise<void> {
    if (this.configService.get(env.environment) === 'test') return;
    if (this.configService.get(env.environment) !== 'production') {
      mail.to = this.configService.get(env.mailer.testTarget);
    }
    await this.authorize();
    await this.send(mail);
  }

  async authorize(): Promise<void> {
    // check token file exists
    await fs.readFile(this.TOKEN_PATH, async (err: Error, tokenFile: any) => {
      let token: Token;
      if (err) {
        token = await this.getNewToken(); // token file not exist
      } else {
        token = JSON.parse(tokenFile);
        if (token.expiry_date - new Date().getTime() < 30000) {
          token = await this.getNewToken(); // token was expired
        }
      }
      this.oAuth2Client.setCredentials(token);
    });
  }

  // refresh token
  async getNewToken(): Promise<Token> {
    const response: AxiosResponse = await axios.post(this.refreshTokenUrl, {
      client_id: this.configService.get(env.mailer.clientId),
      client_secret: this.configService.get(env.mailer.clientSecret),
      grant_type: 'refresh_token',
      refresh_token: this.configService.get(env.mailer.refreshToken),
    });
    const token: Token = response.data;
    if (token.expires_in && !token.expiry_date) {
      token.expiry_date = new Date().getTime() + token.expires_in * 1000;
    }
    await fs.writeFile(this.TOKEN_PATH, JSON.stringify(token), (err: Error) => {
      if (err) throw err;
    });
    return token;
  }

  private async send(mail: Mail.Options): Promise<void> {
    const mailComposer: MailComposer = new MailComposer(mail); // build mail with nodemailer
    mailComposer.compile().build((err: Error, msg: Buffer) => {
      if (err) throw err;
      this.gmailClient.users.messages.send(
        {
          userId: 'me',
          requestBody: {
            raw: GmailService.encodeMessage(msg),
          },
        },
        (err: Error, result: any) => {
          if (err) throw err;
          console.log('NODEMAILER reply from server', result.data);
        },
      );
    });
  }
}
Siner
  • 511
  • 2
  • 5
  • 18
  • This is not what they are asking. They want to run the application without credentials so that they can share the code securely. Storing the creds in the source code would also be a security risk if this code was released on GitHub. – Linda Lawton - DaImTo Dec 23 '20 at 10:27
  • My suggestion is using properties of credentials and without code by refresh token api. And my code is not storing secrets as code, but offered from github secret. Of course not contained in repository source code – Siner Dec 24 '20 at 11:07
0

Instead of passing string, you can pass json object

{
   installed: "web",
   client_id: "<idhere>",
   client_secret: "<secrethere>",
   redirect_uris: ''
}
0

My recommendation is to create a file named credentials.dist.json which replaces the value of client_secret with the string {CLIENT_SECRET}.

credentials.dist.json:

{
  "installed": {
    "client_id": "1234tacosaredelicious5789.apps.googleusercontent.com",
    "project_id": "some-outh-project",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://oauth2.googleapis.com/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_secret": "{CLIENT_SECRET}",
    "redirect_uris": ["http://localhost"]
  }
}

Commit this file to your repo. Add credentials.json to .gitignore to prevent it from being accidentally committed later.

Upon installing this project, have your build tooling or users run the following one-liner to recreate credentials.json:

  SECRET='THE_CLIENT_SECRET'; sed "s|{CLIENT_SECRET}|$SECRET|" credentials.dist.json > credentials.json

The value of SECRET can be stored in a password manager, a k8s secret, an environment variable, etc.

*Note that the leading space will cause most shells to skip saving it to history.

Excalibur
  • 3,258
  • 2
  • 24
  • 32