summaryrefslogtreecommitdiff
path: root/internal/oidc
diff options
context:
space:
mode:
Diffstat (limited to 'internal/oidc')
-rw-r--r--internal/oidc/amr.go19
-rw-r--r--internal/oidc/const.go9
-rw-r--r--internal/oidc/flow_client_credentials.go76
-rw-r--r--internal/oidc/flow_client_credentials_test.go225
-rw-r--r--internal/oidc/flow_refresh.go2
-rw-r--r--internal/oidc/session.go28
-rw-r--r--internal/oidc/types_test.go99
-rw-r--r--internal/oidc/util.go65
-rw-r--r--internal/oidc/util_blackbox_test.go37
9 files changed, 433 insertions, 127 deletions
diff --git a/internal/oidc/amr.go b/internal/oidc/amr.go
index cd2d8d6b0..8c13c5b20 100644
--- a/internal/oidc/amr.go
+++ b/internal/oidc/amr.go
@@ -1,5 +1,24 @@
package oidc
+func NewAuthenticationMethodsReferencesFromClaim(claim []string) (amr AuthenticationMethodsReferences) {
+ for _, ref := range claim {
+ switch ref {
+ case AMRPasswordBasedAuthentication:
+ amr.UsernameAndPassword = true
+ case AMROneTimePassword:
+ amr.TOTP = true
+ case AMRShortMessageService:
+ amr.Duo = true
+ case AMRHardwareSecuredKey:
+ amr.WebAuthn = true
+ case AMRUserPresence:
+ amr.WebAuthnUserVerified = true
+ }
+ }
+
+ return amr
+}
+
// AuthenticationMethodsReferences holds AMR information.
type AuthenticationMethodsReferences struct {
UsernameAndPassword bool
diff --git a/internal/oidc/const.go b/internal/oidc/const.go
index ed5e7837e..fc0338207 100644
--- a/internal/oidc/const.go
+++ b/internal/oidc/const.go
@@ -12,6 +12,8 @@ const (
ScopeProfile = "profile"
ScopeEmail = "email"
ScopeGroups = "groups"
+
+ ScopeAutheliaBearerAuthz = "authelia.bearer.authz"
)
// Registered Claim strings. See https://www.iana.org/assignments/jwt/jwt.xhtml.
@@ -353,3 +355,10 @@ const (
const (
durationZero = time.Duration(0)
)
+
+const (
+ fieldRFC6750Error = "error"
+ fieldRFC6750ErrorDescription = "error_description"
+ fieldRFC6750Realm = "realm"
+ fieldRFC6750Scope = valueScope
+)
diff --git a/internal/oidc/flow_client_credentials.go b/internal/oidc/flow_client_credentials.go
index deb57a487..118f9496e 100644
--- a/internal/oidc/flow_client_credentials.go
+++ b/internal/oidc/flow_client_credentials.go
@@ -2,6 +2,7 @@ package oidc
import (
"context"
+ "net/url"
"time"
"github.com/ory/fosite"
@@ -92,3 +93,78 @@ func (c *ClientCredentialsGrantHandler) CanHandleTokenEndpointRequest(ctx contex
var (
_ fosite.TokenEndpointHandler = (*ClientCredentialsGrantHandler)(nil)
)
+
+// PopulateClientCredentialsFlowSessionWithAccessRequest is used to configure a session when performing a client credentials grant.
+func PopulateClientCredentialsFlowSessionWithAccessRequest(ctx Context, client fosite.Client, session *Session) (err error) {
+ var (
+ issuer *url.URL
+ )
+
+ if issuer, err = ctx.IssuerURL(); err != nil {
+ return fosite.ErrServerError.WithWrap(err).WithDebugf("Failed to determine the issuer with error: %s.", err.Error())
+ }
+
+ if client == nil {
+ return fosite.ErrServerError.WithDebug("Failed to get the client for the request.")
+ }
+
+ session.Subject = ""
+ session.Claims.Subject = client.GetID()
+ session.ClientID = client.GetID()
+ session.DefaultSession.Claims.Issuer = issuer.String()
+ session.DefaultSession.Claims.IssuedAt = ctx.GetClock().Now().UTC()
+ session.DefaultSession.Claims.RequestedAt = ctx.GetClock().Now().UTC()
+ session.ClientCredentials = true
+
+ return nil
+}
+
+// PopulateClientCredentialsFlowRequester is used to grant the authorized scopes and audiences when performing a client
+// credentials grant.
+func PopulateClientCredentialsFlowRequester(ctx Context, config fosite.Configurator, client fosite.Client, requester fosite.Requester) (err error) {
+ if client == nil || config == nil || requester == nil {
+ return fosite.ErrServerError.WithDebug("Failed to get the client, configuration, or requester for the request.")
+ }
+
+ scopes := requester.GetRequestedScopes()
+ audience := requester.GetRequestedAudience()
+
+ var authz, nauthz bool
+
+ strategy := config.GetScopeStrategy(ctx)
+
+ for _, scope := range scopes {
+ switch scope {
+ case ScopeOffline, ScopeOfflineAccess:
+ break
+ case ScopeAutheliaBearerAuthz:
+ authz = true
+ default:
+ nauthz = true
+ }
+
+ if strategy(client.GetScopes(), scope) {
+ requester.GrantScope(scope)
+ } else {
+ return fosite.ErrInvalidScope.WithDebugf("The scope '%s' is not authorized on client with id '%s'.", scope, client.GetID())
+ }
+ }
+
+ if authz && nauthz {
+ return fosite.ErrInvalidScope.WithDebugf("The scope '%s' must only be requested by itself or with the '%s' scope, no other scopes are permitted.", ScopeAutheliaBearerAuthz, ScopeOfflineAccess)
+ }
+
+ if authz && len(audience) == 0 {
+ return fosite.ErrInvalidRequest.WithDebugf("The scope '%s' requires the request also include an audience.", ScopeAutheliaBearerAuthz)
+ }
+
+ if err = config.GetAudienceStrategy(ctx)(client.GetAudience(), audience); err != nil {
+ return err
+ }
+
+ for _, aud := range audience {
+ requester.GrantAudience(aud)
+ }
+
+ return nil
+}
diff --git a/internal/oidc/flow_client_credentials_test.go b/internal/oidc/flow_client_credentials_test.go
index 0e1b0ef50..646263340 100644
--- a/internal/oidc/flow_client_credentials_test.go
+++ b/internal/oidc/flow_client_credentials_test.go
@@ -2,15 +2,20 @@ package oidc_test
import (
"context"
+ "errors"
"net/http"
+ "net/url"
"testing"
"time"
"github.com/ory/fosite"
"github.com/ory/fosite/handler/oauth2"
+ "github.com/ory/fosite/handler/openid"
+ fjwt "github.com/ory/fosite/token/jwt"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
+ "github.com/authelia/authelia/v4/internal/clock"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/oidc"
)
@@ -258,3 +263,223 @@ func TestClientCredentialsGrantHandler_PopulateTokenEndpointResponse(t *testing.
})
}
}
+
+func TestPopulateClientCredentialsFlowSessionWithAccessRequest(t *testing.T) {
+ testCases := []struct {
+ name string
+ setup func(ctx oidc.Context)
+ ctx oidc.Context
+ client fosite.Client
+ have *oidc.Session
+ expected *oidc.Session
+ err string
+ }{
+ {
+ "ShouldHandleIssuerError",
+ nil,
+ &TestContext{
+ IssuerURLFunc: func() (issuerURL *url.URL, err error) {
+ return nil, errors.New("an error")
+ },
+ },
+ nil,
+ oidc.NewSession(),
+ nil,
+ "The authorization server encountered an unexpected condition that prevented it from fulfilling the request. Failed to determine the issuer with error: an error.",
+ },
+ {
+ "ShouldHandleClientError",
+ nil,
+ &TestContext{
+ IssuerURLFunc: func() (issuerURL *url.URL, err error) {
+ return &url.URL{Scheme: "https", Host: "example.com"}, nil
+ },
+ },
+ nil,
+ oidc.NewSession(),
+ nil,
+ "The authorization server encountered an unexpected condition that prevented it from fulfilling the request. Failed to get the client for the request.",
+ },
+ {
+ "ShouldUpdateValues",
+ func(ctx oidc.Context) {
+ c := ctx.(*TestContext)
+
+ c.Clock = clock.NewFixed(time.Unix(10000000000, 0))
+ },
+ &TestContext{
+ IssuerURLFunc: func() (issuerURL *url.URL, err error) {
+ return &url.URL{Scheme: "https", Host: "example.com"}, nil
+ },
+ },
+ &oidc.BaseClient{
+ ID: abc,
+ },
+ oidc.NewSession(),
+ &oidc.Session{
+ Extra: map[string]any{},
+ DefaultSession: &openid.DefaultSession{
+ Headers: &fjwt.Headers{
+ Extra: map[string]any{},
+ },
+ Claims: &fjwt.IDTokenClaims{
+ Issuer: "https://example.com",
+ IssuedAt: time.Unix(10000000000, 0).UTC(),
+ RequestedAt: time.Unix(10000000000, 0).UTC(),
+ Subject: abc,
+ Extra: map[string]any{},
+ },
+ },
+ ClientID: abc,
+ ClientCredentials: true,
+ },
+ "",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ if tc.setup != nil {
+ tc.setup(tc.ctx)
+ }
+
+ err := oidc.PopulateClientCredentialsFlowSessionWithAccessRequest(tc.ctx, tc.client, tc.have)
+
+ assert.Equal(t, "", tc.have.GetSubject())
+
+ if len(tc.err) == 0 {
+ assert.NoError(t, err)
+ assert.EqualValues(t, tc.expected, tc.have)
+ } else {
+ assert.EqualError(t, oidc.ErrorToDebugRFC6749Error(err), tc.err)
+ }
+ })
+ }
+}
+
+func TestPopulateClientCredentialsFlowRequester(t *testing.T) {
+ testCases := []struct {
+ name string
+ setup func(ctx oidc.Context)
+ ctx oidc.Context
+ config fosite.Configurator
+ client fosite.Client
+ have *fosite.Request
+ expected *fosite.Request
+ err string
+ }{
+ {
+ "ShouldHandleBasic",
+ nil,
+ &TestContext{},
+ &oidc.Config{},
+ &oidc.BaseClient{},
+ &fosite.Request{},
+ &fosite.Request{},
+ "",
+ },
+ {
+ "ShouldHandleNilErrorClient",
+ nil,
+ &TestContext{},
+ &oidc.Config{},
+ nil,
+ &fosite.Request{},
+ &fosite.Request{},
+ "The authorization server encountered an unexpected condition that prevented it from fulfilling the request. Failed to get the client, configuration, or requester for the request.",
+ },
+ {
+ "ShouldHandleBadScopeCombinationAuthz",
+ nil,
+ &TestContext{},
+ &oidc.Config{},
+ &oidc.BaseClient{ID: "abc", Scopes: []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOpenID}},
+ &fosite.Request{RequestedScope: fosite.Arguments{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOpenID}},
+ &fosite.Request{},
+ "The requested scope is invalid, unknown, or malformed. The scope 'authelia.bearer.authz' must only be requested by itself or with the 'offline_access' scope, no other scopes are permitted.",
+ },
+ {
+ "ShouldHandleScopeNotPermitted",
+ nil,
+ &TestContext{},
+ &oidc.Config{},
+ &oidc.BaseClient{ID: "abc", Scopes: []string{oidc.ScopeAutheliaBearerAuthz}},
+ &fosite.Request{RequestedScope: fosite.Arguments{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess}},
+ &fosite.Request{},
+ "The requested scope is invalid, unknown, or malformed. The scope 'offline_access' is not authorized on client with id 'abc'.",
+ },
+ {
+ "ShouldHandleGoodScopesWithoutAudience",
+ nil,
+ &TestContext{},
+ &oidc.Config{},
+ &oidc.BaseClient{ID: "abc", Scopes: []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess}},
+ &fosite.Request{RequestedScope: fosite.Arguments{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess}},
+ &fosite.Request{},
+ "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Make sure that the various parameters are correct, be aware of case sensitivity and trim your parameters. Make sure that the client you are using has exactly whitelisted the redirect_uri you specified. The scope 'authelia.bearer.authz' requires the request also include an audience.",
+ },
+ {
+ "ShouldHandleGoodScopesAndBadAudience",
+ nil,
+ &TestContext{},
+ &oidc.Config{},
+ &oidc.BaseClient{ID: "abc", Scopes: []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess}},
+ &fosite.Request{RequestedScope: fosite.Arguments{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess}, RequestedAudience: fosite.Arguments{"https://example.com"}},
+ &fosite.Request{},
+ "The request is missing a required parameter, includes an invalid parameter value, includes a parameter more than once, or is otherwise malformed. Requested audience 'https://example.com' has not been whitelisted by the OAuth 2.0 Client.",
+ },
+ {
+ "ShouldHandleGoodScopesAndAudience",
+ nil,
+ &TestContext{},
+ &oidc.Config{},
+ &oidc.BaseClient{ID: "abc", Scopes: []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess}, Audience: fosite.Arguments{"https://example.com"}},
+ &fosite.Request{
+ RequestedScope: fosite.Arguments{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess},
+ RequestedAudience: fosite.Arguments{"https://example.com"},
+ },
+ &fosite.Request{
+ RequestedScope: fosite.Arguments{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess},
+ GrantedScope: fosite.Arguments{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess},
+ RequestedAudience: fosite.Arguments{"https://example.com"},
+ GrantedAudience: fosite.Arguments{"https://example.com"},
+ },
+ "",
+ },
+ {
+ "ShouldHandleGoodScopesAndAudienceSubSet",
+ nil,
+ &TestContext{},
+ &oidc.Config{},
+ &oidc.BaseClient{ID: "abc", Scopes: []string{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess}, Audience: fosite.Arguments{"https://example.com", "https://app.example.com"}},
+ &fosite.Request{
+ RequestedScope: fosite.Arguments{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess},
+ RequestedAudience: fosite.Arguments{"https://example.com"},
+ },
+ &fosite.Request{
+ RequestedScope: fosite.Arguments{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess},
+ GrantedScope: fosite.Arguments{oidc.ScopeAutheliaBearerAuthz, oidc.ScopeOfflineAccess},
+ RequestedAudience: fosite.Arguments{"https://example.com"},
+ GrantedAudience: fosite.Arguments{"https://example.com"},
+ },
+ "",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.name, func(t *testing.T) {
+ if tc.setup != nil {
+ tc.setup(tc.ctx)
+ }
+
+ err := oidc.PopulateClientCredentialsFlowRequester(tc.ctx, tc.config, tc.client, tc.have)
+
+ if len(tc.err) == 0 {
+ assert.NoError(t, err)
+ assert.EqualValues(t, tc.expected, tc.have)
+ } else {
+ assert.EqualError(t, oidc.ErrorToDebugRFC6749Error(err), tc.err)
+ }
+ })
+ }
+}
diff --git a/internal/oidc/flow_refresh.go b/internal/oidc/flow_refresh.go
index b95e233bb..6d4769b82 100644
--- a/internal/oidc/flow_refresh.go
+++ b/internal/oidc/flow_refresh.go
@@ -92,7 +92,7 @@ func (c *RefreshTokenGrantHandler) HandleTokenEndpointRequest(ctx context.Contex
include any scope not originally granted by the resource owner, and if omitted is treated as equal to
the scope originally granted by the resource owner.
- See https://www.rfc-editor.org/rfc/rfc6749#section-6
+ See https://datatracker.ietf.org/doc/html/rfc6749#section-6
*/
// Addresses point 1 of the text in RFC6749 Section 6.
diff --git a/internal/oidc/session.go b/internal/oidc/session.go
index 719d81c9b..7b283bfd7 100644
--- a/internal/oidc/session.go
+++ b/internal/oidc/session.go
@@ -1,7 +1,6 @@
package oidc
import (
- "context"
"net/url"
"time"
@@ -71,32 +70,6 @@ func NewSessionWithAuthorizeRequest(ctx Context, issuer *url.URL, kid, username
return session
}
-// PopulateClientCredentialsFlowSessionWithAccessRequest is used to configure a session when performing a client credentials grant.
-func PopulateClientCredentialsFlowSessionWithAccessRequest(ctx Context, request fosite.AccessRequester, session *Session, funcGetKID func(ctx context.Context, kid, alg string) string) (err error) {
- var (
- issuer *url.URL
- client Client
- ok bool
- )
-
- if issuer, err = ctx.IssuerURL(); err != nil {
- return fosite.ErrServerError.WithWrap(err).WithDebugf("Failed to determine the issuer with error: %s.", err.Error())
- }
-
- if client, ok = request.GetClient().(Client); !ok {
- return fosite.ErrServerError.WithDebugf("Failed to get the client for the request.")
- }
-
- session.Subject = ""
- session.Claims.Subject = client.GetID()
- session.ClientID = client.GetID()
- session.DefaultSession.Claims.Issuer = issuer.String()
- session.DefaultSession.Claims.IssuedAt = ctx.GetClock().Now().UTC()
- session.DefaultSession.Claims.RequestedAt = ctx.GetClock().Now().UTC()
-
- return nil
-}
-
// Session holds OpenID Connect 1.0 Session information.
type Session struct {
*openid.DefaultSession `json:"id_token"`
@@ -104,6 +77,7 @@ type Session struct {
ChallengeID uuid.NullUUID `json:"challenge_id"`
KID string `json:"kid"`
ClientID string `json:"client_id"`
+ ClientCredentials bool `json:"client_credentials"`
ExcludeNotBeforeClaim bool `json:"exclude_nbf_claim"`
AllowedTopLevelClaims []string `json:"allowed_top_level_claims"`
Extra map[string]any `json:"extra"`
diff --git a/internal/oidc/types_test.go b/internal/oidc/types_test.go
index eeb26bc36..3662ac0e8 100644
--- a/internal/oidc/types_test.go
+++ b/internal/oidc/types_test.go
@@ -2,7 +2,6 @@ package oidc_test
import (
"context"
- "errors"
"net/url"
"testing"
"time"
@@ -10,8 +9,6 @@ import (
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
"github.com/ory/fosite"
- "github.com/ory/fosite/handler/openid"
- fjwt "github.com/ory/fosite/token/jwt"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@@ -108,102 +105,6 @@ func TestNewSessionWithAuthorizeRequest(t *testing.T) {
assert.Nil(t, session.Claims.AuthenticationMethodsReferences)
}
-func TestPopulateClientCredentialsFlowSessionWithAccessRequest(t *testing.T) {
- testCases := []struct {
- name string
- setup func(ctx oidc.Context)
- ctx oidc.Context
- request fosite.AccessRequester
- have *oidc.Session
- expected *oidc.Session
- err string
- }{
- {
- "ShouldHandleIssuerError",
- nil,
- &TestContext{
- IssuerURLFunc: func() (issuerURL *url.URL, err error) {
- return nil, errors.New("an error")
- },
- },
- &fosite.AccessRequest{},
- oidc.NewSession(),
- nil,
- "The authorization server encountered an unexpected condition that prevented it from fulfilling the request. Failed to determine the issuer with error: an error.",
- },
- {
- "ShouldHandleClientError",
- nil,
- &TestContext{
- IssuerURLFunc: func() (issuerURL *url.URL, err error) {
- return &url.URL{Scheme: "https", Host: "example.com"}, nil
- },
- },
- &fosite.AccessRequest{},
- oidc.NewSession(),
- nil,
- "The authorization server encountered an unexpected condition that prevented it from fulfilling the request. Failed to get the client for the request.",
- },
- {
- "ShouldUpdateValues",
- func(ctx oidc.Context) {
- c := ctx.(*TestContext)
-
- c.Clock = clock.NewFixed(time.Unix(10000000000, 0))
- },
- &TestContext{
- IssuerURLFunc: func() (issuerURL *url.URL, err error) {
- return &url.URL{Scheme: "https", Host: "example.com"}, nil
- },
- },
- &fosite.AccessRequest{
- Request: fosite.Request{
- Client: &oidc.BaseClient{
- ID: abc,
- },
- },
- },
- oidc.NewSession(),
- &oidc.Session{
- Extra: map[string]any{},
- DefaultSession: &openid.DefaultSession{
- Headers: &fjwt.Headers{
- Extra: map[string]any{},
- },
- Claims: &fjwt.IDTokenClaims{
- Issuer: "https://example.com",
- IssuedAt: time.Unix(10000000000, 0).UTC(),
- RequestedAt: time.Unix(10000000000, 0).UTC(),
- Subject: abc,
- Extra: map[string]any{},
- },
- },
- ClientID: abc,
- },
- "",
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- if tc.setup != nil {
- tc.setup(tc.ctx)
- }
-
- err := oidc.PopulateClientCredentialsFlowSessionWithAccessRequest(tc.ctx, tc.request, tc.have, nil)
-
- assert.Equal(t, "", tc.have.GetSubject())
-
- if len(tc.err) == 0 {
- assert.NoError(t, err)
- assert.EqualValues(t, tc.expected, tc.have)
- } else {
- assert.EqualError(t, oidc.ErrorToDebugRFC6749Error(err), tc.err)
- }
- })
- }
-}
-
// TestContext is a minimal implementation of Context for the purpose of testing.
type TestContext struct {
context.Context
diff --git a/internal/oidc/util.go b/internal/oidc/util.go
index 81b8789e0..75cf2b02c 100644
--- a/internal/oidc/util.go
+++ b/internal/oidc/util.go
@@ -3,6 +3,7 @@ package oidc
import (
"errors"
"fmt"
+ "sort"
"strings"
"time"
@@ -366,6 +367,70 @@ func IsJWTProfileAccessToken(token *fjwt.Token) bool {
return ok && (typ == JWTHeaderTypeValueAccessTokenJWT)
}
+// RFC6750Header turns a *fosite.RFC6749Error into the values for a RFC6750 format WWW-Authenticate Bearer response
+// header, excluding the Bearer prefix.
+func RFC6750Header(realm, scope string, err *fosite.RFC6749Error) string {
+ values := err.ToValues()
+
+ if realm != "" {
+ values.Set("realm", realm)
+ }
+
+ if scope != "" {
+ values.Set("scope", scope)
+ }
+
+ //nolint:prealloc
+ var (
+ keys []string
+ key string
+ )
+
+ for key = range values {
+ keys = append(keys, key)
+ }
+
+ sort.Slice(keys, func(i, j int) bool {
+ switch keys[i] {
+ case fieldRFC6750Realm:
+ return true
+ case fieldRFC6750Error:
+ switch keys[j] {
+ case fieldRFC6750ErrorDescription, fieldRFC6750Scope:
+ return true
+ default:
+ return false
+ }
+ case fieldRFC6750ErrorDescription:
+ switch keys[j] {
+ case fieldRFC6750Scope:
+ return true
+ default:
+ return false
+ }
+ case fieldRFC6750Scope:
+ switch keys[j] {
+ case fieldRFC6750Realm, fieldRFC6750Error, fieldRFC6750ErrorDescription:
+ return false
+ default:
+ return keys[i] < keys[j]
+ }
+ default:
+ return keys[i] < keys[j]
+ }
+ })
+
+ parts := make([]string, len(keys))
+
+ var i int
+
+ for i, key = range keys {
+ parts[i] = fmt.Sprintf(`%s="%s"`, key, values.Get(key))
+ }
+
+ return strings.Join(parts, ",")
+}
+
// AccessResponderToClearMap returns a clear friendly map copy of the responder map values.
func AccessResponderToClearMap(responder fosite.AccessResponder) map[string]any {
m := responder.ToMap()
diff --git a/internal/oidc/util_blackbox_test.go b/internal/oidc/util_blackbox_test.go
index 3cafc5bf9..65b07f00a 100644
--- a/internal/oidc/util_blackbox_test.go
+++ b/internal/oidc/util_blackbox_test.go
@@ -55,6 +55,43 @@ func TestSortedJSONWebKey(t *testing.T) {
}
}
+func TestRFC6750Header(t *testing.T) {
+ testCaes := []struct {
+ name string
+ have *fosite.RFC6749Error
+ realm string
+ scope string
+ expected string
+ }{
+ {
+ "ShouldEncodeAll",
+ &fosite.RFC6749Error{
+ ErrorField: "invalid_example",
+ DescriptionField: "A description",
+ },
+ "abc",
+ "openid",
+ `realm="abc",error="invalid_example",error_description="A description",scope="openid"`,
+ },
+ {
+ "ShouldEncodeBasic",
+ &fosite.RFC6749Error{
+ ErrorField: "invalid_example",
+ DescriptionField: "A description",
+ },
+ "",
+ "",
+ `error="invalid_example",error_description="A description"`,
+ },
+ }
+
+ for _, tc := range testCaes {
+ t.Run(tc.name, func(t *testing.T) {
+ assert.Equal(t, tc.expected, oidc.RFC6750Header(tc.realm, tc.scope, tc.have))
+ })
+ }
+}
+
func TestIntrospectionResponseToMap(t *testing.T) {
testCases := []struct {
name string