Add a "Sign in with MusiMap" button to your product and let users authenticate with their existing MusiMap account — no new password to remember, no new profile to manage. It works the same way as "Sign in with Google" or "Sign in with Apple": your app hands off the login UI to us, and we hand back a verified user identity.
MusiMap is a standard OpenID Connect (OIDC) provider on top of OAuth 2.0 Authorization Code flow with PKCE. If your framework already has an OIDC client, point it at our discovery document and you're done.
Use the client-credentials flow instead. This page is for letting end users sign in.
Overview
The round-trip looks like this:
- User clicks Sign in with MusiMap in your app.
- You redirect them to our authorize URL with your
client_id, aredirect_uri, astate, anonceand a PKCEcode_challenge. - We authenticate the user (password or Google SSO, per your client's policy) and show them a consent screen listing the scopes you asked for.
- We redirect back to your
redirect_uriwith a one-timecodeand the samestate. - Your server exchanges the
code(plus the PKCEcode_verifier) for anaccess_token, anid_token, and arefresh_token. - You verify the
id_token, create a local session for the user, and you're done.
All the moving parts — authorize, token, userinfo, revoke, JWKS — are advertised via our OIDC discovery document.
Prerequisites
-
A MusiMap OAuth client of type
publicorconfidential(we'll tell you which in the dashboard). -
At least one registered redirect URI.
http://localhostis allowed for local dev; production must behttps://. -
The ability to run the
code_verifier/code_challengePKCE exchange on a server (for confidential clients) or in a backend-for-frontend (for SPAs). -
A session store on your side (cookies, Redis, a database — anything) to hold
state,nonceand the PKCEverifierbetween the two hops.
Step 1 — Build the authorize URL
When the user clicks your sign-in button, redirect their browser to
https://accounts.musimap.com/oauth/authorize with the
parameters below. Generate a fresh state, nonce
and PKCE pair for every request, and persist them server-side keyed by
the user's session:
import base64, hashlib, os, secrets
from urllib.parse import urlencode
def build_authorize_url(client_id: str, redirect_uri: str):
# PKCE — generate a verifier + S256 challenge.
verifier = secrets.token_urlsafe(64)
challenge = base64.urlsafe_b64encode(
hashlib.sha256(verifier.encode()).digest()
).rstrip(b'=').decode()
# Random state + nonce (CSRF + replay protection).
state = secrets.token_urlsafe(32)
nonce = secrets.token_urlsafe(32)
# You MUST persist (verifier, state, nonce) server-side
# to validate them when the user comes back.
params = {
'response_type': 'code',
'client_id': client_id,
'redirect_uri': redirect_uri,
'scope': 'openid profile email',
'state': state,
'nonce': nonce,
'code_challenge': challenge,
'code_challenge_method': 'S256',
}
return (
'https://accounts.musimap.com/oauth/authorize?'
+ urlencode(params),
verifier,
state,
nonce,
)
async function buildAuthorizeUrl(clientId: string, redirectUri: string) {
// PKCE — generate a verifier + S256 challenge.
const verifier = base64url(crypto.getRandomValues(new Uint8Array(64)));
const digest = await crypto.subtle.digest(
"SHA-256", new TextEncoder().encode(verifier),
);
const challenge = base64url(new Uint8Array(digest));
// Random state + nonce (CSRF + replay protection).
const state = base64url(crypto.getRandomValues(new Uint8Array(32)));
const nonce = base64url(crypto.getRandomValues(new Uint8Array(32)));
// You MUST persist (verifier, state, nonce) on the server
// (e.g. an httpOnly cookie) to validate them on return.
const params = new URLSearchParams({
response_type: "code",
client_id: clientId,
redirect_uri: redirectUri,
scope: "openid profile email",
state,
nonce,
code_challenge: challenge,
code_challenge_method: "S256",
});
return {
url: `https://accounts.musimap.com/oauth/authorize?${params}`,
verifier, state, nonce,
};
}
function base64url(bytes: Uint8Array) {
let s = btoa(String.fromCharCode(...bytes));
return s.replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
}
PKCE (S256) is required for all clients — public and confidential. It costs you two lines of code and it's the single best defence against code-injection attacks in OAuth. If you omit code_challenge, the authorize request will be rejected.
Step 2 — User authenticates and consents
We take over from here. The user sees the MusiMap login page (password or Sign in with Google, depending on how your OAuth client is configured), then — on first use or when scopes change — a consent screen listing exactly what your app is asking for.
If the user denies, we redirect back to your
redirect_uri with
?error=access_denied&error_description=...&state=....
Show a friendly "OK, no problem" screen; don't silently retry.
If the user allows, we redirect back with
?code=...&state=.... The code is a
one-time value that expires in 60 seconds.
Step 3 — Exchange the code for tokens
On your redirect_uri handler, first verify that the
state matches what you stored. Then POST the
code plus the PKCE code_verifier you kept
around to our token endpoint:
curl -X POST https://api.musimap.com/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=authorization_code' \
-d 'code=THE_CODE_FROM_THE_CALLBACK' \
-d 'redirect_uri=https://your-app.example.com/oauth/callback' \
-d 'client_id=YOUR_CLIENT_ID' \
-d 'client_secret=YOUR_CLIENT_SECRET' \
-d 'code_verifier=THE_VERIFIER_YOU_STORED'
import requests
def exchange_code(code: str, verifier: str):
r = requests.post(
'https://api.musimap.com/oauth/token',
data={
'grant_type': 'authorization_code',
'code': code,
'redirect_uri': 'https://your-app.example.com/oauth/callback',
'client_id': os.environ['MUSIMAP_CLIENT_ID'],
'client_secret': os.environ['MUSIMAP_CLIENT_SECRET'],
'code_verifier': verifier,
},
timeout=10,
)
r.raise_for_status()
return r.json()
async function exchangeCode(code: string, verifier: string) {
const body = new URLSearchParams({
grant_type: "authorization_code",
code,
redirect_uri: "https://your-app.example.com/oauth/callback",
client_id: process.env.MUSIMAP_CLIENT_ID!,
client_secret: process.env.MUSIMAP_CLIENT_SECRET!,
code_verifier: verifier,
});
const r = await fetch("https://api.musimap.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
if (!r.ok) throw new Error(await r.text());
return r.json();
}
The response contains everything you need to log the user in:
{
"access_token": "eyJhbGciOiJSUzI1NiIs...",
"id_token": "eyJhbGciOiJSUzI1NiIs...",
"refresh_token": "rt_01HXYZ...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_expires_in": 2592000,
"scope": "openid profile email"
}
Before trusting the id_token, validate it against the
signing keys at our JWKS endpoint.
Every mainstream OIDC library does this for you — check the
iss, aud, exp and
nonce claims.
Step 4 — Read the user's profile
The id_token already contains the standard claims for the
scopes you requested. If you'd rather fetch them fresh (e.g. after a
refresh, or to get the latest avatar), hit the
/oauth/userinfo endpoint with the access token:
curl https://api.musimap.com/oauth/userinfo \
-H "Authorization: Bearer $ACCESS_TOKEN"
requests.get(
'https://api.musimap.com/oauth/userinfo',
headers={'Authorization': f'Bearer {access_token}'},
).json()
await fetch("https://api.musimap.com/oauth/userinfo", {
headers: { Authorization: `Bearer ${accessToken}` },
}).then((r) => r.json());
{
"sub": "user_01HXYZABCD...",
"name": "Alex Morgan",
"given_name": "Alex",
"family_name": "Morgan",
"picture": "https://cdn.musimap.com/avatars/alex.jpg",
"locale": "en",
"email": "alex@example.com",
"email_verified": true
}
The fields returned depend on the scopes actually granted — which may be a subset of what you asked for if the user unchecked something in the consent screen.
Scopes
Request only what you need. Users are much more likely to say "yes" to a narrow scope list than to a broad one, and our consent screen shows them exactly what you're asking for.
| Scope | Required | Grants access to |
|---|---|---|
openid |
Yes | Required for OpenID Connect. Returns the user's stable MusiMap ID (sub claim) and an ID token. |
profile |
No | User's display name, given/family name, picture and locale. |
email |
No | User's email address and a boolean telling you whether it's been verified. |
ID token claims
Here are the standard OIDC claims you'll see in the
id_token and /userinfo response, grouped by
the scope that unlocks them:
| Claim | Scope | Example | Description |
|---|---|---|---|
iss |
openid |
https://accounts.musimap.com | The issuer — always this exact string. Always verify it. |
sub |
openid |
user_01HXYZABCD... | Stable MusiMap user ID. Never changes. Use this as your primary key. |
aud |
openid |
your_client_id | The client ID you passed in the authorize request. Verify this matches yours. |
exp |
openid |
1745170820 | ID token expiry, epoch seconds. Tokens are short-lived (~10 min). |
iat |
openid |
1745170220 | Issued-at, epoch seconds. |
nonce |
openid |
a3f1c... | The nonce you sent in the authorize request. Verify it matches. |
name |
profile |
Alex Morgan | Preferred display name, as set in their MusiMap profile. |
given_name |
profile |
Alex | First name. |
family_name |
profile |
Morgan | Last name. |
picture |
profile |
https://cdn.musimap.com/avatars/... | Avatar URL. May be null if the user hasn't set one. |
locale |
profile |
en | BCP-47 language tag. One of en, fr, es today. |
email |
email |
alex@example.com | User's email address. |
email_verified |
email |
true | Whether we've verified the address. Don't trust unverified emails for auth decisions. |
Calling the Web API after sign-in
Once you have an access token from the authorisation code flow, you can call
versioned Web API endpoints at https://api.musimap.com/v1/* with
Authorization: Bearer <token>. Use
GET /v1/me to confirm the user's organisation and catalog context
before making business calls.
For OpenID Connect user claims (email, name, and so on), use
GET /oauth/userinfo instead. That endpoint returns OIDC claims
directly and is not wrapped in the MusiMap envelope. See
Authentication
for the difference between /oauth/* and /v1/*.
Refreshing the access token
Access tokens live for one hour. Refresh tokens live longer (currently
30 days — see refresh_expires_in in the response). When
the access token expires, trade the refresh token in for a new pair:
curl -X POST https://api.musimap.com/oauth/token \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'grant_type=refresh_token' \
-d 'refresh_token=THE_REFRESH_TOKEN' \
-d 'client_id=YOUR_CLIENT_ID' \
-d 'client_secret=YOUR_CLIENT_SECRET'
r = requests.post(
'https://api.musimap.com/oauth/token',
data={
'grant_type': 'refresh_token',
'refresh_token': refresh_token,
'client_id': os.environ['MUSIMAP_CLIENT_ID'],
'client_secret': os.environ['MUSIMAP_CLIENT_SECRET'],
},
timeout=10,
)
r.raise_for_status()
new_tokens = r.json()
const body = new URLSearchParams({
grant_type: "refresh_token",
refresh_token: refreshToken,
client_id: process.env.MUSIMAP_CLIENT_ID!,
client_secret: process.env.MUSIMAP_CLIENT_SECRET!,
});
const r = await fetch("https://api.musimap.com/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body,
});
if (!r.ok) throw new Error(await r.text());
const newTokens = await r.json();
If the refresh call fails with invalid_grant, the
refresh token has been revoked or has expired — you must send the user
back through the full login flow. Clear your local session and show
the sign-in button again.
Logging the user out
Logging the user out of your app is your business (drop the
cookie, clear the session). To also invalidate the tokens on our side,
call /oauth/revoke. We recommend doing this on logout,
especially for shared devices:
curl -X POST https://api.musimap.com/oauth/revoke \
-H 'Content-Type: application/x-www-form-urlencoded' \
-d 'token=THE_ACCESS_OR_REFRESH_TOKEN' \
-d 'token_type_hint=refresh_token' \
-d 'client_id=YOUR_CLIENT_ID' \
-d 'client_secret=YOUR_CLIENT_SECRET'
Revoking a refresh token invalidates all access tokens derived from
it. You can use token_type_hint of either
access_token or refresh_token; if you're
unsure, omit the hint — we'll figure it out.
Discovery & JWKS
Most OIDC libraries only need one thing: the URL of the discovery document. Ours lives here and lists every endpoint, every supported algorithm, every scope:
{
"issuer": "https://accounts.musimap.com",
"authorization_endpoint": "https://accounts.musimap.com/oauth/authorize",
"token_endpoint": "https://api.musimap.com/oauth/token",
"userinfo_endpoint": "https://api.musimap.com/oauth/userinfo",
"revocation_endpoint": "https://api.musimap.com/oauth/revoke",
"jwks_uri": "https://accounts.musimap.com/.well-known/jwks.json",
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"subject_types_supported": ["public"],
"id_token_signing_alg_values_supported": ["RS256"],
"scopes_supported": ["openid", "profile", "email"],
"code_challenge_methods_supported": ["S256"],
"token_endpoint_auth_methods_supported": ["client_secret_post", "client_secret_basic"]
}
The public signing keys used to sign id_tokens are
published at
https://accounts.musimap.com/.well-known/jwks.json and
rotate periodically — don't pin a specific kid in your
code; always fetch the JWKS dynamically and cache it for a few hours.
Security checklist
- Always use PKCE (S256). It's required.
- Always generate and verify
state. Tie it to the user's pre-login session to prevent CSRF on the callback. - Always generate and verify
noncein theid_token. It prevents token replay. - Never put
client_secretin a browser. SPAs and native apps should use a thin backend-for-frontend that holds the secret. - Store session tokens in
httpOnlycookies where possible — notlocalStorage. UseSecure+SameSite=Laxat minimum. - Validate
issandaudon every ID token. A library will do this; make sure it's actually switched on. - Don't trust
emailwithoutemail_verified: true. Usesubas your primary key.
Button assets
Branded "Sign in with MusiMap" buttons (light, dark, monochrome) will ship with the launch of the developers portal. In the meantime, use a neutral button with the text "Sign in with MusiMap" and our wordmark. We'll publish updated brand guidelines and downloadable SVGs before GA.