Kuadrant / authorino

K8s-native AuthN/AuthZ service to protect your APIs.
Apache License 2.0
195 stars 30 forks source link

OAuth 2.0 Token Exchange (RFC 8693) #437

Open guicassolato opened 11 months ago

guicassolato commented 11 months ago

Authorino does not provide any built-in functionality for obtaining OAuth2 access tokens on behalf of clients trying to access a service that is protected by an AuthConfig. This is by design to not violate a principle of the OAuth2 protocol, apart from a good practice of application security in general, according which each client is responsible for managing its own access tokens and credentials.[^1]

[^1]: For Security Considerations of the OAuth2 protocol, see Section 10 of RFC 6749.

Users of Authorino nevertheless often look out for ways of leveraging the flexibility provided by the authorization service, to handle the flows for obtaining and the management of access tokens. A lightweight example of this, that may not involve storing client authentication secrets in the AuthConfig, is relying on built-in OPA support or HTTP GET/GET-by-POST metadata to simply refresh an access token previously obtained by the client by its own means.

Example: OAuth2 token refresh performed by Authorino Follow the steps ① to ④ of the [OpenID Connect Discovery and authentication with JWTs](https://github.com/Kuadrant/authorino/blob/main/docs/user-guides/oidc-jwt-authentication.md#user-guide-openid-connect-discovery-and-authentication-with-jwts) user guide. Then, create the following AuthConfig that uses the refresh token passed in a `Request-Token` HTTP request header to exchange it on behalf of the client for a new access token. ```yaml apiVersion: authorino.kuadrant.io/v1beta2 kind: AuthConfig metadata: name: refresh-token spec: hosts: - talker-api.127.0.0.1.nip.io authentication: "keycloak-users": jwt: issuerUrl: http://keycloak.keycloak.svc.cluster.local:8080/auth/realms/kuadrant "anonymous-access": # enables anonymous access when it fails to validate the jwt and a refresh token is present when: - selector: request.headers.refresh-token operator: neq value: "" anonymous: {} priority: 1 metadata: "refresh-token": # tries to refresh the token when it's anonymous access when: - selector: auth.identity.anonymous operator: eq value: "true" http: url: http://keycloak.keycloak.svc.cluster.local:8080/auth/realms/kuadrant/protocol/openid-connect/token method: POST bodyParameters: "grant_type": value: refresh_token "client_id": value: demo "refresh_token": selector: request.headers.refresh-token authorization: "valid-token": # ensures a valid token is present, either by checking it is not anonymous access or because it succeeded refreshing the token when: patternMatching: patterns: - any: - selector: auth.identity.anonymous operator: neq value: "true" - selector: auth.metadata.refresh-token.access_token operator: neq value: "" response: success: headers: "authorization": # replaces the authorization header with the new token if obtained when: - selector: auth.metadata.refresh-token.access_token operator: neq value: "" plain: selector: "Bearer {auth.metadata.refresh-token.access_token}" "refresh-token": # replaces the refresh-token header with the new refresh token if obtained when: - selector: auth.metadata.refresh-token.refresh_token operator: neq value: "" plain: selector: auth.metadata.refresh-token.refresh_token ```

Instead of handling the OAuth2 grant/OIDC flows on behalf of the clients – a functionality more directly provided by solutions such as OAuth2 Proxy and Envoy's OAuth2 filter – Authorino implemented its own built-in Security Token Service (STS), called Festival Wristband.

Clients can exchange tokens – including tokens not limited to OIDC-issued JWTs (OAuth2 opaque tokens, API keys, etc) – for a highly customizable new JWT that the Authorino instance itself provides the keys to verify. This solution is particularly useful for implementing Edge Authentication Architecture (EAE) and applications in general that require reducing the scope of access tokens, avoid sharing tokens across services, etc.

Although it doesn't replace the need by the clients to handle the initial OIDC flow when that is the method of choice for authentication, Authorino's Festival Wristband tokens do provide an alternative to managing and to refreshing secondary tokens issued under the new scope. On the flip side, Authorino loses transparency in the identity management flow and becomes a token issuer authority.

Aiming to delegate the responsibility of issuing the wristband tokens to a proper authentication server and thus avoiding Authorino becoming a central actor in the identity management flow, this feature was proposed to be extended.

Since the OAuth 2.0 Token Exchange protocol (RFC 8693) became a standard, it opened up new possibilities for the implementation of the aforementioned proposal – if not to fully replace the feature.

One important aspect of OAuth 2.0 Token Exchange though is its built-in support for impersonation, which means, if adopted, that Authorino would lose transparency in the process of obtaining the new token, but (i) without becoming a central piece of the identity management flow – i.e., without becoming a token issuer authority – and (ii) by sticking with a proper industry protocol for the purpose of exchanging tokens. Some flexibility to define claims that end up in the new token may be lost.

Token Exchange can also be key to avoid sharing tokens across services, while it may require Authorino to keep track (occasionally refreshing) its own tokens on the other hand.

denniskniep commented 3 months ago

Moved this feature request here: https://github.com/Kuadrant/authorino/issues/477, because its not releated OAuth 2.0 Token Exchange (RFC 8693)

Sorry for confusion

guicassolato commented 3 months ago

Thank you so much, @denniskniep!

I promise to loop back with more detailed comments in a couple of days. Right now, of the top of my head,

  1. I would like to focus on urn:ietf:params:oauth:grant-type:token-exchange grant type and not authorization_code
  2. Client secret spec should probably be based on SecretKeyReference type
  3. Unlike DenyWithSpec which influences the proxy regarding the response to build to the client, WrappedSuccessResponseSpec affects the request that is sent to upstream, so it won't work out of the box for setting cookies. I need to read more about your proposed ErrorDenyWith though I believe it goes along the lines of this other request: #402, and therefore I'd probably separate it into its own change.