LibreSign / LibreSign/libresign
Finalize signer geolocation backend with IP-based location
Nobody has claimed this yet.
- Dominant language
- PHP
- Stars
- 818
- Forks
- 146
- Avg merge
- 11h 31m
- Merged PRs (30d)
- 326
Description
Context
LibreSign already has device-reported signer geolocation from #6960.
That implementation added the first location source before the complete geolocation model was known.
We now also need approximate location derived from the signer IP address.
This issue defines the final backend model for signer geolocation before this feature becomes stable.
It includes:
- cleanup of the current device geolocation contract;
- backend support for IP-based geolocation;
- GeoIP database configuration and validation;
- policy snapshot behavior for existing and new signature requests;
- the API contract required by #8087;
- the required runtime dependency in
LibreSign/3rdparty.
The frontend is handled by #8087.
Blocks:
- #8087 — frontend support for IP-based signer geolocation.
Goal
Provide one signer geolocation domain with two independent location sources:
geolocation
├── device
└── ip
The two sources must not overwrite each other.
Device location is reported by the signer device.
IP location is resolved by LibreSign on the server from the client IP.
Both sources may exist for the same signer.
The backend must not choose one source as the true location.
Device location may be more precise, but it is not proof of physical presence.
IP-based location is approximate and may be affected by VPNs, proxies, mobile networks and similar infrastructure.
Policies
Use two independent policies.
Device geolocation
Rename the current policy:
signer_geolocation
to:
signer_device_geolocation
Keep the existing values and behavior:
disabledoptionalrequired
Keep:
- system scope;
- group scope;
- user scope;
- inheritance;
- delegation;
- per-signer requirement when the effective policy is
optional.
When the policy is:
disabled: device location cannot be required;optional: the requester may require it for selected signers;required: all signers must provide device location.
IP geolocation
Add:
signer_ip_geolocation
Values:
disabledenabled
Default:
disabled
Use the same policy scopes and delegation model used by device geolocation.
There is no requester per-signer override for IP geolocation.
When enabled for a signature request, LibreSign attempts GeoIP lookup for every signer when they sign.
Policy Workbench relationship
Do not create a third master policy only to group these two values.
The two policies must remain independent in the backend.
#8087 should present them together under the same Signer geolocation area in the Policy Workbench.
This keeps policy inheritance independent while still providing one clear configuration area.
Freeze policy values per signature request
Geolocation policy behavior must be frozen when the signature request or envelope is created and its policy snapshot is generated.
The current effective policy controls only new signature requests.
For an existing signature request, runtime behavior must come from the policy value stored in its policy_snapshot, not from the current effective policy.
Policy disabled
When a new signature request is created while a geolocation policy is disabled:
- store the disabled state in the policy snapshot;
- do not collect data for that geolocation source;
- do not allow that source to become active later for the same signature request because the effective policy changed.
If the effective policy becomes enabled later, this existing signature request must remain disabled for that source.
Policy enabled
When a new signature request is created while a geolocation policy is enabled:
- store the enabled or effective state in the policy snapshot;
- use that stored value for the complete lifetime of the signature request.
If the effective policy becomes disabled later, this existing signature request must continue using the stored enabled state.
New signature requests created after the policy is disabled must use the disabled state.
Existing requests without a snapshot value
An existing signature request that does not contain a geolocation policy in its policy_snapshot must not inherit a policy that was enabled later.
For an existing signature request:
- use the stored snapshot value when present;
- when the policy is absent from the snapshot, treat that geolocation source as disabled.
Do not fall back to the current effective policy for an existing signature request.
The current effective policy may be used only when creating a new signature request and generating its policy snapshot.
Device geolocation
For signer_device_geolocation, the policy snapshot defines whether device geolocation can be used.
When the effective mode is optional, keep the final per-signer requirement frozen in:
deviceGeolocationRequirement
Later policy changes must not change that stored signer requirement.
IP geolocation
For signer_ip_geolocation, the policy snapshot defines whether IP geolocation must be collected when each signer signs.
If the snapshot has IP geolocation enabled, collection must continue even if the effective policy becomes disabled later.
If the snapshot has IP geolocation disabled, enabling the effective policy later must not start GeoIP collection for that existing signature request.
Device geolocation contract cleanup
Device geolocation is not yet a stable production contract, so update its names to make the source explicit.
Rename the requester input:
geolocationRequired
to:
deviceGeolocationRequired
Rename the frozen signer metadata:
geolocationRequirement
to:
deviceGeolocationRequirement
Rename the signing input:
geolocation
to:
deviceGeolocation
Only client-provided device location may be accepted in deviceGeolocation.
The client must never submit IP geolocation data.
Stored signer metadata
Store signer location under one geolocation object.
Example:
{
"deviceGeolocationRequirement": "required",
"geolocation": {
"device": {
"status": "collected",
"latitude": -23.5505,
"longitude": -46.6333,
"accuracy": 25,
"timestamp": 1700000000000
},
"ip": {
"status": "resolved",
"sourceIp": "200.100.50.25",
"countryCode": "BR",
"country": "Brazil",
"regionCode": "SP",
"region": "São Paulo",
"city": "São Paulo",
"latitude": -23.55,
"longitude": -46.63,
"accuracyRadius": 20
}
}
}
accuracyRadius is expressed in kilometers.
Only store location attributes that are present in the GeoIP database result.
Do not infer missing:
- country;
- region;
- city;
- coordinates;
- accuracy values.
Fields generated by LibreSign for audit and lookup state, such as status, sourceIp and reason, are still stored as defined in this issue.
Do not store postal address information.
When coordinates are stored and the database provides accuracyRadius, store it together with the coordinates.
Device and IP location must never replace each other.
Relationship with collect_metadata
Do not refactor the existing collect_metadata policy in this issue.
It keeps its current behavior for generic request metadata:
remote-address;user-agent.
GeoIP is a separate audit feature.
When signer_ip_geolocation is enabled in the signature request policy snapshot, the source IP used for the lookup must be stored in:
geolocation.ip.sourceIp
This must happen even when collect_metadata is disabled.
This does not enable collect_metadata.
This also must not start storing the User-Agent.
Expected behavior:
collect_metadata = false, GeoIP disabled
Do not store:
remote-address;user-agent;geolocation.ip.
collect_metadata = true, GeoIP disabled
Keep the current behavior:
- store
remote-address; - store
user-agent; - do not store
geolocation.ip.
collect_metadata = false, GeoIP enabled
Store:
geolocation.ip, includingsourceIp.
Do not store generic:
remote-address;user-agent.
collect_metadata = true, GeoIP enabled
Keep the current request metadata and also store geolocation.ip.
The same IP may exist as:
remote-address;geolocation.ip.sourceIp.
This is acceptable because the fields have different audit meaning.
GeoIP runtime dependency
Use the official geoip2/geoip2 PHP package for MaxMind database access.
Runtime PHP dependencies shipped with LibreSign are managed in the LibreSign/3rdparty repository.
Add the GeoIP dependency there and follow the existing 3rdparty process:
- declare the dependency in
LibreSign/3rdparty; - update its Composer lock file;
- regenerate the scoped Composer dependencies with PHP-Scoper;
- make sure the library works with the
OCA\Libresign\Vendornamespace used by LibreSign; - update the
3rdpartysubmodule in the LibreSign implementation PR.
Do not add the runtime dependency only to the main LibreSign composer.json.
The dependency must follow the same update and maintenance process as the other PHP runtime libraries shipped through LibreSign/3rdparty.
The LibreSign PR must be tested with the exact 3rdparty commit that contains the GeoIP dependency before it is merged.
The rest of the LibreSign backend should not depend directly on the upstream package namespace.
Keep MaxMind-specific access behind the internal GeoIP lookup service.
GeoIP database
For this first implementation, support local MaxMind City databases:
- GeoLite2 City;
- GeoIP2 City.
Other providers and database types are out of scope.
Store the full database path as instance-level LibreSign app configuration using IAppConfig.
Use:
geoip_database_path
This is technical instance configuration.
It is not a policy and must not have group or user scopes.
Do not automatically download or update GeoIP databases.
GeoIP lookup must use the configured local database.
Do not send signer IP addresses to MaxMind or another external GeoIP service.
Nextcloud GeoBlocker reference
The Nextcloud GeoBlocker app may be used as a reference for the operational model of using an administrator-configured local MaxMind database.
Reference:
HomeITAdmin/nextcloud_geoblocker
LibreSign must not depend on that app.
Do not copy its lookup or validation model directly.
In particular:
- do not load
geoip2.phar; - do not search for the library next to the database file;
- do not validate the database by checking a fixed public IP;
- do not create support for multiple GeoIP providers in this issue.
LibreSign should use its own dependency, service and testing architecture.
The issue must remain fully implementable without reading the GeoBlocker source.
Database validation
Validate the configured database without using a lookup against a hard-coded public IP.
Check that:
- a path is configured;
- the file exists;
- the file is readable;
- the database can be opened;
- the database type is supported.
Use database metadata provided by the MaxMind reader when possible.
Expose one of these states:
not_configurednot_foundnot_readableinvalid_databaseunsupported_databaseready
When available, also expose:
- database type;
- database build time;
- file modification time.
Invalid GeoIP configuration must not break normal LibreSign requests.
Do not expose internal exception messages, stack traces or unnecessary filesystem details through the API.
Admin API
Provide an admin-only API for #8087.
Read configuration
GET /api/v1/admin/geoip
Example:
{
"path": "/usr/share/GeoIP/GeoLite2-City.mmdb",
"status": "ready",
"databaseType": "GeoLite2-City",
"buildEpoch": 1700000000,
"modifiedAt": "2026-09-01T10:00:00+00:00"
}
Optional database metadata may be omitted when unavailable.
Save configuration
POST /api/v1/admin/geoip
Input:
{
"path": "/usr/share/GeoIP/GeoLite2-City.mmdb"
}
Return the same configuration structure used by the GET endpoint.
An empty path clears the configuration.
Allow a path to be saved even when the file is not available yet.
The returned status must describe the current state.
Only administrators may read or update this configuration.
This API must expose only GeoIP configuration and status.
It must not expose signer IP addresses or signer geolocation metadata.
Client IP source
Use the client address already resolved by Nextcloud through:
IRequest::getRemoteAddress()
Do not parse these headers directly:
X-Forwarded-For;Forwarded;- similar proxy headers.
Do not add a second trusted proxy configuration in LibreSign.
Nextcloud remains responsible for resolving the client address.
GeoIP lookup flow
GeoIP is passive audit collection.
It must not require new signer input or permission.
When the signer sends the signing request:
- load the existing signature request and related file;
- read the frozen
signer_ip_geolocationvalue from itspolicy_snapshot; - if the snapshot value is missing or disabled, do not perform GeoIP work;
- get the client IP from the current HTTP request;
- check if the GeoIP database is ready;
- resolve the IP using the local database;
- normalize the result;
- add the result to
geolocation.ip; - continue through the existing signing flow;
- persist the same metadata for synchronous or asynchronous signing.
Do not consult the current effective policy to decide GeoIP behavior for an existing signature request.
The lookup must happen before an asynchronous signing job is queued.
Do not perform the lookup inside the background worker.
The worker does not represent the original signer HTTP connection.
GeoIP result
Successful lookup:
{
"status": "resolved",
"sourceIp": "200.100.50.25",
"countryCode": "BR",
"country": "Brazil",
"regionCode": "SP",
"region": "São Paulo",
"city": "São Paulo",
"latitude": -23.55,
"longitude": -46.63,
"accuracyRadius": 20
}
Valid IP with no matching database record:
{
"status": "not_found",
"sourceIp": "200.100.50.25"
}
Lookup unavailable:
{
"status": "unavailable",
"sourceIp": "200.100.50.25",
"reason": "database_not_ready"
}
Supported unavailable reasons:
database_not_readyaddress_unavailablelookup_failed
If no client address is available, omit sourceIp.
Do not store internal exception messages in signer metadata.
Technical details may be written to the server log, but signer IP addresses, coordinates and complete geolocation payloads must not be added to log messages or log context.
Failure behavior
GeoIP is additional audit evidence.
It must never block signing.
Signing continues when:
- the database is not configured;
- the database file is missing;
- the database is invalid;
- the client address is unavailable;
- the address is not found;
- the lookup fails.
When the signature request policy snapshot has GeoIP enabled, store the lookup status where possible.
This allows API and frontend consumers to distinguish:
- GeoIP not enabled for the signature request;
- lookup unavailable;
- address not found;
- resolved location.
Development-state terminology
Signer geolocation and GeoIP have not been released yet.
Do not treat the current implementation as a legacy contract.
During this work:
- update the current implementation directly to the final model;
- do not add aliases for current development field names;
- do not add fallback policy names for the current development state;
- do not add compatibility layers for unreleased geolocation contracts;
- do not keep current metadata shapes only because they already exist in
main; - do not describe unreleased geolocation code, policies, APIs or data structures as legacy.
There is only one intended geolocation contract after this issue: the final contract defined here.
Upgrade compatibility
Signer geolocation has not been released as a stable LibreSign feature.
Do not add migrations only to preserve intermediate geolocation states created during development.
If a current unreleased migration already contains geolocation-related behavior, update that migration so fresh installations and supported upgrades use the final contract defined by this issue directly.
Do not add another migration only to preserve a development database that already executed an earlier version of that unreleased migration.
Developers using such a database may need to recreate or reset their development environment.
Existing data from released LibreSign versions must remain unchanged.
Tests must cover the supported installation and upgrade paths using the final migration state.
Do not keep current development API names, policy names or metadata structures only for development compatibility.
API output
Expose geolocation from stored signer metadata.
Do not rebuild historical audit data from the current policy.
The existing file list and validation APIs must expose, when available:
{
"deviceGeolocationRequirement": "required",
"geolocation": {
"device": {},
"ip": {}
}
}
Only include sources that exist.
Keep the current metadata filtering behavior.
Do not expose the complete internal SignRequest.metadata object.
Geolocation data must follow the same authorization rules as the signer metadata exposed by the existing audit and validation APIs.
For existing signature requests, API behavior related to whether a geolocation source is active must follow the stored policy snapshot, not the current effective policy.
Update:
ResponseDefinitions;- OpenAPI definitions;
- generated TypeScript OpenAPI types.
After this issue is complete, #8087 must not require a new backend field, endpoint or business rule.
Implementation structure
Keep the GeoIP code separated from controllers and from the signing engine.
Use small backend services for:
- GeoIP configuration and database status;
- GeoIP lookup;
- result normalization;
- signer geolocation orchestration.
The HTTP/signing layer should only decide when collection is needed and pass normalized metadata through the existing persistence flow.
Keep MaxMind-specific code behind a LibreSign lookup boundary.
Do not spread the scoped MaxMind reader usage through controllers or signing services.
Reuse an opened database reader during the same request when possible.
Do not create a generic multiple-provider framework until another provider is needed.
Do not create a separate GeoIP persistence layer, history or cache containing signer IP addresses or geolocation results.
Use the existing signer metadata persistence defined by this issue.
Tests
Prefer real behavior for GeoIP database and lookup tests.
Use a small public MaxMind test .mmdb fixture.
Keep any required license or attribution with the fixture.
Do not use:
- Internet access;
- a production GeoLite2 database;
- an external GeoIP service.
Keep mocks mainly at framework boundaries such as:
IRequest;- app configuration;
- existing service collaborators when needed.
Do not mock the MaxMind reader in tests that verify real database parsing or lookup.
Policy tests
Cover:
- device policy keeps
disabled,optionalandrequired; - IP policy defaults to
disabled; - system resolution;
- group resolution;
- user resolution;
- inheritance;
- delegation;
- policy snapshot creation;
- later policy changes do not affect existing signature requests.
Add regression coverage for both geolocation sources:
- create a signature request while the policy is disabled, enable it later, and confirm the existing request remains disabled;
- create a signature request while the policy is enabled, disable it later, and confirm the existing request keeps the stored behavior;
- confirm new signature requests created after a policy change use the new effective policy;
- for an existing signature request without the policy in
policy_snapshot, confirm the source remains disabled even when the current effective policy is enabled; - confirm runtime behavior for an existing signature request never falls back to the current effective policy.
For device geolocation, also cover that the frozen deviceGeolocationRequirement remains unchanged after later policy changes.
Device regression tests
Keep or update existing coverage for:
- optional per-signer device requirement;
- required device location;
- server-side validation;
- device collection independent from
collect_metadata; - device location persistence.
Database tests
Cover:
- no configured path;
- missing file;
- unreadable file where supported;
- invalid database;
- unsupported database type;
- valid City database;
- database metadata;
- API responses do not expose internal exception messages or stack traces.
Lookup tests
Cover:
- successful IPv4 lookup;
- IPv6 when available in the fixture;
- address not found;
- unavailable client address;
- unavailable database;
- lookup failure;
- normalization of country, region, city, coordinates and accuracy radius.
Signing tests
Cover:
- GeoIP disabled in the policy snapshot performs no lookup;
- missing GeoIP policy in the policy snapshot performs no lookup;
- GeoIP enabled in the policy snapshot stores
sourceIp; - successful result is stored;
- lookup failure does not block signing;
collect_metadata = falsewith GeoIP enabled stores GeoIP source IP but not genericremote-addressoruser-agent;collect_metadata = truekeeps its current behavior;- device and IP sources coexist;
- one source never overwrites the other;
- synchronous signing persists the expected result;
- asynchronous signing uses data collected from the original HTTP request and does not resolve GeoIP inside the worker;
- disabling the current effective GeoIP policy after request creation does not stop collection for a request whose snapshot has GeoIP enabled;
- enabling the current effective GeoIP policy after request creation does not start collection for a request whose snapshot has GeoIP disabled or missing.
API authorization tests
Cover:
- GeoIP administration endpoints are restricted to administrators;
- signer geolocation follows the same access rules as existing signer audit and validation metadata;
- unauthorized users cannot retrieve signer IP addresses or geolocation metadata;
- the admin GeoIP configuration API never returns signer data.
Logging tests
Where practical, cover failure paths and confirm signer IP addresses, coordinates and complete geolocation payloads are not added to log context.
Installation and upgrade tests
Cover:
- fresh installation uses the final policy names and geolocation contract defined in this issue;
- supported upgrade from the latest released LibreSign version results in the same final contract;
- unrelated signer metadata is preserved;
- unrelated file metadata is preserved;
- unrelated policy data is preserved.
Do not add tests for intermediate geolocation contracts that were never released.
3rdparty integration tests
Verify that:
- the GeoIP dependency is available through the scoped
LibreSign/3rdpartyautoloader; - the LibreSign implementation can perform a real lookup using the scoped package shipped by the app.
Use data providers where they reduce duplicated cases.
Mutation testing
Cover new isolated GeoIP backend logic with Infection.
For small isolated classes:
- no mutation errors;
- aim for no escaped mutants.
Do not exclude new production code only to make mutation testing pass.
Quality gates
The implementation must pass the existing backend checks:
- PHPUnit;
- Behat/integration tests;
- Psalm;
- PHPCS;
- Infection;
- OpenAPI generation and validation.
The related LibreSign/3rdparty change must also pass its existing CI and dependency generation checks.
Done when
- Device geolocation uses the final source-specific contract.
-
signer_device_geolocationandsigner_ip_geolocationare independently resolved. - Existing device geolocation behavior still works with the final contract.
- Both geolocation policies are frozen when the signature request policy snapshot is generated.
- Existing signature requests use their stored snapshot and never fall back to the current effective policy.
- Existing requests without a geolocation policy in the snapshot treat that source as disabled.
- Enabling a geolocation policy affects only new signature requests.
- Disabling a geolocation policy does not change requests already created with that source enabled.
- GeoIP database configuration and validation are implemented.
- The admin API provides the complete GeoIP configuration state required by #8087.
- GeoIP uses the client IP resolved by Nextcloud.
- GeoIP lookup is local and does not send signer IP addresses to external services.
- The source IP is stored as part of GeoIP audit evidence.
- Enabling GeoIP does not enable or change
collect_metadata. - Device and IP geolocation can coexist.
- No location source overwrites another.
- GeoIP lookup happens before synchronous and asynchronous signing paths split.
- GeoIP failures never block signing.
- Signer IP addresses, coordinates and complete geolocation payloads are not added to normal application logs.
- Geolocation data follows the same authorization rules as existing signer audit and validation metadata.
- The GeoIP admin API exposes configuration and status only, never signer data.
- Stored location data is exposed through the existing audit/validation APIs.
- OpenAPI and generated types describe the final backend contract.
- Current unreleased geolocation migrations, if present, use the final contract directly.
- No compatibility layer is added only for an intermediate development state.
- Fresh installation and supported upgrade paths are covered by automated tests.
- The GeoIP PHP dependency is shipped through
LibreSign/3rdparty. - The LibreSign submodule points to the reviewed 3rdparty commit containing the dependency.
- Runtime behavior is covered by automated tests.
- #8087 can implement the complete frontend without adding new backend behavior.
Out of scope
This issue does not implement:
- frontend GeoIP settings;
- Policy Workbench frontend changes;
- audit frontend changes;
- browser geolocation UI;
- compatibility support only for intermediate development states;
- GeoIP database upload;
- automatic GeoIP database download or update;
- external GeoIP services;
- multiple GeoIP providers;
- separate GeoIP analytics or history;
- GeoIP-based signing restrictions;
- risk or fraud scoring;
- rules that compare device and IP location;
- map visualization;
- refactoring or splitting the existing
collect_metadatapolicy.
Frontend support is handled by #8087.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reviewing the existing device geolocation implementation from #6960 and tracing policy snapshots through signature-request creation and signing. Then inspect the admin API entry points for GET/POST /api/v1/admin/geoip and the LibreSign/3rdparty dependency process. Done means independent device and IP sources, frozen policy behavior, validated local database configuration, and the specified admin API contract.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- php
- Domain
- api, backend, databases, security
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 38/100