Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 25 additions & 6 deletions internal/api/oauthserver/authorize.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type AuthorizationDetailsResponse struct {
Client ClientDetailsResponse `json:"client,omitempty"`
User UserDetailsResponse `json:"user,omitempty"`
Scope string `json:"scope,omitempty"`
Resource string `json:"resource,omitempty"`
}

// ClientDetailsResponse represents client details in authorization response
Expand Down Expand Up @@ -78,8 +79,14 @@ const (
oAuth2ErrorInvalidRequest = "invalid_request"
oAuth2ErrorServerError = "server_error"
oAuth2ErrorAccessDenied = "access_denied"

// RFC 8707 error code for invalid resource indicators
oAuth2ErrorInvalidTarget = "invalid_target"
)

// maxResourceLength matches the length limit of the resource columns
const maxResourceLength = 2048

// OAuthServerAuthorize handles GET /oauth/authorize
func (s *Server) OAuthServerAuthorize(w http.ResponseWriter, r *http.Request) error {
ctx := r.Context()
Expand Down Expand Up @@ -135,6 +142,18 @@ func (s *Server) OAuthServerAuthorize(w http.ResponseWriter, r *http.Request) er
return nil
}

// Resource indicator errors use the invalid_target code (RFC 8707)
if len(query["resource"]) > 1 {
err = errors.New("only one resource parameter is supported")
} else {
err = s.validateResourceParam(params.Resource)
}
if err != nil {
errorRedirectURL := s.buildErrorRedirectURL(params.RedirectURI, oAuth2ErrorInvalidTarget, err.Error(), params.State)
http.Redirect(w, r, errorRedirectURL, http.StatusFound)
return nil
}

// Store authorization request in database (without user initially)
authorization := models.NewOAuthServerAuthorization(models.NewOAuthServerAuthorizationParams{
ClientID: client.ID,
Expand Down Expand Up @@ -291,7 +310,8 @@ func (s *Server) OAuthServerGetAuthorization(w http.ResponseWriter, r *http.Requ
ID: user.ID.String(),
Email: user.Email.String(),
},
Scope: authorization.Scope,
Scope: authorization.Scope,
Resource: utilities.StringValue(authorization.Resource),
}

return shared.SendJSON(w, http.StatusOK, response)
Expand Down Expand Up @@ -462,11 +482,6 @@ func (s *Server) validateRemainingAuthorizeParams(params *AuthorizeParams) error
return err
}

// Resource parameter validation (per RFC 8707)
if err := s.validateResourceParam(params.Resource); err != nil {
return err
}

// PKCE validation
if err := s.validatePKCEParams(params.CodeChallengeMethod, params.CodeChallenge); err != nil {
return err
Expand Down Expand Up @@ -502,6 +517,10 @@ func (s *Server) validateResourceParam(resource string) error {
return nil
}

if len(resource) > maxResourceLength {
return fmt.Errorf("resource must be at most %d characters", maxResourceLength)
}

// Parse URL to validate it's an absolute URI
parsedURL, err := url.Parse(resource)
if err != nil {
Expand Down
13 changes: 12 additions & 1 deletion internal/api/oauthserver/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,13 @@ func (s *Server) OAuthToken(w http.ResponseWriter, r *http.Request) error {
params.ClientID = r.FormValue("client_id")
params.ClientSecret = r.FormValue("client_secret")
params.CodeVerifier = r.FormValue("code_verifier")

// RFC 8707 allows several resource parameters, but each grant is
// bound to a single resource
if len(r.Form["resource"]) > 1 {
return apierrors.NewOAuthError("invalid_target", "Only one resource parameter is supported")
}
params.Resource = r.FormValue("resource")
}

// Validate grant_type
Expand Down Expand Up @@ -355,7 +362,7 @@ func (s *Server) handleAuthorizationCodeGrant(ctx context.Context, w http.Respon

// Validate that (if exists) the resource parameter matches the authorization code resource
if params.Resource != "" && params.Resource != utilities.StringValue(authorization.Resource) {
return apierrors.NewOAuthError("invalid_grant", "Authorization code resource does not match the resource parameter")
return apierrors.NewOAuthError("invalid_target", "Authorization code resource does not match the resource parameter")
}

// Validate redirect_uri if provided - must match the one used in authorization
Expand Down Expand Up @@ -395,6 +402,9 @@ func (s *Server) handleAuthorizationCodeGrant(ctx context.Context, w http.Respon
scopes := authorization.Scope
grantParams.Scopes = &scopes

// Store the granted resource in session so access tokens are bound to it
grantParams.Resource = authorization.Resource

err = db.Transaction(func(tx *storage.Connection) error {
if _, terr := models.FindOAuthServerAuthorizationByIDForUpdate(tx, authorization.AuthorizationID); terr != nil {
if models.IsNotFoundError(terr) {
Expand Down Expand Up @@ -506,6 +516,7 @@ func (s *Server) handleRefreshTokenGrant(ctx context.Context, w http.ResponseWri
tokenResponse, err := tokenService.RefreshTokenGrant(ctx, db, r, w.Header(), tokens.RefreshTokenGrantParams{
RefreshToken: params.RefreshToken,
ClientID: clientID,
Resource: params.Resource,
})
if err != nil {
return err
Expand Down
Loading