Identity
Sign in with Maiaddy
Let people sign in to your web or mobile app with their Maiaddy account, over standard OpenID Connect.
Sign in with Maiaddy lets people use their Maiaddy account in your app. They sign in on Maiaddy's own page, see what your app is asking for, and choose to allow it. Your app gets a stable ID for the person and, if they agree, their name, photo, email address and phone number.
It is standard OpenID Connect, so any certified client library works. This guide
builds a small web app with Node's openid-client, then shows the same sign-in
in a Flutter app with flutter_appauth.
Discovery
Point your library at Maiaddy's issuer and it finds everything else:
Issuer https://accounts.maiaddy.com
Discovery https://accounts.maiaddy.com/.well-known/openid-configuration
People sign in on accounts.maiaddy.com. The token, userinfo, revocation and key
endpoints are on accounts-api.maiaddy.com. Take them from discovery rather than
writing them into your code.
| What | Maiaddy |
|---|---|
| Flow | Authorization code, with PKCE (S256) |
| Response mode | query |
| Grants | authorization_code, refresh_token |
| Client authentication | client_secret_basic or client_secret_post for web apps; none for single-page and native apps |
| ID token signing | RS256 |
| Subject type | pairwise |
| Scopes | openid, profile, email, phone, offline_access |
There is no implicit or hybrid flow, no request, request_uri or claims
parameter, and no logout endpoint (see Signing out).
Create an app
In the console, open Apps and create one. Pick the type that matches where your code runs:
| Type | For | Secret | PKCE |
|---|---|---|---|
| Web app | A server that swaps the code for tokens | Yes | Recommended |
| Single-page app | Code that runs only in the browser | No | Required |
| Native app | iOS, Android and desktop apps | No | Required |
A web app's client secret is shown once, when you create the app. Copy it then. If you lose it, rotate it from the app's page. When you rotate, you can keep the old secret working for up to 7 days while you deploy the new one.
Redirect URIs must match exactly: scheme, host, port and path. Wildcards and fragments aren't allowed.
- Every type can use
https. - Web and single-page apps can use
http://localhostorhttp://127.0.0.1only while the app is in testing. - Native apps can also use
http://localhostorhttp://127.0.0.1at any time, or a scheme of their own in reverse-DNS form, likecom.example.yourapp:/oauthredirect.
You can have 25 apps. Each app can have up to 10 redirect URIs, coming to 1,000 characters together.
Testing, then live
A new app starts in testing. While it is:
- Only you and up to 25 test users can sign in. Add them on the app's Test users page, by the email address of their Maiaddy account. Anyone else is told the app is still in testing, and can switch to another account. Maiaddy doesn't send them back to your app, not even with an error.
- The consent screen tells people that Maiaddy hasn't reviewed the app.
- If a sign-in request is wrong, Maiaddy shows what's wrong on its own page instead of sending the person back to your app, so you can see what to fix.
- The
phonescope isn't available.
When it works, choose Ask for a review on the app. Once Maiaddy has reviewed
it, the app goes live: anyone with a Maiaddy account can sign in, and phone
becomes available. Changes to a live app's consent screen wait for review before
people see them.
Scopes and claims
| Scope | What your app gets |
|---|---|
openid | sub. Required in every request. |
profile | name, given_name, family_name, picture |
email | email, email_verified |
phone | phone_number, phone_number_verified. Live apps only. |
offline_access | A refresh token |
The same claims come in the ID token and from the userinfo endpoint. The ID token describes the person as they were at sign-in; userinfo always answers with their details as they are now.
The Sign in with Maiaddy button
Put this button wherever people choose how to sign in. It comes in three themes, light, dark and blue, as a rectangle or a pill, and says Sign in, Continue or Sign up with Maiaddy. Use Continue on a page that both signs people in and signs them up.
Download the buttons (ZIP): an SVG, a PNG and a 2x PNG of every theme, shape and label, plus the mark alone. They are 40px high. The text is outlined, so the files look the same whatever fonts your page has.
In HTML
If you'd rather not use images, build it from this markup and CSS. Swap
maiaddy-button--light for --dark or --blue, and add
maiaddy-button--pill for the pill.
<a class="maiaddy-button maiaddy-button--light" href="/login">
<svg viewBox="0 0 108.88 117.28" aria-hidden="true">
<path fill="#6d38dd" d="M106.92,117.28l-35.94-.02c-.52,0-1.02-.21-1.38-.57l-13.77-13.78c-.38-.39-.88-.58-1.38-.58-.37,0-.75.11-1.08.32-.11.07-.21.15-.31.25h0s-13.78,13.78-13.78,13.78c-.37.37-.87.57-1.39.57H1.96c-.3,0-.56-.07-.8-.18-.12-.05-.24-.12-.34-.19-.91-.65-1.17-2.06-.24-2.98l28.73-28.71c7.53,4.46,16.32,7.01,25.71,7.01s17.61-2.39,24.98-6.58l28.3,28.32c1.23,1.23.36,3.34-1.38,3.34Z"/>
<path fill="#6ca5f6" d="M55.02,0C32.13,0,13.5,18.62,13.5,41.51c0,13.86,6.83,26.15,17.3,33.7,1.67,1.2,3.43,2.28,5.27,3.23,5.69,2.93,12.13,4.59,18.95,4.59s12.7-1.51,18.2-4.21c1.86-.91,3.65-1.96,5.34-3.13,10.85-7.5,17.97-20.03,17.97-34.18C96.53,18.62,77.91,0,55.02,0ZM57.32,62.91c-.76.08-1.52.12-2.3.12-1.11,0-2.21-.08-3.28-.25-2.28-.35-4.45-1.06-6.43-2.07-7-3.55-11.81-10.83-11.81-19.2,0-11.86,9.65-21.51,21.52-21.51s21.51,9.65,21.51,21.51c0,8.67-5.15,16.16-12.56,19.57-2.06.95-4.3,1.58-6.65,1.83Z"/>
</svg>
<span>Sign in with Maiaddy</span>
</a>
.maiaddy-button {
box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 10px;
height: 40px; padding: 0 16px 0 12px; border: 1px solid; border-radius: 8px;
font: 500 14px/1 Lexend, system-ui, sans-serif; text-decoration: none; white-space: nowrap; cursor: pointer;
}
.maiaddy-button svg { height: 20px; width: auto; flex-shrink: 0; }
.maiaddy-button--light { background: #fff; color: #1a1a1a; border-color: #d0d5dd; }
.maiaddy-button--light:hover { background: #f8f9fa; }
.maiaddy-button--dark { background: #133455; color: #fff; border-color: #133455; }
.maiaddy-button--dark:hover { background: #1a4270; border-color: #1a4270; }
.maiaddy-button--blue { background: #336699; color: #fff; border-color: #336699; }
.maiaddy-button--blue:hover { background: #2a5580; border-color: #2a5580; }
.maiaddy-button--dark path, .maiaddy-button--blue path { fill: #fff; }
.maiaddy-button--pill { border-radius: 999px; padding: 0 18px 0 14px; }
.maiaddy-button:focus-visible { outline: 2px solid #336699; outline-offset: 2px; }
The button uses Lexend, a free font, when your page loads it, and your system font otherwise. The sample below uses this markup.
Rules
- Use the button as it is: don't recolour the mark, change the wording or put another mark inside it.
- Keep it at least 32px high, with clear space around it of at least half its height.
- Show it as prominently as any other way to sign in on the page.
- Use the mark alone only where space is tight, and give it the accessible name "Sign in with Maiaddy".
- In a mobile app, use the SVG or PNG files.
A web app in Node
This builds a working web app in one file, server.js. It signs people in,
shows what your app received, refreshes tokens, and signs out. It uses
openid-client, which checks the
state, nonce and PKCE, and the ID token's signature, issuer, audience and expiry,
for you.
You need Node 20.18 or later, and a Web app in the console with the redirect
URI http://localhost:4000/callback and the scopes openid, profile, email
and offline_access.
mkdir maiaddy-sign-in && cd maiaddy-sign-in
npm init -y
npm pkg set type=module
npm install express express-session openid-client
Put the app's details in .env, and keep this file out of version control:
MAIADDY_CLIENT_ID=mid_app_...
MAIADDY_CLIENT_SECRET=mid_cs_...
MAIADDY_REDIRECT_URI=http://localhost:4000/callback
MAIADDY_SCOPES=openid profile email offline_access
SESSION_SECRET=any-long-random-string
Each block below goes into server.js, in order.
Discovery
The app reads its settings and fetches Maiaddy's discovery document once, at
start-up. Everything after this uses the config it returns.
// Sign in with Maiaddy: a small web app (a "Web app" client, with a secret).
// openid-client does the protocol checks: state, nonce, PKCE, and the id_token's
// signature, issuer, audience and expiry against Maiaddy's published keys.
import crypto from "node:crypto";
import express from "express";
import session from "express-session";
import * as client from "openid-client";
const {
MAIADDY_ISSUER = "https://accounts.maiaddy.com",
MAIADDY_CLIENT_ID,
MAIADDY_CLIENT_SECRET,
MAIADDY_REDIRECT_URI = "http://localhost:4000/callback",
MAIADDY_SCOPES = "openid profile email",
PORT = "4000",
SESSION_SECRET,
} = process.env;
if (!MAIADDY_CLIENT_ID || !MAIADDY_CLIENT_SECRET) {
console.error("Set MAIADDY_CLIENT_ID and MAIADDY_CLIENT_SECRET in .env.");
process.exit(1);
}
// Discovery: every endpoint and the signing keys come from the issuer's
// /.well-known/openid-configuration, so nothing else is hard-coded.
const issuer = new URL(MAIADDY_ISSUER);
const local = issuer.protocol === "http:" && ["localhost", "127.0.0.1"].includes(issuer.hostname);
const config = await client.discovery(
issuer,
MAIADDY_CLIENT_ID,
MAIADDY_CLIENT_SECRET,
undefined,
// Plain http only for an issuer on your own machine. Maiaddy itself is https.
local ? { execute: [client.allowInsecureRequests] } : undefined
);
Sessions and the home page
Your app keeps its own session. Maiaddy's sign-in only starts it. The home page shows the person's details once they have signed in.
const app = express();
app.use(
session({
secret: SESSION_SECRET || crypto.randomBytes(32).toString("hex"),
resave: false,
saveUninitialized: false,
// lax: the cookie still comes back on Maiaddy's redirect to /callback.
cookie: { httpOnly: true, sameSite: "lax", secure: new URL(MAIADDY_REDIRECT_URI).protocol === "https:" },
})
);
app.get("/", (req, res) => {
const user = req.session.user;
if (!user) {
return res.send(
page(`
<p>You're signed out.</p>
<p><a class="maiaddy-button maiaddy-button--light" href="/login">${MAIADDY_MARK}<span>Sign in with Maiaddy</span></a></p>
${req.session.notice ? `<p class="notice">${escape(take(req, "notice"))}</p>` : ""}`)
);
}
res.send(
page(`
${user.picture ? `<img class="avatar" src="${escape(user.picture)}" alt="">` : ""}
<p>Signed in as <strong>${escape(user.name ?? user.sub)}</strong>${user.email ? ` (${escape(user.email)})` : ""}.</p>
${req.session.notice ? `<p class="notice">${escape(take(req, "notice"))}</p>` : ""}
<h2>ID token claims</h2>
<pre>${escape(JSON.stringify(req.session.idTokenClaims, null, 2))}</pre>
<h2>Userinfo</h2>
<pre>${escape(JSON.stringify(user, null, 2))}</pre>
<form method="post" action="/logout"><button>Sign out</button></form>
${req.session.refreshToken ? `<form method="post" action="/refresh"><button>Refresh tokens</button></form>` : ""}`)
);
});
Send the person to Maiaddy
Every sign-in gets a fresh state, nonce and PKCE verifier. They stay in your session until the person comes back.
app.get("/login", async (req, res) => {
// A fresh state, nonce and PKCE verifier for every attempt, kept in this
// app's session until Maiaddy sends the person back.
const codeVerifier = client.randomPKCECodeVerifier();
const state = client.randomState();
const nonce = client.randomNonce();
req.session.pending = { codeVerifier, state, nonce };
const url = client.buildAuthorizationUrl(config, {
redirect_uri: MAIADDY_REDIRECT_URI,
scope: MAIADDY_SCOPES,
code_challenge: await client.calculatePKCECodeChallenge(codeVerifier),
code_challenge_method: "S256",
state,
nonce,
});
req.session.save(() => res.redirect(url.href));
});
The person signs in to Maiaddy if they aren't already, and sees your consent screen the first time. Later sign-ins skip consent unless you ask for more scopes.
Handle the callback
Maiaddy sends the person back with a code, which your server swaps for tokens.
authorizationCodeGrant checks the state, the iss parameter Maiaddy adds to
every response, the PKCE verifier and the ID token. Then the app reads the
person's details from userinfo.
app.get("/callback", async (req, res) => {
const pending = req.session.pending;
delete req.session.pending;
if (!pending) {
return signedOut(req, res, "That sign-in had expired. Try again.");
}
// The URL Maiaddy sent the person back to, on the registered redirect URI.
const currentUrl = new URL(MAIADDY_REDIRECT_URI);
currentUrl.search = new URL(req.originalUrl, currentUrl).search;
try {
const tokens = await client.authorizationCodeGrant(config, currentUrl, {
pkceCodeVerifier: pending.codeVerifier,
expectedState: pending.state,
expectedNonce: pending.nonce,
idTokenExpected: true,
});
const claims = tokens.claims();
// userinfo answers for the access token's person; passing sub makes sure
// it is the same person the id_token named.
const user = await client.fetchUserInfo(config, tokens.access_token, claims.sub);
req.session.regenerate((err) => {
if (err) return res.status(500).send(page("<p>Couldn't start a session.</p>"));
req.session.user = user;
req.session.idTokenClaims = claims;
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
req.session.save(() => res.redirect("/"));
});
} catch (error) {
// The person said no, the app can't be used right now, or a check failed.
// error.error carries the OAuth error code when Maiaddy sent one.
console.error("Sign-in failed:", error.error ?? error.message);
signedOut(req, res, describe(error));
}
});
Refresh, and sign out
With offline_access, your app gets a refresh token. Signing out of your app
revokes it, which also ends the access tokens that came with it.
app.post("/refresh", async (req, res) => {
if (!req.session.refreshToken) return res.redirect("/");
try {
// Refresh tokens rotate: every use answers a new one, and the old one stops working.
const tokens = await client.refreshTokenGrant(config, req.session.refreshToken);
req.session.accessToken = tokens.access_token;
req.session.refreshToken = tokens.refresh_token;
req.session.user = await client.fetchUserInfo(config, tokens.access_token, req.session.idTokenClaims.sub);
req.session.notice = "Tokens refreshed.";
res.redirect("/");
} catch (error) {
// invalid_grant: the person removed the app, changed their password, or the token expired.
console.error("Refresh failed:", error.error ?? error.message);
signedOut(req, res, "Your Maiaddy sign-in has ended. Sign in again.");
}
});
app.post("/logout", async (req, res) => {
// Signing out of this app leaves the person signed in to Maiaddy itself, as
// with any OpenID provider. Revoking the refresh token ends this app's grant
// of tokens; the person isn't asked to consent again next time.
const refreshToken = req.session.refreshToken;
if (refreshToken) {
await client.tokenRevocation(config, refreshToken, { token_type_hint: "refresh_token" }).catch((error) => {
console.error("Revocation failed:", error.message);
});
}
req.session.destroy(() => res.redirect("/"));
});
app.listen(Number(PORT), () => {
console.log(`Sample running on http://localhost:${PORT} (issuer ${issuer.href})`);
});
Helpers
The last block holds the small helpers the routes use: messages for the signed-out page, the Maiaddy mark for the button, and the page itself.
function signedOut(req, res, notice) {
req.session.regenerate(() => {
req.session.notice = notice;
req.session.save(() => res.redirect("/"));
});
}
function describe(error) {
switch (error.error) {
case "access_denied":
return "You chose not to sign in with Maiaddy.";
case "unauthorized_client":
case "invalid_client":
return "This app can't use Sign in with Maiaddy right now.";
default:
return "Sign-in didn't work. Try again.";
}
}
function take(req, key) {
const value = req.session[key];
delete req.session[key];
return value;
}
function escape(value) {
return String(value).replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
}
// The Maiaddy ID mark, for the Sign in with Maiaddy button.
const MAIADDY_MARK = `<svg viewBox="0 0 108.88 117.28" aria-hidden="true"><path fill="#6d38dd" d="M106.92,117.28l-35.94-.02c-.52,0-1.02-.21-1.38-.57l-13.77-13.78c-.38-.39-.88-.58-1.38-.58-.37,0-.75.11-1.08.32-.11.07-.21.15-.31.25h0s-13.78,13.78-13.78,13.78c-.37.37-.87.57-1.39.57H1.96c-.3,0-.56-.07-.8-.18-.12-.05-.24-.12-.34-.19-.91-.65-1.17-2.06-.24-2.98l28.73-28.71c7.53,4.46,16.32,7.01,25.71,7.01s17.61-2.39,24.98-6.58l28.3,28.32c1.23,1.23.36,3.34-1.38,3.34Z"/><path fill="#6ca5f6" d="M55.02,0C32.13,0,13.5,18.62,13.5,41.51c0,13.86,6.83,26.15,17.3,33.7,1.67,1.2,3.43,2.28,5.27,3.23,5.69,2.93,12.13,4.59,18.95,4.59s12.7-1.51,18.2-4.21c1.86-.91,3.65-1.96,5.34-3.13,10.85-7.5,17.97-20.03,17.97-34.18C96.53,18.62,77.91,0,55.02,0ZM57.32,62.91c-.76.08-1.52.12-2.3.12-1.11,0-2.21-.08-3.28-.25-2.28-.35-4.45-1.06-6.43-2.07-7-3.55-11.81-10.83-11.81-19.2,0-11.86,9.65-21.51,21.52-21.51s21.51,9.65,21.51,21.51c0,8.67-5.15,16.16-12.56,19.57-2.06.95-4.3,1.58-6.65,1.83Z"/></svg>`;
function page(body) {
return `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Sign in with Maiaddy sample</title>
<style>
body { font: 15px/1.5 system-ui, sans-serif; max-width: 720px; margin: 40px auto; padding: 0 16px; color: #111; }
.button, button { display: inline-block; padding: 9px 16px; border-radius: 8px; border: 1px solid #336699; background: #336699; color: #fff; text-decoration: none; font: inherit; cursor: pointer; }
.maiaddy-button { box-sizing: border-box; display: inline-flex; align-items: center; gap: 10px; height: 40px; padding: 0 16px 0 12px; border: 1px solid; border-radius: 8px; font: 500 14px/1 Lexend, system-ui, sans-serif; text-decoration: none; }
.maiaddy-button--light { background: #fff; color: #1a1a1a; border-color: #d0d5dd; }
.maiaddy-button--light:hover { background: #f8f9fa; }
.maiaddy-button:focus-visible { outline: 2px solid #336699; outline-offset: 2px; }
.maiaddy-button svg { height: 20px; width: auto; }
form { display: inline-block; margin: 0 8px 8px 0; }
pre { background: #f4f5f7; padding: 12px; border-radius: 8px; overflow-x: auto; font-size: 13px; }
.avatar { width: 56px; height: 56px; border-radius: 50%; }
.notice { color: #8a4b00; }
</style>
</head>
<body>
<h1>Sign in with Maiaddy sample</h1>
${body}
</body>
</html>`;
}
Run it
node --env-file=.env server.js
Open http://localhost:4000 and choose Sign in with Maiaddy. Sign in as
yourself or one of your test users.
A Flutter app
A mobile app is a Native app in the console. It has no secret. Instead, the
app proves it started the sign-in with PKCE, which
flutter_appauth does for you.
Give the app a redirect URI on a scheme of your own, in reverse-DNS form and in
lowercase, such as com.example.yourapp:/oauthredirect.
flutter pub add flutter_appauth http
On Android, tell AppAuth your scheme in android/app/build.gradle.kts:
android {
defaultConfig {
manifestPlaceholders.putAll(
mapOf("appAuthRedirectScheme" to "com.example.yourapp")
)
}
}
On iOS, add it to ios/Runner/Info.plist:
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>com.example.yourapp</string>
</array>
</dict>
</array>
Then sign in, refresh and read userinfo:
import 'dart:convert';
import 'package:flutter_appauth/flutter_appauth.dart';
import 'package:http/http.dart' as http;
const clientId = 'mid_app_...';
const redirectUrl = 'com.example.yourapp:/oauthredirect';
const discoveryUrl = 'https://accounts.maiaddy.com/.well-known/openid-configuration';
const scopes = ['openid', 'profile', 'email', 'offline_access'];
final appAuth = FlutterAppAuth();
/// Signs the person in. Returns null if they close the sign-in page.
Future<AuthorizationTokenResponse?> signIn() async {
try {
return await appAuth.authorizeAndExchangeCode(
AuthorizationTokenRequest(clientId, redirectUrl, discoveryUrl: discoveryUrl, scopes: scopes),
);
} on FlutterAppAuthUserCancelledException {
return null;
}
}
/// Swaps the refresh token for new tokens. Store the new refresh token: the old
/// one stops working.
Future<TokenResponse> refresh(String refreshToken) {
return appAuth.token(
TokenRequest(clientId, redirectUrl, discoveryUrl: discoveryUrl, refreshToken: refreshToken, scopes: scopes),
);
}
/// The person's current details. The address is userinfo_endpoint from discovery.
Future<Map<String, dynamic>> userinfo(String accessToken) async {
final response = await http.get(
Uri.parse('https://accounts-api.maiaddy.com/oauth2/userinfo'),
headers: {'Authorization': 'Bearer $accessToken'},
);
if (response.statusCode != 200) {
throw Exception('userinfo failed: ${response.statusCode}');
}
return jsonDecode(response.body) as Map<String, dynamic>;
}
Keep the tokens in secure storage, such as flutter_secure_storage, not in
shared preferences. If the person chooses Cancel on the consent screen,
authorizeAndExchangeCode throws a FlutterAppAuthPlatformException with the
error access_denied.
If your app has its own backend, don't treat an ID token that the phone sends it as proof of who signed in until the backend has checked its signature. Or have the backend call userinfo with the access token.
Tokens
| Token | Lasts | Notes |
|---|---|---|
| Authorization code | 60 seconds | Works once. A second use is refused, and can end the tokens the first use gave. |
| Access token | 1 hour | Opaque, starts mid_at_. Send it to userinfo. |
| ID token | 1 hour | A JWT signed with RS256. Check it, then use its claims. |
| Refresh token | 30 days unused | Only with offline_access. Starts mid_rt_. |
Every token response has an ID token, refreshes included. A refreshed ID token
has no nonce.
At the token endpoint, a web app sends its secret with HTTP Basic or in the form
body, never both. Single-page and native apps send only their client_id and the
PKCE code_verifier. The token, userinfo and revocation endpoints accept calls
from any origin, so a single-page app can call them from the browser.
The revocation endpoint always answers 200. Revoking a refresh token also ends
the access tokens that came with it.
Signing out
Maiaddy has no logout endpoint for apps, and doesn't tell your app when someone signs out of Maiaddy. To sign someone out of your app, end your own session and revoke the refresh token, as the web app above does.
The person stays signed in to Maiaddy, so their next sign-in may come straight
back without a password. To make them sign in again, add prompt=login to the
authorization request. To let them pick another Maiaddy account, use
prompt=select_account.
What ends your tokens
| When | Your tokens | Consent |
|---|---|---|
| The person changes their password or signs out everywhere | End | Kept |
| The person removes your app from their connected apps | End | Removed: they're asked again |
| You pause the app | Refused while paused, then work again | Kept |
| You delete the app | End | Removed |
| Maiaddy suspends the app | End | Kept |
| The person signs out of Maiaddy | Keep working | Kept |
A refresh that fails with invalid_grant means the person has to sign in again.
Errors
At the authorization endpoint, Maiaddy sends the person back to your redirect
URI with error, error_description, your state and iss:
error | Why |
|---|---|
access_denied | The person chose Cancel. |
invalid_request | Something in the request is missing or wrong, such as PKCE for a single-page or native app. |
invalid_scope | openid is missing, or the app isn't set up for a scope it asked for. |
login_required | prompt=none, and the person isn't signed in to Maiaddy. |
consent_required | prompt=none, and the person hasn't allowed everything you asked for. |
account_selection_required | prompt=none, and more than one account is signed in. Send login_hint with the person's email address. |
If the client_id is unknown or the redirect URI isn't registered, Maiaddy
can't safely send the person back, so it shows an error page instead. It does
the same while your app is paused or suspended. While your app is in testing,
it also shows request errors such as invalid_request and invalid_scope on the
page, and prompt=none errors where Maiaddy can't tell who is signing in.
The token endpoint answers errors as JSON, error and error_description:
error | Why |
|---|---|
invalid_grant | The code or refresh token is expired, used, or revoked, or the person removed your app. |
invalid_client | The client ID or secret is wrong. |
unauthorized_client | The app is paused or suspended. |
invalid_request | Something is missing or wrong, such as the secret sent both ways at once. |
unsupported_grant_type | Only authorization_code and refresh_token are supported. |
Userinfo answers 401 with invalid_token for a token it doesn't accept, and
403 with insufficient_scope for one granted without openid.
What's next
- Manage your apps, test users and consent screen in Apps.