Overview
MCAuth is an OAuth2 authorization server that lets your application verify a user's Minecraft identity. Instead of passwords or API keys, players prove ownership of their Minecraft account by physically joining a server and entering a short code.
The result is a verified Minecraft UUID and username that your application can trust — useful for linking Minecraft accounts to web accounts, gating access to services based on in-game status, or any scenario where you need to know "this web user is this Minecraft player."
MCAuth implements the standard OAuth2 authorization code flow (RFC 6749 §4.1). If you have integrated with Google, GitHub, or Discord OAuth2 before, the pattern is identical — the only difference is that the user verifies themselves in Minecraft rather than clicking an "Allow" button.
Quickstart
1. Register an integrator account
Go to /register and create an account. This account owns your applications and their credentials.
2. Create an application
From the dashboard, click New Application. You will be asked for:
| Field | Description |
|---|---|
| Name | A human-readable name shown to users on the authorization page. |
| Redirect URI | The URL MCAuth redirects to after the user authenticates. Must exactly match the URI you use in authorization requests. Use https:// in production. |
| Code Expiry | How many seconds a Minecraft code remains valid after the player joins (10–1800). The default of 300 seconds (5 minutes) is appropriate for most use cases. |
After creation you will be shown your Client Secret once. Copy it immediately — it cannot be retrieved again. If you lose it, regenerate it from the application page (which immediately invalidates the old one).
3. Implement the flow
Use your client_id (shown on the application page) and
client_secret to implement the steps below.
Auth Flow
The complete flow involves four parties: your server, your user's browser, the MCAuth server, and the Minecraft server.
client_id and redirect_uri,
then shows the user a page explaining what to do next.
redirect_uri with a
short-lived authorization code and your original state
value appended as query parameters.
client_secret, your server makes a back-channel
POST request to /oauth/token. MCAuth returns the verified
Minecraft UUID and username. The code is consumed and cannot be used again.
Why authorization codes? The code in the redirect
URL is useless on its own — it only becomes player identity when combined with
your client_secret in a server-to-server call. This means a
malicious user who intercepts the redirect cannot impersonate the player.
Step 2–3 — User Verification
MCAuth handles this step entirely. The user joins the Minecraft server listed on
the authorization page and receives a 6-character alphanumeric code (e.g.
WX4KP2). They then click "I have my code" and enter it on the
MCAuth website.
Codes consist of uppercase letters and digits, with visually ambiguous characters
(0, O, 1, I) excluded to
reduce entry errors.
Once the code is validated, MCAuth redirects the user's browser to your
redirect_uri:
https://yourapp.com/auth/callback ?code=a3f8c2d1e4b5f6a7b8c9d0e1f2a3b4c5 &state=k3jH9mXpQ2wRvTz8
Code Expiry
Two expiry limits apply to the in-game code:
- Global hard limit: 30 minutes from when the player joined the server.
- Per-application soft limit: The
Code Expiryvalue you configured (default 300 seconds). This lets you enforce a tighter window for your specific use case.
Both checks must pass. If the code expires, the user must rejoin the Minecraft server to get a new one.
The authorization code in the callback URL (not the in-game code) expires
after 10 minutes and can only be exchanged once.
Step 4 — Token Exchange
Exchange the authorization code for the player's verified Minecraft identity.
This request must be made from your server — never from the browser — because
it requires your client_secret.
Request
Send as application/x-www-form-urlencoded:
| Parameter | Description | |
|---|---|---|
| grant_type | required | Must be authorization_code. |
| code | required | The authorization code received in the callback URL. |
| client_id | required | Your application's client ID. |
| client_secret | required | Your application's client secret. |
| redirect_uri | required | Must exactly match the redirect URI used in the authorization request. |
Example Request
POST /oauth/token Content-Type: application/x-www-form-urlencoded grant_type=authorization_code &code=a3f8c2d1e4b5f6a7b8c9d0e1f2a3b4c5 &client_id=3f7a2b19-04cd-4e8a-b91d-0c2f5e6d7a8b &client_secret=YOUR_CLIENT_SECRET &redirect_uri=https%3A%2F%2Fyourapp.com%2Fauth%2Fcallback
Success Response
200 OK, Content-Type: application/json:
{
"minecraft_uuid": "069a79f4-e23c-3084-97a0-5e27a4b1c0d2",
"minecraft_username": "Pinkcommando"
}
| Field | Description |
|---|---|
| minecraft_uuid | The player's Mojang UUID (stable, tied to the Mojang account). Use this as the canonical identifier for the player — usernames can change, UUIDs cannot. |
| minecraft_username | The player's current in-game username at the time of authentication. |
Error Responses
All errors return JSON with an error field:
| Error | Status | Cause |
|---|---|---|
| invalid_request | 400 | Missing or malformed parameters, or grant_type is not authorization_code. |
| invalid_client | 401 | Unknown client_id or incorrect client_secret. |
| invalid_grant | 400 | The code is unknown, already used, expired, belongs to a different client, or the redirect_uri does not match. |
| server_error | 500 | Internal server error. |
Security Considerations
Validate the state parameter
Before exchanging the authorization code, verify that the state value
in the callback matches the one you stored in the user's session before redirecting.
Skipping this check makes your callback endpoint vulnerable to CSRF attacks where a
malicious site tricks your server into linking the wrong Minecraft account.
Keep your client_secret secret
The client_secret must never appear in client-side code, version
control, or log files. Store it in an environment variable or secrets manager.
If it is compromised, regenerate it immediately from the application page — this
invalidates the old secret and all in-flight auth codes instantly.
Use HTTPS for your redirect URI
The authorization code is delivered via a redirect URL. Over plain HTTP
it can be intercepted by network observers. Always use https:// for
production redirect URIs. http://localhost is acceptable for local
development only.
Exchange codes server-side
The token exchange (step 4) must happen on your backend. If you make this call
from the browser, your client_secret is visible to anyone who opens
DevTools. The whole point of the authorization code flow is that the actual identity
data travels server-to-server, never through the browser.
Use minecraft_uuid as the stable identifier
Minecraft usernames can be changed. Always use minecraft_uuid as the
primary key when storing or looking up a player in your database.
Store the username alongside it for display purposes, but update it on each
successful auth in case it has changed.
Authorization codes are single-use
Each authorization code can be exchanged exactly once and expires 10 minutes after
issue. If your server receives an invalid_grant error on a code you
have not used, treat it as a potential replay attack and investigate.
Do not trust the code or state values from the callback URL as proof of authentication on their own. They are only meaningful once your server has successfully completed the token exchange.
Error Handling
Your application should handle several failure scenarios gracefully:
- User abandons the flow — the user navigates away before entering their code. Your callback will never fire. Implement a timeout or allow the user to restart the auth flow.
- Code expired — the user took too long to enter their code. MCAuth will show them an error and they can rejoin the server. You do not need to handle this; the user restarts from step 2.
-
invalid_granton token exchange — the code was already used, expired (10-minute window), or theredirect_uridoes not match. Redirect the user to restart the auth flow. - State mismatch — your server-side state does not match the callback. Reject the request and log it — this may indicate a CSRF attempt.
-
server_error— transient issue. Retry with exponential backoff or prompt the user to try again.
Code Examples
Python (stdlib only)
import secrets, urllib.parse, urllib.request, json
from http.server import BaseHTTPRequestHandler, HTTPServer
CLIENT_ID = "your-client-id"
CLIENT_SECRET = "your-client-secret"
REDIRECT_URI = "http://localhost:8081/callback"
MCAUTH_URL = "https://mc-auth.net"
# 1. Generate state and redirect the user
state = secrets.token_hex(16)
authorize_url = (
f"{MCAUTH_URL}/oauth/authorize"
f"?client_id={urllib.parse.quote(CLIENT_ID)}"
f"&redirect_uri={urllib.parse.quote(REDIRECT_URI)}"
f"&state={state}"
)
print(f"Direct user to: {authorize_url}")
# 2. On callback — exchange the code
def exchange_code(code):
data = urllib.parse.urlencode({
"grant_type": "authorization_code",
"code": code,
"client_id": CLIENT_ID,
"client_secret": CLIENT_SECRET,
"redirect_uri": REDIRECT_URI,
}).encode()
req = urllib.request.Request(
f"{MCAUTH_URL}/oauth/token", data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"},
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
# result: {"minecraft_uuid": "...", "minecraft_username": "..."}
Node.js (built-in fetch)
const MCAUTH_URL = "https://mc-auth.net";
const CLIENT_ID = "your-client-id";
const CLIENT_SECRET = "your-client-secret";
const REDIRECT_URI = "https://yourapp.com/auth/callback";
// 1. Build authorization URL (do this server-side, store state in session)
function buildAuthURL(state) {
const params = new URLSearchParams({
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
state: state,
});
return `${MCAUTH_URL}/oauth/authorize?${params}`;
}
// 2. Exchange code for player identity (call this in your callback handler)
async function exchangeCode(code) {
const resp = await fetch(`${MCAUTH_URL}/oauth/token`, {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: new URLSearchParams({
grant_type: "authorization_code",
code: code,
client_id: CLIENT_ID,
client_secret: CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
}),
});
if (!resp.ok) {
const err = await resp.json();
throw new Error(`Token exchange failed: ${err.error}`);
}
return resp.json();
// { minecraft_uuid: "...", minecraft_username: "..." }
}
// 3. Example Express callback route
app.get("/auth/callback", async (req, res) => {
const { code, state } = req.query;
// Always verify state before proceeding
if (state !== req.session.oauthState) {
return res.status(403).send("State mismatch");
}
const player = await exchangeCode(code);
// Link or create account by minecraft_uuid
await db.upsertPlayer({
uuid: player.minecraft_uuid,
username: player.minecraft_username,
});
req.session.playerUUID = player.minecraft_uuid;
res.redirect("/dashboard");
});
Go
package main
import (
"encoding/json"
"net/http"
"net/url"
"strings"
)
const (
mcauthURL = "https://mc-auth.net"
clientID = "your-client-id"
clientSecret = "your-client-secret"
redirectURI = "https://yourapp.com/auth/callback"
)
type PlayerIdentity struct {
UUID string `json:"minecraft_uuid"`
Username string `json:"minecraft_username"`
}
func exchangeCode(code string) (*PlayerIdentity, error) {
resp, err := http.PostForm(mcauthURL+"/oauth/token", url.Values{
"grant_type": {"authorization_code"},
"code": {code},
"client_id": {clientID},
"client_secret": {clientSecret},
"redirect_uri": {redirectURI},
})
if err != nil {
return nil, err
}
defer resp.Body.Close()
var identity PlayerIdentity
if err := json.NewDecoder(resp.Body).Decode(&identity); err != nil {
return nil, err
}
return &identity, nil
}
// In your callback handler:
func callbackHandler(w http.ResponseWriter, r *http.Request) {
code := r.URL.Query().Get("code")
state := r.URL.Query().Get("state")
// Verify state matches session value
sessionState := getSessionState(r)
if state != sessionState {
http.Error(w, "state mismatch", http.StatusForbidden)
return
}
player, err := exchangeCode(code)
if err != nil {
http.Error(w, "auth failed", http.StatusInternalServerError)
return
}
// player.UUID and player.Username are now verified
_ = player
}