OAuth2 / OpenID Connect – connecting a client application

HORES includes its own OAuth 2.0 / OpenID Connect authorization server. It runs in the daemon (hores_daemon.py) as the OAuth2 interface and is available under the /auth path. This page describes what a client application developer needs to connect to HORES.

1. Addresses

The base address (issuer) is https://<server>/auth. The actual value is set by the HORES administrator in the OAuth2 interface settings (Public server URL (issuer), e.g. https://pms.example.com/auth). The setting is required, the interface does not start without a valid value: an absolute URL whose path ends with /auth, no trailing slash, https (http only for localhost). The issuer in every response (iss, the addresses in discovery) is always this value and does not depend on the address a client used to reach the server.

The daemon listens on port 7443 (HTTPS) and 7080 (HTTP). Only HTTPS is used in production, usually behind a reverse proxy. TLS is up to the deployment: HORES itself does not enforce HTTPS. The login page cookie gets the Secure flag when the issuer starts with https://.

If your client library supports it, just give it the discovery URL. It loads all other addresses by itself:

https://<server>/auth/.well-known/openid-configuration

Endpoint

URL

Method

Discovery

/auth/.well-known/openid-configuration

GET

JWKS (keys for id_token verification)

/auth/.well-known/jwks.json

GET

Authorization

/auth/authorize

GET

Token

/auth/token

POST

UserInfo

/auth/userinfo

GET, POST

Token revocation (RFC 7009)

/auth/revoke

POST

Token introspection (RFC 7662)

/auth/introspect

POST

Logout from the login page

/auth/logout

GET

2. Client registration

Clients cannot be registered dynamically. The HORES administrator creates the client in the desktop application, menu OAuth2 settings → Client applications. The administrator gives you:

  • client_id: HORES prefills a randomly generated value, the administrator may replace it with a readable one (max. 48 characters, only A-Z a-z 0-9 . _ ~ -). It cannot be changed once saved.

  • client_secret (confidential clients only): shown only once, when it is generated. HORES stores only its hash, so a forgotten secret cannot be recovered, only a new one generated.

Tell the administrator:

Item

Note

Client type

Confidential (server application that can keep a secret): client_secret_basic or client_secret_post.

Public (native mobile or desktop application): none, no secret. Single-page applications (SPA) in a browser are not supported yet, see 2.1 Web applications (SPA).

Grant types

authorization_code, refresh_token, client_credentials (see below). HORES does not support the implicit grant.

Redirect URIs

One or more, must match exactly. Allowed are https://, http:// only for localhost/127.0.0.1/::1, and a custom scheme for native applications (com.example.app:/callback). No fragment (#…).

Scopes

Which of the scopes listed below the client may request.

Act as user

Only for client_credentials, see 4.3 Client Credentials (server to server).

Resource server

Only for a confidential client that verifies tokens issued to other clients through /auth/introspect (see oauth2-introspection).

2.1 Web applications (SPA)

A JavaScript application running in a browser on a different domain than HORES is not supported yet. Neither the /auth/token, /auth/userinfo and /auth/revoke endpoints nor the HORES APIs send CORS headers, so the browser blocks fetch() calls to them. Connect a web application as a confidential client through its own backend (the „backend for frontend“ pattern): the application server exchanges the code, stores the tokens and calls the HORES API, and the browser never sees the tokens.

3. Scopes

Scope

Meaning

openid

Sign-in via OpenID Connect, the response contains an id_token.

profile

User details: preferred_username, name, locale.

hores_api

Access to the HORES GraphQL API (/api2/graphql) on behalf of the user.

web_api

Access to the HORES Web API (/api/...) on behalf of the user.

A client gets at most the scopes allowed in its registration. A request for an unknown scope is rejected with invalid_scope. What the user may actually do in the API is decided by their HORES user rights, not by the scope. The scope only determines which service the token can be used for.

4. Flows (grant types)

4.2 Refresh token

POST /auth/token
grant_type=refresh_token&refresh_token=<refresh_token>

Client authentication is the same as for the code exchange. Refresh tokens are rotated: every use returns a new access + refresh token pair and invalidates the old pair. The client must therefore always store the new refresh_token.

  • Reusing an old refresh token is treated as possible theft. The request fails with invalid_grant and all tokens that came from the same sign-in are invalidated as well, including the newest pair, so the user has to sign in again. The same happens when a client sends the same refresh token twice concurrently, so serialize refreshes in the client.

  • Lifetime: a refresh token expires after 60 days of inactivity. Every rotation issues a new token with another 60 days, so a client that refreshes regularly stays signed in. Unused for 60 days, the user has to sign in again.

  • On refresh the server checks the current state again: the user must still be allowed to sign in (API access, validity, not deleted), otherwise the refresh fails with invalid_grant and the tokens of that sign-in are invalidated. The new token only gets the scopes the client’s registration allows now: a scope the administrator has removed from the client since is not renewed.

4.3 Client Credentials (server to server)

Confidential clients only (client_secret_basic / client_secret_post), no refresh token is issued.

POST /auth/token
Authorization: Basic base64(client_id:client_secret)

grant_type=client_credentials&scope=hores_api
  • If the client has Act as user set in HORES, the token is issued on behalf of that technical user. That user’s rights apply, and hores_api, web_api, openid/profile and /userinfo all work with it. If the user has been deactivated in the meantime, the server returns unauthorized_client.

  • Without an assigned user the token gets no user-bound scope (openid, profile, hores_api, web_api), so it cannot be used for the HORES API.

4.4 Implicit (not supported)

HORES does not support the implicit grant (response_type=token), which RFC 9700 says must no longer be used. A request with response_type=token is rejected with unsupported_response_type. Use Authorization Code + PKCE.

5. Tokens

Format

Validity

Access token

opaque string

8 hours

Refresh token

opaque string

60 days since last use, rotated on every use

id_token

JWT, RS256 signature

iss = issuer, aud = client_id, sub = HORES user ID, nonce if sent

Authorization code

opaque string

5 minutes, single use

  • Verify the id_token with the key from JWKS (kid in the header) and check iss, aud, exp and nonce.

  • The access token is opaque and the client must not decode it. Use /auth/introspect to check its state.

  • sub is the stable internal HORES user ID (a number as a string). The username is in preferred_username. HORES does not issue the email claim.

6. Calling HORES API with an access token

The access token is sent in the header:

Authorization: Bearer <access_token>

Service

URL

Required scope

HORES GraphQL API (GraphQL HORES API)

https://<server>/api2/graphql

hores_api

HORES Web API (HORES HTTP API / Web API)

https://<server>/api/...

web_api

The token must contain the scope of the service and be issued on behalf of a user. On every request the server checks that the user is still active and has API access enabled. The admin user cannot access the API. An invalid, expired or revoked token, or a missing scope, returns 401 with the header WWW-Authenticate: Bearer error="invalid_token".

The /api2/login endpoint and the /api2/ws WebSocket do not accept bearer tokens, and you don’t need to call them when using an access token.

7. UserInfo, revocation, introspection

  • UserInfo: GET /auth/userinfo with Authorization: Bearer <access_token>. The token must have the openid scope. Returns sub, and with the profile scope also preferred_username, name and locale.

  • Current state of the user: tokens and authorization codes are only valid while the user is active (not deleted, API access enabled, not expired). Once the user is deactivated, /auth/userinfo answers 401 (invalid_token), introspection returns {"active": false}, and exchanging a code issued earlier or refreshing fails with invalid_grant.

  • Revocation: POST /auth/revoke, parameters token and optionally token_type_hint. Requires client authentication (client_secret_basic/post), so a public client cannot call it. token_type_hint only speeds up the lookup (RFC 7009). What gets invalidated depends on the actual type of the token sent:

    • access token: only the access token, the refresh token remains usable,

    • refresh token: the refresh token, its access token and all tokens that came from the same sign-in.

  • Introspection: POST /auth/introspect, parameters token and optionally token_type_hint (only speeds up the lookup). Confidential clients only. Returns active, client_id, scope, sub, username, iss, iat and exp; for a refresh token they describe the refresh token itself. A client may introspect only its own tokens. Only a client the administrator marked as Resource server may verify tokens of other clients. For anyone else the server returns {"active": false}, the same as for an unknown token.

  • Failed attempt limit: after 10 failed client authentications (wrong client_secret) within 15 minutes from one IP address, /auth/token, /auth/revoke and /auth/introspect answer 429 with the error slow_down for that client_id, even with the correct secret.

8. Common errors

Error

Cause

invalid_request – Missing ‚code_challenge‘ / ‚code_challenge_method‘

PKCE is missing, or code_challenge_method=S256 was not sent.

invalid_scope

A scope unknown to HORES.

invalid_grant

The code was already used or has expired, code_verifier or redirect_uri does not match, an old (already rotated) refresh token was used, or the user is no longer allowed to sign in.

unsupported_response_type

A response_type other than code (e.g. implicit token).

429 / slow_down

Too many failed client authentications, try again later.

invalid_client

Wrong client_id/client_secret, or a client authentication method that is not allowed.

unauthorized_client

The client does not have the grant enabled, or the user assigned for client_credentials is not eligible.

Insecure transport error (InsecureTransportError)

redirect_uri over http:// other than localhost.

Wrong iss / WebAuthn not working

Public server URL (issuer) in the OAuth2 interface does not match the address users actually open the server at.