Skip to content
Merged
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
13 changes: 12 additions & 1 deletion pkg/authserver/server/handlers/token.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func (h *Handler) TokenHandler(w http.ResponseWriter, req *http.Request) {
server.ErrInvalidTarget.WithHint("Multiple resource parameters are not supported"))
return
}
if len(resources) == 1 {
if len(resources) == 1 && resources[0] != "" {
resource := resources[0]
// Validate URI format per RFC 8707
if err := server.ValidateAudienceURI(resource); err != nil {
Expand All @@ -75,6 +75,17 @@ func (h *Handler) TokenHandler(w http.ResponseWriter, req *http.Request) {
"resource", resource,
)
accessRequest.GrantAudience(resource)
} else if accessRequest.GetGrantTypes().ExactOne("authorization_code") && len(h.config.AllowedAudiences) == 1 {
// No resource parameter provided (or provided as empty) during an authorization_code
// exchange; default to the sole allowed audience. The len == 1 guard makes the
// intended audience unambiguous and the index access safe. We restrict this defaulting
// to authorization_code grants: for refresh_token grants, fosite already carries the
// originally-granted audience forward through the session, so re-granting here would
// conflict with fosite's audience matching strategy.
slog.Debug("no resource parameter, defaulting to sole allowed audience",
"audience", h.config.AllowedAudiences[0],
)
accessRequest.GrantAudience(h.config.AllowedAudiences[0])
}

// Generate the access response (tokens)
Expand Down
96 changes: 71 additions & 25 deletions pkg/authserver/server/handlers/token_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,16 @@
package handlers

import (
"encoding/json"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"

"github.com/go-jose/go-jose/v4"
josejwt "github.com/go-jose/go-jose/v4/jwt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

Expand Down Expand Up @@ -173,35 +176,78 @@ func TestTokenHandler_Success(t *testing.T) {
assert.Contains(t, body, "expires_in")
}

func TestTokenHandler_ResourceParameter(t *testing.T) {
func TestTokenHandler_AudienceClaim(t *testing.T) {
t.Parallel()
handler, storState, _ := handlerTestSetup(t)

// Simulate authorize flow
authorizeCode := simulateAuthorizeFlow(t, handler, storState)

// Exchange code with RFC 8707 resource parameter
form := url.Values{
"grant_type": {"authorization_code"},
"client_id": {testAuthClientID},
"redirect_uri": {testAuthRedirectURI},
"code": {authorizeCode},
"code_verifier": {testPKCEVerifier},
"resource": {"https://api.example.com"},
// ptr is a helper to take the address of a string literal.
ptr := func(s string) *string { return &s }

tests := []struct {
name string
resource *string // nil = omit parameter; non-nil = include (possibly empty)
wantAud string
}{
{
name: "explicit resource grants matching audience",
resource: ptr("https://api.example.com"),
wantAud: "https://api.example.com",
},
{
name: "absent resource defaults to sole AllowedAudience",
resource: nil,
wantAud: "https://api.example.com",
},
{
name: "explicit empty resource defaults to sole AllowedAudience",
resource: ptr(""),
wantAud: "https://api.example.com",
},
}
req := httptest.NewRequest(http.MethodPost, "/oauth/token", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()

handler.TokenHandler(rec, req)

require.Equal(t, http.StatusOK, rec.Code, "expected 200 OK, got %d: %s", rec.Code, rec.Body.String())

// The resource parameter should be granted as audience in the JWT
// We can't easily verify the JWT contents here without decoding,
// but we verify the request succeeded
body := rec.Body.String()
assert.Contains(t, body, "access_token")
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
handler, storState, _ := handlerTestSetup(t)
authorizeCode := simulateAuthorizeFlow(t, handler, storState)

form := url.Values{
"grant_type": {"authorization_code"},
"client_id": {testAuthClientID},
"redirect_uri": {testAuthRedirectURI},
"code": {authorizeCode},
"code_verifier": {testPKCEVerifier},
}
if tc.resource != nil {
form.Set("resource", *tc.resource)
}

req := httptest.NewRequest(http.MethodPost, "/oauth/token", strings.NewReader(form.Encode()))
req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
rec := httptest.NewRecorder()

handler.TokenHandler(rec, req)

require.Equal(t, http.StatusOK, rec.Code, "got %d: %s", rec.Code, rec.Body.String())

var tokenResp map[string]any
require.NoError(t, json.NewDecoder(rec.Body).Decode(&tokenResp))

accessToken, ok := tokenResp["access_token"].(string)
require.True(t, ok, "access_token should be a string")
require.NotEmpty(t, accessToken)

parsedToken, err := josejwt.ParseSigned(accessToken, []jose.SignatureAlgorithm{jose.RS256})
require.NoError(t, err)

var claims map[string]any
require.NoError(t, parsedToken.UnsafeClaimsWithoutVerification(&claims))

aud, ok := claims["aud"].([]any)
require.True(t, ok, "aud claim should be an array, got: %T %v", claims["aud"], claims["aud"])
require.Len(t, aud, 1)
assert.Equal(t, tc.wantAud, aud[0])
})
}
}

func TestTokenHandler_RouteRegistered(t *testing.T) {
Expand Down
Loading