mirror of https://github.com/dexidp/dex.git
15 changed files with 795 additions and 30 deletions
@ -0,0 +1,70 @@ |
|||||||
|
package server |
||||||
|
|
||||||
|
import "fmt" |
||||||
|
|
||||||
|
// TokenExchangePolicy defines per-client access control for ID-JAG token exchange.
|
||||||
|
type TokenExchangePolicy struct { |
||||||
|
// ClientID is the client this policy applies to. Use "*" for a default policy.
|
||||||
|
ClientID string `json:"clientID"` |
||||||
|
AllowedAudiences []string `json:"allowedAudiences"` |
||||||
|
AllowedScopes []string `json:"allowedScopes"` |
||||||
|
} |
||||||
|
|
||||||
|
// evaluateIDJAGPolicy checks whether the client is permitted to obtain an ID-JAG
|
||||||
|
// for the given audience and scopes. No policies configured means allow all.
|
||||||
|
func evaluateIDJAGPolicy(policies []TokenExchangePolicy, clientID, audience string, scopes []string) error { |
||||||
|
if len(policies) == 0 { |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// Find the most-specific policy for this client: exact match first, then wildcard.
|
||||||
|
var matched *TokenExchangePolicy |
||||||
|
for i := range policies { |
||||||
|
p := &policies[i] |
||||||
|
if p.ClientID == clientID { |
||||||
|
matched = p |
||||||
|
break |
||||||
|
} |
||||||
|
if p.ClientID == "*" && matched == nil { |
||||||
|
matched = p |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if matched == nil { |
||||||
|
return fmt.Errorf("no policy found for client %q: access_denied", clientID) |
||||||
|
} |
||||||
|
|
||||||
|
// Check audience.
|
||||||
|
if !audienceAllowed(matched.AllowedAudiences, audience) { |
||||||
|
return fmt.Errorf("audience %q is not allowed for client %q: access_denied", audience, clientID) |
||||||
|
} |
||||||
|
|
||||||
|
// Check scopes (only if policy restricts them).
|
||||||
|
if len(matched.AllowedScopes) > 0 { |
||||||
|
for _, scope := range scopes { |
||||||
|
if !scopeAllowed(matched.AllowedScopes, scope) { |
||||||
|
return fmt.Errorf("scope %q is not allowed for client %q: access_denied", scope, clientID) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
func audienceAllowed(allowedAudiences []string, audience string) bool { |
||||||
|
for _, a := range allowedAudiences { |
||||||
|
if a == audience { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
|
|
||||||
|
func scopeAllowed(allowedScopes []string, scope string) bool { |
||||||
|
for _, s := range allowedScopes { |
||||||
|
if s == scope { |
||||||
|
return true |
||||||
|
} |
||||||
|
} |
||||||
|
return false |
||||||
|
} |
||||||
@ -0,0 +1,102 @@ |
|||||||
|
package server |
||||||
|
|
||||||
|
import ( |
||||||
|
"testing" |
||||||
|
) |
||||||
|
|
||||||
|
func TestEvaluateIDJAGPolicy(t *testing.T) { |
||||||
|
tests := []struct { |
||||||
|
name string |
||||||
|
policies []TokenExchangePolicy |
||||||
|
clientID string |
||||||
|
audience string |
||||||
|
scopes []string |
||||||
|
wantErr bool |
||||||
|
}{ |
||||||
|
{ |
||||||
|
name: "no policies: allow all", |
||||||
|
policies: nil, |
||||||
|
clientID: "any-client", |
||||||
|
audience: "https://resource.example.com", |
||||||
|
wantErr: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "exact match allowed", |
||||||
|
policies: []TokenExchangePolicy{ |
||||||
|
{ClientID: "client-a", AllowedAudiences: []string{"https://resource.example.com"}}, |
||||||
|
}, |
||||||
|
clientID: "client-a", |
||||||
|
audience: "https://resource.example.com", |
||||||
|
wantErr: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "audience not allowed", |
||||||
|
policies: []TokenExchangePolicy{ |
||||||
|
{ClientID: "client-a", AllowedAudiences: []string{"https://other.example.com"}}, |
||||||
|
}, |
||||||
|
clientID: "client-a", |
||||||
|
audience: "https://resource.example.com", |
||||||
|
wantErr: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "client not found: denied", |
||||||
|
policies: []TokenExchangePolicy{ |
||||||
|
{ClientID: "client-a", AllowedAudiences: []string{"https://resource.example.com"}}, |
||||||
|
}, |
||||||
|
clientID: "unknown-client", |
||||||
|
audience: "https://resource.example.com", |
||||||
|
wantErr: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "wildcard client matches", |
||||||
|
policies: []TokenExchangePolicy{ |
||||||
|
{ClientID: "*", AllowedAudiences: []string{"https://resource.example.com"}}, |
||||||
|
}, |
||||||
|
clientID: "any-client", |
||||||
|
audience: "https://resource.example.com", |
||||||
|
wantErr: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "exact match takes priority over wildcard", |
||||||
|
policies: []TokenExchangePolicy{ |
||||||
|
{ClientID: "*", AllowedAudiences: []string{"https://other.example.com"}}, |
||||||
|
{ClientID: "client-a", AllowedAudiences: []string{"https://resource.example.com"}}, |
||||||
|
}, |
||||||
|
clientID: "client-a", |
||||||
|
audience: "https://resource.example.com", |
||||||
|
wantErr: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "scope denied by policy", |
||||||
|
policies: []TokenExchangePolicy{ |
||||||
|
{ClientID: "client-a", AllowedAudiences: []string{"https://resource.example.com"}, AllowedScopes: []string{"read"}}, |
||||||
|
}, |
||||||
|
clientID: "client-a", |
||||||
|
audience: "https://resource.example.com", |
||||||
|
scopes: []string{"admin"}, |
||||||
|
wantErr: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
name: "allowed scope passes", |
||||||
|
policies: []TokenExchangePolicy{ |
||||||
|
{ClientID: "client-a", AllowedAudiences: []string{"https://resource.example.com"}, AllowedScopes: []string{"read", "write"}}, |
||||||
|
}, |
||||||
|
clientID: "client-a", |
||||||
|
audience: "https://resource.example.com", |
||||||
|
scopes: []string{"read"}, |
||||||
|
wantErr: false, |
||||||
|
}, |
||||||
|
} |
||||||
|
|
||||||
|
for _, tc := range tests { |
||||||
|
t.Run(tc.name, func(t *testing.T) { |
||||||
|
err := evaluateIDJAGPolicy(tc.policies, tc.clientID, tc.audience, tc.scopes) |
||||||
|
if tc.wantErr && err == nil { |
||||||
|
t.Error("expected error but got none") |
||||||
|
} |
||||||
|
if !tc.wantErr && err != nil { |
||||||
|
t.Errorf("unexpected error: %v", err) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue