Kintone plugin OAuth 2.0 migration — technical implementation guide
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:
- Authorization code: redirect user to Google/Microsoft consent screen, receive authorization code
- Token exchange: swap authorization code for access token + refresh token
- Token storage: encrypt and persist in a Kintone app or application database
- 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
- Go to Google Cloud Console (console.cloud.google.com)
- Create a project (e.g., kinplug-mail-production)
- APIs & Services → Credentials → Create OAuth 2.0 Client ID
- Register redirect URI (e.g.,
https://api.your-plugin.com/oauth/google/callback) - Scope to
https://www.googleapis.com/auth/gmail.sendonly (principle of least privilege) - Enable Gmail API
Microsoft Entra ID side
- Azure Portal (portal.azure.com)
- Entra ID → App registrations → New registration
- Register redirect URI
- API permissions → Microsoft Graph →
Mail.Send,User.Read,offline_access - Select delegated permissions (admin consent may be required)
- 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:
- Recipients → Shared mailboxes → select target mailbox
- Mailbox permissions → Manage others
- 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
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.