Compare commits

...

4 Commits

Author SHA1 Message Date
James Elliott 33fdacb6e1
Merge branch 'master' into feat-oidc-policies 2023-06-20 15:46:19 +10:00
James Elliott f290fd90b1
feat: denied
Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
2023-06-19 12:02:38 +10:00
James Elliott 2f9da2b7e0
feat(oidc): per-client auth policy applied per-subject
Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
2023-06-19 12:02:38 +10:00
James Elliott 5e5eead729
feat(oidc): per-client auth policy applied per-subject
Signed-off-by: James Elliott <james-d-elliott@users.noreply.github.com>
2023-06-19 12:02:38 +10:00
17 changed files with 266 additions and 126 deletions

View File

@ -208,6 +208,10 @@ func domainToPrefixSuffix(domain string) (prefix, suffix string) {
return parts[0], strings.Join(parts[1:], ".") return parts[0], strings.Join(parts[1:], ".")
} }
func NewSubjects(subjectRules [][]string) (subjects []AccessControlSubjects) {
return schemaSubjectsToACL(subjectRules)
}
// IsAuthLevelSufficient returns true if the current authenticationLevel is above the authorizationLevel. // IsAuthLevelSufficient returns true if the current authenticationLevel is above the authorizationLevel.
func IsAuthLevelSufficient(authenticationLevel authentication.Level, authorizationLevel Level) bool { func IsAuthLevelSufficient(authenticationLevel authentication.Level, authorizationLevel Level) bool {
switch authorizationLevel { switch authorizationLevel {

View File

@ -35,11 +35,25 @@ type OpenIDConnect struct {
Clients []OpenIDConnectClient `koanf:"clients"` Clients []OpenIDConnectClient `koanf:"clients"`
Policies map[string]OpenIDConnectPolicy `koanf:"policies"`
Discovery OpenIDConnectDiscovery // MetaData value. Not configurable by users. Discovery OpenIDConnectDiscovery // MetaData value. Not configurable by users.
} }
type OpenIDConnectPolicy struct {
DefaultPolicy string `koanf:"default_policy"`
Rules []OpenIDConnectPolicyRule `koanf:"rules"`
}
type OpenIDConnectPolicyRule struct {
Policy string `koanf:"policy"`
Subjects [][]string `koanf:"subject"`
}
// OpenIDConnectDiscovery is information discovered during validation reused for the discovery handlers. // OpenIDConnectDiscovery is information discovered during validation reused for the discovery handlers.
type OpenIDConnectDiscovery struct { type OpenIDConnectDiscovery struct {
Policies []string
DefaultKeyIDs map[string]string DefaultKeyIDs map[string]string
DefaultKeyID string DefaultKeyID string
ResponseObjectSigningKeyIDs []string ResponseObjectSigningKeyIDs []string

View File

@ -72,6 +72,11 @@ var Keys = []string{
"identity_providers.oidc.clients[].public_keys.values[].key", "identity_providers.oidc.clients[].public_keys.values[].key",
"identity_providers.oidc.clients[].public_keys.values[].certificate_chain", "identity_providers.oidc.clients[].public_keys.values[].certificate_chain",
"identity_providers.oidc.clients[]", "identity_providers.oidc.clients[]",
"identity_providers.oidc.policies",
"identity_providers.oidc.policies.*.default_policy",
"identity_providers.oidc.policies.*.rules",
"identity_providers.oidc.policies.*.rules[].policy",
"identity_providers.oidc.policies.*.rules[].subject",
"identity_providers.oidc", "identity_providers.oidc",
"authentication_backend.password_reset.disable", "authentication_backend.password_reset.disable",
"authentication_backend.password_reset.custom_url", "authentication_backend.password_reset.custom_url",

View File

@ -124,6 +124,12 @@ notifier:
identity_providers: identity_providers:
oidc: oidc:
policies:
abc:
default_policy: 'two_factor'
rules:
- subject: 'group:admin'
policy: 'one_factor'
cors: cors:
allowed_origins: allowed_origins:
- 'https://google.com' - 'https://google.com'

View File

@ -30,6 +30,7 @@ func validateOIDC(config *schema.OpenIDConnect, val *schema.StructValidator) {
setOIDCDefaults(config) setOIDCDefaults(config)
validateOIDCIssuer(config, val) validateOIDCIssuer(config, val)
validateOIDCPolicies(config, val)
sort.Sort(oidc.SortedSigningAlgs(config.Discovery.ResponseObjectSigningAlgs)) sort.Sort(oidc.SortedSigningAlgs(config.Discovery.ResponseObjectSigningAlgs))
@ -58,6 +59,53 @@ func validateOIDC(config *schema.OpenIDConnect, val *schema.StructValidator) {
} }
} }
func validateOIDCPolicies(config *schema.OpenIDConnect, val *schema.StructValidator) {
config.Discovery.Policies = []string{policyOneFactor, policyTwoFactor}
for name, policy := range config.Policies {
switch name {
case "":
val.Push(fmt.Errorf("policy must have a name"))
case policyOneFactor, policyTwoFactor, policyDeny:
val.Push(fmt.Errorf("policy names can't be named any of %s", strJoinAnd([]string{policyOneFactor, policyTwoFactor, policyDeny})))
default:
break
}
switch policy.DefaultPolicy {
case "":
policy.DefaultPolicy = policyTwoFactor
case policyOneFactor, policyTwoFactor, policyDeny:
break
default:
val.Push(fmt.Errorf("policy must be one of %s", strJoinAnd([]string{policyOneFactor, policyTwoFactor, policyDeny})))
}
if len(policy.Rules) == 0 {
val.Push(fmt.Errorf("policy must include at least one rule"))
}
for i, rule := range policy.Rules {
switch rule.Policy {
case "":
policy.Rules[i].Policy = policyTwoFactor
case policyOneFactor, policyTwoFactor, policyDeny:
break
default:
val.Push(fmt.Errorf("policy must be one of %s", strJoinAnd([]string{policyOneFactor, policyTwoFactor, policyDeny})))
}
if len(rule.Subjects) == 0 {
val.Push(fmt.Errorf("policy must include at least one criteria"))
}
}
config.Policies[name] = policy
config.Discovery.Policies = append(config.Discovery.Policies, name)
}
}
func validateOIDCIssuer(config *schema.OpenIDConnect, val *schema.StructValidator) { func validateOIDCIssuer(config *schema.OpenIDConnect, val *schema.StructValidator) {
switch { switch {
case config.IssuerPrivateKey != nil: case config.IssuerPrivateKey != nil:
@ -337,13 +385,13 @@ func validateOIDCClient(c int, config *schema.OpenIDConnect, val *schema.StructV
} }
} }
switch config.Clients[c].Policy { switch {
case "": case config.Clients[c].Policy == "":
config.Clients[c].Policy = schema.DefaultOpenIDConnectClientConfiguration.Policy config.Clients[c].Policy = schema.DefaultOpenIDConnectClientConfiguration.Policy
case policyOneFactor, policyTwoFactor: case utils.IsStringInSlice(config.Clients[c].Policy, config.Discovery.Policies):
break break
default: default:
val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue, config.Clients[c].ID, "authorization_policy", strJoinOr([]string{policyOneFactor, policyTwoFactor}), config.Clients[c].Policy)) val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue, config.Clients[c].ID, "authorization_policy", strJoinOr(config.Discovery.Policies), config.Clients[c].Policy))
} }
switch config.Clients[c].PKCEChallengeMethod { switch config.Clients[c].PKCEChallengeMethod {

View File

@ -67,16 +67,18 @@ KEYS:
// NewKeyPattern returns patterns which are required to match key patterns. // NewKeyPattern returns patterns which are required to match key patterns.
func NewKeyPattern(key string) (pattern *regexp.Regexp, err error) { func NewKeyPattern(key string) (pattern *regexp.Regexp, err error) {
switch { switch {
case strings.Contains(key, ".*."): case reIsMapKey.MatchString(key):
return NewKeyMapPattern(key) return NewKeyMapPattern(key)
default: default:
return nil, nil return nil, nil
} }
} }
var reIsMapKey = regexp.MustCompile(`\.\*(\[]|\.)`)
// NewKeyMapPattern returns a pattern required to match map keys. // NewKeyMapPattern returns a pattern required to match map keys.
func NewKeyMapPattern(key string) (pattern *regexp.Regexp, err error) { func NewKeyMapPattern(key string) (pattern *regexp.Regexp, err error) {
parts := strings.Split(key, ".*.") parts := strings.Split(key, ".*")
buf := &strings.Builder{} buf := &strings.Builder{}
@ -85,11 +87,16 @@ func NewKeyMapPattern(key string) (pattern *regexp.Regexp, err error) {
n := len(parts) - 1 n := len(parts) - 1
for i, part := range parts { for i, part := range parts {
if i != 0 { if i != 0 && !strings.HasPrefix(part, "[]") {
buf.WriteString("\\.") buf.WriteString("\\.")
} }
for _, r := range part { for j, r := range part {
// Skip prefixed period.
if j == 0 && r == '.' {
continue
}
switch r { switch r {
case '[', ']', '.', '{', '}': case '[', ']', '.', '{', '}':
buf.WriteRune('\\') buf.WriteRune('\\')

View File

@ -8,6 +8,7 @@ import (
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/oidc"
@ -117,7 +118,7 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
extraClaims := oidcGrantRequests(requester, consent, &userSession) extraClaims := oidcGrantRequests(requester, consent, &userSession)
if authTime, err = userSession.AuthenticatedTime(client.GetAuthorizationPolicy()); err != nil { if authTime, err = userSession.AuthenticatedTime(client.GetAuthorizationPolicy(authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()})); err != nil {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred checking authentication time: %+v", requester.GetID(), client.GetID(), err) ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: error occurred checking authentication time: %+v", requester.GetID(), client.GetID(), err)
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, fosite.ErrServerError.WithHint("Could not obtain the authentication time.")) ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, fosite.ErrServerError.WithHint("Could not obtain the authentication time."))

View File

@ -11,6 +11,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/oidc"
@ -28,10 +29,12 @@ func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, issuer *url.UR
var handler handlerAuthorizationConsent var handler handlerAuthorizationConsent
policy := client.GetAuthorizationPolicy(authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()})
switch { switch {
case userSession.IsAnonymous(): case userSession.IsAnonymous():
handler = handleOIDCAuthorizationConsentNotAuthenticated handler = handleOIDCAuthorizationConsentNotAuthenticated
case client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel): case authorization.IsAuthLevelSufficient(userSession.AuthenticationLevel, policy):
if subject, err = ctx.Providers.OpenIDConnect.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil { if subject, err = ctx.Providers.OpenIDConnect.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil {
ctx.Logger.Errorf(logFmtErrConsentCantGetSubject, requester.GetID(), client.GetID(), client.GetConsentPolicy(), userSession.Username, client.GetSectorIdentifier(), err) ctx.Logger.Errorf(logFmtErrConsentCantGetSubject, requester.GetID(), client.GetID(), client.GetConsentPolicy(), userSession.Username, client.GetSectorIdentifier(), err)
@ -55,6 +58,14 @@ func handleOIDCAuthorizationConsent(ctx *middlewares.AutheliaCtx, issuer *url.UR
return nil, true return nil, true
} }
default: default:
if policy == authorization.Denied {
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' could not be processed: the user '%s' is not authorized to use this client", requester.GetID(), client.GetID(), userSession.Username)
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, oidc.ErrClientAuthorizationUserAccessDenied)
return nil, true
}
if subject, err = ctx.Providers.OpenIDConnect.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil { if subject, err = ctx.Providers.OpenIDConnect.GetSubject(ctx, client.GetSectorIdentifier(), userSession.Username); err != nil {
ctx.Logger.Errorf(logFmtErrConsentCantGetSubject, requester.GetID(), client.GetID(), client.GetConsentPolicy(), userSession.Username, client.GetSectorIdentifier(), err) ctx.Logger.Errorf(logFmtErrConsentCantGetSubject, requester.GetID(), client.GetID(), client.GetConsentPolicy(), userSession.Username, client.GetSectorIdentifier(), err)
@ -121,7 +132,7 @@ func handleOIDCAuthorizationConsentRedirect(ctx *middlewares.AutheliaCtx, issuer
userSession session.UserSession, rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) { userSession session.UserSession, rw http.ResponseWriter, r *http.Request, requester fosite.AuthorizeRequester) {
var location *url.URL var location *url.URL
if client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) { if client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel, authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()}) {
location, _ = url.ParseRequestURI(issuer.String()) location, _ = url.ParseRequestURI(issuer.String())
location.Path = path.Join(location.Path, oidc.EndpointPathConsent) location.Path = path.Join(location.Path, oidc.EndpointPathConsent)
@ -130,11 +141,11 @@ func handleOIDCAuthorizationConsentRedirect(ctx *middlewares.AutheliaCtx, issuer
location.RawQuery = query.Encode() location.RawQuery = query.Encode()
ctx.Logger.Debugf(logFmtDbgConsentAuthenticationSufficiency, requester.GetID(), client.GetID(), client.GetConsentPolicy(), userSession.AuthenticationLevel.String(), "sufficient", client.GetAuthorizationPolicy()) ctx.Logger.Debugf(logFmtDbgConsentAuthenticationSufficiency, requester.GetID(), client.GetID(), client.GetConsentPolicy(), userSession.AuthenticationLevel.String(), "sufficient", client.GetAuthorizationPolicy(authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()}))
} else { } else {
location = handleOIDCAuthorizationConsentGetRedirectionURL(issuer, consent, requester) location = handleOIDCAuthorizationConsentGetRedirectionURL(issuer, consent, requester)
ctx.Logger.Debugf(logFmtDbgConsentAuthenticationSufficiency, requester.GetID(), client.GetID(), client.GetConsentPolicy(), userSession.AuthenticationLevel.String(), "insufficient", client.GetAuthorizationPolicy()) ctx.Logger.Debugf(logFmtDbgConsentAuthenticationSufficiency, requester.GetID(), client.GetID(), client.GetConsentPolicy(), userSession.AuthenticationLevel.String(), "insufficient", client.GetAuthorizationPolicy(authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()}))
} }
ctx.Logger.Debugf(logFmtDbgConsentRedirect, requester.GetID(), client.GetID(), client.GetConsentPolicy(), location) ctx.Logger.Debugf(logFmtDbgConsentRedirect, requester.GetID(), client.GetID(), client.GetConsentPolicy(), location)

View File

@ -10,6 +10,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/oidc"
@ -86,6 +87,14 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
return return
} }
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel, authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()}) {
ctx.Logger.Errorf("User '%s' can't consent to authorization request for client with id '%s' as they are not sufficiently authenticated",
userSession.Username, consent.ClientID)
ctx.SetJSONError(messageOperationFailed)
return
}
if bodyJSON.Consent { if bodyJSON.Consent {
consent.Grant() consent.Grant()
@ -206,7 +215,7 @@ func oidcConsentGetSessionsAndClient(ctx *middlewares.AutheliaCtx, consentID uui
} }
} }
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) { if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel, authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()}) {
ctx.Logger.Errorf("Unable to perform OpenID Connect Consent for user '%s' and client id '%s': the user is not sufficiently authenticated", userSession.Username, consent.ClientID) ctx.Logger.Errorf("Unable to perform OpenID Connect Consent for user '%s' and client id '%s': the user is not sufficiently authenticated", userSession.Username, consent.ClientID)
ctx.ReplyForbidden() ctx.ReplyForbidden()

View File

@ -221,13 +221,10 @@ func handleOIDCWorkflowResponseWithID(ctx *middlewares.AutheliaCtx, id string) {
return return
} }
if !client.IsAuthenticationLevelSufficient(userSession.AuthenticationLevel) { policy := client.GetAuthorizationPolicy(authorization.Subject{Username: userSession.Username, Groups: userSession.Groups, IP: ctx.RemoteIP()})
ctx.Logger.Warnf("OpenID Connect client '%s' requires 2FA, cannot be redirected yet", client.GetID())
ctx.ReplyOK()
return
}
switch {
case authorization.IsAuthLevelSufficient(userSession.AuthenticationLevel, policy), policy == authorization.Denied:
var ( var (
targetURL *url.URL targetURL *url.URL
form url.Values form url.Values
@ -249,6 +246,12 @@ func handleOIDCWorkflowResponseWithID(ctx *middlewares.AutheliaCtx, id string) {
if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURL.String()}); err != nil { if err = ctx.SetJSONBody(redirectResponse{Redirect: targetURL.String()}); err != nil {
ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err) ctx.Logger.Errorf("Unable to set default redirection URL in body: %s", err)
} }
default:
ctx.Logger.Warnf("OpenID Connect client '%s' requires 2FA, cannot be redirected yet", client.GetID())
ctx.ReplyOK()
return
}
} }
func markAuthenticationAttempt(ctx *middlewares.AutheliaCtx, successful bool, bannedUntil *time.Time, username string, authType string, errAuth error) (err error) { func markAuthenticationAttempt(ctx *middlewares.AutheliaCtx, successful bool, bannedUntil *time.Time, username string, authType string, errAuth error) (err error) {

View File

@ -13,7 +13,7 @@ import (
) )
// NewClient creates a new Client. // NewClient creates a new Client.
func NewClient(config schema.OpenIDConnectClient) (client Client) { func NewClient(config schema.OpenIDConnectClient, c *schema.OpenIDConnect) (client Client) {
base := &BaseClient{ base := &BaseClient{
ID: config.ID, ID: config.ID,
Description: config.Description, Description: config.Description,
@ -39,8 +39,7 @@ func NewClient(config schema.OpenIDConnectClient) (client Client) {
UserinfoSigningAlg: config.UserinfoSigningAlg, UserinfoSigningAlg: config.UserinfoSigningAlg,
UserinfoSigningKeyID: config.UserinfoSigningKeyID, UserinfoSigningKeyID: config.UserinfoSigningKeyID,
Policy: authorization.NewLevel(config.Policy), Policy: NewClientPolicy(config.Policy, c),
Consent: NewClientConsent(config.ConsentMode, config.ConsentPreConfiguredDuration), Consent: NewClientConsent(config.ConsentMode, config.ConsentPreConfiguredDuration),
} }
@ -192,9 +191,18 @@ func (c *BaseClient) GetPKCEChallengeMethod() string {
return c.PKCEChallengeMethod return c.PKCEChallengeMethod
} }
// IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient.
func (c *BaseClient) IsAuthenticationLevelSufficient(level authentication.Level, subject authorization.Subject) bool {
if level == authentication.NotAuthenticated {
return false
}
return authorization.IsAuthLevelSufficient(level, c.GetAuthorizationPolicy(subject))
}
// GetAuthorizationPolicy returns Policy. // GetAuthorizationPolicy returns Policy.
func (c *BaseClient) GetAuthorizationPolicy() authorization.Level { func (c *BaseClient) GetAuthorizationPolicy(subject authorization.Subject) authorization.Level {
return c.Policy return c.Policy.GetRequiredLevel(subject)
} }
// GetConsentPolicy returns Consent. // GetConsentPolicy returns Consent.
@ -223,15 +231,6 @@ func (c *BaseClient) IsPublic() bool {
return c.Public return c.Public
} }
// IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient.
func (c *BaseClient) IsAuthenticationLevelSufficient(level authentication.Level) bool {
if level == authentication.NotAuthenticated {
return false
}
return authorization.IsAuthLevelSufficient(level, c.Policy)
}
// ValidatePKCEPolicy is a helper function to validate PKCE policy constraints on a per-client basis. // ValidatePKCEPolicy is a helper function to validate PKCE policy constraints on a per-client basis.
func (c *BaseClient) ValidatePKCEPolicy(r fosite.Requester) (err error) { func (c *BaseClient) ValidatePKCEPolicy(r fosite.Requester) (err error) {
form := r.GetRequestForm() form := r.GetRequestForm()

View File

@ -0,0 +1,73 @@
package oidc
import (
"github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/configuration/schema"
)
func NewClientPolicy(name string, config *schema.OpenIDConnect) (policy ClientPolicy) {
switch name {
case authorization.OneFactor.String(), authorization.TwoFactor.String():
return ClientPolicy{DefaultPolicy: authorization.NewLevel(name)}
default:
if p, ok := config.Policies[name]; ok {
policy.DefaultPolicy = authorization.NewLevel(p.DefaultPolicy)
for _, r := range p.Rules {
policy.Rules = append(policy.Rules, ClientPolicyRule{
Policy: authorization.NewLevel(r.Policy),
Subjects: authorization.NewSubjects(r.Subjects),
})
}
return policy
}
return ClientPolicy{DefaultPolicy: authorization.TwoFactor}
}
}
// ClientPolicy controls and represents a client policy.
type ClientPolicy struct {
DefaultPolicy authorization.Level
Rules []ClientPolicyRule
}
func (p *ClientPolicy) GetRequiredLevel(subject authorization.Subject) authorization.Level {
for _, rule := range p.Rules {
if rule.IsMatch(subject) {
return rule.Policy
}
}
return p.DefaultPolicy
}
type ClientPolicyRule struct {
Subjects []authorization.AccessControlSubjects
Policy authorization.Level
}
// MatchesSubjects returns true if the rule matches the subjects.
func (p *ClientPolicyRule) MatchesSubjects(subject authorization.Subject) (match bool) {
// If there are no subjects in this rule then the subject condition is a match.
if len(p.Subjects) == 0 {
return true
} else if subject.IsAnonymous() {
return false
}
// Iterate over the subjects until we find a match (return true) or until we exit the loop (return false).
for _, rule := range p.Subjects {
if rule.IsMatch(subject) {
return true
}
}
return false
}
// IsMatch returns true if all elements of an AccessControlRule match the object and subject.
func (p *ClientPolicyRule) IsMatch(subject authorization.Subject) (match bool) {
return p.MatchesSubjects(subject)
}

View File

@ -19,7 +19,7 @@ import (
func TestNewClient(t *testing.T) { func TestNewClient(t *testing.T) {
config := schema.OpenIDConnectClient{} config := schema.OpenIDConnectClient{}
client := oidc.NewClient(config) client := oidc.NewClient(config, &schema.OpenIDConnect{})
assert.Equal(t, "", client.GetID()) assert.Equal(t, "", client.GetID())
assert.Equal(t, "", client.GetDescription()) assert.Equal(t, "", client.GetDescription())
assert.Len(t, client.GetResponseModes(), 0) assert.Len(t, client.GetResponseModes(), 0)
@ -47,17 +47,17 @@ func TestNewClient(t *testing.T) {
ResponseModes: schema.DefaultOpenIDConnectClientConfiguration.ResponseModes, ResponseModes: schema.DefaultOpenIDConnectClientConfiguration.ResponseModes,
} }
client = oidc.NewClient(config) client = oidc.NewClient(config, &schema.OpenIDConnect{})
assert.Equal(t, myclient, client.GetID()) assert.Equal(t, myclient, client.GetID())
require.Len(t, client.GetResponseModes(), 1) require.Len(t, client.GetResponseModes(), 1)
assert.Equal(t, fosite.ResponseModeFormPost, client.GetResponseModes()[0]) assert.Equal(t, fosite.ResponseModeFormPost, client.GetResponseModes()[0])
assert.Equal(t, authorization.TwoFactor, client.GetAuthorizationPolicy()) assert.Equal(t, authorization.TwoFactor, client.GetAuthorizationPolicy(authorization.Subject{}))
config = schema.OpenIDConnectClient{ config = schema.OpenIDConnectClient{
TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretPost, TokenEndpointAuthMethod: oidc.ClientAuthMethodClientSecretPost,
} }
client = oidc.NewClient(config) client = oidc.NewClient(config, &schema.OpenIDConnect{})
fclient, ok := client.(*oidc.FullClient) fclient, ok := client.(*oidc.FullClient)
@ -204,25 +204,25 @@ func TestBaseClient_ValidatePARPolicy(t *testing.T) {
func TestIsAuthenticationLevelSufficient(t *testing.T) { func TestIsAuthenticationLevelSufficient(t *testing.T) {
c := &oidc.FullClient{BaseClient: &oidc.BaseClient{}} c := &oidc.FullClient{BaseClient: &oidc.BaseClient{}}
c.Policy = authorization.Bypass c.Policy = oidc.ClientPolicy{DefaultPolicy: authorization.Bypass}
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated)) assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated, authorization.Subject{}))
assert.True(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor)) assert.True(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor, authorization.Subject{}))
assert.True(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor)) assert.True(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor, authorization.Subject{}))
c.Policy = authorization.OneFactor c.Policy = oidc.ClientPolicy{DefaultPolicy: authorization.OneFactor}
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated)) assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated, authorization.Subject{}))
assert.True(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor)) assert.True(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor, authorization.Subject{}))
assert.True(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor)) assert.True(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor, authorization.Subject{}))
c.Policy = authorization.TwoFactor c.Policy = oidc.ClientPolicy{DefaultPolicy: authorization.TwoFactor}
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated)) assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated, authorization.Subject{}))
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor)) assert.False(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor, authorization.Subject{}))
assert.True(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor)) assert.True(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor, authorization.Subject{}))
c.Policy = authorization.Denied c.Policy = oidc.ClientPolicy{DefaultPolicy: authorization.Denied}
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated)) assert.False(t, c.IsAuthenticationLevelSufficient(authentication.NotAuthenticated, authorization.Subject{}))
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor)) assert.False(t, c.IsAuthenticationLevelSufficient(authentication.OneFactor, authorization.Subject{}))
assert.False(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor)) assert.False(t, c.IsAuthenticationLevelSufficient(authentication.TwoFactor, authorization.Subject{}))
} }
func TestClient_GetConsentResponseBody(t *testing.T) { func TestClient_GetConsentResponseBody(t *testing.T) {
@ -446,7 +446,7 @@ func TestNewClientPKCE(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
client := oidc.NewClient(tc.have) client := oidc.NewClient(tc.have, &schema.OpenIDConnect{})
assert.Equal(t, tc.expectedEnforcePKCE, client.GetPKCEEnforcement()) assert.Equal(t, tc.expectedEnforcePKCE, client.GetPKCEEnforcement())
assert.Equal(t, tc.expectedEnforcePKCEChallengeMethod, client.GetPKCEChallengeMethodEnforcement()) assert.Equal(t, tc.expectedEnforcePKCEChallengeMethod, client.GetPKCEChallengeMethodEnforcement())
@ -511,7 +511,7 @@ func TestNewClientPAR(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
client := oidc.NewClient(tc.have) client := oidc.NewClient(tc.have, &schema.OpenIDConnect{})
assert.Equal(t, tc.expected, client.GetPAREnforcement()) assert.Equal(t, tc.expected, client.GetPAREnforcement())
@ -575,7 +575,7 @@ func TestNewClientResponseModes(t *testing.T) {
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
client := oidc.NewClient(tc.have) client := oidc.NewClient(tc.have, &schema.OpenIDConnect{})
assert.Equal(t, tc.expected, client.GetResponseModes()) assert.Equal(t, tc.expected, client.GetResponseModes())
@ -615,7 +615,7 @@ func TestNewClient_JSONWebKeySetURI(t *testing.T) {
PublicKeys: schema.OpenIDConnectClientPublicKeys{ PublicKeys: schema.OpenIDConnectClientPublicKeys{
URI: MustParseRequestURI("https://google.com"), URI: MustParseRequestURI("https://google.com"),
}, },
}) }, &schema.OpenIDConnect{})
require.NotNil(t, client) require.NotNil(t, client)
@ -630,7 +630,7 @@ func TestNewClient_JSONWebKeySetURI(t *testing.T) {
PublicKeys: schema.OpenIDConnectClientPublicKeys{ PublicKeys: schema.OpenIDConnectClientPublicKeys{
URI: nil, URI: nil,
}, },
}) }, &schema.OpenIDConnect{})
require.NotNil(t, client) require.NotNil(t, client)

View File

@ -32,4 +32,6 @@ var (
// ErrPAREnforcedClientMissingPAR is sent when a client has EnforcePAR configured but the Authorization Request was not Pushed. // ErrPAREnforcedClientMissingPAR is sent when a client has EnforcePAR configured but the Authorization Request was not Pushed.
ErrPAREnforcedClientMissingPAR = fosite.ErrInvalidRequest.WithHint("Pushed Authorization Requests are enforced for this client but no such request was sent.") ErrPAREnforcedClientMissingPAR = fosite.ErrInvalidRequest.WithHint("Pushed Authorization Requests are enforced for this client but no such request was sent.")
ErrClientAuthorizationUserAccessDenied = fosite.ErrAccessDenied.WithHint("The user was denied access to this client.")
) )

View File

@ -31,7 +31,7 @@ func NewStore(config *schema.OpenIDConnect, provider storage.Provider) (store *S
policy := authorization.NewLevel(client.Policy) policy := authorization.NewLevel(client.Policy)
logger.Debugf("Registering client %s with policy %s (%v)", client.ID, client.Policy, policy) logger.Debugf("Registering client %s with policy %s (%v)", client.ID, client.Policy, policy)
store.clients[client.ID] = NewClient(client) store.clients[client.ID] = NewClient(client, config)
} }
return store return store
@ -65,16 +65,6 @@ func (s *Store) GetSubject(ctx context.Context, sectorID, username string) (subj
return opaqueID.Identifier, nil return opaqueID.Identifier, nil
} }
// GetClientPolicy retrieves the policy from the client with the matching provided id.
func (s *Store) GetClientPolicy(id string) (level authorization.Level) {
client, err := s.GetFullClient(id)
if err != nil {
return authorization.TwoFactor
}
return client.GetAuthorizationPolicy()
}
// GetFullClient returns a fosite.Client asserted as an Client matching the provided id. // GetFullClient returns a fosite.Client asserted as an Client matching the provided id.
func (s *Store) GetFullClient(id string) (client Client, err error) { func (s *Store) GetFullClient(id string) (client Client, err error) {
client, ok := s.clients[id] client, ok := s.clients[id]

View File

@ -23,38 +23,6 @@ import (
"github.com/authelia/authelia/v4/internal/storage" "github.com/authelia/authelia/v4/internal/storage"
) )
func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) {
s := oidc.NewStore(&schema.OpenIDConnect{
IssuerCertificateChain: schema.X509CertificateChain{},
IssuerPrivateKey: keyRSA2048,
Clients: []schema.OpenIDConnectClient{
{
ID: myclient,
Description: myclientdesc,
Policy: onefactor,
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeProfile},
Secret: tOpenIDConnectPlainTextClientSecret,
},
{
ID: "myotherclient",
Description: myclientdesc,
Policy: twofactor,
Scopes: []string{oidc.ScopeOpenID, oidc.ScopeProfile},
Secret: tOpenIDConnectPlainTextClientSecret,
},
},
}, nil)
policyOne := s.GetClientPolicy(myclient)
assert.Equal(t, authorization.OneFactor, policyOne)
policyTwo := s.GetClientPolicy("myotherclient")
assert.Equal(t, authorization.TwoFactor, policyTwo)
policyInvalid := s.GetClientPolicy("invalidclient")
assert.Equal(t, authorization.TwoFactor, policyInvalid)
}
func TestOpenIDConnectStore_GetInternalClient(t *testing.T) { func TestOpenIDConnectStore_GetInternalClient(t *testing.T) {
s := oidc.NewStore(&schema.OpenIDConnect{ s := oidc.NewStore(&schema.OpenIDConnect{
IssuerCertificateChain: schema.X509CertificateChain{}, IssuerCertificateChain: schema.X509CertificateChain{},
@ -106,7 +74,7 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) {
assert.Equal(t, fosite.Arguments([]string{oidc.GrantTypeAuthorizationCode}), client.GetGrantTypes()) assert.Equal(t, fosite.Arguments([]string{oidc.GrantTypeAuthorizationCode}), client.GetGrantTypes())
assert.Equal(t, fosite.Arguments([]string{oidc.ResponseTypeAuthorizationCodeFlow}), client.GetResponseTypes()) assert.Equal(t, fosite.Arguments([]string{oidc.ResponseTypeAuthorizationCodeFlow}), client.GetResponseTypes())
assert.Equal(t, []string(nil), client.GetRedirectURIs()) assert.Equal(t, []string(nil), client.GetRedirectURIs())
assert.Equal(t, authorization.OneFactor, client.GetAuthorizationPolicy()) assert.Equal(t, authorization.OneFactor, client.GetAuthorizationPolicy(authorization.Subject{}))
assert.Equal(t, "$plaintext$client-secret", client.GetSecret().Encode()) assert.Equal(t, "$plaintext$client-secret", client.GetSecret().Encode())
} }

View File

@ -10,7 +10,7 @@ import (
"github.com/ory/fosite/handler/openid" "github.com/ory/fosite/handler/openid"
"github.com/ory/fosite/token/jwt" "github.com/ory/fosite/token/jwt"
"github.com/ory/herodot" "github.com/ory/herodot"
"gopkg.in/square/go-jose.v2" jose "gopkg.in/square/go-jose.v2"
"github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization" "github.com/authelia/authelia/v4/internal/authorization"
@ -127,7 +127,7 @@ type BaseClient struct {
UserinfoSigningAlg string UserinfoSigningAlg string
UserinfoSigningKeyID string UserinfoSigningKeyID string
Policy authorization.Level Policy ClientPolicy
Consent ClientConsent Consent ClientConsent
} }
@ -164,14 +164,14 @@ type Client interface {
GetPKCEEnforcement() bool GetPKCEEnforcement() bool
GetPKCEChallengeMethodEnforcement() bool GetPKCEChallengeMethodEnforcement() bool
GetPKCEChallengeMethod() string GetPKCEChallengeMethod() string
GetAuthorizationPolicy() authorization.Level
GetConsentPolicy() ClientConsent
IsAuthenticationLevelSufficient(level authentication.Level) bool
ValidatePKCEPolicy(r fosite.Requester) (err error) ValidatePKCEPolicy(r fosite.Requester) (err error)
ValidatePARPolicy(r fosite.Requester, prefix string) (err error) ValidatePARPolicy(r fosite.Requester, prefix string) (err error)
ValidateResponseModePolicy(r fosite.AuthorizeRequester) (err error) ValidateResponseModePolicy(r fosite.AuthorizeRequester) (err error)
GetConsentPolicy() ClientConsent
IsAuthenticationLevelSufficient(level authentication.Level, subject authorization.Subject) bool
GetAuthorizationPolicy(subject authorization.Subject) authorization.Level
} }
// NewClientConsent converts the schema.OpenIDConnectClientConsentConfig into a oidc.ClientConsent. // NewClientConsent converts the schema.OpenIDConnectClientConsentConfig into a oidc.ClientConsent.