Login with MGX button
Give growers and buyers a familiar one-click way to connect their MGX account to your product. The button starts the authorization-code flow: the user signs in on MGX, picks the team your integration may act as, approves the scopes you asked for, and lands back on your callback with a code.
Three ways to add it, in order of least effort:
- Script embed — one
<div>and one<script>. Generatesstate(and PKCE) for you. - HTML and CSS — link our stylesheet and paste the markup. You build the authorize URL.
- Static images — SVG/PNG files for emails, CMS content, or anywhere you can't add CSS.
You need an OAuth client with a registered redirect URI. Create one in your MGX dashboard — click your name in the top-right corner and choose Developers.
Button builder
Pick a style, drop in your client details, and copy the snippet. The preview is the real button rendered with the hosted stylesheet.
Preview only — add a client ID and redirect URI to test it
https://developers.mygrainexchange.com/login-button/callback is accepted on every MGX client — nothing to register), then click the live preview. You'll sign in on MGX and get the authorization code back here with a ready-made token exchange. Seeing invalid_client? The redirect URI above isn't registered on that client (it must match exactly), or the client ID is wrong. This builder talks to https://dashboard.mygrainexchange.com.<div data-mgx-login
data-client-id="YOUR_CLIENT_ID"
data-redirect-uri="https://example.com/oauth/callback"
data-scope="openid profile email"
data-popup="true"></div>
<script src="https://dashboard.mygrainexchange.com/login-with-mgx/login-with-mgx.js" async></script>
<!-- On the page with the button: -->
<script>
MGXLogin.onLogin(function (login) {
// login.code, login.state, login.stateValid, login.codeVerifier
})
</script>
<!-- On your callback page (https://example.com/oauth/callback): -->
<script src="https://dashboard.mygrainexchange.com/login-with-mgx/login-with-mgx.js"></script>
<script>MGXLogin.completePopup()</script>Testing it
The preview above is inert until you give it a client ID and redirect URI — then it becomes a real button. To try the whole flow without writing any code:
- Paste your client ID into the builder and click Use the docs test callback. This callback is accepted on every MGX OAuth client — nothing to register.
- Click the live preview. Sign in, pick a team, approve the scopes.
MGX opens in a popup (untick Popup to test the full-page redirect instead). When you approve, the result comes straight back into the builder: state is verified, the code (and PKCE verifier, if enabled) is shown, and you get a copy-paste token exchange to run from your server. The docs site never sees your client secret.
If MGX responds with invalid_client / "Client authentication failed" with your own redirect URI, it isn't registered on that client (it has to match character-for-character, including the scheme and port) or the client ID is wrong.
Don't have a client yet? Create one in your MGX dashboard — click your name in the top-right corner and choose Developers. Sandbox clients work too.
Script embed
Drop a placeholder element where the button should appear and load the script once per page. It renders the button, links the hosted stylesheet, and builds the authorize URL with a fresh random state every time the page loads.
<div data-mgx-login
data-client-id="YOUR_CLIENT_ID"
data-redirect-uri="https://example.com/oauth/callback"
data-scope="openid profile email bids.write"></div>
<script src="https://dashboard.mygrainexchange.com/login-with-mgx/login-with-mgx.js" async></script>
- Name
data-client-id- Type
- string
- Description
Your OAuth client ID. Required.
- Name
data-redirect-uri- Type
- string
- Description
Where MGX sends the user after they approve. Must exactly match a redirect URI registered on the client. Required.
- Name
data-scope- Type
- string
- Description
Space-separated scopes. Defaults to
openid profile email.
- Name
data-theme- Type
- dark | light | red
- Description
Colour scheme. Defaults to
dark.
- Name
data-size- Type
- sm | md | lg
- Description
Button height: 36px, 44px, or 52px. Defaults to
md.
- Name
data-shape- Type
- rounded | pill
- Description
Corner style. Defaults to
rounded.
- Name
data-block- Type
- boolean
- Description
Stretch to the full width of the container.
- Name
data-label- Type
- string
- Description
Button text. Defaults to
Login with MGX. See the approved labels below.
- Name
data-pkce- Type
- boolean
- Description
Generate a PKCE
code_verifier/code_challengepair. Required for public clients (no client secret). The verifier is stored insessionStoragefor your callback page.
- Name
data-state- Type
- string
- Description
Supply your own
stateinstead of a random one.
- Name
data-prompt- Type
- login | consent
- Description
Force the user to re-authenticate or re-approve scopes.
- Name
data-popup- Type
- boolean
- Description
Open MGX in a centred popup window instead of navigating away. Your callback page calls
MGXLogin.completePopup()and the page with the button listens withMGXLogin.onLogin()— see below.
Popup mode
With data-popup="true" the user never leaves your page. The popup loads MGX, and after approval your callback page hands the result back to the opener and closes itself:
<div data-mgx-login data-popup="true"
data-client-id="YOUR_CLIENT_ID"
data-redirect-uri="https://example.com/oauth/callback"></div>
<script src="https://dashboard.mygrainexchange.com/login-with-mgx/login-with-mgx.js" async></script>
<script>
window.addEventListener('load', function () {
MGXLogin.onLogin(function (login) {
if (login.error || !login.stateValid) { /* show an error */ return }
// Send login.code (and login.codeVerifier, if PKCE) to your backend
fetch('/api/mgx/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: login.code, codeVerifier: login.codeVerifier }),
})
})
})
</script>
The script also exposes a small API on window.MGXLogin for single-page apps that render the button themselves:
// Render into any element
MGXLogin.render(document.querySelector('#connect'), {
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'https://example.com/oauth/callback',
scope: 'openid profile email bids.write',
theme: 'light',
pkce: true,
})
// Or just get a URL and redirect yourself.
// prepare() generates state (+ PKCE) and stores them in sessionStorage.
const url = await MGXLogin.prepare({
clientId: 'YOUR_CLIENT_ID',
redirectUri: 'https://example.com/oauth/callback',
pkce: true,
})
window.location.assign(url)
HTML and CSS
If you'd rather not load a script, link the stylesheet and use the markup directly. The mark is an inline SVG so it inherits the button's text colour — no extra image request.
<link rel="stylesheet" href="https://dashboard.mygrainexchange.com/login-with-mgx/login-with-mgx.css">
<a class="mgx-login mgx-login--light"
href="https://dashboard.mygrainexchange.com/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback&scope=openid%20profile%20email&state=RANDOM_STATE">
<svg class="mgx-login__mark" viewBox="0 0 880.1 825.2" aria-hidden="true">
<!-- paste the paths from https://dashboard.mygrainexchange.com/login-with-mgx/mgx-mark.svg -->
</svg>
<span class="mgx-login__label">Login with MGX</span>
</a>
Compose the look with modifier classes:
| Class | Effect |
|---|---|
mgx-login | Base button. Dark theme, 44px tall. |
mgx-login--light | White background, grey border, red mark. |
mgx-login--red | MGX red background. |
mgx-login--sm / mgx-login--lg | 36px / 52px tall. |
mgx-login--pill | Fully rounded corners. |
mgx-login--block | Full-width, centred content. |
The builder above generates the full markup, including the inline mark, for any combination.
Static images
For emails, CMS pages, or README files, use a plain link around a hosted image. SVGs stay crisp at any size; the PNGs are 188×44 at 1x and 376×88 at 2x.
<a href="https://dashboard.mygrainexchange.com/oauth/authorize?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fexample.com%2Foauth%2Fcallback&scope=openid%20profile%20email&state=RANDOM_STATE">
<img src="https://dashboard.mygrainexchange.com/login-with-mgx/login-with-mgx-dark.svg"
alt="Login with MGX" width="188" height="44">
</a>
login-with-mgx-dark.svglogin-with-mgx-light.svglogin-with-mgx-red.svgEvery variant is also available as .png (1x) and @2x.png, and the bare mark as mgx-mark.svg. All files live under https://dashboard.mygrainexchange.com/login-with-mgx/. When inlined, the dark half of the mark uses currentColor and the red half stays MGX red, so it adapts to each theme automatically.
Handling the callback
MGX redirects to your redirect_uri with code and state. On your server:
- Verify
statematches the value you generated when rendering the button. Reject the login otherwise. - Exchange the code for tokens at
/oauth/token. This needs your client secret (or the PKCE verifier for public clients), so it must happen server-side. - Read the identity from
/oauth/userinfo—sub(stable MGX user id),email,name,mgx_team_id(the team the user chose),mgx_roles. - Persist the tokens against your user. Access tokens last 15 days; refresh tokens last 30 days and are rotated on every refresh, so always store the newest one.
If you used the script embed, the pending state (and PKCE verifier) are waiting in sessionStorage. Call MGXLogin.consume() on your callback page to read and clear them, then post the code and verifier to your backend:
const params = new URLSearchParams(window.location.search)
const { state, codeVerifier } = MGXLogin.consume()
if (!params.get('code') || params.get('state') !== state) {
throw new Error('Login was tampered with or expired — start again.')
}
await fetch('/api/mgx/exchange', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ code: params.get('code'), codeVerifier }),
})
sessionStorage is per-tab. If your callback opens in a new tab or a
different origin, generate and store state yourself instead of relying on
the embed. The MGXLogin.authorizeUrl() helper still builds the URL for you.
Server-side with the SDKs
Every official SDK ships the same two helpers: exchangeAuthorizationCode(...) returns a token set to persist, and fromAuthorizationCode(...) does the exchange and hands you a client already acting as that user's team. Construct later clients from the stored tokens and pass an onTokenRefresh callback so rotated refresh tokens are written back.
import { MgxClient } from '@mygrainexchange/sdk'
const MGX_HOST = 'https://dashboard.mygrainexchange.com'
const REDIRECT_URI = 'https://example.com/oauth/callback'
app.get('/oauth/callback', async (req, res) => {
const { code, state } = req.query
if (!code || state !== req.session.mgxState) {
return res.status(400).send('Login expired or tampered with — start again.')
}
// 1. Exchange the code (server-side: needs the client secret)
const tokens = await MgxClient.exchangeAuthorizationCode({
clientId: process.env.MGX_CLIENT_ID!,
clientSecret: process.env.MGX_CLIENT_SECRET!,
code: String(code),
redirectUri: REDIRECT_URI,
codeVerifier: req.session.mgxCodeVerifier, // only if you used PKCE
})
// 2. Who is this? Which team did they pick?
const me = await fetch(`${MGX_HOST}/oauth/userinfo`, {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
}).then((r) => r.json())
// 3. Persist per user — refresh tokens rotate, so store the newest set
await db.mgxConnections.upsert({
userId: req.user.id,
mgxUserId: me.sub,
mgxTeamId: me.mgx_team_id,
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
expiresAt: tokens.expiresAt,
})
res.redirect('/settings/integrations')
})
// Later, act as that team. onTokenRefresh keeps the stored tokens current.
async function mgxFor(userId: string) {
const row = await db.mgxConnections.find(userId)
return new MgxClient({
clientId: process.env.MGX_CLIENT_ID!,
clientSecret: process.env.MGX_CLIENT_SECRET!,
accessToken: row.accessToken,
refreshToken: row.refreshToken,
expiresAt: row.expiresAt,
onTokenRefresh: (t) => db.mgxConnections.update(userId, t),
})
}
const mgx = await mgxFor(req.user.id)
for await (const bid of mgx.bids.list()) console.log(bid.id, bid.status)
Prefer one call? fromAuthorizationCode (from_authorization_code / FromAuthorizationCodeAsync) does the exchange and returns the authenticated client together with the tokens to persist:
const { client: mgx, tokens } = await MgxClient.fromAuthorizationCode({
clientId, clientSecret, code, redirectUri: REDIRECT_URI,
onTokenRefresh: (t) => db.mgxConnections.update(userId, t),
})
await db.mgxConnections.upsert({ userId, ...tokens })
const teams = await mgx.teams.list()
Every token issued through Login with MGX is bound to the team the user chose on the consent screen. If a user belongs to several teams and your product needs more than one, send them through the flow once per team and store a token set per team.
Brand guidelines
A few rules keep the button recognisable across partner sites.
- Use an approved label.
Login with MGX,Sign in with MGX,Continue with MGX, orConnect MGX account. Don't abbreviate or translate "MGX". - Keep the mark intact. Don't recolour it outside the three themes, rotate it, add effects, or replace it with another icon.
- Give it room. Leave clear space of at least half the button's height on every side, and never render it shorter than 36px.
- Match your surroundings. Use
lighton dark backgrounds anddarkorredon light ones. Pick the theme that gives the most contrast. - One button per action. Show it once, in the same place you show other sign-in providers. Don't stack multiple MGX buttons with different scopes.