OAuth 2.0 Social Login
RESTHeart Cloudrestheart-accounts supports OAuth 2.0 social login with multiple providers out of the box (Google, GitHub) and a Java SPI for custom providers.
The backend handles the complete Authorization Code flow using ScribeJava; the client application performs only two redirects and never handles tokens directly.
Social login is opt-in: it is enabled only when oauthConfig.enabled: true is set in restheart.yml.
Each provider is individually toggled via its own enabled flag inside the providers map.
|
Note
|
This is not the same as RESTHeart’s built-in OAuth 2.1 authorization server (for MCP). See [_mcp_authentication] for details. |
Flow
sequenceDiagram
participant B as Browser
participant R as RESTHeart
participant P as OAuth Provider
participant M as MongoDB
B->>R: GET /auth/oauth/authorize/google
Note over R: generate CSRF state, store in oauth_codes (TTL 10 min)
R-->>B: 302 → provider consent screen
B->>P: user consent
P-->>B: 302 → /auth/oauth/callback?code=…&state=…
B->>R: GET /auth/oauth/callback/google
Note over R: validate state (constant-time, findOneAndDelete)
R->>P: exchange code for access token
P-->>R: access token
R->>P: fetch user profile
P-->>R: user profile
R->>M: upsert user document
Note over R: issue JWT cookie
R-->>B: 302 + JWT cookie → frontend-url
Configuration
oauthConfig:
# Master switch. Set to false to disable all OAuth endpoints while keeping
# the configuration in place.
enabled: true
# Base URL of the RESTHeart API instance.
# Each provider's callback URL is constructed as:
# {api-base-url}/auth/oauth/callback/{provider}
api-base-url: https://api.example.com
# Browser is redirected here after a successful login.
frontend-success-url: https://app.example.com/app
# Browser is redirected here when the OAuth flow fails (e.g. user denied
# consent, invalid state).
frontend-error-url: https://app.example.com/login?error=oauth_error
providers:
google:
enabled: true
client-id: "123….apps.googleusercontent.com"
client-secret: "GOCSPX-…"
scope: "openid email profile" # optional — this is the default
github:
enabled: true
client-id: "Iv1.…"
client-secret: "…"
scope: "user:email" # optional — this is the default
# Custom provider — requires a matching OAuthProvider plugin in plugins/
# myapp:
# enabled: true
# client-id: "…"
# client-secret: "…"
# scope: "read:profile"
|
Note
|
Store all client-secret values as environment variables.
See Secrets management (RHO).
|
Invite-flow integration
Invited users (those with status: "invited") can complete registration via OAuth
without setting a password. The frontend passes invite context as query parameters
on the authorize redirect:
GET /auth/oauth/authorize/google
?pendingInviteToken=abc123
&consentsAccepted=true
| Parameter | Description |
|---|---|
|
The invite token from the activation email |
|
|
The parameters are stored in the CSRF state token (persisted in MongoDB oauth_codes collection, TTL 10 min) and retrieved on callback.
If consentsAccepted=true, the callback builds a ConsentRecord and passes it to
MembershipProvider.activateViaOAuth(). See OAuth activation for invited users.
Security notes
-
A cryptographically random
stateparameter (256-bit entropy) is generated for every authorization request and stored in theoauth_codesMongoDB collection with a TTL index of 600 seconds. -
The collection’s TTL index ensures automatic cleanup. An additional application-level guard rejects tokens older than 10 minutes even if the MongoDB TTL task has not yet fired (fires every ~60 s).
-
State tokens are consumed atomically via
findOneAndDelete— they cannot be replayed even under concurrent load or in a multi-instance deployment. -
On callback, the
codefield is looked up and the document is always deleted before provider-name and TTL checks, preventing replay probing via different provider names. -
Token exchange is performed entirely server-side by the
OAuthProviderimplementation;client_secretvalues are never exposed to the browser.
State token format and per-tenant routing
The state string encodes the MongoDB database to use for both storing and retrieving the token:
state = base64url(dbName) + "." + base64url(32-random-bytes)
The . separator is safe because it is not part of the base64url alphabet.
When a per-tenant database override (override-users-db) is active, the state is stored in that tenant’s oauth_codes collection instead of the default database.
The callback decodes the database name from the state itself, so cross-instance routing works without any shared in-memory state.
Google Cloud Console setup
-
Open Google Cloud Console and select (or create) your project.
-
Navigate to APIs & Services → OAuth consent screen and complete the consent screen configuration (app name, support email, authorized domains).
-
Navigate to APIs & Services → Credentials.
-
Click Create Credentials → OAuth 2.0 Client ID.
-
Set Application type to Web application.
-
Under Authorized redirect URIs, add:
https://api.example.com/auth/oauth/callback/google
Replace the hostname with your actual
oauthConfig.api-base-url. -
Click Create. Copy the Client ID and Client Secret into
oauthConfig.providers.google. -
Ensure the Google People API (or the
openidscope via Google Identity) is enabled for the project — it is required to retrieveemailandprofileclaims.
User behavior
New user (no matching email in the users collection):
-
A new user document is created with
roles: ["user"](the configureddefault-role). Google has already verified the email address, so no separate email verification step is needed. -
A team is created automatically and the user is assigned the
ownermembership role. -
A JWT cookie is set and the browser is redirected to
frontend-success-url.
Existing user (email matches an existing document):
-
The user’s profile fields (
profile.name,profile.surname) are updated from Google’s response if they have changed. -
The user’s system
rolesare not changed — a user who was previously unverified ($unauthenticated) remains blocked. -
A JWT cookie is set and the browser is redirected to
frontend-success-url.
|
Note
|
If the user was registered with email + password, adding Google login does not remove the password. Both authentication methods remain available. |
GitHub
GitHub OAuth App setup
-
Open GitHub Developer Settings → OAuth Apps → New OAuth App.
-
Fill in the Application name and Homepage URL.
-
Set Authorization callback URL to:
https://api.example.com/auth/oauth/callback/github
Replace the hostname with your actual
oauthConfig.api-base-url. -
Click Register application. Copy the Client ID, then click Generate a new client secret and copy the secret.
-
Add both values to
oauthConfig.providers.githubinrestheart.yml.
Email resolution
GitHub does not always include the user’s email address in the main profile response (e.g. when the user has set their email to private).
When the email is absent from https://api.github.com/user, GitHubOAuthProvider automatically fetches https://api.github.com/user/emails and selects the first verified, primary address.
This secondary fetch requires the user:email scope, which is the default.
Narrowing the scope may cause email resolution to fail and the login flow to return an error.
Custom providers
Any Java plugin can register an additional OAuth provider via the OAuthProvider SPI.
The OAuthProvider interface is defined in restheart-commons (org.restheart.plugins.accounts), so custom implementations depend only on restheart-commons — not on restheart-accounts.
Interface contract
// package org.restheart.plugins.accounts (in restheart-commons)
public interface OAuthProvider {
String getProviderName();
String getAuthorizationUrl(String clientId, String clientSecret,
String callbackUrl, String scope, String state);
BsonDocument fetchUserProfile(String clientId, String clientSecret,
String callbackUrl, String scope, String code) throws Exception;
}
Each provider receives the OAuth credentials and redirect URI on every call — no internal service object is shared with the framework. The provider is free to use any HTTP library or OAuth library internally.
Example implementation
import org.bson.BsonDocument;
import org.bson.BsonNull;
import org.bson.BsonString;
import org.restheart.plugins.Initializer;
import org.restheart.plugins.Inject;
import org.restheart.plugins.RegisterPlugin;
import org.restheart.plugins.accounts.OAuthProvider;
import org.restheart.plugins.accounts.OAuthProviderRegistry;
@RegisterPlugin(name = "myappOAuthProvider", description = "MyApp OAuth provider")
public class MyAppOAuthProvider implements OAuthProvider, Initializer {
// Inject OAuthProviderRegistry (not OAuthService) — available in restheart-commons only
@Inject("oauthService")
private OAuthProviderRegistry oauthService;
@Override
public void init() {
oauthService.registerProvider(this);
}
/**
* Must return the key used in oauthConfig.providers.
* The OAuth endpoints derive the provider name from the URL path:
* /auth/oauth/authorize/myapp → getProviderName() == "myapp"
*/
@Override
public String getProviderName() { return "myapp"; }
/**
* Build and return the provider's authorization URL.
* Include the state parameter verbatim — it is required for CSRF protection.
*/
@Override
public String getAuthorizationUrl(String clientId, String clientSecret,
String callbackUrl, String scope, String state) {
// Use any OAuth library or build the URL manually.
// Example with ScribeJava:
// return new ServiceBuilder(clientId)
// .apiSecret(clientSecret).callback(callbackUrl).defaultScope(scope)
// .build(MyAppApi.instance()).getAuthorizationUrl(state);
return "https://myapp.example.com/oauth/authorize"
+ "?client_id=" + clientId
+ "&redirect_uri=" + callbackUrl
+ "&scope=" + scope
+ "&state=" + state;
}
/**
* Exchange the authorization code for an access token, then fetch the user
* profile. Return a BsonDocument with: email, name, providerId, avatarUrl.
*/
@Override
public BsonDocument fetchUserProfile(String clientId, String clientSecret,
String callbackUrl, String scope, String code)
throws Exception {
// 1. Exchange code for access token (use any HTTP client).
var token = exchangeCodeForToken(clientId, clientSecret, callbackUrl, code);
// 2. Fetch user profile and map to BsonDocument.
return new BsonDocument()
.append("email", new BsonString(fetchEmail(token)))
.append("name", new BsonString(fetchName(token)))
.append("providerId", new BsonString(fetchProviderId(token)))
.append("avatarUrl", BsonNull.VALUE);
}
// private helper methods ...
}
Key rules:
-
Implement both
OAuthProviderandInitializer. -
Inject
oauthServicewith@Inject("oauthService")and callregisterProvider(this)insideinit(). -
getProviderName()must return the exact key used in theoauthConfig.providersmap. -
getAuthorizationUrl()must include thestateparameter unchanged in the returned URL. -
fetchUserProfile()is responsible for both the token exchange and the profile fetch. -
fetchUserProfile()must return aBsonDocumentwith the following fields:
| Field | Type | Required | Notes |
|---|---|---|---|
|
|
Yes |
Used as the user’s unique identifier in MongoDB |
|
|
Yes |
Display name |
|
|
Yes |
Provider-assigned user ID |
|
|
No |
Profile picture URL; use |
Configuration for a custom provider
Add the provider key to the providers map and place the compiled plugin JAR in RESTHeart’s plugins/ directory alongside restheart-accounts.jar.
oauthConfig:
enabled: true
api-base-url: https://api.example.com
frontend-success-url: https://app.example.com/app
frontend-error-url: https://app.example.com/login?error=oauth_error
providers:
myapp:
enabled: true
client-id: "…"
client-secret: "…"
scope: "read:profile"
The provider is auto-discovered at startup via @RegisterPlugin.
See also: Security.
MCP Authentication
RESTHeart ships with a built-in OAuth 2.1 authorization server that exposes standard endpoints such as /authorize, /token, and /introspect.
This server is designed for tool and client authentication in MCP (Model Context Protocol) deployments — i.e. it allows MCP clients to obtain tokens that authorize calls to the RESTHeart API.
This is completely separate from the social login feature documented on this page:
| Feature | Endpoints | Purpose | Implemented by |
|---|---|---|---|
Social login (Google, GitHub, custom) |
|
End-user sign-in via OAuth provider |
|
MCP / OAuth 2.1 server |
|
Tool / client authentication for MCP |
RESTHeart core |
Configuring or disabling oauthConfig in restheart-accounts has no effect on RESTHeart’s built-in OAuth 2.1 server, and vice versa.
Refer to the RESTHeart core documentation for MCP OAuth 2.1 setup instructions.