Skip to content

Commit

Permalink
feat: reduce size of verifiers (#3857)
Browse files Browse the repository at this point in the history
  • Loading branch information
aeneasr authored Oct 12, 2024
1 parent 7f8bd90 commit 0cd00dc
Show file tree
Hide file tree
Showing 24 changed files with 839 additions and 811 deletions.
24 changes: 20 additions & 4 deletions consent/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package consent

import (
"context"
"encoding/json"
"net/http"
"net/url"
Expand Down Expand Up @@ -476,11 +477,12 @@ func (h *Handler) acceptOAuth2LoginRequest(w http.ResponseWriter, r *http.Reques
}
handledLoginRequest.RequestedAt = loginRequest.RequestedAt

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

request, err := h.r.ConsentManager().HandleLoginRequest(ctx, f, challenge, &handledLoginRequest)
if err != nil {
h.r.Writer().WriteError(w, r, errorsx.WithStack(err))
Expand Down Expand Up @@ -575,7 +577,7 @@ func (h *Handler) rejectOAuth2LoginRequest(w http.ResponseWriter, r *http.Reques
return
}

f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, flowctx.AsLoginChallenge)
f, err := h.decodeFlowWithClient(ctx, challenge, flowctx.AsLoginChallenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
Expand Down Expand Up @@ -761,7 +763,7 @@ func (h *Handler) acceptOAuth2ConsentRequest(w http.ResponseWriter, r *http.Requ
p.RequestedAt = cr.RequestedAt
p.HandledAt = sqlxx.NullTime(time.Now().UTC())

f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, flowctx.AsConsentChallenge)
f, err := h.decodeFlowWithClient(ctx, challenge, flowctx.AsConsentChallenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
Expand Down Expand Up @@ -868,7 +870,7 @@ func (h *Handler) rejectOAuth2ConsentRequest(w http.ResponseWriter, r *http.Requ
return
}

f, err := flowctx.Decode[flow.Flow](ctx, h.r.FlowCipher(), challenge, flowctx.AsConsentChallenge)
f, err := h.decodeFlowWithClient(ctx, challenge, flowctx.AsConsentChallenge)
if err != nil {
h.r.Writer().WriteError(w, r, err)
return
Expand Down Expand Up @@ -1044,3 +1046,17 @@ 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
}

f.Client, err = h.r.ClientManager().GetConcreteClient(ctx, f.ClientID)
if err != nil {
return nil, err
}

return f, nil
}
21 changes: 11 additions & 10 deletions consent/strategy_default.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,11 @@ func (s *DefaultStrategy) verifyAuthentication(
return nil, errorsx.WithStack(fosite.ErrAccessDenied.WithHint("The login verifier is invalid."))
}

f.Client, err = s.r.ClientManager().GetConcreteClient(ctx, f.ClientID)
if err != nil {
return nil, err
}

session, err := s.r.ConsentManager().VerifyAndInvalidateLoginRequest(ctx, verifier)
if errors.Is(err, sqlcon.ErrNoRows) {
return nil, errorsx.WithStack(fosite.ErrAccessDenied.WithHint("The login verifier has already been used, has not been granted, or is invalid."))
Expand Down Expand Up @@ -652,6 +657,12 @@ func (s *DefaultStrategy) verifyConsent(ctx context.Context, _ http.ResponseWrit
if err != nil {
return nil, nil, errorsx.WithStack(fosite.ErrAccessDenied.WithHint("The consent verifier has already been used, has not been granted, or is invalid."))
}

f.Client, err = s.r.ClientManager().GetConcreteClient(ctx, f.ClientID)
if err != nil {
return nil, nil, err
}

if f.Client.GetID() != r.URL.Query().Get("client_id") {
return nil, nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("The flow client id does not match the authorize request client id."))
}
Expand Down Expand Up @@ -1170,13 +1181,3 @@ func (s *DefaultStrategy) ObfuscateSubjectIdentifier(ctx context.Context, cl fos
}
return subject, nil
}

func (s *DefaultStrategy) loginSessionFromCookie(r *http.Request) *flow.LoginSession {
clientID := r.URL.Query().Get("client_id")
if clientID == "" {
return nil
}
ls, _ := flowctx.FromCookie[flow.LoginSession](r.Context(), r, s.r.FlowCipher(), flowctx.LoginSessionCookie(flowctx.SuffixFromStatic(clientID)))

return ls
}
105 changes: 60 additions & 45 deletions flow/flow.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,92 +84,91 @@ type Flow struct {
// identify the session.
//
// required: true
ID string `db:"login_challenge"`
NID uuid.UUID `db:"nid"`
ID string `db:"login_challenge" json:"i"`
NID uuid.UUID `db:"nid" json:"n"`

// RequestedScope contains the OAuth 2.0 Scope requested by the OAuth 2.0 Client.
//
// required: true
RequestedScope sqlxx.StringSliceJSONFormat `db:"requested_scope"`
RequestedScope sqlxx.StringSliceJSONFormat `db:"requested_scope" json:"rs,omitempty"`

// RequestedAudience contains the access token audience as requested by the OAuth 2.0 Client.
//
// required: true
RequestedAudience sqlxx.StringSliceJSONFormat `db:"requested_at_audience"`
RequestedAudience sqlxx.StringSliceJSONFormat `db:"requested_at_audience" json:"ra,omitempty"`

// LoginSkip, if true, implies that the client has requested the same scopes from the same user previously.
// If true, you can skip asking the user to grant the requested scopes, and simply forward the user to the redirect URL.
//
// This feature allows you to update / set session information.
//
// required: true
LoginSkip bool `db:"login_skip"`
LoginSkip bool `db:"login_skip" json:"ls,omitempty"`

// Subject is the user ID of the end-user that authenticated. Now, that end user needs to grant or deny the scope
// requested by the OAuth 2.0 client. If this value is set and `skip` is true, you MUST include this subject type
// when accepting the login request, or the request will fail.
//
// required: true
Subject string `db:"subject"`
Subject string `db:"subject" json:"s,omitempty"`

// OpenIDConnectContext provides context for the (potential) OpenID Connect context. Implementation of these
// values in your app are optional but can be useful if you want to be fully compliant with the OpenID Connect spec.
OpenIDConnectContext *OAuth2ConsentRequestOpenIDConnectContext `db:"oidc_context"`
OpenIDConnectContext *OAuth2ConsentRequestOpenIDConnectContext `db:"oidc_context" json:"oc"`

// Client is the OAuth 2.0 Client that initiated the request.
//
// required: true
Client *client.Client `db:"-"`

ClientID string `db:"client_id"`
Client *client.Client `db:"-" json:"client,omitempty"`
ClientID string `db:"client_id" json:"ci,omitempty"`

// RequestURL is the original OAuth 2.0 Authorization URL requested by the OAuth 2.0 client. It is the URL which
// initiates the OAuth 2.0 Authorization Code or OAuth 2.0 Implicit flow. This URL is typically not needed, but
// might come in handy if you want to deal with additional request parameters.
//
// required: true
RequestURL string `db:"request_url"`
RequestURL string `db:"request_url" json:"r,omitempty"`

// SessionID is the login session ID. If the user-agent reuses a login session (via cookie / remember flag)
// this ID will remain the same. If the user-agent did not have an existing authentication session (e.g. remember is false)
// this will be a new random value. This value is used as the "sid" parameter in the ID Token and in OIDC Front-/Back-
// channel logout. Its value can generally be used to associate consecutive login requests by a certain user.
SessionID sqlxx.NullString `db:"login_session_id"`
SessionID sqlxx.NullString `db:"login_session_id" json:"si,omitempty"`

// IdentityProviderSessionID is the session ID of the end-user that authenticated.
// If specified, we will use this value to propagate the logout.
IdentityProviderSessionID sqlxx.NullString `db:"identity_provider_session_id"`
IdentityProviderSessionID sqlxx.NullString `db:"identity_provider_session_id" json:"is,omitempty"`

LoginVerifier string `db:"login_verifier"`
LoginCSRF string `db:"login_csrf"`
LoginVerifier string `db:"login_verifier" json:"lv,omitempty"`
LoginCSRF string `db:"login_csrf" json:"lc,omitempty"`

LoginInitializedAt sqlxx.NullTime `db:"login_initialized_at"`
RequestedAt time.Time `db:"requested_at"`
LoginInitializedAt sqlxx.NullTime `db:"login_initialized_at" json:"li,omitempty"`
RequestedAt time.Time `db:"requested_at" json:"ia,omitempty"`

State int16 `db:"state"`
State int16 `db:"state" json:"q,omitempty"`

// LoginRemember, if set to true, tells ORY Hydra to remember this user by telling the user agent (browser) to store
// a cookie with authentication data. If the same user performs another OAuth 2.0 Authorization Request, he/she
// will not be asked to log in again.
LoginRemember bool `db:"login_remember"`
LoginRemember bool `db:"login_remember" json:"lr,omitempty"`

// LoginRememberFor sets how long the authentication should be remembered for in seconds. If set to `0`, the
// authorization will be remembered for the duration of the browser session (using a session cookie).
LoginRememberFor int `db:"login_remember_for"`
LoginRememberFor int `db:"login_remember_for" json:"lf,omitempty"`

// LoginExtendSessionLifespan, if set to true, session cookie expiry time will be updated when session is
// refreshed (login skip=true).
LoginExtendSessionLifespan bool `db:"login_extend_session_lifespan"`
LoginExtendSessionLifespan bool `db:"login_extend_session_lifespan" json:"ll,omitempty"`

// ACR sets the Authentication AuthorizationContext Class Reference value for this authentication session. You can use it
// to express that, for example, a user authenticated using two factor authentication.
ACR string `db:"acr"`
ACR string `db:"acr" json:"a,omitempty"`

// AMR sets the Authentication Methods References value for this
// authentication session. You can use it to specify the method a user used to
// authenticate. For example, if the acr indicates a user used two factor
// authentication, the amr can express they used a software-secured key.
AMR sqlxx.StringSliceJSONFormat `db:"amr"`
AMR sqlxx.StringSliceJSONFormat `db:"amr" json:"am,omitempty"`

// ForceSubjectIdentifier forces the "pairwise" user ID of the end-user that authenticated. The "pairwise" user ID refers to the
// (Pairwise Identifier Algorithm)[http://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg] of the OpenID
Expand All @@ -188,58 +187,58 @@ type Flow struct {
// other unique value).
//
// If you fail to compute the proper value, then authentication processes which have id_token_hint set might fail.
ForceSubjectIdentifier string `db:"forced_subject_identifier"`
ForceSubjectIdentifier string `db:"forced_subject_identifier" json:"fs,omitempty"`

// Context is an optional object which can hold arbitrary data. The data will be made available when fetching the
// consent request under the "context" field. This is useful in scenarios where login and consent endpoints share
// data.
Context sqlxx.JSONRawMessage `db:"context"`
Context sqlxx.JSONRawMessage `db:"context" json:"ct"`

// LoginWasUsed set to true means that the login request was already handled.
// This can happen on form double-submit or other errors. If this is set we
// recommend redirecting the user to `request_url` to re-initiate the flow.
LoginWasUsed bool `db:"login_was_used"`
LoginWasUsed bool `db:"login_was_used" json:"lu,omitempty"`

LoginError *RequestDeniedError `db:"login_error"`
LoginAuthenticatedAt sqlxx.NullTime `db:"login_authenticated_at"`
LoginError *RequestDeniedError `db:"login_error" json:"le,omitempty"`
LoginAuthenticatedAt sqlxx.NullTime `db:"login_authenticated_at" json:"la,omitempty"`

// ConsentChallengeID is the identifier ("authorization challenge") of the consent authorization request. It is used to
// identify the session.
//
// required: true
ConsentChallengeID sqlxx.NullString `db:"consent_challenge_id"`
ConsentChallengeID sqlxx.NullString `db:"consent_challenge_id" json:"cc,omitempty"`

// ConsentSkip, if true, implies that the client has requested the same scopes from the same user previously.
// If true, you must not ask the user to grant the requested scopes. You must however either allow or deny the
// consent request using the usual API call.
ConsentSkip bool `db:"consent_skip"`
ConsentVerifier sqlxx.NullString `db:"consent_verifier"`
ConsentCSRF sqlxx.NullString `db:"consent_csrf"`
ConsentSkip bool `db:"consent_skip" json:"cs,omitempty"`
ConsentVerifier sqlxx.NullString `db:"consent_verifier" json:"cv,omitempty"`
ConsentCSRF sqlxx.NullString `db:"consent_csrf" json:"cr,omitempty"`

// GrantedScope sets the scope the user authorized the client to use. Should be a subset of `requested_scope`.
GrantedScope sqlxx.StringSliceJSONFormat `db:"granted_scope"`
GrantedScope sqlxx.StringSliceJSONFormat `db:"granted_scope" json:"gs,omitempty"`

// GrantedAudience sets the audience the user authorized the client to use. Should be a subset of `requested_access_token_audience`.
GrantedAudience sqlxx.StringSliceJSONFormat `db:"granted_at_audience"`
GrantedAudience sqlxx.StringSliceJSONFormat `db:"granted_at_audience" json:"ga,omitempty"`

// ConsentRemember, if set to true, tells ORY Hydra to remember this consent authorization and reuse it if the same
// client asks the same user for the same, or a subset of, scope.
ConsentRemember bool `db:"consent_remember"`
ConsentRemember bool `db:"consent_remember" json:"ce,omitempty"`

// ConsentRememberFor sets how long the consent authorization should be remembered for in seconds. If set to `0`, the
// authorization will be remembered indefinitely.
ConsentRememberFor *int `db:"consent_remember_for"`
ConsentRememberFor *int `db:"consent_remember_for" json:"cf"`

// ConsentHandledAt contains the timestamp the consent request was handled.
ConsentHandledAt sqlxx.NullTime `db:"consent_handled_at"`
ConsentHandledAt sqlxx.NullTime `db:"consent_handled_at" json:"ch,omitempty"`

// ConsentWasHandled set to true means that the request was already handled.
// This can happen on form double-submit or other errors. If this is set we
// recommend redirecting the user to `request_url` to re-initiate the flow.
ConsentWasHandled bool `db:"consent_was_used"`
ConsentError *RequestDeniedError `db:"consent_error"`
SessionIDToken sqlxx.MapStringInterface `db:"session_id_token" faker:"-"`
SessionAccessToken sqlxx.MapStringInterface `db:"session_access_token" faker:"-"`
ConsentWasHandled bool `db:"consent_was_used" json:"cw,omitempty"`
ConsentError *RequestDeniedError `db:"consent_error" json:"cx"`
SessionIDToken sqlxx.MapStringInterface `db:"session_id_token" faker:"-" json:"st"`
SessionAccessToken sqlxx.MapStringInterface `db:"session_access_token" faker:"-" json:"sa"`
}

func NewFlow(r *LoginRequest) *Flow {
Expand Down Expand Up @@ -511,21 +510,37 @@ type CipherProvider interface {
}

// ToLoginChallenge converts the flow into a login challenge.
func (f *Flow) ToLoginChallenge(ctx context.Context, cipherProvider CipherProvider) (string, error) {
func (f Flow) ToLoginChallenge(ctx context.Context, cipherProvider CipherProvider) (string, error) {
if f.Client != nil {
f.ClientID = f.Client.GetID()
}
f.Client = nil
return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsLoginChallenge)
}

// ToLoginVerifier converts the flow into a login verifier.
func (f *Flow) ToLoginVerifier(ctx context.Context, cipherProvider CipherProvider) (string, error) {
func (f Flow) ToLoginVerifier(ctx context.Context, cipherProvider CipherProvider) (string, error) {
if f.Client != nil {
f.ClientID = f.Client.GetID()
}
f.Client = nil
return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsLoginVerifier)
}

// ToConsentChallenge converts the flow into a consent challenge.
func (f *Flow) ToConsentChallenge(ctx context.Context, cipherProvider CipherProvider) (string, error) {
func (f Flow) ToConsentChallenge(ctx context.Context, cipherProvider CipherProvider) (string, error) {
if f.Client != nil {
f.ClientID = f.Client.GetID()
}
f.Client = nil
return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsConsentChallenge)
}

// ToConsentVerifier converts the flow into a consent verifier.
func (f *Flow) ToConsentVerifier(ctx context.Context, cipherProvider CipherProvider) (string, error) {
func (f Flow) ToConsentVerifier(ctx context.Context, cipherProvider CipherProvider) (string, error) {
if f.Client != nil {
f.ClientID = f.Client.GetID()
}
f.Client = nil
return flowctx.Encode(ctx, cipherProvider.FlowCipher(), f, flowctx.AsConsentVerifier)
}
34 changes: 0 additions & 34 deletions oauth2/flowctx/cookies.go

This file was deleted.

Loading

0 comments on commit 0cd00dc

Please sign in to comment.