mirror of
https://github.com/coder/coder.git
synced 2025-07-08 11:39:50 +00:00
refactor: move OAuth2 provider code to dedicated package (#18746)
# Refactor OAuth2 Provider Code into Dedicated Package This PR refactors the OAuth2 provider functionality by moving it from the main `coderd` package into a dedicated `oauth2provider` package. The change improves code organization and maintainability without changing functionality. Key changes: - Created a new `oauth2provider` package to house all OAuth2 provider-related code - Moved existing OAuth2 provider functionality from `coderd/identityprovider` to the new package - Refactored handler functions to follow a consistent pattern of returning `http.HandlerFunc` instead of being handlers directly - Split large files into smaller, more focused files organized by functionality: - `app_secrets.go` - Manages OAuth2 application secrets - `apps.go` - Handles OAuth2 application CRUD operations - `authorize.go` - Implements the authorization flow - `metadata.go` - Provides OAuth2 metadata endpoints - `registration.go` - Handles dynamic client registration - `revoke.go` - Implements token revocation - `secrets.go` - Manages secret generation and validation - `tokens.go` - Handles token issuance and validation This refactoring improves code organization and makes the OAuth2 provider functionality more maintainable while preserving all existing behavior.
This commit is contained in:
165
coderd/oauth2provider/authorize.go
Normal file
165
coderd/oauth2provider/authorize.go
Normal file
@ -0,0 +1,165 @@
|
||||
package oauth2provider
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/httpapi"
|
||||
"github.com/coder/coder/v2/coderd/httpmw"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
)
|
||||
|
||||
type authorizeParams struct {
|
||||
clientID string
|
||||
redirectURL *url.URL
|
||||
responseType codersdk.OAuth2ProviderResponseType
|
||||
scope []string
|
||||
state string
|
||||
resource string // RFC 8707 resource indicator
|
||||
codeChallenge string // PKCE code challenge
|
||||
codeChallengeMethod string // PKCE challenge method
|
||||
}
|
||||
|
||||
func extractAuthorizeParams(r *http.Request, callbackURL *url.URL) (authorizeParams, []codersdk.ValidationError, error) {
|
||||
p := httpapi.NewQueryParamParser()
|
||||
vals := r.URL.Query()
|
||||
|
||||
p.RequiredNotEmpty("state", "response_type", "client_id")
|
||||
|
||||
params := authorizeParams{
|
||||
clientID: p.String(vals, "", "client_id"),
|
||||
redirectURL: p.RedirectURL(vals, callbackURL, "redirect_uri"),
|
||||
responseType: httpapi.ParseCustom(p, vals, "", "response_type", httpapi.ParseEnum[codersdk.OAuth2ProviderResponseType]),
|
||||
scope: p.Strings(vals, []string{}, "scope"),
|
||||
state: p.String(vals, "", "state"),
|
||||
resource: p.String(vals, "", "resource"),
|
||||
codeChallenge: p.String(vals, "", "code_challenge"),
|
||||
codeChallengeMethod: p.String(vals, "", "code_challenge_method"),
|
||||
}
|
||||
// Validate resource indicator syntax (RFC 8707): must be absolute URI without fragment
|
||||
if err := validateResourceParameter(params.resource); err != nil {
|
||||
p.Errors = append(p.Errors, codersdk.ValidationError{
|
||||
Field: "resource",
|
||||
Detail: "must be an absolute URI without fragment",
|
||||
})
|
||||
}
|
||||
|
||||
p.ErrorExcessParams(vals)
|
||||
if len(p.Errors) > 0 {
|
||||
// Create a readable error message with validation details
|
||||
var errorDetails []string
|
||||
for _, err := range p.Errors {
|
||||
errorDetails = append(errorDetails, err.Error())
|
||||
}
|
||||
errorMsg := "Invalid query params: " + strings.Join(errorDetails, ", ")
|
||||
return authorizeParams{}, p.Errors, xerrors.Errorf(errorMsg)
|
||||
}
|
||||
return params, nil, nil
|
||||
}
|
||||
|
||||
// ShowAuthorizePage handles GET /oauth2/authorize requests to display the HTML authorization page.
|
||||
// It uses authorizeMW which intercepts GET requests to show the authorization form.
|
||||
func ShowAuthorizePage(db database.Store, accessURL *url.URL) http.HandlerFunc {
|
||||
handler := authorizeMW(accessURL)(ProcessAuthorize(db, accessURL))
|
||||
return handler.ServeHTTP
|
||||
}
|
||||
|
||||
// ProcessAuthorize handles POST /oauth2/authorize requests to process the user's authorization decision
|
||||
// and generate an authorization code. GET requests are handled by authorizeMW.
|
||||
func ProcessAuthorize(db database.Store, accessURL *url.URL) http.HandlerFunc {
|
||||
handler := func(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
apiKey := httpmw.APIKey(r)
|
||||
app := httpmw.OAuth2ProviderApp(r)
|
||||
|
||||
callbackURL, err := url.Parse(app.CallbackURL)
|
||||
if err != nil {
|
||||
httpapi.WriteOAuth2Error(r.Context(), rw, http.StatusInternalServerError, "server_error", "Failed to validate query parameters")
|
||||
return
|
||||
}
|
||||
|
||||
params, _, err := extractAuthorizeParams(r, callbackURL)
|
||||
if err != nil {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_request", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Validate PKCE for public clients (MCP requirement)
|
||||
if params.codeChallenge != "" {
|
||||
// If code_challenge is provided but method is not, default to S256
|
||||
if params.codeChallengeMethod == "" {
|
||||
params.codeChallengeMethod = "S256"
|
||||
}
|
||||
if params.codeChallengeMethod != "S256" {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusBadRequest, "invalid_request", "Invalid code_challenge_method: only S256 is supported")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Ignoring scope for now, but should look into implementing.
|
||||
code, err := GenerateSecret()
|
||||
if err != nil {
|
||||
httpapi.WriteOAuth2Error(r.Context(), rw, http.StatusInternalServerError, "server_error", "Failed to generate OAuth2 app authorization code")
|
||||
return
|
||||
}
|
||||
err = db.InTx(func(tx database.Store) error {
|
||||
// Delete any previous codes.
|
||||
err = tx.DeleteOAuth2ProviderAppCodesByAppAndUserID(ctx, database.DeleteOAuth2ProviderAppCodesByAppAndUserIDParams{
|
||||
AppID: app.ID,
|
||||
UserID: apiKey.UserID,
|
||||
})
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return xerrors.Errorf("delete oauth2 app codes: %w", err)
|
||||
}
|
||||
|
||||
// Insert the new code.
|
||||
_, err = tx.InsertOAuth2ProviderAppCode(ctx, database.InsertOAuth2ProviderAppCodeParams{
|
||||
ID: uuid.New(),
|
||||
CreatedAt: dbtime.Now(),
|
||||
// TODO: Configurable expiration? Ten minutes matches GitHub.
|
||||
// This timeout is only for the code that will be exchanged for the
|
||||
// access token, not the access token itself. It does not need to be
|
||||
// long-lived because normally it will be exchanged immediately after it
|
||||
// is received. If the application does wait before exchanging the
|
||||
// token (for example suppose they ask the user to confirm and the user
|
||||
// has left) then they can just retry immediately and get a new code.
|
||||
ExpiresAt: dbtime.Now().Add(time.Duration(10) * time.Minute),
|
||||
SecretPrefix: []byte(code.Prefix),
|
||||
HashedSecret: []byte(code.Hashed),
|
||||
AppID: app.ID,
|
||||
UserID: apiKey.UserID,
|
||||
ResourceUri: sql.NullString{String: params.resource, Valid: params.resource != ""},
|
||||
CodeChallenge: sql.NullString{String: params.codeChallenge, Valid: params.codeChallenge != ""},
|
||||
CodeChallengeMethod: sql.NullString{String: params.codeChallengeMethod, Valid: params.codeChallengeMethod != ""},
|
||||
})
|
||||
if err != nil {
|
||||
return xerrors.Errorf("insert oauth2 authorization code: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}, nil)
|
||||
if err != nil {
|
||||
httpapi.WriteOAuth2Error(ctx, rw, http.StatusInternalServerError, "server_error", "Failed to generate OAuth2 authorization code")
|
||||
return
|
||||
}
|
||||
|
||||
newQuery := params.redirectURL.Query()
|
||||
newQuery.Add("code", code.Formatted)
|
||||
newQuery.Add("state", params.state)
|
||||
params.redirectURL.RawQuery = newQuery.Encode()
|
||||
|
||||
http.Redirect(rw, r, params.redirectURL.String(), http.StatusTemporaryRedirect)
|
||||
}
|
||||
|
||||
// Always wrap with its custom mw.
|
||||
return authorizeMW(accessURL)(http.HandlerFunc(handler)).ServeHTTP
|
||||
}
|
Reference in New Issue
Block a user