Skip to content
Draft
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
2 changes: 2 additions & 0 deletions api/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -1232,6 +1232,8 @@ type ACLOIDCCompleteAuthRequest struct {
State string
Code string

Iss string

// RedirectURI is the URL that authorization should redirect to. This is a
// required parameter.
RedirectURI string
Expand Down
1 change: 1 addition & 0 deletions command/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ func (l *LoginCommand) loginOIDC(ctx context.Context, client *api.Client) (*api.
ClientNonce: callbackServer.Nonce(),
Code: req.Code,
State: req.State,
Iss: req.Iss,
}

token, _, err := client.ACLAuth().CompleteAuth(&cbArgs, nil)
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.25.3
// Pinned dependencies are noted in github.com/hashicorp/nomad/issues/11826.
replace (
github.com/Microsoft/go-winio => github.com/endocrimes/go-winio v0.4.13-0.20190628114223-fb47a8b41948
github.com/hashicorp/cap => github.com/allisonlarson/cap v0.0.0-20251128192950-7ada9938af25
github.com/hashicorp/hcl => github.com/hashicorp/hcl v1.0.1-nomad-1
)

Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/allisonlarson/cap v0.0.0-20251128192950-7ada9938af25 h1:atbEO0ax7BXODSqywPFdra76CfUHIQqtq3QiACcGdyw=
github.com/allisonlarson/cap v0.0.0-20251128192950-7ada9938af25/go.mod h1:HKbv27kfps+wONFNyNTHpAQmU/DCjjDuB5HF6mFsqPQ=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
github.com/apparentlymart/go-cidr v1.0.1 h1:NmIwLZ/KdsjIUlhf+/Np40atNXm/+lZ5txfTJ/SpF+U=
github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
Expand Down Expand Up @@ -415,8 +417,6 @@ github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnV
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.65 h1:81+kWbE1yErFBMjME0I5k3x3kojjKsWtPYHEAutoPow=
github.com/hashicorp/aws-sdk-go-base/v2 v2.0.0-beta.65/go.mod h1:WtMzv9T++tfWVea+qB2MXoaqxw33S8bpJslzUike2mQ=
github.com/hashicorp/cap v0.11.0 h1:tnMNgIWEdbmyx0fulrlLPNHowsprg34xFWflOEB3t1s=
github.com/hashicorp/cap v0.11.0/go.mod h1:HKbv27kfps+wONFNyNTHpAQmU/DCjjDuB5HF6mFsqPQ=
github.com/hashicorp/cli v1.1.7 h1:/fZJ+hNdwfTSfsxMBa9WWMlfjUZbX8/LnUxgAd7lCVU=
github.com/hashicorp/cli v1.1.7/go.mod h1:e6Mfpga9OCT1vqzFuoGZiiF/KaG9CbUfO5s3ghU3YgU=
github.com/hashicorp/consul-template v0.41.3 h1:kBV74WN+UBl7TL3tzXGXU4AiGug4teUrAGO3vnnz+DI=
Expand Down
1 change: 1 addition & 0 deletions lib/auth/oidc/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ func (s *CallbackServer) ServeHTTP(w http.ResponseWriter, req *http.Request) {
State: q.Get("state"),
ClientNonce: s.clientNonce,
Code: q.Get("code"),
Iss: q.Get("iss"),
}

// Send our result. We don't block here because the channel should be
Expand Down
14 changes: 14 additions & 0 deletions nomad/acl_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -2767,6 +2767,20 @@ func (a *ACL) OIDCCompleteAuth(
return fmt.Errorf("failed to generate OIDC provider: %v", err)
}

// Check if the OIDC provider requires the `iss` parameter to be
// validated
providerMetadata := struct {
AuthorizationResponseIssParameterSupported bool `json:"authorization_response_iss_parameter_supported"`
}{}
if err := oidcProvider.Claims(&providerMetadata); err != nil {
return fmt.Errorf("failed to retrieve OIDC provider metadata: %v", err)
}
if providerMetadata.AuthorizationResponseIssParameterSupported {
if args.Iss == "" || args.Iss != authMethod.Config.OIDCDiscoveryURL {
return errors.New("invalid or missing issuer parameter in callback")
}
}

// Retrieve the request generated in OIDCAuthURL()
oidcReq := a.oidcRequestCache.LoadAndDelete(args.ClientNonce) // I am so done with this NONCENSE
if oidcReq == nil {
Expand Down
123 changes: 123 additions & 0 deletions nomad/acl_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4080,6 +4080,129 @@ func TestACL_OIDCCompleteAuth(t *testing.T) {
})
}

// TestACL_OIDCCompleteAuth_IssEnforcedProvider tests that when an OIDC provider
// enforces the authorization_response_iss_parameter_supported configuration, the
// `iss` parameter is properly validated.
func TestACL_OIDCCompleteAuth_IssEnforcedProvider(t *testing.T) {
ci.Parallel(t)

// setup the ACL server with verbose logging
var buf bytes.Buffer
testServer, _, testServerCleanupFn := TestACLServer(t, func(c *Config) {
c.Logger = hclog.NewInterceptLogger(&hclog.LoggerOptions{
Level: hclog.Debug,
Output: io.MultiWriter(&buf, os.Stderr),
})
})
defer testServerCleanupFn()
codec := rpcClient(t, testServer)
testutil.WaitForLeader(t, testServer.RPC)

// setup the test OIDC provider that requires iss parameter
oidcTestProvider := capOIDC.StartTestProvider(t)
defer oidcTestProvider.Stop()
oidcTestProvider.SetAllowedRedirectURIs([]string{"http://127.0.0.1:4649/oidc/callback"})
oidcTestProvider.SetExpectedAuthNonce("fsSPuaodKevKfDU3IeXa")
oidcTestProvider.SetExpectedAuthCode("codeABC")
oidcTestProvider.SetCustomAudience("mock")
oidcTestProvider.SetCustomClaims(map[string]interface{}{
"azp": "mock",
"http://nomad.internal/policies": []string{"engineering"},
"http://nomad.internal/roles": []string{"engineering"},
})
oidcTestProvider.SetAdditionalConfiguration(map[string]interface{}{
"authorization_response_iss_parameter_supported": true,
})

// setup the OIDC auth method with our test values
mockedAuthMethod := mock.ACLOIDCAuthMethod()
mockedAuthMethod.Config.BoundAudiences = []string{"mock"}
mockedAuthMethod.Config.AllowedRedirectURIs = []string{"http://127.0.0.1:4649/oidc/callback"}
mockedAuthMethod.Config.OIDCDiscoveryURL = oidcTestProvider.Addr()
mockedAuthMethod.Config.SigningAlgs = []string{"ES256"}
mockedAuthMethod.Config.DiscoveryCaPem = []string{oidcTestProvider.CACert()}
mockedAuthMethod.Config.ClaimMappings = map[string]string{}
mockedAuthMethod.Config.ListClaimMappings = map[string]string{
"http://nomad.internal/roles": "roles",
"http://nomad.internal/policies": "policies",
}

must.NoError(t, testServer.fsm.State().UpsertACLAuthMethods(10, []*structs.ACLAuthMethod{mockedAuthMethod}))

// upsert and bind ACL policy and role for use in tests
mockACLPolicy := mock.ACLPolicy()
must.NoError(t, testServer.fsm.State().UpsertACLPolicies(
structs.MsgTypeTestSetup, 20, []*structs.ACLPolicy{mockACLPolicy}))

mockACLRole := mock.ACLRole()
mockACLRole.Policies = []*structs.ACLRolePolicyLink{{Name: mockACLPolicy.Name}}
must.NoError(t, testServer.fsm.State().UpsertACLRoles(
structs.MsgTypeTestSetup, 30, []*structs.ACLRole{mockACLRole}, true))

mockBindingRule := mock.ACLBindingRule()
mockBindingRule.AuthMethod = mockedAuthMethod.Name
mockBindingRule.BindType = structs.ACLBindingRuleBindTypePolicy
mockBindingRule.Selector = "engineering in list.policies"
mockBindingRule.BindName = mockACLPolicy.Name

must.NoError(t, testServer.fsm.State().UpsertACLBindingRules(
40, []*structs.ACLBindingRule{mockBindingRule}, true))

// test a missing iss parameter
missingIssReq := structs.ACLOIDCCompleteAuthRequest{
AuthMethodName: mockedAuthMethod.Name,
RedirectURI: mockedAuthMethod.Config.AllowedRedirectURIs[0],
ClientNonce: "fsSPuaodKevKfDU3IeXa",
State: "st_someweirdstateid",
Code: "codeABC",
WriteRequest: structs.WriteRequest{Region: DefaultRegion},
}
// Pretend that OIDCAuthURL was called as a separate request.
cacheOIDCRequest(t, testServer.oidcRequestCache, missingIssReq)

var missingIssResp structs.ACLLoginResponse
err := msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &missingIssReq, &missingIssResp)
must.Error(t, err)
must.ErrorContains(t, err, "invalid or missing issuer parameter")

// test an incorrect iss parameter
incorrectIssReq := structs.ACLOIDCCompleteAuthRequest{
AuthMethodName: mockedAuthMethod.Name,
RedirectURI: mockedAuthMethod.Config.AllowedRedirectURIs[0],
ClientNonce: "fsSPuaodKevKfDU3IeXa",
State: "st_someweirdstateid",
Code: "codeABC",
Iss: "incorrect-issuer",
WriteRequest: structs.WriteRequest{Region: DefaultRegion},
}
// Pretend that OIDCAuthURL was called as a separate request.
cacheOIDCRequest(t, testServer.oidcRequestCache, incorrectIssReq)

var incorrectIssResp structs.ACLLoginResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &incorrectIssReq, &incorrectIssResp)
must.Error(t, err)
must.ErrorContains(t, err, "invalid or missing issuer parameter")

// test a valid iss parameter
req := structs.ACLOIDCCompleteAuthRequest{
AuthMethodName: mockedAuthMethod.Name,
RedirectURI: mockedAuthMethod.Config.AllowedRedirectURIs[0],
ClientNonce: "fsSPuaodKevKfDU3IeXa",
State: "st_someweirdstateid",
Code: "codeABC",
Iss: oidcTestProvider.Addr(),
WriteRequest: structs.WriteRequest{Region: DefaultRegion},
}
// Pretend that OIDCAuthURL was called as a separate request.
cacheOIDCRequest(t, testServer.oidcRequestCache, req)

var resp structs.ACLLoginResponse
err = msgpackrpc.CallWithCodec(codec, structs.ACLOIDCCompleteAuthRPCMethod, &req, &resp)
must.NoError(t, err)
must.NotNil(t, resp.ACLToken)
must.Eq(t, structs.ACLClientToken, resp.ACLToken.Type)
}

// mockSerializer implements the capOIDC.JWTSerializer interface,
// which is used to provide a client assertion JWT.
type mockSerializer struct {
Expand Down
1 change: 1 addition & 0 deletions nomad/structs/acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -2350,6 +2350,7 @@ type ACLOIDCCompleteAuthRequest struct {
ClientNonce string
State string
Code string
Iss string

// RedirectURI is the URL that authorization should redirect to. This is a
// required parameter.
Expand Down