# OCA auth_api_key vs Odoo's Native API Keys: A Short History of Why Both Exist

> OCA's auth_api_key predates Odoo's own API keys by two years. The timeline from 2018 to today, what core took over in Odoo 18 and 19, and what OCA still uniquely offers.

**Date:** 2026-07-17
**Source:** <https://trobz.com/insights/oca-auth-api-key-vs-native/>

---



This is a companion to [Bootstrapping a /json/2 API Key for Odoo Mobile Apps](https://mobile.trobz.com/blog/introducing-auth-api-key-native-generate). That post was about getting a **native** Odoo API key when all you have is a login and a password. This one steps back and asks a question that comes up on almost every integration project: Odoo has shipped per-user API keys since 14.0, so why does OCA still maintain `auth_api_key` — a whole separate key system — and why do the OCA REST stacks depend on it instead of the native one?

You can answer that with a feature table, but the honest answer is historical. `auth_api_key` was not built as an alternative to native keys — it **predates them by two years**. Core caught up, then kept going, and the relationship between the two changed as it did. So instead of a static comparison, here is the timeline, each step grounded in the code that shipped.

---

## The Timeline at a Glance

```mermaid
flowchart TB
    A["2018 · Odoo 10<br/>OCA ships<br/>auth_api_key"]
    B["2020 · Odoo 14<br/>Core ships<br/>res.users.apikeys"]
    C["2021 · Odoo 14<br/>OCA adds<br/>group + server_env"]
    D["2024 · Odoo 17<br/>OCA adds<br/>fastapi_auth_api_key"]
    E["2024 · Odoo 18<br/>Core adds<br/>auth=bearer"]
    F["2025 · Odoo 19<br/>Core ships<br/>/json/2"]
    G["2025-26 · Odoo 19<br/>OCA mints<br/>native keys"]

    A -->|"core has no keys"| B
    B -->|"custom routes uncovered"| C
    C -->|"REST needs a hook"| D
    D -->|"core takes the boundary"| E
    E -->|"key becomes the front door"| F
    F -->|"bootstrap still missing"| G
```

The same story with the gaps spelled out:

| Year | Odoo | What core shipped | The gap | OCA's answer |
| --- | --- | --- | --- | --- |
| 2018 | 10 → | XML-RPC / JSON-RPC, user + password only | No key auth at all; custom controllers had to invent their own | `auth_api_key` (Akretion seed 2017 → ACSONE, branch 10.0) |
| 2020 | 14 | Native `res.users.apikeys` — hashed, per-user, for RPC | Doesn't cover *custom* controllers | OCA module still required |
| 2021 | 14 | — | No scoping, no secrets-out-of-DB | `auth_api_key_group`, `auth_api_key_server_env`, `base_rest_auth_api_key` |
| 2024 (early) | 17 | — | No first-class REST/FastAPI hook | `fastapi_auth_api_key` |
| 2024 (Sep) | 18 | Generic **`auth="bearer"`** method — native keys now guard *custom* routes | The custom-controller boundary erodes | — |
| 2025 | 19 | `/json/2` — bearer key becomes the primary *external* transport auth | Service-identity provisioning, group gating, env secrets, bootstrap | — |
| 2025–26 | 19 | — | The bootstrap gap (see previous post) | `auth_api_key_native_generate` — **complements** core, doesn't replace it |

(Odoo columns are the branch each item first landed on — verified first-hand against git history — not the newest Odoo of that year. Verification dates and commits are cited inline below.)

The shape of the story: OCA arrived first, core caught up on the mainstream case *and then, in Odoo 18, on the custom-controller boundary itself*, and OCA moved from being a *replacement* to being an *ecosystem* layered around core. Let's walk it.

---

## 2018: Before Odoo Had Any API Key

Back then the only external API was XML-RPC and JSON-RPC, and both authenticated with a username and password on every call. There was no notion of an API key anywhere in core. If you exposed a custom HTTP controller — say a REST endpoint for a partner integration — you had to roll your own authentication.

That is the gap `auth_api_key` was born to fill. The auth-method code traces back to Akretion's work by Sébastien Beau in **2017**; ACSONE packaged it as the `auth_api_key` module, whose first commit ([`6645f37e`](https://github.com/OCA/server-auth/commit/6645f37e), *"New generic module du define REST services"*) lands on the **10.0** branch on 2018-01-31 — its commit message names REST services as the reason it exists:

```python
# server-auth/auth_api_key/models/ir_http.py
# Copyright 2018 ACSONE SA/NV
# Copyright 2017 Akretion — @author Sébastien BEAU
class IrHttp(models.AbstractModel):
    _inherit = "ir.http"

    @classmethod
    def _auth_method_api_key(cls):
        headers = request.httprequest.environ
        api_key = headers.get("HTTP_API_KEY")
        if api_key:
            request.update_env(user=1)
            auth_api_key = request.env["auth.api.key"]._retrieve_api_key(api_key)
            if auth_api_key:
                request._env = None
                request.update_env(user=auth_api_key.user_id.id)
                request.auth_api_key = api_key
                request.auth_api_key_id = auth_api_key.id
                return True
        _logger.error("Wrong HTTP_API_KEY, access denied")
        raise Unauthorized()
```

Defining `_auth_method_api_key` registers a new value for the `auth=` argument of `@http.route`. That is the whole trick, and it is still the module's reason to exist today:

```python
@http.route("/my/custom/endpoint", auth="api_key", type="json")
def my_endpoint(self):
    # request now runs as the service user mapped to the presented key
    ...
```

The data model behind it was — and is — deliberately simple. A key is not a user's personal credential; it is a **named record that maps an opaque string to a service user**:

```python
# server-auth/auth_api_key/models/auth_api_key.py
class AuthApiKey(models.Model):
    _name = "auth.api.key"

    name = fields.Char(required=True)
    key = fields.Char(required=True, help="The API key. Enter a dummy value ... "
                      "if it is obtained from the server environment configuration.")
    user_id = fields.Many2one("res.users", string="User", required=True,
                              help="The user used to process the requests "
                                   "authenticated by the api key")
```

Three essential fields (there is also a computed `active`): a name, the key string, and the user the request runs as. This[ref](https://github.com/OCA/server-auth/tree/19.0/auth_api_key) became the auth primitive for OCA's REST framework (`base_rest`, and later `fastapi`). Remember this shape — the whole rest of the story is core slowly building its *own* key system next to it, without ever adopting this one.

---

## 2020 (Odoo 14): Core Finally Ships API Keys — But Narrow

Odoo 14 introduced native `res.users.apikeys` (verified: the model is absent on branch `13.0` and present on `14.0` — GitHub, checked 2026-07-17)[ref](https://github.com/odoo/odoo/blob/14.0/odoo/addons/base/models/res_users.py). This is the system the [previous post](https://mobile.trobz.com/blog/introducing-auth-api-key-native-generate) dissected: a key is *a real user's own credential*, stored **hashed**, carrying a scope, minted self-service from **Preferences → Account Security**. One documented use is two-factor auth: the core hook `_rpc_api_keys_only` exists "to be overridden if RPC access needs to be restricted to API keys, *e.g.* for 2FA" (its own docstring) — a 2FA user cannot send a plain password over RPC, so they present a key instead.

Crucially, native keys authenticate Odoo's **own external API** — RPC then, `/json/2` now. Not the interactive web login: in `_check_credentials` the API-key branch is only reached on the non-interactive path (`if not interactive:`), so you cannot log into the web client with a key. And in Odoo 14 there was still no hook for a controller *you* wrote. So the moment native keys landed, they did **not** make `auth_api_key` redundant — they overlapped on "authenticate an RPC call," but left the original use case (guard my custom endpoint) exactly where it was.

That was the crux of the comparison **for Odoo 14 through 17** — a design boundary, not an oversight. (Hold that thought: Odoo 18 moves the boundary, and we get there below.)

| | Native `res.users.apikeys` (2020) | OCA `auth.api.key` (2018) |
| --- | --- | --- |
| A key is… | a credential owned by a user | a record an admin maps to any user |
| Storage | hashed (shown once) | plaintext in DB by default |
| Authenticates (v14–17) | Odoo's own external API (RPC) — not the web login, not your controllers | any `@route(auth="api_key")` you define |
| Key value | always a server-generated 160-bit random, shown once | an admin can set an arbitrary value (even from env config) |
| Provisioning | self-service UI, or programmatic `generate()` — which needs an *existing* native key (the bootstrap gap) | admin creates the record; no pre-existing key needed |
| Expiration / rotation | built-in expiration field; standard users capped at 3 months; `generate()`/revoke exist | no expiration field on the base model; rotation is your own process |
| Runs as | the owning user (ACL/record-rules apply) | whatever `user_id` you assign (ACL/record-rules apply) |

Note what the identity difference really is. Both keys ultimately resolve to a `res.users` — OCA's `auth.api.key` requires a `user_id`, a native key can belong to a login-less "bot" user (the pattern [the previous post](https://mobile.trobz.com/blog/introducing-auth-api-key-native-generate) quoted from Odoo's docs), and both run under that user's ACL and record rules. So this is **not** "person vs service account." Two real differences remain: (1) **provisioning** — OCA lets an admin assign an *arbitrary* key value to *any* user with no pre-existing key, whereas native always generates a random value and its programmatic `generate()` requires an existing native key (leaving the first-key bootstrap unsolved — the subject of the previous post); (2) **lifecycle** — native keys have built-in expiration and revocation; the OCA base model has neither.

The line that *was* clean for v14–17: **native keys authenticate Odoo's API; OCA keys authenticate *your* API.** True then. Odoo 18 is where it stops being true — which is the interesting part of the story.

---

## 2021 (Odoo 14): OCA Stops Filling a Gap and Starts Building Patterns

By 2021 the OCA modules were no longer just "the thing core is missing." Camptocamp added capabilities core still does not have.

**`auth_api_key_group`**[ref](https://github.com/OCA/server-auth/tree/19.0/auth_api_key_group) — pluggable scoping. A native key carries a single scope string, and `/json/2` accepts an unscoped key or an `rpc`-scoped one (the SQL match is `scope IS NULL OR scope = 'rpc'`); there is no way to say "this key may hit endpoint A but not B." OCA moved scoping *out* of the key into an extensible concept:

```python
# server-auth/auth_api_key_group/models/auth_api_key_group.py
class AuthApiKeyGroup(models.Model):
    _name = "auth.api.key.group"

    name = fields.Char(required=True)
    code = fields.Char(required=True)
    auth_api_key_ids = fields.Many2many("auth.api.key", ...)
```

The manifest is refreshingly honest about what it does alone:

> Grouping per se does nothing. This feature is supposed to be used by other modules to limit access to services or records based on groups of keys.

It is a hook, not a feature — consumers decide what a group *grants*.

**`auth_api_key_server_env`**[ref](https://github.com/OCA/server-auth/tree/18.0/auth_api_key_server_env) — keys out of the database. This one answers a real operational pain: by default the OCA key sits in the `key` column as plaintext. `server_env` moves the value into the server environment configuration instead:

```python
# server-auth/auth_api_key_server_env/models/auth_api_key.py
class AuthApiKey(models.Model):
    _inherit = ["auth.api.key", "server.env.techname.mixin", "server.env.mixin"]

    def _server_env_section_name(self):
        # section in the config file: [api_key_<name>]
        return f"api_key_{getattr(self, self._server_env_section_name_field)}"

    @property
    def _server_env_fields(self):
        base_fields = super()._server_env_fields
        return {"key": {}, **base_fields}
```

You put a dummy value in the DB and the real key lives in a `[api_key_<name>]` section of your environment config — so, per the manifest, you *"avoid mixing your keys between your various environments when restoring databases."* Restore production into staging and the staging key stays whatever staging's config says. That is 12-factor secret handling; native keys have no equivalent, because a native key hash always travels inside the database dump.

(`base_rest_auth_api_key` landed the same year, wiring `auth_api_key` into `base_rest`'s OpenAPI security scheme — the REST framework consuming the primitive.)

> ℹ️ At the time of writing, `auth_api_key_server_env` has not been migrated past 18.0 — the [19.0 `server-auth` branch](https://github.com/OCA/server-auth/tree/19.0) ships only `auth_api_key` and `auth_api_key_group` (`auth_api_key_native_generate` is still an [open PR](https://github.com/OCA/server-auth/pull/970)). The code below is from the 18.0 branch. If you need it on 19, it is a migration, not an install.

---

## 2024 (Early, Odoo 17): API-Key Auth Becomes a First-Class FastAPI Dependency

When OCA's `fastapi` module made FastAPI a way to build Odoo endpoints, `fastapi_auth_api_key` (first commit [`caaa7fcb`](https://github.com/OCA/rest-framework/commit/caaa7fcb), 2024-02-28, on the 17.0 branch — and, at the time of writing, not yet migrated to 19.0) turned `auth.api.key` into a FastAPI dependency and used the *group* from 2021 to gate each endpoint:

```python
# rest-framework/fastapi_auth_api_key/dependencies.py
def authenticated_auth_api_key(
    key: Annotated[str, Depends(APIKeyHeader(name=HTTP_API_KEY_HEADER))],
    env: Annotated[Environment, Depends(odoo_env)],
    endpoint: Annotated[FastapiEndpoint, Depends(fastapi_endpoint)],
) -> AuthApiKey:
    ...
    admin_env = Environment(env.cr, SUPERUSER_ID, {})
    auth_api_key = admin_env["auth.api.key"]._retrieve_api_key(key)   # 401 if unknown
    # Ensure the key is authorized for THIS endpoint via its group:
    if (endpoint.sudo().auth_api_key_group_id
            and auth_api_key not in endpoint.sudo().auth_api_key_group_id.auth_api_key_ids):
        raise HTTPException(status_code=401, detail=env._("Unauthorized"))
    return auth_api_key
```

An endpoint declares which key group it accepts; the dependency enforces it. This is the payoff of "shared primitive": one `auth.api.key` record works as a header credential *and* as a first-class FastAPI dependency, with per-endpoint group gating, without a second key system. A native bearer key can authenticate such a request, and native keys do carry a `scope` field — but the built-in `auth="bearer"` route layer only checks `scope='rpc'` and offers no per-endpoint configuration, so the *"this key may hit endpoint A but not B"* gating has no configurable equivalent there.

---

## 2024 (Sep, Odoo 18): Core Moves the Boundary Itself

This is where the story turns — and, importantly, it turns **a year before `/json/2`**, which is easy to get wrong. In Odoo 18, core added a **generic `auth="bearer"` method** to `ir.http` (commit [`e6d59451`](https://github.com/odoo/odoo/commit/e6d59451), *"[IMP] core: bearer authorization header"*, 2024-09-16; present on branches [`18.0`](https://github.com/odoo/odoo/blob/18.0/odoo/addons/base/models/ir_http.py) and [`19.0`](https://github.com/odoo/odoo/blob/19.0/odoo/addons/base/models/ir_http.py), absent on `17.0` and earlier — GitHub, checked 2026-07-17):

```python
# odoo/addons/base/models/ir_http.py  (core, 18.0+)
@classmethod
def _auth_method_bearer(cls):
    ...
    if token := get_http_authorization_bearer_token():
        # 'rpc' scope does not really exist, we basically require a global key (scope NULL)
        uid = request.env['res.users.apikeys']._check_credentials(scope='rpc', key=token)
        if not uid:
            raise Unauthorized("Invalid apikey", www_authenticate=WWWAuthenticate('bearer'))
        ...
```

That is the same extension mechanism `auth_api_key` pioneered in 2018 — a value for the `auth=` argument of `@http.route` — except now it is **in core and backed by native keys**. Any module on 18.0+ can write:

```python
@http.route("/my/custom/endpoint", auth="bearer", type="http")
def my_endpoint(self):
    # authenticated by a native res.users.apikeys bearer token, no OCA module
    ...
```

So the boundary that held from Odoo 14 to 17 — *native authenticates Odoo's API, OCA authenticates your API* — **erodes in Odoo 18**. Native keys can now guard a controller you wrote. The single biggest reason `auth_api_key` existed is now shipped in the box.

That does **not** make the OCA stack pointless — it narrows what it uniquely offers to four things `auth="bearer"` still does not give you:

- **Provisioning ergonomics** — an admin can create an OCA key for any user and set its value from environment config, with no pre-existing key. Native programmatic `generate()` always mints a random value and requires an existing native key.
- **Group-based endpoint gating** — `auth_api_key_group` + the FastAPI/base_rest consumers let one key be authorized for endpoint A but not B. Native `auth="bearer"` is all-or-nothing *at the route auth layer* (any valid global key passes; finer control means writing your own ACL/record-rule checks inside the handler).
- **Secrets out of the database** — `auth_api_key_server_env` keeps the key value out of the DB dump. Native key hashes always travel inside the dump.
- **Bootstrap** — as the previous post showed, core still has no unattended password→first-key path.

(The reverse is also true: native keys have **built-in expiration and revocation** that the OCA base `auth.api.key` model lacks — a point in native's favor, covered in the trade-off below.)

## 2025 (Odoo 19): `/json/2` Makes the Bearer Key the Primary External Auth

Odoo 19 shipped `/json/2` (the [`addons/rpc/controllers/json2.py`](https://github.com/odoo/odoo/blob/19.0/addons/rpc/controllers/json2.py) controller — new in 19.0, absent on 18.0) and made the bearer API key the **primary** external transport auth, **deprecating** the password-based XML-RPC/JSON-RPC APIs in favor of JSON-2 (they are deprecated, not yet removed — Odoo's docs schedule removal for Odoo 22; see the [migration post]({{< surl >}}/insights/odoo-json2-api-migration)). The `auth="bearer"` plumbing was already there from 18; 19 is when native keys become the default, front-door way to reach Odoo's own API.

So by 2025 OCA is no longer *the* way, nor even the only way, to key-protect your own endpoint. It is the way you reach for when you need one of the four things above.

Then comes the clearest sign of the shifted relationship: `auth_api_key_native_generate` (the module from the [previous post](https://mobile.trobz.com/blog/introducing-auth-api-key-native-generate)). It targets the same `server-auth` repo (still under review as [PR #970](https://github.com/OCA/server-auth/pull/970) at the time of writing), but it does not add another key system. It **mints native `res.users.apikeys` programmatically** — it closes the one bootstrap gap core left open, and hands you a *core* key to use with `/json/2`.

That is the whole arc in one module. In 2018 OCA existed because core had nothing. By 2026 OCA writes code whose job is to *feed* core's own key system. The two are no longer rivals; they compose.

---

## The Honest Trade-Off You Must Not Skip: Storage

Being code-grounded means naming where OCA is *weaker*, not only where it is more flexible.

Native `res.users.apikeys` stores a **hash**. Steal the database and you cannot recover the keys. OCA's `auth.api.key`, by default, stores the key as **plaintext** in the `key` column, comparing with a timing-safe `consteq`:

```python
# server-auth/auth_api_key/models/auth_api_key.py
@tools.ormcache("key")
def _retrieve_api_key_id(self, key):
    if not self.env.user.has_group("base.group_system"):
        raise AccessError(...)
    for api_key in self.search([], limit=None):
        if api_key.key and consteq(key, api_key.key):   # constant-time compare
            return api_key.id
    raise ValidationError(...)
```

`consteq` protects the *comparison* against timing attacks, but the value at rest is readable — anyone with a dump, a backup, or a `SELECT` sees every live key. The mitigation is `auth_api_key_server_env`: keep the key value out of the DB. It is **strongly recommended** whenever operational secret management is available — but note two caveats. First, it does not magically make a secret safe: the key still lives in plaintext *somewhere* (config file, environment variable, or a mounted secret), so it wants the usual handling — a real secret store (Vault, Kubernetes/Docker secrets), tight file permissions, and rotation. Second, as noted above, `auth_api_key_server_env` is not yet migrated to 19.0. (Note too that retrieval scans **all** keys with `search([])` and `consteq` — fine for a bounded set of integration keys, not for per-user auth at scale. That reinforces the intended use.)

There is a second operational gap that a storage comparison alone misses: **lifecycle**. Native `res.users.apikeys` has a built-in `expiration_date`, a documented 3-month cap for standard users, and native generate/revoke. The OCA base `auth.api.key` model has **no expiration field at all** — a key is valid until someone deletes the record, and rotation is entirely your own process. For anything long-lived, that is a stronger argument for native than the hashing is.

> ⚠️ Default OCA `auth.api.key` keeps the key in plaintext in the `key` column and has no built-in expiration. For a sensitive deployment, pair it with `auth_api_key_server_env` (backed by a proper secret store) and add your own rotation/expiry process.

---

## So Which One Do You Use?

They are not ranked; they fit different jobs — and after the timeline, the split should feel natural.

```mermaid
flowchart TD
    A["What are you protecting?"] --> B["Odoo's own models over /json/2 or RPC"]
    A --> C["A custom endpoint you wrote"]
    B --> N1["Native res.users.apikeys"]
    C --> D{"Odoo 18 or newer?"}
    D -- "No (14-17)" --> O1["OCA auth_api_key"]
    D -- Yes --> E{"Need group gating, admin/env<br/>provisioning, or secrets out of the DB?"}
    E -- No --> N2["Native auth=bearer"]
    E -- Yes --> O2["OCA auth_api_key<br/>+ group / server_env"]
```

Same decision as a lookup table, with the edge cases:

| Your situation | Reach for |
| --- | --- |
| Calling Odoo's own models over `/json/2` (mobile app, RPC client, ETL) | **Native `res.users.apikeys`** — hashed, user-owned, the transport already speaks it |
| Guarding a custom endpoint on **Odoo 18+**, simple all-or-nothing check | **Native `auth="bearer"`** — no extra module needed |
| Guarding a custom endpoint on **Odoo ≤17** | **OCA `auth_api_key`** — core had no `auth="bearer"` yet |
| Per-endpoint / per-key gating ("this key may hit A but not B") | **OCA + `auth_api_key_group`** (+ FastAPI / base_rest consumers) |
| Keys provisioned by an admin, value from environment config | **OCA `auth_api_key`** (+ `auth_api_key_server_env`) |
| Secrets that must stay out of the database dump | **OCA + `auth_api_key_server_env`** (backed by a real secret store) |
| Self-service ownership, built-in expiration and native revocation lifecycle | **Native** |

For **Odoo ≤17**, the old rule is the fastest guide: native authenticates Odoo's API, OCA authenticates yours. From **Odoo 18** that line is gone — `auth="bearer"` lets native keys guard your endpoints too — so the question shifts from *"whose endpoint is it?"* to *"do I need group gating, admin/env provisioning, or secrets out of the DB?"* If yes, OCA; if no, native alone is enough. Either way, thanks to `auth_api_key_native_generate` you can run both on the same instance.

> 🔒 **Security recommendation.** Default to **native** keys — hashed at rest, with built-in expiration and revocation. Adopt the OCA stack only for one of the four gaps above. When you do, `auth_api_key_server_env` is strongly recommended (never leave a live key plaintext in the DB) — backed by a real secret store (Vault, Kubernetes/Docker secrets), tight file permissions, and an explicit rotation/expiry process, since the base model gives you none. For a **greenfield Odoo 18+/19 project, start native-first** and add OCA modules only where a concrete gap demands it.

---

## Takeaways

- **OCA got there first.** `auth_api_key` (2018, Akretion 2017 roots, branch 10.0) predates native `res.users.apikeys` (2020) — it was never an alternative to core, it filled a void core did not fill for two more years.
- **Core caught up in two waves.** Odoo 14 took the RPC case; Odoo 18 added a generic `auth="bearer"` (native-key-backed) that lets native keys guard custom controllers too — the boundary that justified OCA. Odoo 19's `/json/2` then made the bearer key the primary external auth.
- **What's left for OCA is narrower and real.** Per-endpoint group gating, admin/env-based provisioning, secrets out of the DB dump, and the password→first-key bootstrap. Native still does none of these.
- **The relationship flipped.** With `auth_api_key_native_generate`, OCA now *feeds* core's key system instead of replacing it. Rivals became composable.
- **Mind storage and lifecycle.** Default OCA keys are plaintext in the DB and have no expiration; use `auth_api_key_server_env` (backed by a real secret store) plus your own rotation. Native is hashed and carries built-in expiration/revocation out of the box.

---

## References

OCA server-auth, "auth_api_key — first commit 6645f37e, 'New generic module du define REST services' (10.0, 2018-01-31)": https://github.com/OCA/server-auth/commit/6645f37e
OCA server-auth, "auth_api_key module source (19.0)": https://github.com/OCA/server-auth/tree/19.0/auth_api_key
OCA server-auth, "auth_api_key_group module source (19.0)": https://github.com/OCA/server-auth/tree/19.0/auth_api_key_group
OCA server-auth, "auth_api_key_server_env module source (18.0 — not yet migrated to 19.0)": https://github.com/OCA/server-auth/tree/18.0/auth_api_key_server_env
OCA server-auth, "19.0 branch contents": https://github.com/OCA/server-auth/tree/19.0
OCA server-auth, "PR #970 — [19.0][ADD] auth_api_key_native_generate (open at the time of writing)": https://github.com/OCA/server-auth/pull/970
OCA rest-framework, "fastapi_auth_api_key — first commit caaa7fcb (17.0, 2024-02-28)": https://github.com/OCA/rest-framework/commit/caaa7fcb
OCA rest-framework, "fastapi_auth_api_key module source (17.0)": https://github.com/OCA/rest-framework/tree/17.0/fastapi_auth_api_key
Odoo, "res.users.apikeys on branch 14.0 — odoo/addons/base/models/res_users.py": https://github.com/odoo/odoo/blob/14.0/odoo/addons/base/models/res_users.py
Odoo, "commit e6d59451 — [IMP] core: bearer authorization header (2024-09-16)": https://github.com/odoo/odoo/commit/e6d59451
Odoo, "ir.http on branch 18.0 — _auth_method_bearer": https://github.com/odoo/odoo/blob/18.0/odoo/addons/base/models/ir_http.py
Odoo, "ir.http on branch 19.0 — _auth_method_bearer": https://github.com/odoo/odoo/blob/19.0/odoo/addons/base/models/ir_http.py
Odoo, "the /json/2 controller on branch 19.0 — addons/rpc/controllers/json2.py": https://github.com/odoo/odoo/blob/19.0/addons/rpc/controllers/json2.py

