Simple Auth Protocol

Reference implementation in this repo: cme-authentication-mock.

1. Purpose and Scope

This document defines a simplified authentication protocol inspired by OpenID 1.0.

Design constraints:

  • One fixed provider hostname (CME).
  • No OpenID discovery.
  • No Diffie-Hellman association exchange.
  • CME and Relying Parties (RPs) share one secret delivered out-of-band (for example on paper).
  • The protocol does not address user roles and permissions; it only handles authentication.

This is intentionally not full OpenID interoperability. It is a constrained profile for closed deployments.

2. Roles

  • CME (Provider): central login server with fixed hostname.
  • RP (Relying Party): application server that delegates user authentication to CME.
  • User Agent: browser.

Simple interaction overview:

Simple interaction

3. Static Configuration

Each RP and CME must be configured with:

  • provider_endpoint: fixed CME URL, for example https://knihy.90.cz/authentication.
  • shared_secret: high-entropy key exchanged offline (minimum 32 random bytes, base64 encoded in config).
  • allowed_return_to: exact RP callback URL(s), for example https://shift-planner.mathbox.90.cz/callback
  • clock_skew_seconds: default 120.
  • nonce_ttl_seconds: default 600.

4. Protocol Messages

All messages are sent indirectly through browser redirects using HTTP 302 responses.

4.1 Authentication Request (RP → CME)

RP redirects browser to provider_endpoint with query parameters:

  • mode=checkid_setup
  • return_to=<exact RP callback URL>
  • rp_nonce=<RP-generated random nonce>
  • op_ts=<unix timestamp seconds generated by RP>
  • sig=<Base64(HMAC-SHA256(shared_secret, signing_input))>

Signing input is built in this exact order: mode, return_to, op_ts and rp_nonce. CME selects identity after successful user authentication. Nonce value should be a random UUID, preferably UUIDv4. Accepted nonces must be stored on the CME side for the duration defined by nonce_ttl_seconds.

4.2 Positive Authentication Response (CME → RP)

After user login and consent, CME redirects browser to return_to with:

  • mode=id_res
  • useremail=<user email> (optional)
  • username=<user name> (optional)
  • userid=<user id>
  • return_to=<same value as request>
  • rp_nonce=<echo from request>
  • op_ts=<unix timestamp seconds generated by CME>
  • sig=<Base64(HMAC-SHA256(shared_secret, signing_input))>

Signing input is built in this exact order: mode, useremail, username, userid, return_to, rp_nonce, op_ts. If useremail or username is omitted, RP and CME must treat that field as an empty string when rebuilding the signed payload.

4.3 Negative Response (CME -> RP)

It's not necessary to implement it

If authentication is denied or canceled:

  • mode=cancel

No signature is required for cancel responses.

4.4 Diagram

sequence diagram

5. Validation Rules

5.1 Authentication Request Validation (at CME)

CME accepts an authentication request only if all checks pass:

  1. mode equals checkid_setup.
  2. All required fields are present: mode, return_to, rp_nonce, op_ts, sig.
  3. return_to exactly matches an allowlisted callback URL for the requesting RP.
  4. rp_nonce format is valid (recommended: UUIDv4).
  5. op_ts is within allowed clock skew.
  6. Recomputed HMAC-SHA256 signature over mode, return_to, op_ts, rp_nonce exactly matches sig (constant-time compare).

If any check fails, reject the request and log the reason.

5.2 Positive Authentication Response Validation (at RP)

Before accepting identity from id_res, RP must validate:

  1. mode equals id_res.
  2. All required fields are present: mode, userid, return_to, rp_nonce, op_ts, sig. useremail and username are optional and may be empty.
  3. return_to exactly matches the callback URL that is currently handling the response and an allowlisted RP callback URL.
  4. rp_nonce exists, matches the nonce stored for this login attempt, and has not been used before.
  5. op_ts is within allowed clock skew.
  6. Recomputed HMAC-SHA256 signature over mode, useremail, username, userid, return_to, rp_nonce, op_ts exactly matches sig (constant-time compare).
  7. User mapping/authorization policy is satisfied before creating RP session.

If any check fails, reject authentication, clear one-time state as needed, and log the reason.

5.3 Cancel Response Handling

When RP receives mode=cancel, authentication must be treated as failed or canceled.

  1. Do not create RP session.
  2. Invalidate pending login state (nonce/session binding) for that attempt.
  3. Show a user-facing canceled/failed login message.

No signature verification is required for mode=cancel.

6. Security Requirements

  • Use HTTPS for all CME and RP endpoints.
  • Shared secret must be random and never sent in protocol messages.
  • Rotate shared_secret periodically (for example every 90 days).
  • Keep previous secret during grace period to avoid hard cutover failures.
  • Store used nonces for at least nonce_ttl_seconds to prevent replay.
  • Limit and monitor failed signature checks.
  • Use constant-time comparison for signatures.
  • Keep server clocks synchronized (NTP).

6.1 Message Signing

This profile uses deterministic signing with a shared secret.

  1. The message includes sig: Base64(HMAC-SHA256(shared_secret, token_contents)).
  2. token_contents is built by serializing key-value lines in protocol-defined order:
  3. Request (mode=checkid_setup): mode, return_to, op_ts, rp_nonce
  4. Positive response (mode=id_res): mode, useremail, username, userid, return_to, rp_nonce, op_ts
  5. shared_secret MUST NOT be sent in request/response parameters and MUST NOT appear in token_contents.
  6. Key-value line format:
field_name:field_value\n

Rules for token_contents serialization are aligned with OpenID key-value form:

  • No spaces before or after :.
  • Use Unix newline (\n, ASCII 10).
  • Include newline after every line.
  • Use UTF-8 encoding.
  • Optional response fields useremail and username must still occupy their signed position; when absent, serialize them as empty values.

Example token contents for positive response:

mode:id_res
useremail:<value or empty>
username:<value or empty>
userid:<value>
return_to:<value>
rp_nonce:<value>
op_ts:<value>

RP verification MUST rebuild token_contents from received fields in protocol-defined order and compare HMAC in constant time.

6.2 Security Risks and Mitigations

  • Man-in-the-middle (MITM): An attacker intercepts traffic between browser, RP, and CME.
    Mitigation: Enforce HTTPS/TLS everywhere, enable HSTS, and reject mixed-content deployments.
  • Message tampering: An attacker modifies id_res fields in transit.
    Mitigation: Verify sig over the fixed field order and use constant-time signature comparison.
  • Replay attacks: A previously valid signed response is reused later.
    Mitigation: Validate op_ts freshness and store used nonces for nonce_ttl_seconds.
  • Return URL abuse / open redirect: Attacker tries to force callbacks to an untrusted URL.
    Mitigation: CME must strictly allowlist return_to; RP must exact-match callback URL on receipt.
  • Shared secret leakage: Secret is exposed via logs, config mistakes, or backups.
    Mitigation: Store in a secret manager, never log secret material, restrict access, and rotate secrets regularly.
  • Weak nonce generation: Predictable rp_nonce enables replay or request correlation attacks.
    Mitigation: Generate nonces with a cryptographically secure RNG and sufficient entropy.
  • Clock manipulation / skew issues: Incorrect system time weakens op_ts freshness checks.
    Mitigation: Use NTP, enforce bounded clock skew, and alert on significant clock drift.
  • Login CSRF / session confusion at RP: Browser is redirected with a valid response bound to the wrong local session.
    Mitigation: Bind rp_nonce to RP session state and invalidate it immediately after use.
  • Provider impersonation: Client is redirected to a fake provider endpoint.
    Mitigation: Pin the fixed CME hostname in configuration and require valid TLS certificate checks.

7. Minimal Example

7.1 RP → CME

GET https://knihy.90.cz/authentication?
  mode=checkid_setup&
  return_to=https%3A%2F%2Fshift-planner.mathbox.90.cz%2Fcallback&
  op_ts=1772518394&
  rp_nonce=6f7b6b5f9a2c4d5f&
  sig=iKqz7ejTrflNJquQ07r9SiCDBww7zOnAFO4EpEOEfAs=

7.2 CME → RP

GET https://shift-planner.mathbox.90.cz/callback?
  mode=id_res&
  useremail=jan.jirout%40internet-handel.cz&
  username=Honza&
  userid=24234&
  return_to=https%3A%2F%2Fshift-planner.mathbox.90.cz%2Fcallback&
  rp_nonce=6f7b6b5f9a2c4d5f&
  op_ts=1772525600&
  sig=iKqz7ejTrflNJquQ07r9SiCDBww7zOnAFO4EpEOEfAs=

7.3 CME Negative Response (Cancel)

GET <RP app url>/auth/openid/callback?
  mode=cancel

No signature is required for cancel responses.

8. Implementation Checklist

8.1 Provider

  • Fixed endpoint and RP allowlists configured.
  • Shared secret loaded securely.
  • Request validation implemented.
  • Assertion signing implemented with deterministic field order.
  • Audit logging (request id, RP id, decision, reason).

8.2 Relying Party

  • Nonce/state storage implemented.
  • Callback exact URL validation implemented.
  • Signature verification implemented.
  • Replay and timestamp checks implemented.
  • Local user session issuance only after full validation.