How to protect an API with edge — Versola Docs
/docs
Pre-1.00.7.5versola.ioGitHub

How to protect an API with edge

Put an internal API behind edge. Register the resource, grant permissions through a role, add an authorization preset for browser login, add a CEL rule and verify with curl, including a 403.

This guide puts one upstream API behind edge and lets a browser app call it with a session cookie. At the end a user can read their own orders, cannot read anyone else’s, and you have the curl commands that prove it. You need a running installation with a tenant linked to an edge, and your API reachable from edge over the network.

The example uses these names. Replace them with yours.

ThingValue
Public address of edge (edge-url)https://edge.example.com
Public address of authhttps://id.example.com
Your browser apphttps://app.example.com
Upstream APIhttp://orders.internal.example.com:8080
Resource IDorders-api
Clientacme-web
Permission, roleorders:read, customer
Presetweb

Prerequisites

  • An installation with central, auth and edge (Run Versola locally).
  • A tenant on the profile you want, linked to your edge in Tenants. See Security profiles.
  • central has a CA backend for client certificates, because a web client fronted by edge gets an issued certificate (Edge client certificates). The CLI targets configure one.
  • An upstream API with two routes: GET /orders and GET /users/{userId}/orders.

1. Register the resource

  1. Open Resources and press + Create Resource.

  2. Set Resource ID to orders-api and Absolute resource URI to http://orders.internal.example.com:8080. The URI has no path, query or fragment.

  3. Choose the type Internal. edge then calls your API with HTTP Basic credentials and never forwards the user’s token.

  4. Under Audience, tick the client acme-web once it exists (step 3). You can come back to this card.

  5. Press Create resource and copy the secret the banner shows. It appears once. Configure your API to accept Basic authentication with orders-api as the user and that secret as the password. More detail is in How to register a resource.

  6. On the Endpoints card press Edit endpoints, then Add, and add GET /orders. Leave the rules empty.

  7. Press Add again and add GET /users/{userId}/orders. In Allow (CEL) enter:

    request.path.params.userId == token.sub
  8. Press Save endpoints.

The rule compares the userId in the URL with the sub claim of the caller’s token. Other fields and variables are on Endpoint policies.

The panels of an endpoint. The guide's rule goes into Allow (CEL)
Resource endpoint with Allow, step-up and Inject rule.

2. Grant access through a permission and a role

An endpoint answers 403 until a permission covers it.

  1. Open Permissions, create orders:read and attach both endpoints of orders-api (How to register a permission).
  2. Open Roles, create customer and add orders:read (How to register a role).
  3. Open Users and give your test user the role customer.

3. Create the client and the authorization preset

  1. Open Clients and press the create button. In the wizard choose Web app and give the client ID acme-web.

  2. Add the redirect URI https://edge.example.com/complete. This is the /complete route of edge, which receives the code from auth.

  3. Finish the wizard and go back to the Audience field of the resource to add acme-web if you skipped it (How to register a client).

  4. On the client, open Authorization Presets and press + Add Preset:

    FieldValue
    Preset IDweb
    Redirect URIhttps://edge.example.com/complete
    Scopesopenid
    Response Typecode
    Post-login redirect URIhttps://app.example.com/
    Post-logout redirect URI (optional)https://app.example.com/logged-out
    Session cookie domain, path (optional)leave empty for a cookie on the edge host, path /
  5. Press Save Presets. edge loads presets with the rest of its configuration, so allow up to configuration-cache-refresh-interval (generated: 5 minutes, 1 minute locally).

4. Point the browser app at edge

  • Sign in. Send the browser to https://edge.example.com/login/web. After sign-in it comes back to https://app.example.com/ and holds an EDGE_SESSION cookie that JavaScript cannot read. To require a recent login or a stronger one, add acr_values or max_age to the URL.
  • Call the API. Request https://edge.example.com/resources/orders-api/orders with credentials: 'include'. The part after /resources/orders-api is matched against the endpoints you registered.
  • Same site, same origin. The cookie is SameSite=Strict, so the app and edge must be on the same site. edge sends no CORS headers, so serve the app and edge from one origin, for example by routing /resources/* and /login/* from your site’s proxy to edge.
  • Expired session. When the cookie cannot be refreshed, edge answers 401 with a Location: /login/web header. Navigate there.
  • Sign out. Send the browser to https://edge.example.com/logout/web.

5. Verify with curl

Sign in in the browser, then copy the EDGE_SESSION cookie value from the developer tools. It has the form web:<access token>. Take the user ID, the sub claim, from the token or from Users. A token bound to edge’s certificate works only from the cookie, so send it as a cookie and not as Authorization: Bearer.

Read your own orders:

curl -i -H "Cookie: EDGE_SESSION=web:<access token>" \
  https://edge.example.com/resources/orders-api/users/<your-user-id>/orders
HTTP/1.1 200 OK
...

The body is whatever your API returns. Now ask for someone else’s orders:

curl -i -H "Cookie: EDGE_SESSION=web:<access token>" \
  https://edge.example.com/resources/orders-api/users/<another-user-id>/orders
HTTP/1.1 403 Forbidden

The rule evaluated to false. edge logs access_rule_denied for this request and sends no body. The upstream never saw the call. Without a credential:

curl -i https://edge.example.com/resources/orders-api/orders
HTTP/1.1 401 Unauthorized

A path no endpoint matches, with a valid session, answers 404.

DPoP or certificate-bound tokens

The steps above serve a browser. When the caller is another system (a bank, a partner backend, a mobile app), it sends its own token in the Authorization header, and you choose how that token is bound to the caller. edge decides from the token’s cnf claim (Sender-constrained tokens).

DPoP (cnf.jkt)Certificate-bound (cnf.x5t#S256)
Caller provesPossession of a key, with a signed proof on every requestPossession of a client certificate, in the TLS handshake
PortThe plain edge portThe mutual-TLS listener of edge, MPORT
HeaderAuthorization: DPoP plus DPoPAuthorization: Bearer
Key lives inThe application or the device keystoreA certificate issued by a CA, rotated by renewal
What the caller needsA DPoP implementation, a nonce retry if requiredAn mTLS-capable HTTP client, and a plan for certificate renewal
What you runNothing extra. The dpop block of edgeA mutual-tls block with a server certificate, the caller’s CA in trusted-certificates, and a native block with edge-trusted-certificates on auth if endpoints use fetchUserInfo
Token after the caller renewsUnaffected as long as the key staysThe token names the old certificate. The caller gets a new token with the new one
Typical callerMobile and desktop apps, services and scriptsBanks and partners under FAPI 2.0, backends that already hold a client certificate

Choose DPoP when the caller cannot run mutual TLS or you do not want a second public port. Choose certificate-bound tokens when the caller is a regulated back end that already authenticates with a certificate (tls_client_auth), when the policy of your sector asks for mutual TLS, or when you want the proof to sit in the transport and not in every request. A token bound to both a key and a certificate is refused by edge on every presentation, so pick one binding per client.

To serve a certificate-bound caller:

  1. Register the client with authMethod: tls_client_auth (How to register a client). Its tokens are then certificate-bound.
  2. Make sure your tenant has the mutual-TLS settings for the client’s certificate path (Client certificates and their renewal).
  3. Add the mutual-tls block to the edge configuration, with the CA of the client’s certificate in trusted-certificates (Edge configuration).
  4. If an endpoint has Fetch userinfo on, give edge a native block and auth a mutual-tls.edge-trusted-certificates that names the edge CA.
  5. The caller requests a token from auth with its certificate and calls https://<mutual-tls address>/resources/orders-api/orders with Authorization: Bearer <access token> and the same certificate.
curl -i --cert bank.crt --key bank.key \
  -H "Authorization: Bearer <access token>" \
  https://api.example.com:8083/resources/orders-api/orders

The same token on the plain port answers 401 with certificate_bound_token_outside_session in the log.

If it does not work

You seeCheck
404 on /login/webThe preset is not saved, or edge has not reloaded. Wait one refresh interval
403 on every callThe role lacks the permission, or the permission lacks the endpoint. A permission_denied code in the edge log confirms it. Role and permission changes also take up to one refresh interval
403 with audience_denied in the logThe client is missing from the resource’s Audience
403 on your own orderstoken.sub is not the user ID you put in the URL
500A rule does not evaluate. Look for access_rule_failed in the log
Your API answers 401It does not accept Basic credentials orders-api:<secret>

Next steps