Skip to content

Commit

Permalink
feat: revoke token chain by consent challenge ID (#3932)
Browse files Browse the repository at this point in the history
This change adds the ability to revoke token chains by "consent
challenge ID".

## "Consent sessions"

Each time the user goes through a `GET
/oauth2/auth?response_type=code&...` auth code flow, we persist a new
"consent session" to the database.

This is independent of whether the user has previously logged in and/or
granted consent, or whether the user was actively asked to grant consent
by the consent app. A successful journey through the auth code flow
results in a new "consent session".

This consent session is uniquely identified by its "consent challenge
ID". This ID is obtained from the [`GET
/admin/oauth2/auth/requests/consent?consent_challenge=...`](https://www.ory.sh/docs/reference/api#tag/oAuth2/operation/getOAuth2ConsentRequest)
API. Note that it is not the same as the `consent_challenge=...` query
parameter!

Any access and refresh tokens obtained from a token exchange following
that particular user journey are bound to that consent session.

We call the totality of all refresh+access tokens derived from a
particular consent session a "token chain".

## Token revocation

Revoking an access token (AT) is simple: send the AT to `/oauth2/revoke`
and it is revoked. If this AT was derived from a refresh token (RT), the
parent RT is not revoked.

Revoking a refresh token (RT) also revokes associated access tokens.

## Revocation by consent challenge ID

During an authorization code flow, save the consent challenge ID into
the access token session data:

```
GET /admin/oauth2/auth/requests/consent?consent_challenge=abcdef
```
Response:
```
{
  "acr": ...,
  "challenge": "G_TIM3XABG14UwIgDoT1DRfipjhC1uix" # <- this is the ID we need
  ...
}
```

Accept the consent request:
```
PUT /admin/oauth2/auth/requests/consent/accept?consent_challenge=abcdef
{
  "remember": true,
  "remember_for": 3600,
  "session": {
    "access_token": {
      "ccid": "G_TIM3XABG14UwIgDoT1DRfipjhC1uix"
    }
  },
  ...
}
```

To revoke the token chain associated with this consent challenge ID, use

```
POST admin/oauth2/auth/sessions/consent?consent_challenge_id=G_TIM3XABG14UwIgDoT1DRfipjhC1uix
```
  • Loading branch information
alnr authored Feb 11, 2025
1 parent a7579b8 commit 4a40193
Show file tree
Hide file tree
Showing 23 changed files with 234 additions and 372 deletions.
49 changes: 31 additions & 18 deletions consent/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
package consent

import (
"context"
"encoding/json"
"net/http"
"net/url"
Expand Down Expand Up @@ -80,7 +79,6 @@ type revokeOAuth2ConsentSessions struct {
// The subject whose consent sessions should be deleted.
//
// in: query
// required: true
Subject string `json:"subject"`

// OAuth 2.0 Client ID
Expand All @@ -90,6 +88,13 @@ type revokeOAuth2ConsentSessions struct {
// in: query
Client string `json:"client"`

// Consent Challenge ID
//
// If set, revoke all token chains derived from this particular consent request ID.
//
// in: query
ConsentChallengeID string `json:"consent_challenge_id"`

// Revoke All Consent Sessions
//
// If set to `true` deletes all consent sessions by the Subject that have been granted.
Expand Down Expand Up @@ -119,14 +124,23 @@ type revokeOAuth2ConsentSessions struct {
func (h *Handler) revokeOAuth2ConsentSessions(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
subject := r.URL.Query().Get("subject")
client := r.URL.Query().Get("client")
consentChallengeID := r.URL.Query().Get("consent_challenge_id")
allClients := r.URL.Query().Get("all") == "true"
if subject == "" {
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'subject' is not defined but should have been.`)))
if subject == "" && consentChallengeID == "" {
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'subject' or 'consent_challenge_id' are required.`)))
return
}
if consentChallengeID != "" && subject != "" {
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'subject' and 'consent_challenge_id' cannot be set at the same time.`)))
return
}
if consentChallengeID != "" && client != "" {
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter 'client' and 'consent_challenge_id' cannot be set at the same time.`)))
return
}

switch {
case len(client) > 0:
case client != "":
if err := h.r.ConsentManager().RevokeSubjectClientConsentSession(r.Context(), subject, client); err != nil && !errors.Is(err, x.ErrNotFound) {
h.r.Writer().WriteError(w, r, err)
return
Expand All @@ -138,6 +152,12 @@ func (h *Handler) revokeOAuth2ConsentSessions(w http.ResponseWriter, r *http.Req
return
}
events.Trace(r.Context(), events.ConsentRevoked, events.WithSubject(subject))
case consentChallengeID != "":
if err := h.r.ConsentManager().RevokeConsentSessionByID(r.Context(), consentChallengeID); err != nil && !errors.Is(err, x.ErrNotFound) {
h.r.Writer().WriteError(w, r, err)
return
}
return
default:
h.r.Writer().WriteError(w, r, errorsx.WithStack(fosite.ErrInvalidRequest.WithHint(`Query parameter both 'client' and 'all' is not defined but one of them should have been.`)))
return
Expand Down Expand Up @@ -479,7 +499,7 @@ func (h *Handler) acceptOAuth2LoginRequest(w http.ResponseWriter, r *http.Reques
}
handledLoginRequest.RequestedAt = loginRequest.RequestedAt

f, err := h.decodeFlowWithClient(ctx, challenge, flowctx.AsLoginChallenge)
f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, flowctx.AsLoginChallenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
Expand Down Expand Up @@ -579,11 +599,12 @@ func (h *Handler) rejectOAuth2LoginRequest(w http.ResponseWriter, r *http.Reques
return
}

f, err := h.decodeFlowWithClient(ctx, challenge, flowctx.AsLoginChallenge)
f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, flowctx.AsLoginChallenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}

request, err := h.r.ConsentManager().HandleLoginRequest(ctx, f, challenge, &flow.HandledLoginRequest{
Error: &p,
ID: challenge,
Expand Down Expand Up @@ -765,11 +786,12 @@ func (h *Handler) acceptOAuth2ConsentRequest(w http.ResponseWriter, r *http.Requ
p.RequestedAt = cr.RequestedAt
p.HandledAt = sqlxx.NullTime(time.Now().UTC())

f, err := h.decodeFlowWithClient(ctx, challenge, flowctx.AsConsentChallenge)
f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, flowctx.AsConsentChallenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
}

hr, err := h.r.ConsentManager().HandleConsentRequest(ctx, f, &p)
if err != nil {
h.r.Writer().WriteError(w, r, errorsx.WithStack(err))
Expand Down Expand Up @@ -872,7 +894,7 @@ func (h *Handler) rejectOAuth2ConsentRequest(w http.ResponseWriter, r *http.Requ
return
}

f, err := h.decodeFlowWithClient(ctx, challenge, flowctx.AsConsentChallenge)
f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, flowctx.AsConsentChallenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
Expand Down Expand Up @@ -1048,12 +1070,3 @@ func (h *Handler) getOAuth2LogoutRequest(w http.ResponseWriter, r *http.Request,

h.r.Writer().Write(w, r, request)
}

func (h *Handler) decodeFlowWithClient(ctx context.Context, challenge string, opts ...flowctx.CodecOption) (*flow.Flow, error) {
f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, opts...)
if err != nil {
return nil, err
}

return f, nil
}
1 change: 0 additions & 1 deletion consent/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,6 @@ func TestGetConsentRequest(t *testing.T) {
} else if tc.exists {
var result flow.OAuth2ConsentRequest
require.NoError(t, json.NewDecoder(resp.Body).Decode(&result))
require.Equal(t, challenge, result.ID)
require.Equal(t, requestURL, result.RequestURL)
require.NotNil(t, result.Client)
}
Expand Down
1 change: 1 addition & 0 deletions consent/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type (
HandleConsentRequest(ctx context.Context, f *flow.Flow, r *flow.AcceptOAuth2ConsentRequest) (*flow.OAuth2ConsentRequest, error)
RevokeSubjectConsentSession(ctx context.Context, user string) error
RevokeSubjectClientConsentSession(ctx context.Context, user, client string) error
RevokeConsentSessionByID(ctx context.Context, consentChallengeID string) error

VerifyAndInvalidateConsentRequest(ctx context.Context, verifier string) (*flow.AcceptOAuth2ConsentRequest, error)
FindGrantedAndRememberedConsentRequests(ctx context.Context, client, user string) ([]flow.AcceptOAuth2ConsentRequest, error)
Expand Down
Loading

0 comments on commit 4a40193

Please sign in to comment.