Bearer token in request classification #82

Closed
opened 2025-12-28 18:12:27 +00:00 by sami · 12 comments
Owner

Originally created by @alexvanin on GitHub (Sep 5, 2022).

Description

NeoFS service applications are middleware between users and native NeoFS protocol. All NeoFS requests are signed with the key. Service applications do not have access to user's keys, so they use configured keys (middleware keys) to sign requests.

In general, public key is used to identify request sender (owner) to apply access control rules. NeoFS supports token mechanism to allow access to the requests signed by middleware keys: bearer tokens and session tokens.

In the request handler, storage node fetches public key and compares it with container owner, alphabet or container nodes. Here is the simplified scheme of fetch function from v0.32.0 release: RequestOwner().

flowchart LR
    A[Start] --> B{Is session\ntoken present?}
    B -->|Yes| C[Owner == Session token creator]
    B -->|No| D[Owner == Request sender]
    C --> E[End]
    D --> E

There are two cases where token scheme is not applicable.

1. Service application requires access to a private container on behalf of the user

WebUI + REST gateway should provide UX similar to CLI. To access private container, REST Gateway's request should be considered as user's request. The only way to impersonate user is to use session token signed by user's key.

However, session token can't be reused for different requests: object context requires verb and object address.

Session token is a nice solution for peer-to-peer communication, when application uses private key to resign session token for every request. It is not applicable for service application, because token usually is signed once in a while:

  • signing every request might be very bad UX (e.g. with Wallet Connect),
  • session token is not transitive, so the user has to create session with unknown NeoFS storage node, instead of the known gateway.

2. User grants access to the container, which is accessed through gateway

S3 gateway user wants to provide object access for specific user. To do that, container's extended ACL is modified to grant access for user's public key.

When S3 gateway sends request on behalf of granted user, access control record can't be matched, because S3 signed request with middleware key.

Session token is a nice solution for peer-to-peer communication, when client uses private key to resign session token for evert request.

It is not applicable for service application, because token usually is signed once for a while:
- signing every NeoFS request might be very bad UX (e.g. with Wallet Connect),
- session token is not transitive, so the client has to create session with unknown NeoFS Node, instead of the known gateway.

Service applications usually work with bearer tokens, while session is established between gateway and the node.

flowchart LR
    A[Panel.NeoFS] o--o|Bearer token| B[REST Gateway]
    B o--o|Session token| C[NeoFS Node]
    B o--o|Bearer token| C[NeoFS Node]

Private container denies bearer tokens. So service application have to create public containers with restricted extended ACL to imitate private container.

Proposal

To solve this issue, modify bearer token processing behaviour. Provide impersonate flag in the token body.

message BearerToken {
  message Body {
    EACLTable eacl_table = 1 [json_name="eaclTable"];
    neo.fs.v2.refs.OwnerID owner_id = 2 [json_name="ownerID"];
    message TokenLifetime {
      uint64 exp = 1 [json_name="exp"];
      uint64 nbf = 2 [json_name="nbf"];
      uint64 iat = 3 [json_name="iat"];
    }
    TokenLifetime lifetime = 3 [json_name="lifetime"];
+   bool allow_impersonate = 4;
  }
  Body body = 1 [json_name="body"];
  neo.fs.v2.refs.Signature signature = 2 [json_name="signature"];
}

On false (default), behaviour is the same.

On true:

  1. consider token signer as request owner,
  2. do not process extended ACL table in token body.

When token allows impersonating, there is no meaning in replacing container extended ACL table with token extended ACL table. Node should behave as the request was originally signed by token owner. Then S3 Gateway users will be able to provide object access to specific users.

flowchart LR
    A[Start] --> F{Is bearer\ntoken present?}
    F -->|Yes|G{Impersonate?}
    G -->|Yes|H[Owner == Bearer token creator]

    F -->|No| B{Is session\ntoken present?}
    G -->|No| B
    B -->|Yes| C[Owner == Session token creator]
    B -->|No| D[Owner == Request sender]

    C --> E[End]
    D --> E
    H --> E
flowchart LR
    A[Start] --> F{Is bearer\ntoken present?}
    F -->|Yes| G{Impersonate?}
    F -->|No| B[Check container extended ACL]
    G -->|Yes| B
    G -->|No| C[Check bearer token extended ACL]

Tree service

Changes above are applied for object service. However, tree service is also affected by this change.

On false (default), behaviour is the same.

On true:

  1. Consider token signer as request sender,
  2. Do not process extended ACL table in token body, use container ACL
  3. Do not require token issuer to be container owner.

(3) looks very important here. Now S3 Gateway accesses user bucket by attaching bearer token. To access other user buckets it does not attach bearer token, because token issuer is always going to be different from container owner.

To grant access to the container, S3 gateway will specify public key of the user in container extended ACL. To match this rule, S3 gateway has to attach bearer token with impersonate flag. So token issuer check should gone in this case. It makes sense, because impersonate flag can be treated as if "there is no bearer tokens at all, request is signed on behalf of token issuer".


🟢 It is backward compatible
🔴 Classification becomes less clear

Originally created by @alexvanin on GitHub (Sep 5, 2022). # Description NeoFS service applications are middleware between users and native NeoFS protocol. All NeoFS requests are signed with the key. Service applications do not have access to user's keys, so they use configured keys (middleware keys) to sign requests. In general, public key is used to identify request sender (owner) to apply access control rules. NeoFS supports token mechanism to allow access to the requests signed by middleware keys: bearer tokens and session tokens. In the request handler, storage node fetches public key and compares it with container owner, alphabet or container nodes. Here is the simplified scheme of fetch function from v0.32.0 release: [RequestOwner()](https://github.com/nspcc-dev/neofs-node/blob/d6fef68a62ad26cf9d7a7824b16768bedc571395/pkg/services/object/acl/v2/request.go#L111-L137). ```mermaid flowchart LR A[Start] --> B{Is session\ntoken present?} B -->|Yes| C[Owner == Session token creator] B -->|No| D[Owner == Request sender] C --> E[End] D --> E ``` There are two cases where token scheme is not applicable. ### 1. Service application requires access to a private container on behalf of the user WebUI + REST gateway should provide UX similar to CLI. To access private container, REST Gateway's request should be considered as user's request. The only way to impersonate user is to use session token signed by user's key. However, session token can't be reused for different requests: object context requires [verb](https://github.com/nspcc-dev/neofs-api/blob/d95228c40283cf6e188073a87a802af7e5dc0a7d/session/types.proto#L41) and [object address](https://github.com/nspcc-dev/neofs-api/blob/d95228c40283cf6e188073a87a802af7e5dc0a7d/session/types.proto#L52). Session token is a nice solution for peer-to-peer communication, when application uses private key to resign session token for every request. It is not applicable for service application, because token usually is signed once in a while: - signing every request might be very bad UX (e.g. with Wallet Connect), - session token is not transitive, so the user has to create session with unknown NeoFS storage node, instead of the known gateway. ### 2. User grants access to the container, which is accessed through gateway S3 gateway user wants to provide object access for specific user. To do that, container's extended ACL is modified to grant access for user's public key. When S3 gateway sends request on behalf of granted user, access control record can't be matched, because S3 signed request with middleware key. Session token is a nice solution for peer-to-peer communication, when client uses private key to resign session token for evert request. It is not applicable for service application, because token usually is signed once for a while: - signing every NeoFS request might be very bad UX (e.g. with Wallet Connect), - session token is not transitive, so the client has to create session with unknown NeoFS Node, instead of the known gateway. Service applications usually work with bearer tokens, while session is established between gateway and the node. ```mermaid flowchart LR A[Panel.NeoFS] o--o|Bearer token| B[REST Gateway] B o--o|Session token| C[NeoFS Node] B o--o|Bearer token| C[NeoFS Node] ``` Private container denies bearer tokens. So service application have to create public containers with restricted extended ACL to imitate private container. # Proposal To solve this issue, modify bearer token processing behaviour. Provide `impersonate` flag in the token body. ```diff message BearerToken { message Body { EACLTable eacl_table = 1 [json_name="eaclTable"]; neo.fs.v2.refs.OwnerID owner_id = 2 [json_name="ownerID"]; message TokenLifetime { uint64 exp = 1 [json_name="exp"]; uint64 nbf = 2 [json_name="nbf"]; uint64 iat = 3 [json_name="iat"]; } TokenLifetime lifetime = 3 [json_name="lifetime"]; + bool allow_impersonate = 4; } Body body = 1 [json_name="body"]; neo.fs.v2.refs.Signature signature = 2 [json_name="signature"]; } ``` On `false` (default), behaviour is the same. On `true`: 1) consider token signer as request owner, 2) do not process extended ACL table in token body. When token allows impersonating, there is no meaning in replacing container extended ACL table with token extended ACL table. Node should behave as the request was originally signed by token owner. Then S3 Gateway users will be able to provide object access to specific users. ```mermaid flowchart LR A[Start] --> F{Is bearer\ntoken present?} F -->|Yes|G{Impersonate?} G -->|Yes|H[Owner == Bearer token creator] F -->|No| B{Is session\ntoken present?} G -->|No| B B -->|Yes| C[Owner == Session token creator] B -->|No| D[Owner == Request sender] C --> E[End] D --> E H --> E ``` ```mermaid flowchart LR A[Start] --> F{Is bearer\ntoken present?} F -->|Yes| G{Impersonate?} F -->|No| B[Check container extended ACL] G -->|Yes| B G -->|No| C[Check bearer token extended ACL] ``` ### Tree service Changes above are applied for object service. However, tree service is also affected by this change. On `false` (default), behaviour is the same. On `true`: 1) Consider token signer as request sender, 2) Do not process extended ACL table in token body, use container ACL 3) Do not require token issuer to be container owner. (3) looks very important here. Now S3 Gateway accesses user bucket by attaching bearer token. To access other user buckets it does not attach bearer token, because token issuer is always going to be different from container owner. To grant access to the container, S3 gateway will specify public key of the user in container extended ACL. To match this rule, S3 gateway has to attach bearer token with impersonate flag. So token issuer check should gone in this case. It makes sense, because impersonate flag can be treated as if "there is no bearer tokens at all, request is signed on behalf of token issuer". --- :green_circle: It is backward compatible :red_circle: Classification becomes less clear
sami 2025-12-28 18:12:27 +00:00
Author
Owner

@alexvanin commented on GitHub (Oct 26, 2022):

We did some implementation prototyping for service checks:

neofs-api-go/poc/impersonate
neofs-sdk-go/poc/impersonate
neofs-node/poc/impersonate

@alexvanin commented on GitHub (Oct 26, 2022): We did some implementation prototyping for service checks: [neofs-api-go/poc/impersonate](https://github.com/nspcc-dev/neofs-api-go/compare/4d4eaa29436e2b1ce9bcdddd6551133c388a1cdb...alexvanin:neofs-api-go:poc/impersonate) [neofs-sdk-go/poc/impersonate](https://github.com/nspcc-dev/neofs-sdk-go/compare/da4ddcf337dad1b241f7d4c23d0170258f352703...alexvanin:neofs-sdk-go:poc/impersonate) [neofs-node/poc/impersonate](https://github.com/nspcc-dev/neofs-node/compare/926830bb9c8efd73ccb9bbbbf6d19f50d20babe8...alexvanin:neofs-node:poc/impersonate)
Author
Owner

@KirillovDenis commented on GitHub (Oct 26, 2022):

neofs-rest-gw
neofs-s3-gw

@KirillovDenis commented on GitHub (Oct 26, 2022): [neofs-rest-gw](https://github.com/nspcc-dev/neofs-rest-gw/compare/master...KirillovDenis:neofs-rest-gw:poc/impersonate) [neofs-s3-gw](https://github.com/nspcc-dev/neofs-s3-gw/compare/master...KirillovDenis:neofs-s3-gw:poc/impersonate)
Author
Owner

@alexvanin commented on GitHub (Oct 27, 2022):

Let's check scheme with putting object to node outside of the container.

@alexvanin commented on GitHub (Oct 27, 2022): Let's check scheme with putting object to node outside of the container.
Author
Owner

@cthulhu-rider commented on GitHub (Oct 27, 2022):

If in scheme above REST Gateway forms object on its side (lets imagine this 🪄) and stores it in the network, the bearer token will allow the gateway to act on user's behalf only within this particular request. Subsequently, this power of attorney will be lost, and there will not be able to justify why the object was saved.

On the contrary, session token is sewn into the object. Overall, IMO session mechanism fits better since we need to transfer the power of attorney.

@cthulhu-rider commented on GitHub (Oct 27, 2022): If in scheme above REST Gateway forms object on its side (lets imagine this :magic_wand:) and stores it in the network, the bearer token will allow the gateway to act on user's behalf only within this particular request. Subsequently, this power of attorney will be lost, and there will not be able to justify why the object was saved. On the contrary, session token is sewn into the object. Overall, IMO session mechanism fits better since we need to transfer the power of attorney.
Author
Owner

@KirillovDenis commented on GitHub (Nov 10, 2022):

The impersonate flag doesn't affect the integrity problem (object owner doesn't match to session token issuer in general) in the following case:

  1. Issue s3 creds using wallet1.json
  2. Start s3-gw using wallet2.json
  3. Create container/bucket
  4. Put object via s3-gw
  5. Check owner and session token issuer of uploaded object (owner will be from wallet1.json, session token issuer will be from wallet2.json)
@KirillovDenis commented on GitHub (Nov 10, 2022): The `impersonate` flag doesn't affect the integrity problem (object owner doesn't match to session token issuer in general) in the following case: 1. Issue s3 creds using `wallet1.json` 2. Start s3-gw using `wallet2.json` 3. Create container/bucket 4. Put object via s3-gw 5. Check owner and session token issuer of uploaded object (owner will be from `wallet1.json`, session token issuer will be from `wallet2.json`)
Author
Owner

@alexvanin commented on GitHub (Nov 17, 2022):

Probably we still have bearer token rules support in auth procedure. It is relevant when all credentials are issued by the same key.

@alexvanin commented on GitHub (Nov 17, 2022): Probably we still have bearer token rules support in auth procedure. It is relevant when all credentials are issued by the same key.
Author
Owner

@roman-khimov commented on GitHub (Mar 17, 2024):

I'm not sure what's so good about bearer to extend it this way effectively completely changing its meaning. Likely session tokens can be extended or even changed to:

  • allow multi-verb
  • allow multi-key
  • allow for key indirection (via accounts and/or via trusted domains)
@roman-khimov commented on GitHub (Mar 17, 2024): I'm not sure what's so good about bearer to extend it this way effectively completely changing its meaning. Likely session tokens can be extended or even changed to: * allow multi-verb * allow multi-key * allow for key indirection (via accounts and/or via trusted domains)
Author
Owner

@roman-khimov commented on GitHub (Mar 18, 2024):

The only positive side is one token instead of two tokens (with possibility to deprecate session ones). But currently session and bearer can be combined for a request, unlike in this proposal (where impersonation disables eacl).

@roman-khimov commented on GitHub (Mar 18, 2024): The only positive side is one token instead of two tokens (with possibility to deprecate session ones). But currently session and bearer can be combined for a request, unlike in this proposal (where impersonation disables eacl).
Author
Owner

@cthulhu-rider commented on GitHub (Apr 10, 2024):

Union

currently tokens implement completely independent mechanisms:

  • bearer token carries per-request access rules applied to fixed resource - container
  • session token represents power of attorney aplied to various resources

they currently converge in:

  • exactly one issuer who signs the token payload
  • exactly one subject
  • lifetime indicators

in total, tokens can be combined only structurally, not conceptually. Moreover, even being in the same data structure, their payload would be mutex

Proposal

im leaving an example for now. A more detailed format will be offered later

Public key (JWK)

{
   "crv" : "P-521",
   "kty" : "EC",
   "x" : "AH9axRXCnAio0G0YZ-WJG9HGvGJEBpu3DHhk9UqwmoTv4K7S4dhi8AWjy1l-m0tLc_vD4Yw22O4vB5D5prbnEppA",
   "y" : "AS8HljJnBwtJX54peKumQot5rgVXqJMBd5-YHYS-Cmj84JwDHLbpDWsD4tOeLS3hkrAg3Sv98cIcjrHP4a0R5IkI"
}

Proxy token payload (JWT)

{
 "exp": 1712929250,
 "iss": "NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM",
 "trustedActions": [
  "PUT_CONTAINER",
  "GET_OBJECT"
 ],
 "trustedSubjects": [
  "Nhfg3TbpwogLvDGVvAvqyThbsHgoSUKwtn",
  "NPFCqWHfi9ixCJRu7DABRbVfXRbkSEr9Vo"
 ]
}

Proxy token (JWS Compact serialization)

eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJOYlVnVFNGdlBtc1J4bUdlV3B1dUdlSlVvUm9pNlBFcmNNIiwiZXhwIjoxNzEyOTI5MjUwLCJ0cnVzdGVkQWN0aW9ucyI6WyJQVVRfQ09OVEFJTkVSIiwiR0VUX09CSkVDVCJdLCJ0cnVzdGVkU3ViamVjdHMiOlsiTmhmZzNUYnB3b2dMdkRHVnZBdnF5VGhic0hnb1NVS3d0biIsIk5QRkNxV0hmaTlpeENKUnU3REFCUmJWZlhSYmtTRXI5Vm8iXX0.ASZn-15v6qg-OkbajABJToufz5n8cLow5GQ-Nu-rP9DNtMSXpZUSN1I1PWojj6p4koUcf1OXZJUf61yK8xpyWq3dAYT1PBARsPQ4o74piBbQcNzC4oIXboFfSwfEeZ47d9RZJVB7d18L1Ng-6WMcsUOHuqtpbMfOx2LjXoaz2BlMhfbb

it may also be more flex to unite trustedSubjects and trustedActions into one member and have list of them as proxyList

@cthulhu-rider commented on GitHub (Apr 10, 2024): ## Union currently tokens implement completely independent mechanisms: * bearer token carries per-request access rules applied to fixed resource - container * session token represents power of attorney aplied to various resources they currently converge in: * exactly one issuer who signs the token payload * exactly one subject * lifetime indicators in total, tokens can be combined only structurally, not conceptually. Moreover, even being in the same data structure, their payload would be mutex # Proposal im leaving an example for now. A more detailed format will be offered later Public key (JWK) ```json { "crv" : "P-521", "kty" : "EC", "x" : "AH9axRXCnAio0G0YZ-WJG9HGvGJEBpu3DHhk9UqwmoTv4K7S4dhi8AWjy1l-m0tLc_vD4Yw22O4vB5D5prbnEppA", "y" : "AS8HljJnBwtJX54peKumQot5rgVXqJMBd5-YHYS-Cmj84JwDHLbpDWsD4tOeLS3hkrAg3Sv98cIcjrHP4a0R5IkI" } ``` Proxy token payload (JWT) ```json { "exp": 1712929250, "iss": "NbUgTSFvPmsRxmGeWpuuGeJUoRoi6PErcM", "trustedActions": [ "PUT_CONTAINER", "GET_OBJECT" ], "trustedSubjects": [ "Nhfg3TbpwogLvDGVvAvqyThbsHgoSUKwtn", "NPFCqWHfi9ixCJRu7DABRbVfXRbkSEr9Vo" ] } ``` Proxy token (JWS Compact serialization) ``` eyJhbGciOiJFUzUxMiJ9.eyJpc3MiOiJOYlVnVFNGdlBtc1J4bUdlV3B1dUdlSlVvUm9pNlBFcmNNIiwiZXhwIjoxNzEyOTI5MjUwLCJ0cnVzdGVkQWN0aW9ucyI6WyJQVVRfQ09OVEFJTkVSIiwiR0VUX09CSkVDVCJdLCJ0cnVzdGVkU3ViamVjdHMiOlsiTmhmZzNUYnB3b2dMdkRHVnZBdnF5VGhic0hnb1NVS3d0biIsIk5QRkNxV0hmaTlpeENKUnU3REFCUmJWZlhSYmtTRXI5Vm8iXX0.ASZn-15v6qg-OkbajABJToufz5n8cLow5GQ-Nu-rP9DNtMSXpZUSN1I1PWojj6p4koUcf1OXZJUf61yK8xpyWq3dAYT1PBARsPQ4o74piBbQcNzC4oIXboFfSwfEeZ47d9RZJVB7d18L1Ng-6WMcsUOHuqtpbMfOx2LjXoaz2BlMhfbb ``` it may also be more flex to unite `trustedSubjects` and `trustedActions` into one member and have list of them as `proxyList`
Author
Owner

@roman-khimov commented on GitHub (Oct 10, 2025):

Summarizing our discussions around this. We don't need to modify bearer tokens, they're good for the problem they solve allowing container owner to create some access token for third party use. The problem we really have in all of the app-gateway-sn chain scenarios is delegation, power of attorney and it should be solved with updated session token mechanism. Critical things for it:

  • accounts instead of keys
  • multi-account for subjects
  • multi action (verbs)
  • indirect accounts (NNS)
  • chain of trust (think of X.509)

Preferably:

  • time-based

To be decided:

  • JSON (only relevant for REST)

Optional:

  • extensibility (have app-specific claims, not just NeoFS ones)
@roman-khimov commented on GitHub (Oct 10, 2025): Summarizing our discussions around this. We don't need to modify bearer tokens, they're good for the problem they solve allowing container owner to create some access token for third party use. The problem we really have in all of the app-gateway-sn chain scenarios is delegation, power of attorney and it should be solved with updated session token mechanism. Critical things for it: * accounts instead of keys * multi-account for subjects * multi action (verbs) * indirect accounts (NNS) * chain of trust (think of X.509) Preferably: * time-based To be decided: * JSON (only relevant for REST) Optional: * extensibility (have app-specific claims, not just NeoFS ones)
Author
Owner

@roman-khimov commented on GitHub (Oct 15, 2025):

A note on JSON. While currently it's only relevant for REST, there are other possibilities as well if we're to follow JSON route, like https://github.com/neo-project/neo/issues/2866. It is possible to parse JSON in a smart contract (unlike protobuf) and this can create possibilities for contract-level action check and traceability.

@roman-khimov commented on GitHub (Oct 15, 2025): A note on JSON. While currently it's only relevant for REST, there are other possibilities as well if we're to follow JSON route, like https://github.com/neo-project/neo/issues/2866. It is possible to parse JSON in a smart contract (unlike protobuf) and this can create possibilities for contract-level action check and traceability.
Author
Owner

@roman-khimov commented on GitHub (Dec 6, 2025):

Fixed in #350.

@roman-khimov commented on GitHub (Dec 6, 2025): Fixed in #350.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
nspcc-dev/neofs-api#82
No description provided.