OAuth 2.1 developer guide
Authenticate to the eLabNext API with OAuth 2.1 — the Authorization Code flow with PKCE and JWT access tokens.
OAuth 2.1 developer guide — eLabNext API authentication
This guide shows you how to authenticate to the eLabNext API with OAuth 2.1: register a client, run the Authorization Code flow with PKCE, and use the JWT access token you get back. Here eLabNext is the authorization server — it issues the tokens your app uses to call the eLabNext API.
Building an add-on that signs users in to your own external service? That is the other direction of OAuth, where eLabNext acts as the client. See OAuth setup for add-ons instead.
Before you start
- The
MCPIntegrationfeature must be enabled for your installation (available on Private Cloud and On-Premises only), or the OAuth endpoints return404. See Enable OAuth. - Managing clients through the API requires a static API token for the
Authorizationheader. You can also create and manage clients in the web UI, where no token is needed. - Your client library must support PKCE with the
S256method.
Why OAuth
OAuth 2.1 gives each integration:
- Scoped access — a client is limited to the permissions it needs (for example
sample_readorexperiment_write). - Short-lived tokens — access tokens expire one calendar month after issuance; repeat the flow to get a new one.
- User consent — the eLabNext user approves each client's requested scopes on a consent page.
- Per-client revocation — deactivate or rotate one client without affecting any other.
Enable OAuth
OAuth 2.1 — and the MCP integration it powers — is available only on Private Cloud and On-Premises installations, not on the shared Cloud (SaaS) environment.
On a supported installation, OAuth 2.1 is controlled by the MCPIntegration feature toggle, which is disabled by default. Contact eLabNext Support ([email protected]) and request that MCPIntegration be enabled for your instance.
Once enabled, the following become available:
- OAuth 2.1 client management (UI + API)
- The authorize, token, and metadata endpoints
- The consent page
- JWT-based access token validation
How the flow works
eLabNext implements the Authorization Code flow with PKCE (OAuth 2.1, consolidating RFC 6749 §4.1 + RFC 7636) and issues JWT access tokens (RFC 9068). PKCE (S256) is required for all clients — the implicit grant, password grant, and plaintext code challenges are not supported.
+------------+ +------------+ +------------+
| Your | | eLabNext | | eLabNext |
| App | (A) auth | Login | (B) shows | Consent |
| (Client) | --------> | (optional) | --------> | Page |
+------------+ +------------+ +------------+
| |
| (C) Redirect to redirect_uri with code + state |
|<-------------------------------------------------+
|
| (D) POST code + client_secret + code_verifier +------------+
+------------------------------------------------->| eLabNext |
| | Token |
| (E) JWT access_token + expires_in | Endpoint |
|<-------------------------------------------------+------------+
|
| (F) Authorization: Bearer <JWT> +------------+
+------------------------------------------------->| eLabNext |
| API |
+------------+
- Your app redirects the user to the eLabNext authorize endpoint with
response_type=code,client_id,redirect_uri,scope,state, and PKCE parameters. - The user authenticates (if not already logged in) and is presented with a consent page listing the scopes the client is requesting.
- The user approves or denies. On approval, eLabNext redirects back to your
redirect_uriwith a short-lived authorizationcode(plusstatefor CSRF protection). On denial, the redirect carries?error=access_deniedinstead. - Your app exchanges the code by POSTing it (along with
client_secret,redirect_uri, andcode_verifier) to the token endpoint. - eLabNext returns a JWT access token (
access_token), its type (Bearer), and expiry (expires_in). - Your app uses the access token in the
Authorization: Bearer <token>header for subsequent API calls.
Specifications
| Specification | Link |
|---|---|
| OAuth 2.1 Authorization Framework (draft) | https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1 |
| OAuth 2.0 Authorization Framework (RFC 6749) | https://datatracker.ietf.org/doc/html/rfc6749 |
| Proof Key for Code Exchange — PKCE (RFC 7636) | https://datatracker.ietf.org/doc/html/rfc7636 |
| OAuth 2.0 Authorization Server Metadata (RFC 8414) | https://datatracker.ietf.org/doc/html/rfc8414 |
| JSON Web Token Profile for OAuth 2.0 Access Tokens (RFC 9068) | https://datatracker.ietf.org/doc/html/rfc9068 |
| OAuth 2.0 Threat Model and Security Considerations (RFC 6819) | https://datatracker.ietf.org/doc/html/rfc6819 |
| OAuth Security Best Current Practice (BCP) | https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics |
Creating an OAuth client
Via the web UI
OAuth 2.1 clients belong to a subgroup, and the management UI only appears once the MCPIntegration toggle is enabled for the installation.
- In eLabNext, go to Administration > Groups and edit the subgroup the client should belong to.
- Open the OAuth Applications tab.
- Click Add application (or Create your first application if the group has none yet).
- Fill in:
- Application name — a human-readable name shown on the consent page.
- Client ID — a public identifier (3–64 characters: letters, digits,
-or_). This is immutable once set. - Redirect URIs — add one or more. Each must use HTTPS (except
http://localhostfor local development); use Add redirect URI for more than one. - Expires (optional) — an optional expiration date for the client registration.
- Scopes — select the permissions this client needs, grouped by resource with read and write for each. At least one is required; the Read only, Select all, and Clear all shortcuts help.
- Click Create. The client secret is shown exactly once — copy and store it securely. If lost, you can regenerate it from the client's Actions menu.
Via the API
Create a client
POST /api/v1/oauth2/clients
Authorization: Bearer <static-api-token>
Content-Type: application/json
{
"client_id": "my-integration",
"client_name": "My Integration",
"redirect_uris": ["https://myapp.com/callback"],
"scopes": ["sample_read", "experiment_read"],
"expires": "2027-12-31T23:59:59Z"
}Response (HTTP 201):
{
"client_id": "my-integration",
"client_name": "My Integration",
"client_secret": "a3Bf...<auto-generated-secret>",
"redirect_uris": ["https://myapp.com/callback"],
"scopes": ["sample_read", "experiment_read"],
"is_active": true,
"created": "2026-07-02T12:00:00Z",
"expires": "2027-12-31T23:59:59Z",
"created_by_user_id": 42,
"subgroupID": 7
}The client_secret is returned only on creation (and on regeneration). Subsequent GET requests omit it.
List clients
GET /api/v1/oauth2/clients
Authorization: Bearer <static-api-token>Returns all OAuth 2.1 clients for the caller's primary working subgroup.
Get a single client
GET /api/v1/oauth2/clients/{clientId}
Authorization: Bearer <static-api-token>Update a client
PUT /api/v1/oauth2/clients/{clientId}
Authorization: Bearer <static-api-token>
Content-Type: application/json
{
"client_name": "Renamed Integration",
"redirect_uris": ["https://myapp.com/new-callback"],
"scopes": ["sample_read", "sample_write", "experiment_read"],
"is_active": true,
"expires": "2028-01-01T23:59:59Z"
}The client_id is immutable and cannot be changed.
Delete a client
DELETE /api/v1/oauth2/clients/{clientId}
Authorization: Bearer <static-api-token>Deletes the client and all associated authorization codes. Existing access tokens are revoked implicitly because the client no longer resolves.
Regenerate client secret
POST /api/v1/oauth2/clients/{clientId}/regenerate-secret
Authorization: Bearer <static-api-token>Response:
{
"client_id": "my-integration",
"client_secret": "new-auto-generated-secret"
}The old secret is immediately invalidated. Update your application with the new secret.
Performing the authorization code flow
Prerequisites
- An OAuth 2.1 client with a known
client_idandclient_secret - At least one registered
redirect_uri - PKCE support in your client library (S256 method)
1. Discover the endpoints
eLabNext publishes an RFC 8414 metadata endpoint:
GET /.well-known/oauth-authorization-serverResponse:
{
"issuer": "https://<your-instance>",
"authorization_endpoint": "https://<your-instance>/api/v1/auth/oauth2/authorize",
"token_endpoint": "https://<your-instance>/api/v1/auth/oauth2/token",
"jwks_uri": "https://<your-instance>/.well-known/jwks.json",
"scopes_supported": ["sample_read", "sample_write", "experiment_read", ...],
"response_types_supported": ["code"],
"grant_types_supported": ["authorization_code"],
"token_endpoint_auth_methods_supported": ["client_secret_post"],
"code_challenge_methods_supported": ["S256"]
}
<your-instance>is your instance's base URL — the address you use to sign in to your eLabNext / SciSure Research environment. It varies by deployment (for exampleacme.elabnext.com,acme.elabjournal.com, or a custom domain for a self-hosted instance), so substitute your own and read the exact endpoints from this metadata response rather than assuming a domain.
2. Generate PKCE parameters
Create a cryptographically random code_verifier (43–128 characters from the unreserved character set: A-Z, a-z, 0-9, -, ., _, ~). Compute the code_challenge as:
code_challenge = BASE64URL(SHA256(ASCII(code_verifier)))
3. Initiate authorization
Redirect the user's browser to:
GET /api/v1/auth/oauth2/authorize
?response_type=code
&client_id=my-integration
&redirect_uri=https://myapp.com/callback
&scope=sample_read experiment_read
&state=<random-csrf-token>
&code_challenge=<sha256-base64url-of-verifier>
&code_challenge_method=S256response_type— must becodeclient_id— your OAuth 2.1 client identifierredirect_uri— must match one of the registered URIsscope— space-separated list of requested scopesstate— (recommended) a random value for CSRF protection; echoed back in the redirectcode_challenge— the PKCE challenge derived from your verifiercode_challenge_method— must beS256
If the user is not authenticated, they are redirected to the login page first. After login, they see the consent page and must approve the requested scopes.
4. Receive the authorization code
On approval, eLabNext redirects to your redirect_uri:
https://myapp.com/callback?code=<short-lived-code>&state=<original-state>
Validate that state matches the value you sent (CSRF protection).
5. Exchange the code for an access token
POST /api/v1/auth/oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code
&code=<code-from-redirect>
&client_id=my-integration
&client_secret=<your-client-secret>
&redirect_uri=https://myapp.com/callback
&code_verifier=<original-code-verifier>Successful response (HTTP 200):
{
"access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6Ij[...]",
"token_type": "Bearer",
"expires_in": 2678400,
"scope": "sample_read experiment_read"
}The access_token is a JWT (RFC 9068) signed with RS256. Its public key is available at /.well-known/jwks.json for external verification. The token is valid until one calendar month after the moment it was issued. Because calendar months differ in length, expires_in varies from roughly 2,419,200 to 2,678,400 seconds (28–31 days) — always read it from the response rather than hard-coding a fixed value.
6. Use the access token
Include the token in the Authorization header for API requests:
GET /api/v1/samples
Authorization: Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6Ij[...]Renewing an access token
eLabNext does not currently issue refresh tokens. When an access token expires — one calendar month after it was issued — run the Authorization Code flow again to obtain a new one. Build your integration to repeat the flow on expiry rather than relying on a refresh endpoint.
Error responses
Errors follow RFC 6749 §5.2:
{
"error": "invalid_grant",
"error_description": "PKCE verification failed"
}Common error codes:
| Code | Meaning |
|---|---|
invalid_request | Missing or malformed parameter |
invalid_client | Unknown client_id or wrong client_secret |
invalid_grant | Expired, used, or invalid authorization code; PKCE mismatch |
unsupported_grant_type | grant_type is not authorization_code |
access_denied | User denied the consent request |
server_error | Internal server error |
Requesting a scope the client is not registered for is not returned as a structured OAuth error. The authorize endpoint responds with an HTTP 400 and a plain-text message instead, so detect scope problems by the status code rather than by an error field.
Troubleshooting
| Symptom | Cause / fix |
|---|---|
| OAuth endpoints return 404 | MCPIntegration is not enabled for the installation. See Enable OAuth. |
invalid_client on token exchange | Wrong client_id or client_secret, or the client was deactivated or deleted. |
invalid_grant — "PKCE verification failed" | The code_verifier does not match the code_challenge you sent. Send the verifier that produced the challenge. |
invalid_grant on the authorization code | The code expired or was already used — codes are single-use. Restart the flow. |
| HTTP 400 with a plain-text scope message | You requested a scope the client is not registered for. Add it to the client or drop it from the request. |
redirect_uri rejected | The redirect_uri must exactly match one registered on the client and use HTTPS (except http://localhost). |
JWT access token structure
Tokens are JWTs signed with RS256 using the instance's OAUTH2SIGNING certificate. Decoded payload:
{
"iss": "https://<your-instance>",
"aud": "https://<your-instance>",
"sub": "42",
"client_id": "my-integration",
"scope": "sample_read experiment_read",
"group_id": "7",
"jti": "550e8400-e29b-41d4-a716-446655440000",
"iat": 1688140800,
"nbf": 1688140800,
"exp": 1690822800
}| Claim | Description |
|---|---|
iss | Issuer — the eLabNext instance base URL |
aud | Audience — same as issuer (instance is its own resource server) |
sub | eLabNext user ID |
client_id | The OAuth 2.1 client that obtained the token |
scope | Space-separated granted scopes |
group_id | The subgroup the client belongs to |
jti | Unique token identifier |
iat | Issued-at timestamp |
nbf | Not-before timestamp; equal to iat |
exp | Expiration timestamp (one calendar month after issuance) |
The header contains "typ": "at+jwt" per RFC 9068 and "kid" matching the JWKS key at /.well-known/jwks.json.
Validating access tokens
If you verify tokens yourself instead of treating them as opaque, fetch the signing keys from the JWKS endpoint and validate the token as an RS256 JWT:
- Retrieve the public keys from
/.well-known/jwks.jsonand select the one whosekidmatches the token header. - Verify the RS256 signature.
- Check that
issandaudboth equal your instance base URL, and thatexpis in the future.
Use a standard JWT library for your language — for example jsonwebtoken with jwks-rsa in Node.js, or PyJWT with PyJWKClient in Python — rather than validating by hand.
Available scopes
The full list of supported scopes is available from the metadata endpoint (/.well-known/oauth-authorization-server, field scopes_supported). Scopes follow the pattern <resource>_<action>:
| Scope | Permission |
|---|---|
sample_read | Read samples |
sample_write | Create and update samples |
experiment_read | Read experiments |
experiment_write | Create and update experiments |
protocol_read | Read protocols |
protocol_write | Create and update protocols |
sample_type_read | Read sample types |
sample_type_write | Create and update sample types |
file_storage_read | Read files and attachments |
file_storage_write | Upload and modify files |
project_read | Read projects |
project_write | Create and update projects |
study_read | Read studies |
study_write | Create and update studies |
notification_read | Read notifications |
notification_write | Manage notification settings |
signature_workflow_read | Read signature workflows |
signature_workflow_write | Create and update signature workflows |
equipment_read | Read equipment |
equipment_write | Create and update equipment |
task_read | Read tasks |
task_write | Create and update tasks |
profile_read | Read user profile |
The authoritative list is always available from the metadata endpoint (/.well-known/oauth-authorization-server, field scopes_supported).
Code examples
A complete, runnable walk-through — PKCE generation, the authorize redirect, the token exchange, and an authenticated API call — is available as a recipe in JavaScript and Python:
To try the token exchange by hand from the command line:
# 1. Generate PKCE challenge
CODE_VERIFIER=$(openssl rand -base64 32 | tr '+/' '-_' | tr -d '=')
CODE_CHALLENGE=$(echo -n "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 -A | tr '+/' '-_' | tr -d '=')
# 2. Open in browser (paste URL)
echo "https://<your-instance>/api/v1/auth/oauth2/authorize?response_type=code&client_id=my-integration&redirect_uri=https://myapp.com/callback&scope=sample_read&state=xyz&code_challenge=$CODE_CHALLENGE&code_challenge_method=S256"
# 3. After redirect, extract ?code=... and exchange it
curl -X POST "https://<your-instance>/api/v1/auth/oauth2/token" \
-d "grant_type=authorization_code" \
-d "code=CODE_FROM_REDIRECT" \
-d "client_id=my-integration" \
-d "client_secret=YOUR_SECRET" \
-d "redirect_uri=https://myapp.com/callback" \
-d "code_verifier=$CODE_VERIFIER"
# 4. Use the token
curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://<your-instance>/api/v1/samples"Security considerations
- HTTPS is mandatory — all OAuth 2.1 endpoints and redirect URIs must use HTTPS. Plain HTTP is accepted only for
localhostduring development. - Client secrets are encrypted at rest and returned in plaintext only at creation or regeneration time. Store them securely (environment variables, secret manager, vault).
- PKCE is required — the authorize endpoint rejects requests without a valid
code_challengewithS256. This protects against authorization code interception even if the client secret is compromised. - Use the
stateparameter to prevent CSRF attacks against the authorization flow. Validate it on receipt. - Access tokens expire one calendar month after issuance and are signed JWTs (RS256). Validate the signature, issuer, audience, and expiry if you verify tokens externally.
- Client deactivation/revocation — deactivate or delete a client from the administration UI or API to immediately prevent new authorizations. Existing tokens are invalidated because the client no longer resolves.
- Do not embed client secrets in client-side code — the Authorization Code flow with PKCE is designed for confidential clients. For browser-only apps, proxy the flow through a backend.
Support
- Enable OAuth 2.1 — contact eLabNext Support at [email protected] to request the
MCPIntegrationfeature toggle for your installation. - Bug reports & questions — file tickets via eLabNext Support.
Updated about 3 hours ago