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:

FieldDescription
NameA human-readable name shown to users on the authorization page.
Redirect URIThe URL MCAuth redirects to after the user authenticates. Must exactly match the URI you use in authorization requests. Use https:// in production.
Code ExpiryHow 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.

1
Your server redirects the user to MCAuth You construct an authorization URL and send the user's browser there. MCAuth validates your client_id and redirect_uri, then shows the user a page explaining what to do next.
2
The user joins the Minecraft server Upon joining, the MCAuth Velocity plugin calls your configured MCAuth server and generates a unique 6-character code. The code is displayed in the player's chat and as a title on screen.
3
The user enters their code on the MCAuth website The user clicks "I have my code" on the MCAuth authorize page, enters the code they saw in Minecraft, and submits. MCAuth validates the code and marks the auth session as complete.
4
MCAuth redirects back to your application The user's browser is redirected to your redirect_uri with a short-lived authorization code and your original state value appended as query parameters.
5
Your server exchanges the code for player identity Using your 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 1 — Authorization Request

GET
/oauth/authorize

Redirect the user's browser to this URL. MCAuth will validate your parameters, create a pending auth session, and present the user with instructions for joining the Minecraft server.

Query Parameters

ParameterDescription
client_id required Your application's client ID, available on the application page.
redirect_uri required The URI to redirect to after authentication. Must exactly match the redirect URI registered for your application — including scheme, host, port, and path.
state required An opaque value you generate. MCAuth returns it unchanged in the callback. You must verify it matches to prevent CSRF attacks. Use a cryptographically random string tied to the user's session.

Example

GET /oauth/authorize
  ?client_id=3f7a2b19-04cd-4e8a-b91d-0c2f5e6d7a8b
  &redirect_uri=https%3A%2F%2Fyourapp.com%2Fauth%2Fcallback
  &state=k3jH9mXpQ2wRvTz8

Error Responses

If parameters are invalid, MCAuth returns an HTTP error directly (not a redirect):

  • 400 Bad Request — missing parameters, unknown client_id, or redirect_uri mismatch.

Do not construct the authorization URL client-side. Generate the state value on your server, store it in the user's session, then redirect. This is the only way to verify it on the callback.

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 Expiry value 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

POST
/oauth/token

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:

ParameterDescription
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"
}
FieldDescription
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:

ErrorStatusCause
invalid_request400Missing or malformed parameters, or grant_type is not authorization_code.
invalid_client401Unknown client_id or incorrect client_secret.
invalid_grant400The code is unknown, already used, expired, belongs to a different client, or the redirect_uri does not match.
server_error500Internal 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_grant on token exchange — the code was already used, expired (10-minute window), or the redirect_uri does 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
}