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 |
|
GET |
JWKS (keys for id_token verification) |
|
GET |
Authorization |
|
GET |
Token |
|
POST |
UserInfo |
|
GET, POST |
Token revocation (RFC 7009) |
|
POST |
Token introspection (RFC 7662) |
|
POST |
Logout from the login page |
|
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): Public (native mobile or desktop application): |
Grant types |
|
Redirect URIs |
One or more, must match exactly. Allowed are |
Scopes |
Which of the scopes listed below the client may request. |
Act as user |
Only for |
Resource server |
Only for a confidential client that verifies tokens issued to other clients through |
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 |
|---|---|
|
Sign-in via OpenID Connect, the response contains an |
|
User details: |
|
Access to the HORES GraphQL API ( |
|
Access to the HORES Web API ( |
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_grantand 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_grantand 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/profileand/userinfoall work with it. If the user has been deactivated in the meantime, the server returnsunauthorized_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 |
|
Authorization code |
opaque string |
5 minutes, single use |
Verify the
id_tokenwith the key from JWKS (kidin the header) and checkiss,aud,expandnonce.The access token is opaque and the client must not decode it. Use
/auth/introspectto check its state.subis the stable internal HORES user ID (a number as a string). The username is inpreferred_username. HORES does not issue theemailclaim.
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) |
|
|
HORES Web API (HORES HTTP 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/userinfowithAuthorization: Bearer <access_token>. The token must have theopenidscope. Returnssub, and with theprofilescope alsopreferred_username,nameandlocale.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/userinfoanswers401(invalid_token), introspection returns{"active": false}, and exchanging a code issued earlier or refreshing fails withinvalid_grant.Revocation:
POST /auth/revoke, parameterstokenand optionallytoken_type_hint. Requires client authentication (client_secret_basic/post), so a public client cannot call it.token_type_hintonly 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, parameterstokenand optionallytoken_type_hint(only speeds up the lookup). Confidential clients only. Returnsactive,client_id,scope,sub,username,iss,iatandexp; 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/revokeand/auth/introspectanswer429with the errorslow_downfor thatclient_id, even with the correct secret.
8. Common errors¶
Error |
Cause |
|---|---|
|
PKCE is missing, or |
|
A scope unknown to HORES. |
|
The code was already used or has expired, |
|
A |
|
Too many failed client authentications, try again later. |
|
Wrong |
|
The client does not have the grant enabled, or the user assigned for |
Insecure transport error (InsecureTransportError) |
|
Wrong |
Public server URL (issuer) in the OAuth2 interface does not match the address users actually open the server at. |