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
17 changes: 12 additions & 5 deletions auth/authorization_code.go
Original file line number Diff line number Diff line change
Expand Up @@ -308,11 +308,6 @@ func (h *AuthorizationCodeHandler) Authorize(ctx context.Context, req *http.Requ
}
}

resolvedClientConfig, err := h.handleRegistration(ctx, asm)
if err != nil {
return err
}

requestedScopes := scopesFromChallenges(wwwChallenges)
if len(requestedScopes) == 0 && len(prm.ScopesSupported) > 0 {
requestedScopes = prm.ScopesSupported
Expand All @@ -334,6 +329,18 @@ func (h *AuthorizationCodeHandler) Authorize(ctx context.Context, req *http.Requ
h.mu.RUnlock()
requestedScopes = authutil.UnionScopes(granted, requestedScopes)

// Propagate discovered scopes into the DCR metadata before registration,
// so the client is registered for the same scopes it will request.
// This prevents invalid_scope errors on strict authorization servers.
if dcrCfg := h.config.DynamicClientRegistrationConfig; dcrCfg != nil && dcrCfg.Metadata.Scope == "" && len(requestedScopes) > 0 {
dcrCfg.Metadata.Scope = strings.Join(requestedScopes, " ")
}

resolvedClientConfig, err := h.handleRegistration(ctx, asm)
if err != nil {
return err
}

cfg := &oauth2.Config{
ClientID: resolvedClientConfig.clientID,
ClientSecret: resolvedClientConfig.clientSecret,
Expand Down
173 changes: 173 additions & 0 deletions auth/authorization_code_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -915,6 +915,179 @@ func TestDynamicRegistration(t *testing.T) {
}
}

func TestDCRScopePropagation(t *testing.T) {
s := oauthtest.NewFakeAuthorizationServer(oauthtest.Config{
RegistrationConfig: &oauthtest.RegistrationConfig{
DynamicClientRegistrationEnabled: true,
},
ScopesSupported: []string{"read", "write"},
})
s.Start(t)

resourceMux := http.NewServeMux()
resourceServer := httptest.NewServer(resourceMux)
t.Cleanup(resourceServer.Close)
resourceURL := resourceServer.URL + "/resource"

resourceMux.Handle("/.well-known/oauth-protected-resource/resource", ProtectedResourceMetadataHandler(&oauthex.ProtectedResourceMetadata{
Resource: resourceURL,
AuthorizationServers: []string{s.URL()},
}))

dcrMetadata := &oauthex.ClientRegistrationMetadata{
RedirectURIs: []string{"http://localhost:12345/callback"},
}
handler, err := NewAuthorizationCodeHandler(&AuthorizationCodeHandlerConfig{
DynamicClientRegistrationConfig: &DynamicClientRegistrationConfig{
Metadata: dcrMetadata,
},
RedirectURL: "http://localhost:12345/callback",
AuthorizationCodeFetcher: func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error) {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(args.URL)
if err != nil {
return nil, fmt.Errorf("failed to visit auth URL: %v", err)
}
defer resp.Body.Close()
location, err := resp.Location()
if err != nil {
return nil, fmt.Errorf("failed to get location header: %v", err)
}
return &AuthorizationResult{
Code: location.Query().Get("code"),
State: location.Query().Get("state"),
Iss: location.Query().Get("iss"),
}, nil
},
})
if err != nil {
t.Fatalf("NewAuthorizationCodeHandler() error = %v", err)
}

req := httptest.NewRequest(http.MethodGet, resourceURL, nil)
resp := &http.Response{
StatusCode: http.StatusUnauthorized,
Header: make(http.Header),
Body: http.NoBody,
Request: req,
}
resp.Header.Set(
"WWW-Authenticate",
"Bearer scope=\"read write\", resource_metadata="+resourceServer.URL+"/.well-known/oauth-protected-resource/resource",
)

if err := handler.Authorize(context.Background(), req, resp); err != nil {
t.Fatalf("Authorize failed: %v", err)
}

if got := dcrMetadata.Scope; got != "read write" {
t.Errorf("DCR metadata Scope = %q, want %q", got, "read write")
}

tokenSource, err := handler.TokenSource(t.Context())
if err != nil {
t.Fatalf("Failed to get token source: %v", err)
}
token, err := tokenSource.Token()
if err != nil {
t.Fatalf("Failed to get token: %v", err)
}
if token.AccessToken != "test_access_token" {
t.Errorf("Expected access token 'test_access_token', got '%s'", token.AccessToken)
}
}

func TestDCRScopePropagation_PreservesExplicitScope(t *testing.T) {
s := oauthtest.NewFakeAuthorizationServer(oauthtest.Config{
RegistrationConfig: &oauthtest.RegistrationConfig{
DynamicClientRegistrationEnabled: true,
},
ScopesSupported: []string{"read", "write"},
})
s.Start(t)

resourceMux := http.NewServeMux()
resourceServer := httptest.NewServer(resourceMux)
t.Cleanup(resourceServer.Close)
resourceURL := resourceServer.URL + "/resource"

resourceMux.Handle("/.well-known/oauth-protected-resource/resource", ProtectedResourceMetadataHandler(&oauthex.ProtectedResourceMetadata{
Resource: resourceURL,
AuthorizationServers: []string{s.URL()},
}))

dcrMetadata := &oauthex.ClientRegistrationMetadata{
RedirectURIs: []string{"http://localhost:12345/callback"},
Scope: "explicit_scope",
}
handler, err := NewAuthorizationCodeHandler(&AuthorizationCodeHandlerConfig{
DynamicClientRegistrationConfig: &DynamicClientRegistrationConfig{
Metadata: dcrMetadata,
},
RedirectURL: "http://localhost:12345/callback",
AuthorizationCodeFetcher: func(ctx context.Context, args *AuthorizationArgs) (*AuthorizationResult, error) {
client := &http.Client{
CheckRedirect: func(req *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
}
resp, err := client.Get(args.URL)
if err != nil {
return nil, fmt.Errorf("failed to visit auth URL: %v", err)
}
defer resp.Body.Close()
location, err := resp.Location()
if err != nil {
return nil, fmt.Errorf("failed to get location header: %v", err)
}
return &AuthorizationResult{
Code: location.Query().Get("code"),
State: location.Query().Get("state"),
Iss: location.Query().Get("iss"),
}, nil
},
})
if err != nil {
t.Fatalf("NewAuthorizationCodeHandler() error = %v", err)
}

req := httptest.NewRequest(http.MethodGet, resourceURL, nil)
resp := &http.Response{
StatusCode: http.StatusUnauthorized,
Header: make(http.Header),
Body: http.NoBody,
Request: req,
}
resp.Header.Set(
"WWW-Authenticate",
"Bearer scope=\"read write\", resource_metadata="+resourceServer.URL+"/.well-known/oauth-protected-resource/resource",
)

if err := handler.Authorize(context.Background(), req, resp); err != nil {
t.Fatalf("Authorize failed: %v", err)
}

if got := dcrMetadata.Scope; got != "explicit_scope" {
t.Errorf("DCR metadata Scope = %q, want %q (explicit scope should not be overridden)", got, "explicit_scope")
}

tokenSource, err := handler.TokenSource(t.Context())
if err != nil {
t.Fatalf("Failed to get token source: %v", err)
}
token, err := tokenSource.Token()
if err != nil {
t.Fatalf("Failed to get token: %v", err)
}
if token.AccessToken != "test_access_token" {
t.Errorf("Expected access token 'test_access_token', got '%s'", token.AccessToken)
}
}

func TestValidateIssuerResponse(t *testing.T) {
const expectedIssuer = "https://auth.example.com"

Expand Down
16 changes: 12 additions & 4 deletions internal/oauthtest/fake_authorization_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,9 @@ import (
)

type ClientInfo struct {
Secret string
RedirectURIs []string
Secret string
RedirectURIs []string
RegisteredScope string
}

type MetadataEndpointConfig struct {
Expand Down Expand Up @@ -245,8 +246,9 @@ func (s *FakeAuthorizationServer) handleRegister(w http.ResponseWriter, r *http.
w.WriteHeader(http.StatusCreated)
clientID := rand.Text()
ci := ClientInfo{
Secret: rand.Text(),
RedirectURIs: metadata.RedirectURIs,
Secret: rand.Text(),
RedirectURIs: metadata.RedirectURIs,
RegisteredScope: metadata.Scope,
}
s.clients[clientID] = ci
metadata.TokenEndpointAuthMethod = "client_secret_basic"
Expand Down Expand Up @@ -443,3 +445,9 @@ func (s *FakeAuthorizationServer) authenticateClient(r *http.Request) error {
}
return nil
}

// GetClient returns the ClientInfo for the given clientID, or false if not found.
func (s *FakeAuthorizationServer) GetClient(clientID string) (ClientInfo, bool) {
ci, ok := s.clients[clientID]
return ci, ok
}