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 MCPIntegration feature must be enabled for your installation (available on Private Cloud and On-Premises only), or the OAuth endpoints return 404. See Enable OAuth.
  • Managing clients through the API requires a static API token for the Authorization header. You can also create and manage clients in the web UI, where no token is needed.
  • Your client library must support PKCE with the S256 method.

Why OAuth

OAuth 2.1 gives each integration:

  • Scoped access — a client is limited to the permissions it needs (for example sample_read or experiment_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     |
                                                          +------------+
  1. Your app redirects the user to the eLabNext authorize endpoint with response_type=code, client_id, redirect_uri, scope, state, and PKCE parameters.
  2. The user authenticates (if not already logged in) and is presented with a consent page listing the scopes the client is requesting.
  3. The user approves or denies. On approval, eLabNext redirects back to your redirect_uri with a short-lived authorization code (plus state for CSRF protection). On denial, the redirect carries ?error=access_denied instead.
  4. Your app exchanges the code by POSTing it (along with client_secret, redirect_uri, and code_verifier) to the token endpoint.
  5. eLabNext returns a JWT access token (access_token), its type (Bearer), and expiry (expires_in).
  6. Your app uses the access token in the Authorization: Bearer <token> header for subsequent API calls.

Specifications

SpecificationLink
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.

  1. In eLabNext, go to Administration > Groups and edit the subgroup the client should belong to.
  2. Open the OAuth Applications tab.
  3. Click Add application (or Create your first application if the group has none yet).
  4. 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://localhost for 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.
  5. 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_id and client_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-server

Response:

{
  "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 example acme.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=S256
  • response_type — must be code
  • client_id — your OAuth 2.1 client identifier
  • redirect_uri — must match one of the registered URIs
  • scope — space-separated list of requested scopes
  • state — (recommended) a random value for CSRF protection; echoed back in the redirect
  • code_challenge — the PKCE challenge derived from your verifier
  • code_challenge_method — must be S256

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:

CodeMeaning
invalid_requestMissing or malformed parameter
invalid_clientUnknown client_id or wrong client_secret
invalid_grantExpired, used, or invalid authorization code; PKCE mismatch
unsupported_grant_typegrant_type is not authorization_code
access_deniedUser denied the consent request
server_errorInternal 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

SymptomCause / fix
OAuth endpoints return 404MCPIntegration is not enabled for the installation. See Enable OAuth.
invalid_client on token exchangeWrong 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 codeThe code expired or was already used — codes are single-use. Restart the flow.
HTTP 400 with a plain-text scope messageYou requested a scope the client is not registered for. Add it to the client or drop it from the request.
redirect_uri rejectedThe 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
}
ClaimDescription
issIssuer — the eLabNext instance base URL
audAudience — same as issuer (instance is its own resource server)
subeLabNext user ID
client_idThe OAuth 2.1 client that obtained the token
scopeSpace-separated granted scopes
group_idThe subgroup the client belongs to
jtiUnique token identifier
iatIssued-at timestamp
nbfNot-before timestamp; equal to iat
expExpiration 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:

  1. Retrieve the public keys from /.well-known/jwks.json and select the one whose kid matches the token header.
  2. Verify the RS256 signature.
  3. Check that iss and aud both equal your instance base URL, and that exp is 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>:

ScopePermission
sample_readRead samples
sample_writeCreate and update samples
experiment_readRead experiments
experiment_writeCreate and update experiments
protocol_readRead protocols
protocol_writeCreate and update protocols
sample_type_readRead sample types
sample_type_writeCreate and update sample types
file_storage_readRead files and attachments
file_storage_writeUpload and modify files
project_readRead projects
project_writeCreate and update projects
study_readRead studies
study_writeCreate and update studies
notification_readRead notifications
notification_writeManage notification settings
signature_workflow_readRead signature workflows
signature_workflow_writeCreate and update signature workflows
equipment_readRead equipment
equipment_writeCreate and update equipment
task_readRead tasks
task_writeCreate and update tasks
profile_readRead 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 localhost during 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_challenge with S256. This protects against authorization code interception even if the client secret is compromised.
  • Use the state parameter 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 MCPIntegration feature toggle for your installation.
  • Bug reports & questions — file tickets via eLabNext Support.

Did this page help you?