Operated by Edamame Inc. · Tokyo · Manila · Kintone work since 2019
Journal

Kintone plugin OAuth 2.0 migration — technical implementation guide

2026-04-18 · 8 min read · Tom Arai · Founder, Edamame Inc.

This post walks through migrating a Kintone mail plugin from SMTP basic authentication to OAuth 2.0 — with code and configuration examples. Given Microsoft 365's April 2026 basic-auth shutdown, many teams are starting this migration now.

The short version

OAuth 2.0 migration isn't just swapping auth — it requires rethinking token management, refresh logic, scope design, and error handling. New implementation: 3-5 days. Existing plugin refactor: 5-10 days. Node.js + Kintone implementation patterns and production operational considerations included.

Assess the current state first

Check what auth the current plugin uses. Typical SMTP basic auth code:

// SMTP basic-auth example (Node.js + nodemailer)
const transporter = nodemailer.createTransport({
  host: 'smtp.office365.com',
  port: 587,
  secure: false,
  auth: {
    user: 'sender@your-company.com',
    pass: 'password_here'  // basic auth — unavailable after April 2026
  }
});

This pattern breaks on both Microsoft 365 and Gmail, progressively between 2024-2026.

OAuth 2.0 architecture overview

OAuth 2.0 mail sending follows 4 steps:

  1. Authorization code: redirect user to Google/Microsoft consent screen, receive authorization code
  2. Token exchange: swap authorization code for access token + refresh token
  3. Token storage: encrypt and persist in a Kintone app or application database
  4. API calls: use access token for Gmail API / Microsoft Graph API. On expiry, auto-refresh via refresh token

Step 1: OAuth app registration

Google Cloud side

  1. Go to Google Cloud Console (console.cloud.google.com)
  2. Create a project (e.g., kinplug-mail-production)
  3. APIs & Services → Credentials → Create OAuth 2.0 Client ID
  4. Register redirect URI (e.g., https://api.your-plugin.com/oauth/google/callback)
  5. Scope to https://www.googleapis.com/auth/gmail.send only (principle of least privilege)
  6. Enable Gmail API

Microsoft Entra ID side

  1. Azure Portal (portal.azure.com)
  2. Entra ID → App registrations → New registration
  3. Register redirect URI
  4. API permissions → Microsoft Graph → Mail.Send, User.Read, offline_access
  5. Select delegated permissions (admin consent may be required)
  6. Issue client secret (24-month expiry recommended)

Step 2: authorization code flow implementation

// Node.js + Express example (Microsoft Graph)
app.get('/oauth/microsoft/start', (req, res) => {
  const params = new URLSearchParams({
    client_id: MS_CLIENT_ID,
    response_type: 'code',
    redirect_uri: 'https://api.your-plugin.com/oauth/microsoft/callback',
    scope: 'offline_access Mail.Send User.Read',
    state: generateSecureState(req), // CSRF protection
    prompt: 'select_account'
  });
  res.redirect(`https://login.microsoftonline.com/common/oauth2/v2.0/authorize?${params}`);
});

app.get('/oauth/microsoft/callback', async (req, res) => {
  const { code, state } = req.query;
  validateState(state, req); // CSRF validation

  const tokenResponse = await fetch('https://login.microsoftonline.com/common/oauth2/v2.0/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      client_id: MS_CLIENT_ID,
      client_secret: MS_CLIENT_SECRET,
      code,
      redirect_uri: 'https://api.your-plugin.com/oauth/microsoft/callback',
      grant_type: 'authorization_code'
    })
  });

  const tokens = await tokenResponse.json();
  // Save tokens.access_token, tokens.refresh_token, tokens.expires_in
  await saveTokens(userId, tokens);
  res.redirect('/success');
});

Step 3: token storage design

Encrypt tokens before persistence. Encryption keys must not be hardcoded — use environment variables or KMS.

// AES-256-GCM encryption example
const crypto = require('crypto');

function encryptToken(token, key) {
  const iv = crypto.randomBytes(12);
  const cipher = crypto.createCipheriv('aes-256-gcm', Buffer.from(key, 'hex'), iv);
  const encrypted = Buffer.concat([cipher.update(token, 'utf8'), cipher.final()]);
  const authTag = cipher.getAuthTag();
  return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted.toString('hex');
}

Storage options:

  • Dedicated Kintone app (recommended): Create one OAuth connections app, store encrypted tokens. Restrict app permissions tightly. Plugin access via API key only.
  • Server-side DB: PostgreSQL etc. But this creates legal responsibility for holding user credentials — privacy policy must be explicit.
  • Secrets Manager (AWS etc.): Cloud vendor secret-management service. May reduce cost efficiency.

Step 4: token validation and refresh at send time

async function sendEmailViaGraph(userId, mailBody) {
  let tokens = await loadTokens(userId);

  // Check expiry with 5-minute margin
  if (tokens.expires_at < Date.now() + 5 * 60 * 1000) {
    tokens = await refreshAccessToken(tokens.refresh_token);
    await saveTokens(userId, tokens);
  }

  const response = await fetch('https://graph.microsoft.com/v1.0/me/sendMail', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${tokens.access_token}`,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ message: mailBody })
  });

  if (response.status === 401) {
    // Token invalidated — prompt re-auth
    throw new TokenExpiredError('OAuth re-authentication required');
  }
}

Kintone-side integration

Plugin settings UI

// Plugin settings UI JavaScript
document.getElementById('connect-google').addEventListener('click', () => {
  const authUrl = `https://api.your-plugin.com/oauth/google/start?subdomain=${kintone.getLoginUser().domain}`;
  const popup = window.open(authUrl, 'oauth', 'width=500,height=600');

  window.addEventListener('message', (e) => {
    if (e.data.type === 'oauth-complete') {
      popup.close();
      refreshConnectionList();
    }
  });
});

Subdomain scoping

With multi-subdomain deployments, OAuth connections should be scoped per subdomain. A Gmail account connected on sales.kintone.com should not be usable from ops.kintone.com.

Troubleshooting during migration

"Admin consent required" error

If the scope requires organizational admin consent (e.g., Mail.Send.Shared), regular user authentication fails. Solution: tenant admin grants consent for the entire organization upfront, or restructure to per-user consent flow.

"invalid_grant" error

Occurs when refresh token is invalidated. User password change, MFA reset, or session invalidation all revoke refresh tokens. Detect this error and prompt re-authentication.

"Rate limit exceeded" error

Microsoft Graph API rate limits (~100 req/sec per tenant). Detect 429 responses, read Retry-After header, back off and retry.

Send As (delegated sending) implementation

For sending from shared mailboxes with user OAuth tokens, Microsoft-side Send As delegation is required.

Exchange Admin Center:

  1. Recipients → Shared mailboxes → select target mailbox
  2. Mailbox permissions → Manage others
  3. Add users who should have Send As rights

Implementation:

// Send As with Graph API
await fetch(`https://graph.microsoft.com/v1.0/users/${sharedMailboxId}/sendMail`, {
  // users/{id}/sendMail sends on behalf of the delegated user
  headers: {
    'Authorization': `Bearer ${delegatedUserToken}`,
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    message: {
      // from is automatically the shared mailbox
      toRecipients: [...]
    }
  })
});

Summary

SMTP basic auth → OAuth 2.0 isn't just swapping the auth call. It requires rebuilding token management, error handling, scope design. The Node.js + Kintone pattern above implements in 3-5 days. In enterprise environments, add admin consent, Send As delegation, and rate-limit handling.

Kinplug Mail implements all of this natively — OAuth 2.0 from day one. Free migration support from any existing SMTP-based plugin.

Related

Next step

Try kinplug free for 14 days

No credit card. Sign in with Google or Microsoft, enter your Kintone subdomain, and go live in 90 seconds.

Start free See plugins
Get started

14 days, every feature,
no credit card.

Sign in with Google or Microsoft, enter your Kintone subdomain, install the plugin. Live in 90 seconds.