Skip to content
This repository was archived by the owner on Jun 24, 2025. It is now read-only.

Commit 5ac0fba

Browse files
committed
enforce ACR, allow specifying more than one
1 parent 6609724 commit 5ac0fba

File tree

6 files changed

+89
-17
lines changed

6 files changed

+89
-17
lines changed

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,12 @@ Default: (no value)
6262

6363
If specified, the required value of the `acr` claim in the token for authentication to pass.
6464

65+
#### require\_acrs
66+
67+
Default: (no value)
68+
69+
If specified, a comma-separated list of acrs one of which must match the `acr` claim in the token for authentication to pass.
70+
6571
#### http\_proxy
6672

6773
Default: (no value)

authenticator.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,11 @@ type authenticator struct {
4040
// to pass.
4141
AuthorizedGroups []string
4242

43-
// RequireACR is the required value of the acr claim in the token for
44-
// authentication to pass.
43+
// RequireACRs is a list of required values of the acr claim in the token for
44+
// authentication to pass. At least one of the acrs must be present if specified
4545
//
46-
// If empty, the ACR value is not checked.
47-
RequireACR string
46+
// If the list is empty, the ACR value is not checked.
47+
RequireACRs []string
4848

4949
verifier *oidc.Verifier
5050
aud string
@@ -131,9 +131,11 @@ func (a *authenticator) Authenticate(ctx context.Context, user string, token str
131131
}
132132
}
133133

134-
// Validate RequireACR
135-
if len(a.RequireACR) > 0 && a.RequireACR != claims.ACR {
136-
return fmt.Errorf("acr is %q, but %q is required", claims.ACR, a.RequireACR)
134+
// Validate RequireACRs
135+
if len(a.RequireACRs) > 0 {
136+
if !isACRPresent(a.RequireACRs, claims.ACR) {
137+
return fmt.Errorf("acr is %q, but one of %v is required", claims.ACR, a.RequireACRs)
138+
}
137139
}
138140

139141
return nil
@@ -150,3 +152,13 @@ func isMemberOfAtLeastOneGroup(authorizedGroups []string, groups []string) bool
150152

151153
return false
152154
}
155+
156+
func isACRPresent(authorizedACRs []string, acr string) bool {
157+
for _, wantACR := range authorizedACRs {
158+
if wantACR == acr {
159+
return true
160+
}
161+
}
162+
163+
return false
164+
}

authenticator_test.go

Lines changed: 42 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ func TestAuthenticate(t *testing.T) {
4949
userTemplate string
5050
groupsClaimKey string
5151
authorizedGroups []string
52-
requireACR string
52+
requireACRs []string
5353
wantErr string
5454
}{
5555
{
@@ -147,9 +147,27 @@ func TestAuthenticate(t *testing.T) {
147147
"acr": "foo",
148148
},
149149
}),
150-
requireACR: "foo",
151-
wantErr: "",
150+
requireACRs: []string{"foo"},
151+
wantErr: "",
152152
},
153+
{
154+
name: "valid user, valid token, matching one of required ACRs",
155+
user: "jdoe",
156+
token: mustJWT(t, signer, oidc.Claims{
157+
Issuer: "https://example.com",
158+
Subject: "jdoe",
159+
Audience: []string{"valid-aud"},
160+
Expiry: oidc.UnixTime(now.Add(10 * time.Minute).Unix()),
161+
NotBefore: oidc.UnixTime(now.Add(-10 * time.Minute).Unix()),
162+
IssuedAt: oidc.UnixTime(now.Unix()),
163+
Extra: map[string]interface{}{
164+
"acr": "biz",
165+
},
166+
}),
167+
requireACRs: []string{"foo", "biz"},
168+
wantErr: "",
169+
},
170+
153171
{
154172
name: "valid user, valid token, not matching required ACR",
155173
user: "jdoe",
@@ -164,9 +182,27 @@ func TestAuthenticate(t *testing.T) {
164182
"acr": "foo2",
165183
},
166184
}),
167-
requireACR: "foo",
168-
wantErr: "acr is \"foo2\", but \"foo\" is required",
185+
requireACRs: []string{"foo"},
186+
wantErr: "acr is \"foo2\", but one of [foo] is required",
169187
},
188+
{
189+
name: "valid user, valid token, not matching one of required ACR",
190+
user: "jdoe",
191+
token: mustJWT(t, signer, oidc.Claims{
192+
Issuer: "https://example.com",
193+
Subject: "jdoe",
194+
Audience: []string{"valid-aud"},
195+
Expiry: oidc.UnixTime(now.Add(10 * time.Minute).Unix()),
196+
NotBefore: oidc.UnixTime(now.Add(-10 * time.Minute).Unix()),
197+
IssuedAt: oidc.UnixTime(now.Unix()),
198+
Extra: map[string]interface{}{
199+
"acr": "foo2",
200+
},
201+
}),
202+
requireACRs: []string{"foo", "bar", "biz"},
203+
wantErr: "acr is \"foo2\", but one of [foo bar biz] is required",
204+
},
205+
170206
{
171207
name: "valid user, valid token, invalid custom user template",
172208
user: "jdoe",
@@ -235,7 +271,7 @@ func TestAuthenticate(t *testing.T) {
235271
auth.UserTemplate = tc.userTemplate
236272
auth.GroupsClaimKey = tc.groupsClaimKey
237273
auth.AuthorizedGroups = tc.authorizedGroups
238-
auth.RequireACR = tc.requireACR
274+
auth.RequireACRs = tc.requireACRs
239275

240276
err := auth.Authenticate(ctx, tc.user, tc.token)
241277
if err != nil && tc.wantErr == "" {

config.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ type config struct {
2525
// A user must be a member of at least one of the groups in the list, if
2626
// specified.
2727
AuthorizedGroups []string
28-
// RequireACR is the required ACR value that must be present in the claims.
29-
RequireACR string
28+
// RequireACRs is a list of required ACRs required for authentication to pass.
29+
// one of the acr values must be present in the claims.
30+
RequireACRs []string
3031
// HTTPProxy is the HTTP proxy server used to connect to HTTP services.
3132
HTTPProxy string
3233
}
@@ -52,7 +53,9 @@ func configFromArgs(args []string) (*config, error) {
5253
case "authorized_groups":
5354
c.AuthorizedGroups = strings.Split(parts[1], ",")
5455
case "require_acr":
55-
c.RequireACR = parts[1]
56+
c.RequireACRs = []string{parts[1]}
57+
case "require_acrs":
58+
c.RequireACRs = strings.Split(parts[1], ",")
5659
case "http_proxy":
5760
c.HTTPProxy = parts[1]
5861
default:

config_test.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,24 @@ func TestParseConfigFromArgs(t *testing.T) {
3636
UserTemplate: `{{.Email}}`,
3737
GroupsClaimKey: "roles",
3838
AuthorizedGroups: []string{"foo", "bar", "baz"},
39-
RequireACR: "foo",
39+
RequireACRs: []string{"foo"},
4040
HTTPProxy: "http://example.com:8080",
4141
},
4242
},
43+
{
44+
name: "overriding defaults required_acrs",
45+
args: []string{"issuer=https://example.com", "aud=example-aud", "user_template={{.Email}}", "groups_claim_key=roles", "authorized_groups=foo,bar,baz", "require_acrs=acr1,acr2,acr3", "http_proxy=http://example.com:8080"},
46+
want: &config{
47+
Issuer: "https://example.com",
48+
Aud: "example-aud",
49+
UserTemplate: `{{.Email}}`,
50+
GroupsClaimKey: "roles",
51+
AuthorizedGroups: []string{"foo", "bar", "baz"},
52+
RequireACRs: []string{"acr1", "acr2", "acr3"},
53+
HTTPProxy: "http://example.com:8080",
54+
},
55+
},
56+
4357
{
4458
name: "invalid option",
4559
args: []string{"issuer=https://example.com", "invalid=foo"},

pam_oidc.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ func pam_sm_authenticate_go(pamh *C.pam_handle_t, flags C.int, argc C.int, argv
8787
auth.UserTemplate = cfg.UserTemplate
8888
auth.GroupsClaimKey = cfg.GroupsClaimKey
8989
auth.AuthorizedGroups = cfg.AuthorizedGroups
90+
auth.RequireACRs = cfg.RequireACRs
9091

9192
if err := auth.Authenticate(ctx, user, token); err != nil {
9293
pamSyslog(pamh, syslog.LOG_WARNING, "failed to authenticate: %v", err)

0 commit comments

Comments
 (0)